feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
308
views/ai-use-case-submission-detail.ejs
Normal file
308
views/ai-use-case-submission-detail.ejs
Normal file
@@ -0,0 +1,308 @@
|
||||
<!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><%= title %> - AI 활용 사례 - XAVIS</title>
|
||||
<link rel="stylesheet" href="/public/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<%- include('partials/nav', { activeMenu: 'ai-cases' }) %>
|
||||
<div class="content-area">
|
||||
<main class="viewer-wrap ai-case-viewer">
|
||||
<a href="/ai-cases" class="back-link">← AI 활용 사례로 돌아가기</a>
|
||||
<% if (typeof updatedOk !== "undefined" && updatedOk) { %>
|
||||
<p class="form-message" style="margin: 0 0 12px; color: #047857">수정이 저장되었습니다.</p>
|
||||
<% } %>
|
||||
<h1><%= title %></h1>
|
||||
<p class="description">일반 제출 사례 · <%= author %><% if (submitterEmail) { %> (<%= submitterEmail %>)<% } %></p>
|
||||
<div class="ppt-tools ai-case-ppt-tools">
|
||||
<div class="ai-case-tools-meta">
|
||||
<span>조회 <b id="submissionViewCount"><%= typeof viewCount !== "undefined" ? viewCount : 0 %></b></span>
|
||||
<span class="ai-case-tool-sep" aria-hidden="true">|</span>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-lib-like ai-case-submission-like<%= typeof myLike !== "undefined" && myLike ? " is-liked" : "" %>"
|
||||
id="submissionLikeBtn"
|
||||
title="<%= typeof opsUserEmail !== "undefined" && opsUserEmail ? "좋아요" : "로그인 후 사용" %>"
|
||||
<% if (typeof opsUserEmail === "undefined" || !opsUserEmail) { %>disabled<% } %>
|
||||
>
|
||||
<span class="prompt-lib-like-icon" aria-hidden="true">♥</span>
|
||||
<span id="submissionLikeCount"><%= typeof likeCount !== "undefined" ? likeCount : 0 %></span>
|
||||
</button>
|
||||
<span class="ai-case-tool-sep" aria-hidden="true">|</span>
|
||||
<span><%= typeof department !== "undefined" ? department : "일반 제출" %> · <%= author %><% if (publishedAt) { %> · <%= publishedAt %><% } %></span>
|
||||
<% if (typeof canEditSubmission !== "undefined" && canEditSubmission && typeof submissionId !== "undefined" && submissionId) { %>
|
||||
<span class="ai-case-tool-sep" aria-hidden="true">|</span>
|
||||
<a href="/ai-cases/compose?edit=<%= encodeURIComponent(submissionId) %>" class="ai-case-inline-link">수정하기</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% if ((tags || []).length) { %>
|
||||
<div class="tag-row ai-case-tag-row">
|
||||
<% (tags || []).forEach((oneTag) => { %>
|
||||
<span class="tag-chip">#<%= oneTag %></span>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
<section class="slide-list">
|
||||
<article class="slide-card">
|
||||
<header>
|
||||
<h2>1. Situation (배경)</h2>
|
||||
</header>
|
||||
<div class="chat-md-body success-detail-body-in-card success-submission-html"><%- situationHtml %></div>
|
||||
</article>
|
||||
<article class="slide-card">
|
||||
<header>
|
||||
<h2>2. Task (과제/목표)</h2>
|
||||
</header>
|
||||
<div class="chat-md-body success-detail-body-in-card success-submission-html"><%- taskHtml %></div>
|
||||
</article>
|
||||
<article class="slide-card">
|
||||
<header>
|
||||
<h2>3. Action (행동)</h2>
|
||||
</header>
|
||||
<div class="chat-md-body success-detail-body-in-card success-submission-html"><%- actionHtml %></div>
|
||||
</article>
|
||||
<article class="slide-card">
|
||||
<header>
|
||||
<h2>4. Result (결과)</h2>
|
||||
</header>
|
||||
<div class="chat-md-body success-detail-body-in-card success-submission-html"><%- resultHtml %></div>
|
||||
</article>
|
||||
</section>
|
||||
<% if (typeof thumbnailImages !== "undefined" && thumbnailImages && thumbnailImages.length) { %>
|
||||
<section class="success-submission-cover-section" id="submissionScreenshots">
|
||||
<div class="success-submission-cover-header">
|
||||
<div>
|
||||
<h2 class="success-submission-cover-title">실행 화면</h2>
|
||||
<p class="success-submission-cover-hint">이미지를 클릭하면 원본 크기로 자세히 볼 수 있습니다.</p>
|
||||
</div>
|
||||
<% if (thumbnailImages.length > 1) { %>
|
||||
<div class="slide-layout-toggle" role="group" aria-label="실행 화면 단 수">
|
||||
<span class="slide-layout-toggle-label">보기</span>
|
||||
<button type="button" class="slide-layout-btn js-screenshot-cols-btn" data-cols="1" aria-pressed="false">1단</button>
|
||||
<button type="button" class="slide-layout-btn js-screenshot-cols-btn is-active" data-cols="2" aria-pressed="true">2단</button>
|
||||
<button type="button" class="slide-layout-btn js-screenshot-cols-btn" data-cols="4" aria-pressed="false">4단</button>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="success-submission-cover-grid success-submission-cover-grid--cols-2" id="submissionScreenshotGrid">
|
||||
<% thumbnailImages.forEach(function (t, idx) { var tfn = (t.originalName || "실행 화면").replace(/[<>"]/g, ""); %>
|
||||
<figure class="success-submission-cover-item">
|
||||
<button
|
||||
type="button"
|
||||
class="success-submission-screenshot-btn js-screenshot-open"
|
||||
data-screenshot-index="<%= idx %>"
|
||||
aria-label="<%= tfn %> 크게 보기"
|
||||
>
|
||||
<img src="<%= t.relativePath %>" alt="<%= tfn %>" loading="lazy" decoding="async" />
|
||||
</button>
|
||||
<figcaption class="success-submission-screenshot-caption"><%= tfn %></figcaption>
|
||||
</figure>
|
||||
<% }); %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else if (coverImageUrl) { %>
|
||||
<section class="success-submission-cover-section success-submission-cover--single" id="submissionScreenshots">
|
||||
<h2 class="success-submission-cover-title">실행 화면</h2>
|
||||
<p class="success-submission-cover-hint">이미지를 클릭하면 원본 크기로 자세히 볼 수 있습니다.</p>
|
||||
<figure class="success-submission-cover-item">
|
||||
<button type="button" class="success-submission-screenshot-btn js-screenshot-open" data-screenshot-index="0" aria-label="실행 화면 크게 보기">
|
||||
<img src="<%= coverImageUrl %>" alt="실행 화면" loading="lazy" decoding="async" />
|
||||
</button>
|
||||
</figure>
|
||||
</section>
|
||||
<% } %>
|
||||
<% if (attachments && attachments.length) { %>
|
||||
<section class="slide-card" style="margin-top: 12px">
|
||||
<header>
|
||||
<h2>첨부 파일</h2>
|
||||
</header>
|
||||
<ul class="success-submission-attach-list">
|
||||
<% attachments.forEach((a) => { %>
|
||||
<li>
|
||||
<a href="<%= a.relativePath %>" target="_blank" rel="noopener noreferrer"><%= a.originalName || "파일" %></a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</section>
|
||||
<% } %>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-case-lightbox" id="aiCaseLightbox" hidden role="dialog" aria-modal="true" aria-labelledby="aiCaseLightboxCaption">
|
||||
<button type="button" class="ai-case-lightbox__close" id="aiCaseLightboxClose" aria-label="닫기">×</button>
|
||||
<button type="button" class="ai-case-lightbox__nav ai-case-lightbox__nav--prev" id="aiCaseLightboxPrev" aria-label="이전 이미지">‹</button>
|
||||
<button type="button" class="ai-case-lightbox__nav ai-case-lightbox__nav--next" id="aiCaseLightboxNext" aria-label="다음 이미지">›</button>
|
||||
<div class="ai-case-lightbox__dialog">
|
||||
<img class="ai-case-lightbox__img" id="aiCaseLightboxImg" alt="" />
|
||||
<p class="ai-case-lightbox__caption" id="aiCaseLightboxCaption"></p>
|
||||
<p class="ai-case-lightbox__meta" id="aiCaseLightboxMeta"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var screenshotSection = document.getElementById("submissionScreenshots");
|
||||
if (screenshotSection) {
|
||||
var openButtons = screenshotSection.querySelectorAll(".js-screenshot-open");
|
||||
var lightbox = document.getElementById("aiCaseLightbox");
|
||||
var lightboxImg = document.getElementById("aiCaseLightboxImg");
|
||||
var lightboxCaption = document.getElementById("aiCaseLightboxCaption");
|
||||
var lightboxMeta = document.getElementById("aiCaseLightboxMeta");
|
||||
var lightboxClose = document.getElementById("aiCaseLightboxClose");
|
||||
var lightboxPrev = document.getElementById("aiCaseLightboxPrev");
|
||||
var lightboxNext = document.getElementById("aiCaseLightboxNext");
|
||||
var slides = [];
|
||||
var activeIndex = 0;
|
||||
var lastFocus = null;
|
||||
|
||||
openButtons.forEach(function (btn) {
|
||||
var img = btn.querySelector("img");
|
||||
if (!img) return;
|
||||
slides.push({
|
||||
src: img.getAttribute("src") || "",
|
||||
alt: img.getAttribute("alt") || "실행 화면",
|
||||
});
|
||||
});
|
||||
|
||||
var screenshotGrid = document.getElementById("submissionScreenshotGrid");
|
||||
var colButtons = screenshotSection.querySelectorAll(".js-screenshot-cols-btn");
|
||||
var colsStorageKey = "aiCaseScreenshotColumns";
|
||||
|
||||
function applyScreenshotCols(n) {
|
||||
if (!screenshotGrid) return;
|
||||
screenshotGrid.classList.remove(
|
||||
"success-submission-cover-grid--cols-1",
|
||||
"success-submission-cover-grid--cols-2",
|
||||
"success-submission-cover-grid--cols-4",
|
||||
);
|
||||
screenshotGrid.classList.add("success-submission-cover-grid--cols-" + n);
|
||||
colButtons.forEach(function (btn) {
|
||||
var on = btn.getAttribute("data-cols") === String(n);
|
||||
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
btn.classList.toggle("is-active", on);
|
||||
});
|
||||
try {
|
||||
localStorage.setItem(colsStorageKey, String(n));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (screenshotGrid && colButtons.length) {
|
||||
var savedCols = null;
|
||||
try {
|
||||
savedCols = localStorage.getItem(colsStorageKey);
|
||||
} catch (e) {}
|
||||
var initialCols = savedCols === "1" || savedCols === "2" || savedCols === "4" ? savedCols : "2";
|
||||
applyScreenshotCols(initialCols);
|
||||
colButtons.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var n = btn.getAttribute("data-cols");
|
||||
if (n === "1" || n === "2" || n === "4") applyScreenshotCols(n);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderLightbox(index) {
|
||||
if (!slides.length || !lightboxImg) return;
|
||||
activeIndex = (index + slides.length) % slides.length;
|
||||
var slide = slides[activeIndex];
|
||||
lightboxImg.src = slide.src;
|
||||
lightboxImg.alt = slide.alt;
|
||||
if (lightboxCaption) lightboxCaption.textContent = slide.alt;
|
||||
if (lightboxMeta) {
|
||||
lightboxMeta.textContent = slides.length > 1 ? activeIndex + 1 + " / " + slides.length : "";
|
||||
}
|
||||
if (lightboxPrev) lightboxPrev.hidden = slides.length <= 1;
|
||||
if (lightboxNext) lightboxNext.hidden = slides.length <= 1;
|
||||
}
|
||||
|
||||
function openLightbox(index) {
|
||||
if (!lightbox || !slides.length) return;
|
||||
lastFocus = document.activeElement;
|
||||
renderLightbox(index);
|
||||
lightbox.hidden = false;
|
||||
document.body.style.overflow = "hidden";
|
||||
if (lightboxClose) lightboxClose.focus();
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
if (!lightbox) return;
|
||||
lightbox.hidden = true;
|
||||
document.body.style.overflow = "";
|
||||
if (lightboxImg) lightboxImg.removeAttribute("src");
|
||||
if (lastFocus && typeof lastFocus.focus === "function") lastFocus.focus();
|
||||
}
|
||||
|
||||
openButtons.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var idx = parseInt(btn.getAttribute("data-screenshot-index") || "0", 10);
|
||||
openLightbox(Number.isFinite(idx) ? idx : 0);
|
||||
});
|
||||
});
|
||||
|
||||
if (lightboxClose) lightboxClose.addEventListener("click", closeLightbox);
|
||||
if (lightboxPrev) {
|
||||
lightboxPrev.addEventListener("click", function () {
|
||||
renderLightbox(activeIndex - 1);
|
||||
});
|
||||
}
|
||||
if (lightboxNext) {
|
||||
lightboxNext.addEventListener("click", function () {
|
||||
renderLightbox(activeIndex + 1);
|
||||
});
|
||||
}
|
||||
if (lightbox) {
|
||||
lightbox.addEventListener("click", function (ev) {
|
||||
if (ev.target === lightbox) closeLightbox();
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (!lightbox || lightbox.hidden) return;
|
||||
if (ev.key === "Escape") closeLightbox();
|
||||
if (ev.key === "ArrowLeft" && lightboxPrev && !lightboxPrev.hidden) renderLightbox(activeIndex - 1);
|
||||
if (ev.key === "ArrowRight" && lightboxNext && !lightboxNext.hidden) renderLightbox(activeIndex + 1);
|
||||
});
|
||||
}
|
||||
|
||||
var likeBtn = document.getElementById("submissionLikeBtn");
|
||||
var likeCountEl = document.getElementById("submissionLikeCount");
|
||||
var submissionId = <%- JSON.stringify(typeof submissionId !== "undefined" ? submissionId : null) %>;
|
||||
if (!likeBtn || !submissionId) return;
|
||||
likeBtn.addEventListener("click", function () {
|
||||
if (likeBtn.disabled) return;
|
||||
likeBtn.disabled = true;
|
||||
fetch("/api/ai-use-case-submissions/" + encodeURIComponent(submissionId) + "/like/toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "same-origin",
|
||||
body: "{}",
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json().then(function (j) {
|
||||
return { ok: r.ok, j: j };
|
||||
});
|
||||
})
|
||||
.then(function (o) {
|
||||
if (o.ok && o.j && o.j.ok) {
|
||||
if (likeCountEl) likeCountEl.textContent = String(o.j.likeCount || 0);
|
||||
likeBtn.classList.toggle("is-liked", !!o.j.liked);
|
||||
} else if (o.j && o.j.error) {
|
||||
alert(o.j.error);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
alert("좋아요 처리에 실패했습니다.");
|
||||
})
|
||||
.finally(function () {
|
||||
likeBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user