feat: 대시보드 메뉴 및 경영성과 대시보드 카드 페이지 추가

- 성공 사례 아래 구분선과 대시보드 메뉴
- /dashboard: AI 탐색과 동일한 카드·검색 레이아웃
- 첫 카드 경영성과 대시보드 → /dashboard/business-performance

Made-with: Cursor
This commit is contained in:
2026-04-13 10:38:56 +09:00
parent 6126291257
commit 485bd31798
5 changed files with 151 additions and 1 deletions

View File

@@ -19,7 +19,7 @@
- **학습센터 뷰어** (`/learning`): 강의 검색/필터 + 카드 목록 (일반 사용자)
- **관리자** (`/admin`): 강의 등록(YouTube/PPT/웹 링크 등), 썸네일·AI 추가 등 통합 관리
- **강의 상세**: 카드 클릭 시 유튜브 재생 또는 PPT 뷰어
- **기타 메뉴**: 채팅(`/chat`), AI(`/ai-explore`), **프롬프트 라이브러리**(`/ai-explore/prompts`), **AI 성공 사례**(`/ai-cases`), AX 과제 신청(`/ax-apply`)
- **기타 메뉴**: 채팅(`/chat`), AI(`/ai-explore`), **프롬프트 라이브러리**(`/ai-explore/prompts`), **AI 성공 사례**(`/ai-cases`), **대시보드**(`/dashboard`, 성공 사례 아래 메뉴), AX 과제 신청(`/ax-apply`)
- **관리자 이벤트 로그**: `http://localhost:8030/admin/thumbnail-events?token={ADMIN_TOKEN}`
### 접속이 안 될 때 (트러블슈팅)
@@ -41,6 +41,7 @@
- 학습센터 UI (좌측 메뉴 + 상단 헤더 + 강의 카드 레이아웃)
- **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동
- **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
- 검색/필터/페이지네이션
- 검색어(`q`) 기반 제목/설명/태그 필터
@@ -93,6 +94,8 @@ ai_platform/
│ ├─ learning-admin.ejs # 학습센터 관리 (업로드·삭제·썸네일)
│ ├─ chat.ejs # 채팅
│ ├─ ai-explore.ejs # AI 탐색 (전체 너비, 프롬프트 카드·검색)
│ ├─ dashboard.ejs # 대시보드 목록(카드·검색)
│ ├─ dashboard-business-performance.ejs # 경영성과 대시보드(진입·안내)
│ ├─ ai-prompts.ejs # 프롬프트 라이브러리 (카드·미리보기·복사)
│ ├─ ai-cases.ejs # AI 성공 사례 목록(카드)
│ ├─ ai-case-detail.ejs # AI 성공 사례 상세(마크다운)

View File

