254 lines
12 KiB
Plaintext
254 lines
12 KiB
Plaintext
<!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>AI 성공 사례 관리 - XAVIS</title>
|
|
<link rel="stylesheet" href="/public/styles.css" />
|
|
<style>
|
|
.visually-hidden {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
.pdf-upload-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.pdf-upload-status {
|
|
font-size: 0.9em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<% if (typeof allStories === 'undefined') { allStories = []; } %>
|
|
<% if (typeof story === 'undefined') { story = null; } %>
|
|
<div class="app-shell">
|
|
<%- include('partials/nav', { activeMenu: 'ai-cases' }) %>
|
|
<div class="content-area">
|
|
<header class="topbar">
|
|
<h1>AI 성공 사례 관리</h1>
|
|
<a class="top-action-link" href="/ai-cases">목록(사용자 화면)</a>
|
|
</header>
|
|
<main class="container">
|
|
<section class="panel">
|
|
<p class="breadcrumb"><a href="/ai-cases">AI 성공 사례</a> > 관리자</p>
|
|
<p class="muted admin-hint">슬러그는 URL에 쓰이므로 영문·숫자·하이픈만 사용하세요. <strong>원문 PDF 경로</strong>(<code>/public/...</code>)가 있으면 상세는 PDF 페이지 이미지로 보여 주며, 이때 <strong>본문(Markdown)은 비워도 됩니다</strong>. PDF가 없을 때는 본문이 필수입니다.</p>
|
|
</section>
|
|
|
|
<% if (typeof listPagination !== 'undefined' && listPagination.totalCount > 0) { %>
|
|
<section class="panel">
|
|
<div class="section-head" style="margin-bottom:8px;align-items:baseline;">
|
|
<h2 style="margin:0;">등록된 사례</h2>
|
|
<% if (listPagination.totalPages > 1) { %>
|
|
<span class="muted" style="font-size:13px;">총 <%= listPagination.totalCount %>건 · <%= listPagination.page %>/<%= listPagination.totalPages %> 페이지</span>
|
|
<% } else { %>
|
|
<span class="muted" style="font-size:13px;">총 <%= listPagination.totalCount %>건</span>
|
|
<% } %>
|
|
</div>
|
|
<ul class="admin-story-list">
|
|
<% allStories.forEach(function(s) { %>
|
|
<li>
|
|
<% var _listPageQ = listPagination.page > 1 ? ('&page=' + listPagination.page) : ''; %>
|
|
<a href="/ai-cases/write?edit=<%= encodeURIComponent(s.slug) %><%= _listPageQ %>"><strong><%= s.title %></strong></a>
|
|
<span class="muted">/<%= s.slug %></span>
|
|
<button type="button" class="btn-ghost btn-sm js-delete-story" data-id="<%= s.id %>" data-title="<%= s.title %>">삭제</button>
|
|
</li>
|
|
<% }) %>
|
|
</ul>
|
|
<% if (listPagination.totalPages > 1) { %>
|
|
<nav class="admin-story-list-pagination" aria-label="등록된 사례 페이지">
|
|
<% var _pq = editSlug ? ('edit=' + encodeURIComponent(editSlug) + '&') : ''; %>
|
|
<% if (listPagination.hasPrev) { %>
|
|
<a class="btn-ghost btn-sm" href="/ai-cases/write?<%= _pq %>page=<%= listPagination.page - 1 %>">이전</a>
|
|
<% } else { %>
|
|
<span class="btn-ghost btn-sm" style="opacity:0.45;pointer-events:none;">이전</span>
|
|
<% } %>
|
|
<span class="muted" style="font-size:13px;padding:0 8px;"><%= listPagination.page %> / <%= listPagination.totalPages %></span>
|
|
<% if (listPagination.hasNext) { %>
|
|
<a class="btn-ghost btn-sm" href="/ai-cases/write?<%= _pq %>page=<%= listPagination.page + 1 %>">다음</a>
|
|
<% } else { %>
|
|
<span class="btn-ghost btn-sm" style="opacity:0.45;pointer-events:none;">다음</span>
|
|
<% } %>
|
|
</nav>
|
|
<% } %>
|
|
</section>
|
|
<% } %>
|
|
|
|
<section class="panel">
|
|
<h2><%= story ? '사례 수정' : '새 사례 등록' %></h2>
|
|
<form id="successStoryForm" class="form-grid">
|
|
<input type="hidden" id="storyId" value="<%= story ? story.id : '' %>" />
|
|
<label class="full">
|
|
슬러그 (URL, 영문·숫자·하이픈)
|
|
<input type="text" id="slug" name="slug" required placeholder="예: jojung-sook-hr-claude-cowork" value="<%= story ? story.slug : '' %>" />
|
|
</label>
|
|
<label class="full">
|
|
제목
|
|
<input type="text" id="title" name="title" required placeholder="제목" value="<%= story ? story.title : '' %>" />
|
|
</label>
|
|
<label class="full">
|
|
한 줄 요약 (카드에 표시)
|
|
<input type="text" id="excerpt" name="excerpt" placeholder="카드용 짧은 설명" value="<%= story ? story.excerpt : '' %>" />
|
|
</label>
|
|
<label>
|
|
부서
|
|
<input type="text" id="department" name="department" placeholder="예: 인사총무팀" value="<%= story ? story.department : '' %>" />
|
|
</label>
|
|
<label>
|
|
작성자
|
|
<input type="text" id="author" name="author" placeholder="예: 조정숙" value="<%= story ? story.author : '' %>" />
|
|
</label>
|
|
<label>
|
|
게시일 (YYYY-MM-DD)
|
|
<input type="text" id="publishedAt" name="publishedAt" placeholder="2026-03-25" value="<%= story ? story.publishedAt : '' %>" />
|
|
</label>
|
|
<label class="full">
|
|
태그 (쉼표로 구분)
|
|
<input type="text" id="tags" name="tags" placeholder="인사총무, Claude Cowork" value="<%= story ? (story.tags || []).join(', ') : '' %>" />
|
|
</label>
|
|
<label class="full">
|
|
원문 PDF (선택)
|
|
<span class="muted" style="display:block;font-weight:normal;font-size:0.9em;margin-bottom:0.35rem;">로컬 PDF를 선택하면 서버에 저장되고 아래 URL이 자동으로 채워집니다. 필요하면 URL을 직접 수정할 수 있습니다.</span>
|
|
<div class="pdf-upload-row">
|
|
<input type="file" id="pdfFileInput" accept="application/pdf,.pdf" class="visually-hidden" tabindex="-1" />
|
|
<button type="button" class="btn-ghost" id="pdfFilePickBtn" aria-label="PDF 파일 선택">PDF 파일 선택…</button>
|
|
<span id="pdfUploadStatus" class="muted pdf-upload-status" role="status" aria-live="polite"></span>
|
|
</div>
|
|
<input type="text" id="pdfUrl" name="pdfUrl" placeholder="/public/resources/ai-success/..." value="<%= story && story.pdfUrl ? story.pdfUrl : '' %>" autocomplete="off" />
|
|
</label>
|
|
<label class="full">
|
|
본문 (Markdown)
|
|
<textarea id="bodyMarkdown" name="bodyMarkdown" rows="22" placeholder="# 제목 본문..."><% if (story) { %><%- story.bodyMarkdown %><% } %></textarea>
|
|
</label>
|
|
<div class="form-actions full">
|
|
<button type="button" class="btn-ghost" onclick="location.href='/ai-cases'">취소</button>
|
|
<button type="submit" class="top-action"><%= story ? '저장' : '등록' %></button>
|
|
</div>
|
|
</form>
|
|
<p id="formMessage" class="form-message" role="status" aria-live="polite"></p>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var form = document.getElementById('successStoryForm');
|
|
var msg = document.getElementById('formMessage');
|
|
if (!form) return;
|
|
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
if (msg) msg.textContent = '';
|
|
var id = (document.getElementById('storyId') || {}).value || '';
|
|
var pdfVal = ((document.getElementById('pdfUrl') || {}).value || '').trim();
|
|
var mdVal = ((document.getElementById('bodyMarkdown') || {}).value || '').trim();
|
|
if (!mdVal && !pdfVal) {
|
|
if (msg) msg.textContent = '본문(Markdown) 또는 원문 PDF 중 하나는 입력해 주세요.';
|
|
return;
|
|
}
|
|
var payload = {
|
|
slug: (document.getElementById('slug') || {}).value || '',
|
|
title: (document.getElementById('title') || {}).value || '',
|
|
excerpt: (document.getElementById('excerpt') || {}).value || '',
|
|
department: (document.getElementById('department') || {}).value || '',
|
|
author: (document.getElementById('author') || {}).value || '',
|
|
publishedAt: (document.getElementById('publishedAt') || {}).value || '',
|
|
tags: (document.getElementById('tags') || {}).value || '',
|
|
pdfUrl: (document.getElementById('pdfUrl') || {}).value || '',
|
|
bodyMarkdown: (document.getElementById('bodyMarkdown') || {}).value || ''
|
|
};
|
|
var url = id ? '/api/ai-success-stories/' + encodeURIComponent(id) : '/api/ai-success-stories';
|
|
var method = id ? 'PUT' : 'POST';
|
|
fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(function(r) { return r.json().then(function(j) { return { ok: r.ok, j: j }; }); })
|
|
.then(function(_ref) {
|
|
if (_ref.ok) {
|
|
if (msg) msg.textContent = '저장되었습니다.';
|
|
setTimeout(function() { window.location.href = '/ai-cases'; }, 600);
|
|
} else {
|
|
if (msg) msg.textContent = _ref.j.error || '저장에 실패했습니다.';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
if (msg) msg.textContent = '네트워크 오류';
|
|
});
|
|
});
|
|
|
|
var pdfFileInput = document.getElementById('pdfFileInput');
|
|
var pdfPickBtn = document.getElementById('pdfFilePickBtn');
|
|
var pdfUploadStatus = document.getElementById('pdfUploadStatus');
|
|
if (pdfPickBtn && pdfFileInput) {
|
|
pdfPickBtn.addEventListener('click', function() {
|
|
pdfFileInput.click();
|
|
});
|
|
}
|
|
if (pdfFileInput) {
|
|
pdfFileInput.addEventListener('change', function() {
|
|
var f = pdfFileInput.files && pdfFileInput.files[0];
|
|
if (!f) return;
|
|
if (pdfUploadStatus) pdfUploadStatus.textContent = '업로드 중…';
|
|
var fd = new FormData();
|
|
fd.append('pdfFile', f);
|
|
fetch('/api/ai-success-stories/upload-pdf', {
|
|
method: 'POST',
|
|
body: fd,
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(function(r) {
|
|
return r.json().then(function(j) {
|
|
return { ok: r.ok, j: j };
|
|
});
|
|
})
|
|
.then(function(_ref) {
|
|
if (_ref.ok && _ref.j.pdfUrl) {
|
|
var pdfEl = document.getElementById('pdfUrl');
|
|
if (pdfEl) pdfEl.value = _ref.j.pdfUrl;
|
|
if (pdfUploadStatus) {
|
|
pdfUploadStatus.textContent =
|
|
'업로드 완료 (' + (_ref.j.filename || 'PDF') + ')';
|
|
}
|
|
} else {
|
|
if (pdfUploadStatus) {
|
|
pdfUploadStatus.textContent = _ref.j.error || '업로드 실패';
|
|
}
|
|
}
|
|
pdfFileInput.value = '';
|
|
})
|
|
.catch(function() {
|
|
if (pdfUploadStatus) pdfUploadStatus.textContent = '네트워크 오류';
|
|
pdfFileInput.value = '';
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.js-delete-story').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var id = btn.getAttribute('data-id');
|
|
var title = btn.getAttribute('data-title') || '';
|
|
if (!id || !confirm('삭제할까요? ' + title)) return;
|
|
fetch('/api/ai-success-stories/' + encodeURIComponent(id), { method: 'DELETE' })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function() { window.location.reload(); })
|
|
.catch(function() { alert('삭제 실패'); });
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|