Files
sam-manage/resources/views/rd/design-insight/index.blade.php
김보곤 4b28a868ac feat: [rd] 디자인 인사이트 도움말 모달 추가
- ? 버튼 클릭 시 7개 탭 도움말 모달 표시
- 개요, 툴바, 카드 유형, 뷰 모드, 사이드바, 단축키, 워크플로우
- 각 기능별 상세 설명 및 빠른 시작 가이드
2026-03-08 10:02:57 +09:00

2258 lines
108 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

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

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

@extends('layouts.app')
@section('title', '디자인 인사이트')
@section('content')
<style>
/* ===== Design Insight Core ===== */
:root {
--di-sidebar: 280px;
--di-toolbar: 48px;
--di-blue: #3b82f6;
--di-indigo: #6366f1;
--di-green: #10b981;
--di-amber: #f59e0b;
--di-red: #ef4444;
--di-purple: #8b5cf6;
--di-pink: #ec4899;
--di-cyan: #0ea5e9;
--di-slate: #64748b;
--di-bg: #f8fafc;
--di-card-bg: #ffffff;
--di-border: #e2e8f0;
--di-text: #1e293b;
--di-text-secondary: #64748b;
--di-radius: 10px;
--di-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
--di-shadow-lg: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);
}
/* Wrap */
.di-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 56px);
background: var(--di-bg);
overflow: hidden;
font-family: 'Pretendard', -apple-system, sans-serif;
}
/* Toolbar */
.di-toolbar {
display: flex;
align-items: center;
height: var(--di-toolbar);
padding: 0 16px;
background: #fff;
border-bottom: 1px solid var(--di-border);
gap: 12px;
flex-shrink: 0;
z-index: 20;
}
.di-toolbar .di-title-input {
border: none;
background: transparent;
font-size: 15px;
font-weight: 600;
color: var(--di-text);
padding: 4px 8px;
border-radius: 6px;
min-width: 200px;
}
.di-toolbar .di-title-input:hover { background: #f1f5f9; }
.di-toolbar .di-title-input:focus { outline: 2px solid var(--di-blue); background: #fff; }
.di-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--di-border);
background: #fff;
color: var(--di-text);
transition: all .15s;
white-space: nowrap;
}
.di-btn:hover { background: #f1f5f9; border-color: #cbd5e1; }
.di-btn.primary { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
.di-btn.primary:hover { background: #2563eb; }
.di-btn.sm { padding: 4px 8px; font-size: 11.5px; }
.di-btn.ghost { border: none; background: transparent; }
.di-btn.ghost:hover { background: #f1f5f9; }
.di-view-tabs {
display: flex;
gap: 2px;
background: #f1f5f9;
border-radius: 8px;
padding: 3px;
}
.di-view-tab {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
color: var(--di-text-secondary);
transition: all .15s;
border: none;
background: transparent;
}
.di-view-tab:hover { color: var(--di-text); }
.di-view-tab.active { background: #fff; color: var(--di-text); box-shadow: 0 1px 2px rgba(0,0,0,.08); font-weight: 600; }
/* Body */
.di-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.di-sidebar {
width: var(--di-sidebar);
background: #fff;
border-right: 1px solid var(--di-border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
transition: width .2s;
}
.di-sidebar.collapsed { width: 0; overflow: hidden; padding: 0; border-right: none; }
.di-sidebar-section {
padding: 12px 14px;
border-bottom: 1px solid var(--di-border);
}
.di-sidebar-section h4 {
font-size: 10.5px;
font-weight: 700;
color: var(--di-text-secondary);
text-transform: uppercase;
letter-spacing: .5px;
margin-bottom: 8px;
}
.di-sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--di-text);
cursor: pointer;
transition: all .12s;
}
.di-sidebar-item:hover { background: #f1f5f9; }
.di-sidebar-item.active { background: #eff6ff; color: var(--di-blue); font-weight: 600; }
.di-sidebar-item .cnt {
margin-left: auto;
font-size: 11px;
color: var(--di-text-secondary);
background: #f1f5f9;
padding: 1px 7px;
border-radius: 10px;
}
.di-tag-chip {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 11.5px;
cursor: pointer;
border: 1px solid var(--di-border);
background: #fff;
color: var(--di-text-secondary);
margin: 2px;
transition: all .12s;
}
.di-tag-chip:hover { border-color: var(--di-blue); color: var(--di-blue); }
.di-tag-chip.active { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
.di-search-input {
width: 100%;
padding: 7px 10px 7px 32px;
border: 1px solid var(--di-border);
border-radius: 8px;
font-size: 12.5px;
background: #f8fafc;
transition: all .15s;
}
.di-search-input:focus { outline: none; border-color: var(--di-blue); background: #fff; box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
/* Main */
.di-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Category Tabs */
.di-cat-tabs {
display: flex;
gap: 1px;
padding: 10px 20px 0;
background: #fff;
border-bottom: 1px solid var(--di-border);
overflow-x: auto;
flex-shrink: 0;
}
.di-cat-tab {
padding: 8px 16px;
font-size: 13px;
color: var(--di-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all .15s;
white-space: nowrap;
background: none;
border-top: none;
border-left: none;
border-right: none;
}
.di-cat-tab:hover { color: var(--di-text); }
.di-cat-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
/* Content */
.di-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
/* Board View */
.di-board {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
/* Card */
.di-card {
background: var(--di-card-bg);
border-radius: var(--di-radius);
border: 1px solid var(--di-border);
overflow: hidden;
cursor: pointer;
transition: all .2s;
position: relative;
}
.di-card:hover { box-shadow: var(--di-shadow-lg); border-color: #cbd5e1; transform: translateY(-1px); }
.di-card .card-img {
width: 100%;
height: 180px;
object-fit: cover;
background: #f1f5f9;
display: block;
}
.di-card .card-img-placeholder {
width: 100%;
height: 180px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 32px;
}
.di-card .card-body {
padding: 14px;
}
.di-card .card-type {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.5px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 6px;
}
.di-card .card-type.ref { background: #eff6ff; color: #2563eb; }
.di-card .card-type.analysis { background: #fef3c7; color: #d97706; }
.di-card .card-type.pattern { background: #ecfdf5; color: #059669; }
.di-card .card-type.comparison { background: #fae8ff; color: #a855f7; }
.di-card .card-title {
font-size: 14px;
font-weight: 600;
color: var(--di-text);
margin-bottom: 6px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.di-card .card-memo {
font-size: 12.5px;
color: var(--di-text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
}
.di-card .card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.di-card .card-tag {
font-size: 10.5px;
padding: 2px 7px;
border-radius: 4px;
background: #f1f5f9;
color: var(--di-text-secondary);
}
.di-card .card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
border-top: 1px solid #f1f5f9;
}
.di-card .card-rating { color: #f59e0b; font-size: 12px; letter-spacing: 1px; }
.di-card .card-date { font-size: 11px; color: #94a3b8; }
.di-card .card-pin {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255,255,255,.9);
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
opacity: 0;
transition: opacity .15s;
border: 1px solid var(--di-border);
}
.di-card:hover .card-pin { opacity: 1; }
.di-card .card-pin.pinned { opacity: 1; color: var(--di-amber); }
/* Add Card Button */
.di-add-card {
background: var(--di-card-bg);
border-radius: var(--di-radius);
border: 2px dashed var(--di-border);
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all .2s;
gap: 8px;
color: var(--di-text-secondary);
}
.di-add-card:hover { border-color: var(--di-blue); color: var(--di-blue); background: #f0f7ff; }
.di-add-card i { font-size: 28px; }
.di-add-card span { font-size: 13px; font-weight: 500; }
/* Gallery View */
.di-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
}
.di-gallery .di-card .card-img { height: 220px; }
.di-gallery .di-card .card-body { padding: 10px; }
/* List View */
.di-list { display: flex; flex-direction: column; gap: 4px; }
.di-list-item {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
background: #fff;
border-radius: 8px;
border: 1px solid var(--di-border);
cursor: pointer;
transition: all .12s;
}
.di-list-item:hover { background: #f8fafc; box-shadow: var(--di-shadow); }
.di-list-item .li-thumb {
width: 48px;
height: 48px;
border-radius: 6px;
object-fit: cover;
background: #f1f5f9;
flex-shrink: 0;
}
.di-list-item .li-thumb-empty {
width: 48px;
height: 48px;
border-radius: 6px;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 18px;
flex-shrink: 0;
}
.di-list-item .li-info { flex: 1; min-width: 0; }
.di-list-item .li-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.di-list-item .li-memo { font-size: 12px; color: var(--di-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.di-list-item .li-tags { display: flex; gap: 4px; flex-shrink: 0; }
.di-list-item .li-date { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
/* Modal */
.di-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.4);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.di-modal {
background: #fff;
border-radius: 14px;
width: 680px;
max-width: 95vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,.2);
}
.di-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--di-border);
}
.di-modal-header h3 { font-size: 16px; font-weight: 700; color: var(--di-text); }
.di-modal-body { padding: 20px; }
.di-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 14px 20px;
border-top: 1px solid var(--di-border);
}
.di-field { margin-bottom: 16px; }
.di-field label {
display: block;
font-size: 12.5px;
font-weight: 600;
color: var(--di-text);
margin-bottom: 5px;
}
.di-field input[type="text"],
.di-field textarea,
.di-field select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--di-border);
border-radius: 8px;
font-size: 13px;
transition: all .15s;
background: #fff;
}
.di-field input:focus, .di-field textarea:focus, .di-field select:focus {
outline: none;
border-color: var(--di-blue);
box-shadow: 0 0 0 3px rgba(59,130,246,.1);
}
.di-field textarea { min-height: 80px; resize: vertical; }
/* Image Drop Zone */
.di-drop-zone {
border: 2px dashed var(--di-border);
border-radius: 10px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all .2s;
background: #fafbfc;
}
.di-drop-zone:hover, .di-drop-zone.dragover {
border-color: var(--di-blue);
background: #eff6ff;
}
.di-drop-zone img {
max-width: 100%;
max-height: 250px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 10px;
}
/* Before/After comparison */
.di-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.di-comparison .comp-side {
border: 2px dashed var(--di-border);
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all .2s;
background: #fafbfc;
min-height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.di-comparison .comp-side:hover { border-color: var(--di-blue); background: #eff6ff; }
.di-comparison .comp-side img { max-width: 100%; max-height: 200px; object-fit: contain; border-radius: 8px; }
.di-comparison .comp-label {
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 4px;
margin-bottom: 8px;
}
.di-comparison .comp-before .comp-label { background: #fef2f2; color: #dc2626; }
.di-comparison .comp-after .comp-label { background: #ecfdf5; color: #059669; }
/* CRAP Checklist */
.di-crap-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.di-crap-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--di-border);
cursor: pointer;
transition: all .12s;
font-size: 12.5px;
}
.di-crap-item:hover { background: #f8fafc; }
.di-crap-item.pass { background: #ecfdf5; border-color: #a7f3d0; }
.di-crap-item.fail { background: #fef2f2; border-color: #fecaca; }
.di-crap-item.warn { background: #fffbeb; border-color: #fde68a; }
/* Rating Stars */
.di-stars {
display: flex;
gap: 2px;
}
.di-star {
font-size: 20px;
cursor: pointer;
color: #e2e8f0;
transition: color .1s;
}
.di-star.filled { color: #f59e0b; }
.di-star:hover { color: #fbbf24; }
/* Help Modal */
.di-help-modal { width: 740px; }
.di-help-modal .di-modal-body { padding: 0; }
.di-help-tabs {
display: flex;
border-bottom: 1px solid var(--di-border);
padding: 0 20px;
gap: 0;
overflow-x: auto;
}
.di-help-tab {
padding: 12px 16px;
font-size: 13px;
color: var(--di-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
white-space: nowrap;
background: none;
border-top: none; border-left: none; border-right: none;
transition: all .15s;
}
.di-help-tab:hover { color: var(--di-text); }
.di-help-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
.di-help-content { padding: 24px; max-height: 60vh; overflow-y: auto; }
.di-help-content h4 { font-size: 15px; font-weight: 700; color: var(--di-text); margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }
.di-help-content h4 i { font-size: 18px; color: var(--di-blue); }
.di-help-content p { font-size: 13px; color: var(--di-text-secondary); line-height: 1.7; margin: 0 0 16px; }
.di-help-section { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #f1f5f9; }
.di-help-section:last-child { border-bottom: none; margin-bottom: 0; }
.di-help-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
.di-help-item {
display: flex;
gap: 10px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #f1f5f9;
}
.di-help-item .h-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.di-help-item .h-body { flex: 1; }
.di-help-item .h-title { font-size: 13px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
.di-help-item .h-desc { font-size: 11.5px; color: var(--di-text-secondary); line-height: 1.5; }
.di-help-kbd {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 11.5px;
font-family: monospace;
color: var(--di-text);
margin: 2px;
}
.di-help-step {
display: flex;
gap: 12px;
margin-bottom: 12px;
align-items: flex-start;
}
.di-help-step .step-num {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--di-blue);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.di-help-step .step-body { flex: 1; padding-top: 3px; }
.di-help-step .step-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
.di-help-step .step-desc { font-size: 12.5px; color: var(--di-text-secondary); line-height: 1.6; }
/* 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>
</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>
<button class="di-btn primary" @click="openNewCardModal('reference')">
<i class="ri-add-line"></i> 번째 카드 추가
</button>
</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="openEditCardModal(card)">
<!-- Pin -->
<div class="card-pin" :class="card.pinned && 'pinned'"
@click.stop="togglePin(card)" x-text="card.pinned ? '&#x1F4CC;' : '&#x1F4CC;'"></div>
<!-- Image -->
<template x-if="card.type === 'comparison'">
<div style="display: grid; grid-template-columns: 1fr 1fr; height: 180px;">
<template x-if="card.beforeImage">
<img :src="card.beforeImage" style="width: 100%; height: 180px; object-fit: cover; border-right: 1px solid var(--di-border);">
</template>
<template x-if="!card.beforeImage">
<div class="card-img-placeholder" style="border-right: 1px solid var(--di-border);"><i class="ri-arrow-left-line"></i></div>
</template>
<template x-if="card.afterImage">
<img :src="card.afterImage" style="width: 100%; height: 180px; object-fit: cover;">
</template>
<template x-if="!card.afterImage">
<div class="card-img-placeholder"><i class="ri-arrow-right-line"></i></div>
</template>
</div>
</template>
<template x-if="card.type !== 'comparison' && card.image">
<img :src="card.image" class="card-img">
</template>
<template x-if="card.type !== 'comparison' && !card.image">
<div class="card-img-placeholder">
<i :class="getTypeIcon(card.type)"></i>
</div>
</template>
<!-- Body -->
<div class="card-body">
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type">
<span x-text="getTypeLabel(card.type)"></span>
</span>
<div class="card-title" x-text="card.title || '(제목 없음)'"></div>
<div class="card-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
<div class="card-tags" x-show="card.tags && card.tags.length > 0">
<template x-for="tag in (card.tags || []).slice(0, 4)" :key="tag">
<span class="card-tag" x-text="tag"></span>
</template>
<template x-if="(card.tags || []).length > 4">
<span class="card-tag" x-text="'+' + ((card.tags || []).length - 4)"></span>
</template>
</div>
<div class="card-footer">
<div class="card-rating" x-text="getRatingStars(card.rating || 0)"></div>
<div class="card-date" x-text="formatDate(card.createdAt)"></div>
</div>
</div>
</div>
</template>
<!-- Add New Card -->
<div class="di-add-card" @click="openNewCardModal('reference')">
<i class="ri-add-circle-line"></i>
<span> 카드 추가</span>
</div>
</div>
</template>
<!-- Gallery View -->
<template x-if="viewMode === 'gallery' && filteredCards.length > 0">
<div class="di-gallery">
<template x-for="card in filteredCards" :key="card.id">
<div class="di-card" @click="openEditCardModal(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="openEditCardModal(card)">
<template x-if="card.image">
<img :src="card.image" class="li-thumb">
</template>
<template x-if="!card.image">
<div class="li-thumb-empty"><i :class="getTypeIcon(card.type)"></i></div>
</template>
<div class="li-info">
<div class="li-title">
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type"
x-text="getTypeLabel(card.type)" style="font-size: 10px; margin-right: 6px;"></span>
<span x-text="card.title || '(제목 없음)'"></span>
</div>
<div class="li-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
</div>
<div class="li-tags">
<template x-for="tag in (card.tags || []).slice(0, 3)" :key="tag">
<span class="card-tag" x-text="tag"></span>
</template>
</div>
<div class="card-rating" x-text="getRatingStars(card.rating || 0)" style="flex-shrink: 0;"></div>
<div class="li-date" x-text="formatDate(card.createdAt)"></div>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="di-statusbar">
<span class="stat"><i class="ri-sticky-note-line"></i> 카드 <strong x-text="currentProject.cards?.length || 0"></strong></span>
<span class="stat"><i class="ri-price-tag-3-line"></i> 태그 <strong x-text="allTags.length"></strong></span>
<span class="stat"><i class="ri-time-line"></i> 마지막 저장 <span x-text="lastSaved || '-'"></span></span>
<div style="flex:1;"></div>
<span class="di-paste-hint" style="position: static; opacity: .6; background: transparent; color: var(--di-text-secondary); padding: 0;">
<kbd>Ctrl+V</kbd> 붙여넣기
</span>
</div>
<!-- ===== Card Edit/Create Modal ===== -->
<template x-if="showCardModal">
<div class="di-modal-overlay" @click.self="closeCardModal()">
<div class="di-modal">
<div class="di-modal-header">
<h3 x-text="editingCard.id ? '카드 편집' : '새 카드'"></h3>
<div style="display: flex; gap: 6px; align-items: center;">
<template x-if="editingCard.id">
<button class="di-btn sm" style="color: var(--di-red); border-color: #fecaca;" @click="deleteCard(editingCard.id)">
<i class="ri-delete-bin-line"></i> 삭제
</button>
</template>
<button class="di-btn ghost" @click="closeCardModal()"><i class="ri-close-line" style="font-size: 18px;"></i></button>
</div>
</div>
<div class="di-modal-body">
<!-- Card Type -->
<div class="di-field">
<label>카드 유형</label>
<div style="display: flex; gap: 6px;">
<template x-for="type in cardTypes" :key="type.code">
<button class="di-btn sm" :class="editingCard.type === type.code && 'primary'"
@click="editingCard.type = type.code">
<span x-text="type.icon + ' ' + type.label"></span>
</button>
</template>
</div>
</div>
<!-- Title -->
<div class="di-field">
<label>제목</label>
<input type="text" x-model="editingCard.title" placeholder="인사이트 제목">
</div>
<!-- Image (Reference / Analysis / Pattern) -->
<template x-if="editingCard.type !== 'comparison'">
<div class="di-field">
<label>이미지</label>
<div class="di-drop-zone"
@click="$refs.fileInput.click()"
@drop.prevent="handleDrop($event)"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<template x-if="editingCard.image">
<img :src="editingCard.image">
</template>
<template x-if="!editingCard.image">
<div>
<i class="ri-image-add-line" style="font-size: 32px; color: #94a3b8; display: block; margin-bottom: 8px;"></i>
<span style="font-size: 13px; color: var(--di-text-secondary);">클릭 또는 드래그로 이미지 추가</span>
</div>
</template>
</div>
<input type="file" x-ref="fileInput" accept="image/*" style="display: none;" @change="handleFileSelect($event)">
</div>
</template>
<!-- Before/After Images (Comparison) -->
<template x-if="editingCard.type === 'comparison'">
<div class="di-field">
<label>Before / After 이미지</label>
<div class="di-comparison">
<div class="comp-side comp-before"
@click="$refs.beforeInput.click()"
@drop.prevent="handleCompDrop($event, 'before')"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<span class="comp-label">Before</span>
<template x-if="editingCard.beforeImage">
<img :src="editingCard.beforeImage">
</template>
<template x-if="!editingCard.beforeImage">
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
</template>
</div>
<div class="comp-side comp-after"
@click="$refs.afterInput.click()"
@drop.prevent="handleCompDrop($event, 'after')"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<span class="comp-label">After</span>
<template x-if="editingCard.afterImage">
<img :src="editingCard.afterImage">
</template>
<template x-if="!editingCard.afterImage">
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
</template>
</div>
</div>
<input type="file" x-ref="beforeInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'before')">
<input type="file" x-ref="afterInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'after')">
</div>
</template>
<!-- Memo (Reference) -->
<template x-if="editingCard.type === 'reference'">
<div>
<div class="di-field">
<label>인사이트 메모</label>
<textarea x-model="editingCard.memo" placeholder="이 화면/패턴이 왜 좋은가? (또는 나쁜가?)"></textarea>
</div>
<div class="di-field">
<label>출처</label>
<input type="text" x-model="editingCard.source" placeholder="URL, 앱 이름, 서비스명 등">
</div>
</div>
</template>
<!-- Analysis Fields -->
<template x-if="editingCard.type === 'analysis'">
<div>
<div class="di-field">
<label>CRAP 디자인 원칙 체크</label>
<div class="di-crap-grid">
<template x-for="p in designPrinciples" :key="p.key">
<div class="di-crap-item"
:class="getPrincipleStatus(p.key)"
@click="cyclePrinciple(p.key)">
<span x-text="p.icon" style="font-size: 16px;"></span>
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 12px;" x-text="p.label"></div>
<div style="font-size: 10.5px; color: var(--di-text-secondary);" x-text="p.desc"></div>
</div>
<span x-text="getPrincipleIcon(p.key)" style="font-size: 14px;"></span>
</div>
</template>
</div>
</div>
<div class="di-field">
<label>개선 제안</label>
<textarea x-model="editingCard.suggestion" placeholder="어떻게 개선할 수 있는가?"></textarea>
</div>
<div class="di-field">
<label>심각도</label>
<div style="display: flex; gap: 6px;">
<button class="di-btn sm" :class="editingCard.severity === 'info' && 'primary'" @click="editingCard.severity = 'info'"> 정보</button>
<button class="di-btn sm" :class="editingCard.severity === 'warning' && 'primary'" @click="editingCard.severity = 'warning'" style="--di-blue: #f59e0b;">⚠️ 경고</button>
<button class="di-btn sm" :class="editingCard.severity === 'critical' && 'primary'" @click="editingCard.severity = 'critical'" style="--di-blue: #ef4444;">🔴 심각</button>
</div>
</div>
</div>
</template>
<!-- Pattern Fields -->
<template x-if="editingCard.type === 'pattern'">
<div>
<div class="di-field">
<label>사용처</label>
<input type="text" x-model="editingCard.usedInText" placeholder="수주 목록, 거래처 목록 (콤마 구분)">
</div>
<div class="di-field">
<label>구성 요소</label>
<div style="display: flex; flex-direction: column; gap: 4px;">
<template x-for="(comp, ci) in (editingCard.components || [])" :key="ci">
<div style="display: flex; gap: 6px; align-items: center;">
<input type="checkbox" x-model="comp.required">
<input type="text" x-model="comp.name" style="flex: 1; padding: 5px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12.5px;" placeholder="구성 요소명">
<button class="di-btn ghost sm" @click="editingCard.components.splice(ci, 1)"><i class="ri-close-line"></i></button>
</div>
</template>
<button class="di-btn sm" @click="editingCard.components = [...(editingCard.components || []), {name: '', required: true}]">
<i class="ri-add-line"></i> 구성 요소 추가
</button>
</div>
</div>
<div class="di-field">
<label>사용 가이드라인</label>
<textarea x-model="editingCard.guidelines" placeholder="이 패턴을 사용할 때 주의사항이나 가이드"></textarea>
</div>
</div>
</template>
<!-- Comparison Fields -->
<template x-if="editingCard.type === 'comparison'">
<div>
<div class="di-field">
<label>변경 포인트</label>
<textarea x-model="editingCard.changesText" placeholder="1. 탭 구조 → 섹션 접기/펼치기 변경&#10;2. 좌우 2컬럼 → 단일 컬럼" style="min-height: 100px;"></textarea>
</div>
<div class="di-field">
<label>개선 효과</label>
<input type="text" x-model="editingCard.effect" placeholder="스크롤 40% 감소, 작업 완료 시간 단축">
</div>
</div>
</template>
<!-- Common Fields -->
<div class="di-field">
<label>카테고리</label>
<select x-model="editingCard.category">
<template x-for="cat in categories" :key="cat.code">
<option :value="cat.code" x-text="cat.icon + ' ' + cat.label"></option>
</template>
</select>
</div>
<div class="di-field">
<label>태그 (콤마 구분)</label>
<input type="text" x-model="editingCard.tagsText" placeholder="대시보드, 카드, 레이아웃"
@keydown.enter.prevent>
</div>
<div class="di-field">
<label>평점</label>
<div class="di-stars">
<template x-for="s in [1,2,3,4,5]" :key="s">
<span class="di-star" :class="s <= (editingCard.rating || 0) && 'filled'"
@click="editingCard.rating = editingCard.rating === s ? 0 : s"
x-text="s <= (editingCard.rating || 0) ? '\u2605' : '\u2606'"></span>
</template>
</div>
</div>
</div>
<div class="di-modal-footer">
<button class="di-btn" @click="closeCardModal()">취소</button>
<button class="di-btn primary" @click="saveCard()">
<i class="ri-check-line"></i> <span x-text="editingCard.id ? '수정' : '추가'"></span>
</button>
</div>
</div>
</div>
</template>
<!-- ===== Projects Modal ===== -->
<template x-if="showProjectsModal">
<div class="di-modal-overlay" @click.self="showProjectsModal = false">
<div class="di-modal" style="width: 500px;">
<div class="di-modal-header">
<h3>프로젝트 관리</h3>
<button class="di-btn ghost" @click="showProjectsModal = false"><i class="ri-close-line" style="font-size: 18px;"></i></button>
</div>
<div class="di-modal-body">
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<input type="text" x-model="newProjectTitle" placeholder="새 프로젝트 이름"
style="flex: 1; padding: 8px 12px; border: 1px solid var(--di-border); border-radius: 8px; font-size: 13px;"
@keydown.enter="createProject()">
<button class="di-btn primary" @click="createProject()"><i class="ri-add-line"></i> 생성</button>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
<template x-for="proj in projects" :key="proj.id">
<div class="di-proj-item" :class="proj.id === currentProject.id && 'active'" @click="switchProject(proj.id)">
<i class="ri-folder-line" style="font-size: 16px; color: var(--di-blue);"></i>
<span class="proj-title" x-text="proj.title"></span>
<span class="proj-count" x-text="(proj.cards?.length || 0) + '개'"></span>
<span class="proj-date" x-text="formatDate(proj.createdAt)"></span>
<button class="di-btn ghost sm" @click.stop="deleteProject(proj.id)" x-show="projects.length > 1"
title="삭제"><i class="ri-delete-bin-line" style="color: var(--di-red);"></i></button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<!-- ===== 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 ERP 화면을 만들 참고할 <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 ERP 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',
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 ERP 디자인 연구',
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;
},
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);
},
};
}
</script>
@endpush