@@ -1167,6 +1167,20 @@ pageRouter.get("/ai-explore/task-checklist", (req, res) =>
opsState: normalizeOpsState(),
})
);
pageRouter.get("/dashboard", (req, res) =>
res.render("dashboard", {
activeMenu: "dashboard",
adminMode: res.locals.adminMode,
opsUserEmail: !!res.locals.opsUserEmail,
})
);
pageRouter.get("/dashboard/business-performance", (req, res) =>
res.render("dashboard-business-performance", {
activeMenu: "dashboard",
adminMode: res.locals.adminMode,
opsUserEmail: !!res.locals.opsUserEmail,
})
);
const AI_SUCCESS_ADMIN_LIST_PAGE_SIZE = 5;
pageRouter.get("/ai-cases/write", (req, res) => {

View File

@@ -0,0 +1,34 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%- include('partials/favicon') %>
<title>경영성과 대시보드 - XAVIS</title>
<link rel="stylesheet" href="/public/styles.css" />
</head>
<body>
<div class="app-shell">
<%- include('partials/nav', {
activeMenu: 'dashboard',
adminMode: typeof adminMode !== 'undefined' ? adminMode : false,
}) %>
<div class="content-area">
<header class="topbar">
<h1>경영성과 대시보드</h1>
</header>
<main class="container">
<p class="breadcrumb" style="margin-bottom: 16px">
<a href="/dashboard">← 대시보드 목록으로</a>
</p>
<section class="panel">
<p class="subtitle">
이 화면은 향후 경영·성과 지표 연동 및 위젯 구성을 위한 진입점입니다. 필요한 데이터 소스와 차트
요구사항이 정해지면 이어서 구현할 수 있습니다.
</p>
</section>
</main>
</div>
</div>
</body>
</html>

97
views/dashboard.ejs Normal file
View File

@@ -0,0 +1,97 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%- include('partials/favicon') %>
<title>대시보드 - XAVIS</title>
<link rel="stylesheet" href="/public/styles.css" />
</head>
<body class="ai-explore-page dashboard-page">
<div class="app-shell">
<%- include('partials/nav', {
activeMenu: 'dashboard',
adminMode: typeof adminMode !== 'undefined' ? adminMode : false,
}) %>
<div class="content-area">
<header class="topbar">
<h1>대시보드</h1>
</header>
<main class="container container-ai-full">
<section class="panel">
<p class="subtitle">
경영·업무 지표를 한눈에 보고, 필요한 대시보드를 선택해 활용하세요.
</p>
<form class="search-bar-wrap" id="dashboardSearchForm" role="search">
<input
type="search"
id="dashboardSearch"
placeholder="제목, 설명으로 검색하세요"
class="search-input"
autocomplete="off"
aria-label="대시보드 제목·설명 검색"
/>
</form>
</section>
<section class="panel">
<h2>대시보드</h2>
<div class="ai-card-grid">
<a href="/dashboard/business-performance" class="ai-card ai-card-link">
<div class="ai-card-header">
<span class="status-chip public">공개중</span>
</div>
<h3>경영성과 대시보드</h3>
<p>
조직·팀 단위 성과와 핵심 지표를 시각화하고, 의사결정에 활용할 수 있는 경영 성과 화면입니다.
</p>
<div class="tag-row">
<span class="tag-chip">#경영</span>
<span class="tag-chip">#성과</span>
<span class="tag-chip">#KPI</span>
</div>
</a>
</div>
</section>
</main>
</div>
</div>
<script>
(function () {
var form = document.getElementById("dashboardSearchForm");
var input = document.getElementById("dashboardSearch");
var grid = document.querySelector(".ai-card-grid");
if (!form || !input || !grid) return;
var cards = grid.querySelectorAll(".ai-card");
function cardTitleDescriptionText(el) {
var parts = [];
var h3 = el.querySelector("h3");
if (h3) parts.push(h3.textContent || "");
el.querySelectorAll("p").forEach(function (p) {
if (!p.closest(".tag-row")) parts.push(p.textContent || "");
});
return parts.join(" ").replace(/\s+/g, " ").trim().toLowerCase();
}
function applyFilter() {
var q = (input.value || "").trim().toLowerCase();
cards.forEach(function (el) {
var text = cardTitleDescriptionText(el);
var show = !q || text.indexOf(q) !== -1;
el.hidden = !show;
el.setAttribute("aria-hidden", show ? "false" : "true");
});
}
input.addEventListener("input", applyFilter);
input.addEventListener("search", applyFilter);
form.addEventListener("submit", function (e) {
e.preventDefault();
applyFilter();
});
})();
</script>
</body>
</html>

View File

@@ -28,6 +28,8 @@
<a href="/learning" class="nav-item <%= activeMenu === 'learning' ? 'active' : '' %>">학습센터</a>
<a href="/ax-apply" class="nav-item <%= activeMenu === 'ax-apply' ? 'active' : '' %>">과제신청</a>
<a href="/ai-cases" class="nav-item <%= activeMenu === 'ai-cases' ? 'active' : '' %>">성공사례</a>
<div class="nav-separator"></div>
<a href="/dashboard" class="nav-item <%= activeMenu === 'dashboard' ? 'active' : '' %>">대시보드</a>
<div class="nav-footer">
<% var _opsLoggedIn = typeof opsUserEmail !== 'undefined' && opsUserEmail; %>
<% var _admin = typeof adminMode !== 'undefined' && adminMode; %>