Files
ai_platform/views/ai-cases-compose.ejs
dsyoon f7df18f181 feat: XAVIS 브랜드 이미지를 NCue 로고·favicon으로 교체
로그인·네비·F-Scan 로고, favicon, 페이지 타이틀, 인증 메일 브랜딩을 NCue로 통일.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:46:40 +09:00

772 lines
34 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
<!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 활용 사례 &gt; 작성하기 - NCue</title>
<link rel="stylesheet" href="https://uicdn.toast.com/editor/3.2.2/toastui-editor.min.css" />
<link rel="stylesheet" href="/public/styles.css" />
</head>
<body>
<div class="app-shell">
<%- include('partials/nav', { activeMenu: 'ai-cases' }) %>
<div class="content-area">
<header class="topbar">
<h1>AI 활용 사례 &gt; <%= editPayload ? "수정" : "작성" %></h1>
<a class="top-action-link top-action-link--ghost" href="/ai-cases">목록</a>
</header>
<main class="container container-narrow use-case-compose">
<p class="use-case-compose__hint">
<strong>작성자: </strong> <%= typeof submitterEmail !== "undefined" ? submitterEmail : "" %>
</p>
<form id="use-case-form" class="use-case-compose__form" novalidate>
<div class="use-case-field">
<label for="uctitle">제목 <span class="req">*</span></label>
<input
type="text"
id="uctitle"
name="title"
class="use-case-title-input"
required
maxlength="200"
placeholder="제목을 입력하세요"
autocomplete="off"
autofocus
/>
</div>
<div class="use-case-section use-case-section--rich use-case-section--merged">
<label class="label-block" for="edUseCaseBody">본문 (STAR: 1~4번) <span class="req">*</span></label>
<p class="field-hint use-case-merge-hint">한 편집기 안에서 1.Situation → 4.Result 순서로 작성합니다.<br>구역을 나누는 제목·블록을 삭제하면 저장이 실패할 수 있습니다.</p>
<div id="edUseCaseBody" class="use-case-editor-host use-case-editor-host--merged" aria-label="STAR 본문"></div>
</div>
<p class="use-case-charcount" id="ucCharLine" aria-live="polite">0 / <%= maxBodyTotal %>자</p>
<div class="use-case-compose__meta">
<div class="use-case-field use-case-field--tags">
<label for="uctags">활용 AI</label>
<input
type="text"
id="uctags"
name="tags"
class="use-case-tag-input"
placeholder="쉼표로 구분 (예, Claude, Slack, Canvas)"
autocomplete="off"
/>
<p class="field-hint">활용한 AI 툴을 입력해주세요.</p>
</div>
<div class="use-case-field" id="ucThumbField">
<span class="label-block">썸네일 이미지 <% if (!editPayload) { %><span class="req">*</span><% } else { %><span class="opt">(변경·추가 시에만)</span><% } %></span>
<p class="field-hint" id="ucThumbFieldHint">최대 <%= maxThumbCount %>개, 개당 5MB, 가급 1:1 비율 권장</p>
<% if (typeof editPayload !== "undefined" && editPayload) { %>
<p class="field-hint use-case-attach-hint">이미지 우측 상단 <strong>X</strong>로 삭제할 수 있습니다. 새 이미지는 아래에서 고르면 목록 끝에 추가됩니다(합계 최대 <%= maxThumbCount %>개).</p>
<% } %>
<div class="use-case-dropzone" id="ucThumbZone">
<input type="file" id="ucThumb" name="thumbnail" accept="image/*" class="use-case-file-input" multiple />
<label for="ucThumb" class="use-case-dropzone__label"
>클릭하거나 드래그하여 썸네일을 선택하세요 (최대 <%= maxThumbCount %>개)</label
>
</div>
<div class="use-case-thumb-grid" id="ucThumbPreviewGrid" hidden>
<% if (typeof editPayload !== "undefined" && editPayload && editPayload.existingThumbnails && editPayload.existingThumbnails.length) { %>
<% editPayload.existingThumbnails.forEach(function (t) { var fn = (t.originalName || "썸네일").replace(/[<>"]/g, "") || (t.relativePath && t.relativePath.split("/").pop()) || "썸네일"; %>
<figure class="use-case-thumb-tile" data-existing-rp="<%- encodeURIComponent(t.relativePath) %>">
<img src="<%= t.relativePath %>" alt="<%= fn %>" loading="lazy" decoding="async" />
<button type="button" class="use-case-thumb-remove" aria-label="<%= fn %> 제거" title="제거">×</button>
</figure>
<% }); %>
<% } %>
</div>
</div>
<div class="use-case-field">
<span class="label-block">첨부 파일 <span class="opt">(선택, 최대 <%= maxAttachCount %>개·20MB)</span></span>
<% if (typeof editPayload !== "undefined" && editPayload) { %>
<p class="field-hint use-case-attach-hint">수정 시: 아래 <strong>기존 첨부</strong>를 체크해 삭제한 뒤 새 파일을 선택할 수 있습니다(최대 <%= maxAttachCount %>개).</p>
<% } %>
<input type="file" id="ucFiles" name="attachments" class="use-case-file-multi" />
<% if (typeof editPayload !== "undefined" && editPayload && editPayload.existingAttachments && editPayload.existingAttachments.length) { %>
<p class="field-hint use-case-existing-attach__title">기존 첨부 — 제거하려면 체크</p>
<ul class="use-case-existing-attach">
<% editPayload.existingAttachments.forEach(function (a) { var fn = (a.originalName || "").replace(/[<>"]/g, "") || (a.relativePath && a.relativePath.split("/").pop()) || "첨부"; %>
<li>
<label class="use-case-cb"
><input type="checkbox" class="js-uc-remove-attach" data-rp="<%- encodeURIComponent(a.relativePath) %>" /> <%= fn %></label
>
</li>
<% }); %>
</ul>
<% } %>
<ul class="use-case-file-list" id="ucFileList" hidden></ul>
</div>
</div>
<div class="use-case-actions">
<a href="/ai-cases" class="btn-ghost use-case-btn-cancel">취소</a>
<button type="submit" class="top-action use-case-btn-save" id="ucSubmit"><%= editPayload ? "수정 저장" : "저장하기" %></button>
</div>
<p class="form-message use-case-form-msg" id="ucFormMsg" hidden></p>
<% if (editPayload) { %>
<script type="application/json" id="uc-edit-json">
<%- JSON.stringify(editPayload).replace(/</g, "\\u003c") %>
</script>
<% } %>
</form>
</main>
</div>
</div>
<script src="https://uicdn.toast.com/editor/3.2.2/toastui-editor-all.min.js"></script>
<script src="https://uicdn.toast.com/editor/3.2.2/i18n/ko-kr.min.js"></script>
<script>
(function () {
var maxTotal = <%= maxBodyTotal %>;
var maxThumbCount = <%= maxThumbCount %>;
var maxAttachCount = <%= maxAttachCount %>;
var ph = {
situation: <%- JSON.stringify(placeholders.situation) %>,
task: <%- JSON.stringify(placeholders.task) %>,
action: <%- JSON.stringify(placeholders.action) %>,
result: <%- JSON.stringify(placeholders.result) %>,
};
var charLine = document.getElementById("ucCharLine");
var editJson = document.getElementById("uc-edit-json");
var editPayload = null;
if (editJson) {
try {
editPayload = JSON.parse(editJson.textContent);
} catch (e) {
editPayload = null;
}
}
var form = document.getElementById("use-case-form");
var msg = document.getElementById("ucFormMsg");
var submitBtn = document.getElementById("ucSubmit");
var thumb = document.getElementById("ucThumb");
var thumbGrid = document.getElementById("ucThumbPreviewGrid");
var pendingThumbFiles = [];
var pendingThumbUrls = [];
var filesIn = document.getElementById("ucFiles");
var fileList = document.getElementById("ucFileList");
var sectionIds = ["uc-situation", "uc-task", "uc-action", "uc-result"];
var fieldKeys = ["situation", "taskGoal", "actionTaken", "resultOutcome"];
var phKeysByField = {
situation: "situation",
taskGoal: "task",
actionTaken: "action",
resultOutcome: "result",
};
var placeholdersActive = !editPayload;
var examplesComposeMode = !editPayload;
if (typeof toastui === "undefined" || !toastui.Editor) {
if (form) {
var p = document.createElement("p");
p.className = "form-message use-case-form-msg";
p.style.color = "#b91c1c";
p.textContent = "에디터를 불러오지 못했습니다. 네트워크·방화벽을 확인하거나 uicdn.toast.com 접속을 허용해 주세요.";
form.insertBefore(p, form.firstChild);
}
return;
}
function esc(s) {
if (s == null) return "";
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function paraFromPh(text) {
if (!text) return "<p><br></p>";
return (
'<p class="uc-example-text" data-uc-example="1">' +
esc(text).replace(/\r\n/g, "\n").split("\n").join("<br>") +
"</p>"
);
}
function buildHtmlFromSections(sec) {
sec = sec || {};
return (
'<div class="uc-doc">' +
'<div id="uc-situation" class="uc-section"><h2>1. Situation (배경)</h2>' +
(sec.situation || "<p><br></p>") +
"</div>" +
'<div id="uc-task" class="uc-section"><h2>2. Task (과제/목표)</h2>' +
(sec.taskGoal || "<p><br></p>") +
"</div>" +
'<div id="uc-action" class="uc-section"><h2>3. Action (행동)</h2>' +
(sec.actionTaken || "<p><br></p>") +
"</div>" +
'<div id="uc-result" class="uc-section"><h2>4. Result (결과)</h2>' +
(sec.resultOutcome || "<p><br></p>") +
"</div>" +
"</div>"
);
}
function buildInitialHtml() {
return buildHtmlFromSections({
situation: paraFromPh(ph.situation),
taskGoal: paraFromPh(ph.task),
actionTaken: paraFromPh(ph.action),
resultOutcome: paraFromPh(ph.result),
});
}
function normalizePlain(s) {
return String(s || "")
.replace(/\s+/g, " ")
.trim();
}
function isPlaceholderBody(html, phKey) {
var phPlain = normalizePlain(ph[phKey]);
if (!phPlain) return false;
return normalizePlain(stripForCount(html)) === phPlain;
}
function sectionHasExampleMarker(html) {
return /uc-example-text|data-uc-example/.test(String(html || ""));
}
function isSectionBodyEmpty(html) {
return stripForCount(html).length === 0;
}
function hasUserBodyInput() {
var ex = extractSections(editor.getHTML());
for (var i = 0; i < fieldKeys.length; i++) {
var body = ex[fieldKeys[i]] || "";
if (sectionHasExampleMarker(body)) return false;
if (!isSectionBodyEmpty(body) && !isPlaceholderBody(body, phKeysByField[fieldKeys[i]])) {
return true;
}
}
return false;
}
function restoreExamplesIfEmptyDraft() {
if (!examplesComposeMode || placeholdersActive) return;
if (hasUserBodyInput()) return;
placeholdersActive = true;
editor.setHTML(buildInitialHtml());
count();
}
/** 본문 영역 첫 클릭 시 STAR 1~4 예시 문단만 제거(제목은 유지) */
function clearExampleContentOnActivate() {
if (!placeholdersActive) return;
placeholdersActive = false;
var empty = "<p><br></p>";
var wrapper = document.createElement("div");
wrapper.innerHTML = editor.getHTML();
var clearedById = false;
for (var i = 0; i < sectionIds.length; i++) {
var sec = wrapper.querySelector("#" + sectionIds[i]);
if (!sec) continue;
var head = sec.querySelector(":scope > h1, :scope > h2, :scope > h3, :scope > h4");
sec.innerHTML = (head ? head.outerHTML : "") + empty;
clearedById = true;
}
setTimeout(function () {
if (clearedById) {
editor.setHTML(wrapper.innerHTML);
} else {
editor.setHTML(
buildHtmlFromSections({
situation: empty,
taskGoal: empty,
actionTaken: empty,
resultOutcome: empty,
})
);
}
count();
}, 0);
}
/** @returns {{ situation: string, taskGoal: string, actionTaken: string, resultOutcome: string, ok: boolean, err?: string }} */
function extractSections(html) {
var out = { situation: "", taskGoal: "", actionTaken: "", resultOutcome: "", ok: true };
var w = document.createElement("div");
w.innerHTML = html;
var got = 0;
for (var i = 0; i < sectionIds.length; i++) {
var sec = w.querySelector("#" + sectionIds[i]);
if (sec) {
var c = sec.cloneNode(true);
var hx = c.querySelector(":scope > h1, :scope > h2, :scope > h3, :scope > h4");
if (hx) hx.remove();
out[fieldKeys[i]] = c.innerHTML.trim();
got++;
}
}
if (got === 4) {
out.ok = true;
return out;
}
var byH = extractByHeadings(w);
if (byH) {
byH.ok = true;
return byH;
}
return { situation: out.situation, taskGoal: out.taskGoal, actionTaken: out.actionTaken, resultOutcome: out.resultOutcome, ok: false, err: "split" };
}
function extractByHeadings(wrap) {
var patterns = [
{ key: "situation", re: /1\.\s*Situation/i },
{ key: "taskGoal", re: /2\.\s*Task/i },
{ key: "actionTaken", re: /3\.\s*Action/i },
{ key: "resultOutcome", re: /4\.\s*Result/i },
];
var heads = wrap.querySelectorAll("h1, h2, h3, h4");
var found = [];
for (var h = 0; h < heads.length; h++) {
var t = (heads[h].textContent || "").replace(/\s+/g, " ").trim();
for (var p = 0; p < patterns.length; p++) {
if (patterns[p].re.test(t)) {
found.push({ key: patterns[p].key, el: heads[h] });
break;
}
}
}
if (found.length < 4) {
return null;
}
var seen = {};
for (var f = 0; f < found.length; f++) {
if (seen[found[f].key]) return null;
seen[found[f].key] = true;
}
if (Object.keys(seen).length !== 4) return null;
found.sort(function (a, b) {
var po = a.el.compareDocumentPosition(b.el);
if (po & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
if (po & Node.DOCUMENT_POSITION_PRECEDING) return 1;
return 0;
});
var res = { situation: "", taskGoal: "", actionTaken: "", resultOutcome: "" };
for (var s = 0; s < found.length; s++) {
var head = found[s].el;
var boundary = s + 1 < found.length ? found[s + 1].el : null;
var n = head.nextSibling;
var parts = [];
while (n) {
if (boundary && n === boundary) break;
if (n.nodeType === 1) {
parts.push(n.outerHTML);
}
n = n.nextSibling;
}
res[found[s].key] = parts.join("").trim();
}
return res;
}
function stripForCount(html) {
if (!html) return "";
return String(html)
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/&#[0-9]+;/g, " ")
.replace(/&[a-zA-Z0-9]+;/g, " ")
.replace(/\s+/g, " ")
.trim();
}
var edHost = document.getElementById("edUseCaseBody");
var editor = new toastui.Editor({
el: edHost,
height: "720px",
initialEditType: "wysiwyg",
previewStyle: "vertical",
hideModeSwitch: true,
useCommandShortcut: true,
usageStatistics: false,
language: "ko",
placeholder: "STAR 4구역(1~4번)이 보이지 않으면 페이지를 새로고침하세요.",
toolbarItems: [
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", "link"],
["code", "codeblock"],
],
});
if (editPayload && editPayload.html) {
document.getElementById("uctitle").value = editPayload.title || "";
document.getElementById("uctags").value = editPayload.tags || "";
editor.setHTML(editPayload.html);
if (editPayload.existingThumbnails && editPayload.existingThumbnails.length) {
var h = document.getElementById("ucThumbFieldHint");
if (h) h.textContent = "새로 선택하면 목록 끝에 추가됩니다. (최대 " + maxThumbCount + "개, 개당 5MB, 1:1 권장)";
if (thumbGrid) thumbGrid.hidden = false;
}
} else {
editor.setHTML(buildInitialHtml());
}
var titleInput = document.getElementById("uctitle");
function focusTitleInput() {
if (titleInput) titleInput.focus({ preventScroll: true });
}
if (examplesComposeMode) {
edHost.addEventListener(
"mousedown",
function (ev) {
if (!placeholdersActive || ev.button !== 0) return;
if (!ev.target.closest(".toastui-editor-contents")) return;
clearExampleContentOnActivate();
},
true
);
document.addEventListener(
"mousedown",
function (ev) {
if (ev.button !== 0 || placeholdersActive) return;
if (edHost.contains(ev.target)) return;
restoreExamplesIfEmptyDraft();
},
true
);
}
focusTitleInput();
setTimeout(focusTitleInput, 0);
setTimeout(focusTitleInput, 100);
function count() {
var ex = extractSections(editor.getHTML());
var n = 0;
for (var i = 0; i < fieldKeys.length; i++) {
n += stripForCount(ex[fieldKeys[i]] || "").length;
}
if (charLine) {
charLine.textContent = n + " / " + maxTotal + "자";
}
return n;
}
editor.on("change", function () {
count();
});
count();
function countKeptExistingThumbs() {
if (!thumbGrid) return 0;
return thumbGrid.querySelectorAll(".use-case-thumb-tile[data-existing-rp]:not(.is-removed)").length;
}
function countPendingThumbs() {
return pendingThumbFiles.length;
}
function countRemainingThumbsOnSubmit() {
return countKeptExistingThumbs() + countPendingThumbs();
}
function revokePendingThumbUrls() {
for (var ui = 0; ui < pendingThumbUrls.length; ui++) {
try {
URL.revokeObjectURL(pendingThumbUrls[ui]);
} catch (e) {}
}
pendingThumbUrls = [];
}
function updateThumbGridVisibility() {
if (!thumbGrid) return;
var visible =
thumbGrid.querySelectorAll(".use-case-thumb-tile:not(.is-removed)").length > 0 ||
countPendingThumbs() > 0;
thumbGrid.hidden = !visible;
}
function renderPendingThumbTiles() {
if (!thumbGrid) return;
var oldPending = thumbGrid.querySelectorAll(".use-case-thumb-tile[data-pending-index]");
for (var oi = 0; oi < oldPending.length; oi++) {
oldPending[oi].remove();
}
revokePendingThumbUrls();
for (var pi = 0; pi < pendingThumbFiles.length; pi++) {
var file = pendingThumbFiles[pi];
var url = URL.createObjectURL(file);
pendingThumbUrls.push(url);
var fig = document.createElement("figure");
fig.className = "use-case-thumb-tile";
fig.setAttribute("data-pending-index", String(pi));
var img = document.createElement("img");
img.src = url;
img.alt = file.name || "썸네일";
img.loading = "lazy";
img.decoding = "async";
var btn = document.createElement("button");
btn.type = "button";
btn.className = "use-case-thumb-remove";
btn.setAttribute("aria-label", (file.name || "썸네일") + " 제거");
btn.title = "제거";
btn.textContent = "×";
fig.appendChild(img);
fig.appendChild(btn);
thumbGrid.appendChild(fig);
}
updateThumbGridVisibility();
}
function notifyThumbLimitPopup(addedCount, selectedCount) {
var base = "썸네일은 최대 " + maxThumbCount + "개까지 업로드할 수 있습니다.";
if (typeof addedCount === "number" && typeof selectedCount === "number" && selectedCount > addedCount && addedCount > 0) {
window.alert(base + "\n\n선택하신 " + selectedCount + "개 중 " + addedCount + "개만 추가되었습니다.");
return;
}
if (typeof addedCount === "number" && addedCount === 0) {
window.alert(base + "\n\n더 이상 추가할 수 없습니다. 기존 썸네일을 삭제한 뒤 다시 선택해 주세요.");
return;
}
window.alert(base);
}
function addPendingThumbFiles(fileList) {
if (!fileList || !fileList.length) return true;
var keptExisting = countKeptExistingThumbs();
var remaining = maxThumbCount - keptExisting - pendingThumbFiles.length;
if (remaining <= 0) {
notifyThumbLimitPopup(0, fileList.length);
return false;
}
var selectedCount = fileList.length;
var addedCount = 0;
for (var ai = 0; ai < fileList.length; ai++) {
if (addedCount >= remaining) break;
var f = fileList[ai];
if (!/^image\//.test(f.type || "")) {
msg.textContent = "썸네일은 이미지 파일만 업로드할 수 있습니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return false;
}
if (f.size > 5 * 1024 * 1024) {
msg.textContent = '"' + (f.name || "파일") + '"은 5MB 이하여야 합니다.';
msg.style.color = "#b91c1c";
msg.hidden = false;
return false;
}
pendingThumbFiles.push(f);
addedCount++;
}
if (selectedCount > remaining) {
notifyThumbLimitPopup(addedCount, selectedCount);
}
msg.hidden = true;
renderPendingThumbTiles();
return true;
}
if (thumbGrid) {
thumbGrid.addEventListener("click", function (ev) {
var btn = ev.target.closest(".use-case-thumb-remove");
if (!btn) return;
ev.preventDefault();
var tile = btn.closest(".use-case-thumb-tile");
if (!tile) return;
if (tile.hasAttribute("data-existing-rp")) {
tile.classList.add("is-removed");
updateThumbGridVisibility();
return;
}
if (tile.hasAttribute("data-pending-index")) {
var idx = parseInt(tile.getAttribute("data-pending-index"), 10);
if (!Number.isNaN(idx) && idx >= 0 && idx < pendingThumbFiles.length) {
pendingThumbFiles.splice(idx, 1);
renderPendingThumbTiles();
}
}
});
}
if (thumb) {
thumb.addEventListener("change", function () {
if (!thumb.files || !thumb.files.length) return;
addPendingThumbFiles(thumb.files);
thumb.value = "";
});
}
if (filesIn && fileList) {
filesIn.addEventListener("change", function () {
fileList.innerHTML = "";
if (!filesIn.files || !filesIn.files.length) {
fileList.hidden = true;
return;
}
if (filesIn.files.length > maxAttachCount) {
msg.textContent = "첨부 파일은 최대 " + maxAttachCount + "개만 선택할 수 있습니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
filesIn.value = "";
fileList.hidden = true;
return;
}
var keptAttach = 0;
var attachCbs = document.querySelectorAll(".js-uc-remove-attach");
for (var ai = 0; ai < attachCbs.length; ai++) {
if (!attachCbs[ai].checked) keptAttach++;
}
if (keptAttach + filesIn.files.length > maxAttachCount) {
msg.textContent =
"첨부 파일은 최대 " +
maxAttachCount +
"개입니다. 기존 첨부를 제거한 뒤 새 파일을 선택해 주세요.";
msg.style.color = "#b91c1c";
msg.hidden = false;
filesIn.value = "";
fileList.hidden = true;
return;
}
msg.hidden = true;
fileList.hidden = false;
for (var k = 0; k < filesIn.files.length; k++) {
var li = document.createElement("li");
li.textContent = filesIn.files[k].name;
fileList.appendChild(li);
}
});
}
if (form) {
form.addEventListener("submit", function (ev) {
ev.preventDefault();
var ex = extractSections(editor.getHTML());
if (ex && ex.ok === false) {
msg.textContent =
"1~4번 구역(#uc-situation 등)을 찾을 수 없습니다. 구역을 삭제하지 말고, 제목(1. Situation …)이 보이는지 확인해 주세요.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
if (count() > maxTotal) {
msg.textContent = "본문(4개 섹션 합계)은 " + maxTotal + "자 이하여야 합니다. (보이는 텍스트 기준)";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
for (var v = 0; v < fieldKeys.length; v++) {
var fk = fieldKeys[v];
var body = ex[fk];
if (
isSectionBodyEmpty(body) ||
isPlaceholderBody(body, phKeysByField[fk]) ||
sectionHasExampleMarker(body)
) {
msg.textContent =
(v + 1) +
"번 구역에 내용이 없습니다. 예시는 본문 영역 클릭 시 자동으로 지워집니다. 본인 내용을 입력해 주세요. (빈 줄·서식만이면 제출할 수 없습니다.)";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
}
if (!editPayload) {
if (countPendingThumbs() < 1) {
msg.textContent = "썸네일 이미지를 1개 이상 선택해 주세요.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
} else if (countRemainingThumbsOnSubmit() < 1) {
msg.textContent = "썸네일은 1개 이상 남겨야 합니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
if (countRemainingThumbsOnSubmit() > maxThumbCount) {
notifyThumbLimitPopup();
msg.textContent = "썸네일은 최대 " + maxThumbCount + "개까지 등록할 수 있습니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
for (var tv = 0; tv < pendingThumbFiles.length; tv++) {
if (pendingThumbFiles[tv].size > 5 * 1024 * 1024) {
msg.textContent = "썸네일 " + (tv + 1) + "번째 파일은 5MB 이하여야 합니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
}
var isEdit = editPayload && editPayload.id;
var fd = new FormData();
fd.append("title", document.getElementById("uctitle").value.trim());
fd.append("situation", ex.situation);
fd.append("taskGoal", ex.taskGoal);
fd.append("actionTaken", ex.actionTaken);
fd.append("resultOutcome", ex.resultOutcome);
fd.append("tags", document.getElementById("uctags").value);
for (var t = 0; t < pendingThumbFiles.length; t++) {
fd.append("thumbnail", pendingThumbFiles[t]);
}
var keptAttachSubmit = 0;
var attachCbsSubmit = document.querySelectorAll(".js-uc-remove-attach");
for (var asi = 0; asi < attachCbsSubmit.length; asi++) {
if (!attachCbsSubmit[asi].checked) keptAttachSubmit++;
}
var newAttachCount = filesIn && filesIn.files ? filesIn.files.length : 0;
if (keptAttachSubmit + newAttachCount > maxAttachCount) {
msg.textContent =
"첨부 파일은 최대 " +
maxAttachCount +
"개입니다. 기존 첨부를 제거한 뒤 새 파일을 선택해 주세요.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
if (filesIn && filesIn.files) {
for (var f = 0; f < filesIn.files.length; f++) {
if (filesIn.files[f].size > 20 * 1024 * 1024) {
msg.textContent = "첨부 파일은 20MB 이하여야 합니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
return;
}
fd.append("attachments", filesIn.files[f]);
}
}
if (isEdit) {
var rmThumb = [];
if (thumbGrid) {
var removedTiles = thumbGrid.querySelectorAll(".use-case-thumb-tile[data-existing-rp].is-removed");
for (var ti = 0; ti < removedTiles.length; ti++) {
try {
var trp = decodeURIComponent(removedTiles[ti].getAttribute("data-existing-rp") || "");
if (trp) rmThumb.push(trp);
} catch (e) {}
}
}
if (rmThumb.length) {
fd.append("removeThumbnailPaths", JSON.stringify(rmThumb));
}
var rm = [];
var cbs = document.querySelectorAll(".js-uc-remove-attach");
for (var ci = 0; ci < cbs.length; ci++) {
if (cbs[ci].checked) {
try {
var rp = decodeURIComponent(cbs[ci].getAttribute("data-rp") || "");
if (rp) rm.push(rp);
} catch (e) {}
}
}
if (rm.length) {
fd.append("removeAttachmentPaths", JSON.stringify(rm));
}
}
msg.hidden = true;
submitBtn.disabled = true;
var url = isEdit ? "/api/ai-use-case-submissions/" + encodeURIComponent(editPayload.id) : "/api/ai-use-case-submissions";
var method = isEdit ? "PUT" : "POST";
fetch(url, { method: method, body: fd, credentials: "same-origin" })
.then(function (r) {
return r.json().then(function (j) {
return { ok: r.ok, j: j, status: r.status };
});
})
.then(function (o) {
if (o.ok && o.j && o.j.ok) {
if (isEdit && o.j && o.j.id) {
window.location.href = "/ai-cases/submit/" + encodeURIComponent(o.j.id) + "?updated=1";
} else {
window.location.href = "/ai-cases?submitted=1";
}
return;
}
msg.textContent = (o.j && o.j.error) || "저장에 실패했습니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
})
.catch(function () {
msg.textContent = "요청에 실패했습니다.";
msg.style.color = "#b91c1c";
msg.hidden = false;
})
.finally(function () {
submitBtn.disabled = false;
});
});
}
})();
</script>
</body>
</html>