//////////////////////////////////////////////////////////////////////////////////////// // Document Ready //////////////////////////////////////////////////////////////////////////////////////// function execDocReady() { var pluginGroups = [ ["../reference/light-blue/lib/vendor/jquery.ui.widget.js", "../reference/lightblue4/docs/lib/widgster/widgster.js"], ["../reference/lightblue4/docs/lib/bootstrap-select/dist/js/bootstrap-select.min.js"], ["../../cover/js/util/authorize.js"], ["../cover/css/blog/blog-editor.css"] ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { $(".widget").widgster(); $("#sidebar").hide(); $(".wrap").css("margin-left", 0); $("#footer").load("/cover/html/template/landing-footer.html"); validateAuthorization(); }) .catch(function (error) { console.error("블로그 에디터 플러그인 로드 중 오류 발생"); console.error(error); }); } // 전역 블로그 아티클 아이디 var globalArticleId = new URLSearchParams(window.location.search).get("id"); // 편집 모드 여부 체크 function isEditMode() { return Boolean(globalArticleId); } // 블로그 에디터 페이지 초기화 function initializeBlogEditor() { $("#page-title").text(isEditMode() ? "글 수정" : "글쓰기"); $("#footer-publish-btn #publish-text").text(isEditMode() ? "수정" : "등록"); $("#footer-publish-btn").addClass(isEditMode() ? "btn-success" : "btn-primary"); onLoadCKEditor(); loadArticle(globalArticleId); } // 기존 게시글 데이터 로드 (수정 모드) function loadArticle(articleId) { if (!articleId) return; $.ajax({ url: "/auth-anon/api/arms/blog/getBlog.do", data: { c_id: articleId }, method: "GET", dataType: "json", success: function (article) { renderArticle(article); }, error: function (xhr, status, error) { showFullScreenError("not-found"); } }); } // 게시글 데이터 폼에 바인딩 function renderArticle(article) { if (article == null) { showFullScreenError("not-found"); return; } $("#article-id").val(article.c_id); $("#article-title").val(article.c_blog_title); $("#article-desc").val(article.c_blog_desc); $("#article-author-id").val(article.c_blog_author_id || "익명"); // CKEditor 내용 설정 (로드 대기) let editor_instance_wait = setInterval(function () { try { var editor_instance = CKEDITOR.instances["article_editor"]; if (editor_instance) { CKEDITOR.instances.article_editor.setData(article.c_blog_contents); CKEDITOR.instances.article_editor.setReadOnly(false); clearInterval(editor_instance_wait); } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 재시도 중..."); } }, 313 /*milli*/); if (article.c_blog_thumbnail_url) { // 썸네일 이미지 미리보기 설정 const imgElement = document.createElement("img"); imgElement.src = article.c_blog_thumbnail_url; $("#preview").append(imgElement); } // 내용 저장 $("#content-editor").html(article.c_blog_desc || ""); // 기본 내용으로 excerpt 사용 // 해시태그가 있으면 설정, 디비에 해시태그가 현재 없어서 안 보이는 것. 상세 정의가 필요 if (article.tags && article.tags.length > 0) { setHashtags(article.tags); } } //////////////////////////////////////////////////////////////////////////////////////// // 에디터 이벤트 리스너 초기화 //////////////////////////////////////////////////////////////////////////////////////// function initializeEditorEventListeners() { // 글쓰기 저장 클릭시 $(document).on("click", "#publish-btn, #footer-publish-btn", saveBlogArticle); // 글쓰기 취소 최종 확인 클릭 시 $(document).on("click", "#cancel-confirm-yes", function () { $("#confirmModal").modal("hide"); if (globalArticleId) routeToBlogDetail(globalArticleId); else window.location.href = "/cover/template.html?page=blog"; }); // 글쓰기 취소 버튼 클릭 시 모달 표시 $(document).on("click", "#footer-cancel-btn", function () { $("#confirmModal").modal("show"); }); // 본문 에디터 포커스 및 커서 표시 강제 $(document).on("click", "#content-editor", function () { const editor = this; editor.focus(); // 커서 위치 설정 (클릭한 위치에) if (window.getSelection) { const selection = window.getSelection(); if (selection.rangeCount === 0) { const range = document.createRange(); range.setStart(editor, 0); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } }); // 에디터 로드시 초기 포커스 설정 $(document).ready(function () { setTimeout(function () { const editor = document.getElementById("content-editor"); if (editor) { editor.focus(); } }, 500); }); // 해시태그 기능 초기화 initializeHashtagFunctionality(); // 썸네일 이미지 미리보기 ( mode 공통 ) $("input[name='thumbnail']").on("change", function (e) { const file = e.target.files[0]; if (file) { if (!Validator.validateCondition(isImage(file), "#blog-image-error")) return; $(".fileupload-loading").show(); // 1. Blob URL 생성 (디버깅용) const blobUrl = URL.createObjectURL(file); // 2. FileReader를 사용하여 파일 읽기 const reader = new FileReader(); reader.onerror = reader.onabort = function () { $(".fileupload-loading").hide(); URL.revokeObjectURL(blobUrl); }; reader.onload = function (e) { const base64Data = e.target.result; // Base64 데이터 URL (예: data:image/jpeg;base64,...) // 3. Image 객체 생성 const img = new Image(); // 이미지 로드 실패 시 스피너 끄기 img.onerror = function () { $(".fileupload-loading").hide(); URL.revokeObjectURL(blobUrl); }; img.onload = function () { try { // 4. 를 이용하여 WebP로 변환 및 압축 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); // toDataURL() 메소드를 사용하여 WebP로 변환 const webpDataUrl = canvas.toDataURL("image/webp", 0.8); // 0.8은 압축 품질 (0.0 ~ 1.0) // console.log("WebP Data URL:", webpDataUrl); // 5. 태그 생성 및 src 할당 const imgElement = document.createElement("img"); imgElement.src = webpDataUrl; // 화면에 이미지 표시 $("#preview").empty().append(imgElement); } finally { $(".fileupload-loading").hide(); URL.revokeObjectURL(blobUrl); } }; img.src = base64Data; }; reader.readAsDataURL(file); // 파일을 Base64로 읽기 } }); } // 블로그 글 저장 (신규/수정) function saveBlogArticle() { const $title = $("#article-title"); const $id = $("#article-id"); const $desc = $("#article-desc"); const $authorId = $("#article-author-id"); // [우선순위] 1. 직접 입력한 desc를 적용한다. 2. \n을 기준으로 첫 줄에서 자른 desc를 적용한다. const parsedDesc = () => { if ($desc.val()) return $desc.val().trim(); const editorOriginalData = editor.getData(); const sliceOffset = editorOriginalData.indexOf("\n") === -1 ? editorOriginalData.length : editorOriginalData.indexOf("\n"); return editorOriginalData.substring(0, sliceOffset).trim(); }; const requestBody = { c_id: $id.val() || "", c_blog_title: ($title.val() || "").trim(), c_blog_desc: parsedDesc(), c_blog_contents: editor.getData(), c_blog_author_id: $authorId.val() || "익명", c_blog_thumbnail_url: $("#preview img").attr("src") || "" }; const isTitleValid = Validator.validateEmptyField(requestBody.c_blog_title, "#blog-title-error", $title); const isContentValid = Validator.validateEmptyField(requestBody.c_blog_contents, "#blog-content-error"); if (!isTitleValid || !isContentValid) return; $.ajax({ url: "/auth-anon/api/arms/blog" + (globalArticleId ? "/updateBlog.do" : "/addBlog.do"), type: globalArticleId ? "PUT" : "POST", data: JSON.stringify(requestBody), contentType: "application/json; charset=UTF-8", success: function (newBlogEntity) { jSuccess("블로그를 성공적으로 " + (globalArticleId ? "수정" : "등록") + "했습니다."); setTimeout(() => { routeToBlogDetail(newBlogEntity.c_id); }, 1500); }, error: function (xhr, status, error) { jError("요청을 실패했습니다. 다시 시도하거나 관리자에게 문의해주세요."); } }); } function showFullScreenError(errorCase, targetArgs = $(".blog-editor-container")) { const target = $(targetArgs); const getErrorHtml = (errorCode, title, message, iconClass) => `

${title}

${message}

블로그 홈으로
`; let errorHtml; switch (errorCase) { case "not-found": errorHtml = getErrorHtml( "not-found", "블로그를 찾을 수 없습니다", "요청하신 아티클이 존재하지 않거나 삭제되었습니다.", "fa-exclamation-triangle" ); break; case "not-authorized": errorHtml = getErrorHtml("not-authorized", "권한이 없습니다", "이 작업을 수행할 권한이 없습니다.", "fa-lock"); break; default: errorHtml = getErrorHtml( "default", "알 수 없는 오류가 발생했습니다", "잠시 후 다시 시도해주세요.", "fa-exclamation-triangle" ); break; } $(target).html(errorHtml); } function routeToBlogDetail(articleId) { window.location.href = "/cover/template.html?page=blogDetail&id=" + articleId; } function isImage(file) { return file && file.type.startsWith("image/"); } function onLoadCKEditor() { var waitCKEDITOR = setInterval(function () { try { if (window.CKEDITOR) { if (window.CKEDITOR.status === "loaded") { editor = CKEDITOR.replace("article_editor", { contentsCss: "./css/contents.css" }); clearInterval(waitCKEDITOR); } } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 초기화 재시도 중..."); } }, 313 /*milli*/); } function validateAuthorization() { function valid(json) { initializeBlogEditor(); initializeEditorEventListeners(); // admin 아이디를 등록자 아이디로 세팅 $("#article-author-id").val(json.preferred_username); } function invalid() { showFullScreenError("not-authorized"); } function error() { showFullScreenError("default"); } validateAdminRole(valid, invalid, error); } //////////////////////////////////////////////////////////////////////////////////////// // 해시태그 기능 //////////////////////////////////////////////////////////////////////////////////////// let hashtagsArray = []; const MAX_HASHTAGS = 10; function initializeHashtagFunctionality() { // 해시태그 입력 이벤트 $(document).on("keydown", "#hashtag-input", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); addHashtagFromInput(); } }); // 해시태그 입력 포커스 아웃 시에도 추가 $(document).on("blur", "#hashtag-input", function () { addHashtagFromInput(); }); // 해시태그 제거 이벤트 (동적으로 생성되는 요소에 대한 이벤트 위임) $(document).on("click", ".hashtag-remove", function () { const hashtag = $(this).closest(".hashtag-item").data("hashtag"); removeHashtag(hashtag); }); } function addHashtagFromInput() { const input = $("#hashtag-input"); const value = input.val().trim(); if (value === "") return; // # 제거 (있을 경우) let hashtag = value.replace(/^#/, ""); // 빈 값 체크 if (hashtag === "") return; // 최대 개수 체크 if (hashtagsArray.length >= MAX_HASHTAGS) { return; } // 중복 체크 if (hashtagsArray.includes(hashtag)) { input.val(""); return; } // 해시태그 추가 hashtagsArray.push(hashtag); input.val(""); // UI 업데이트 updateHashtagDisplay(); console.log("해시태그 추가:", hashtag, "현재 해시태그들:", hashtagsArray); } function removeHashtag(hashtag) { const index = hashtagsArray.indexOf(hashtag); if (index > -1) { hashtagsArray.splice(index, 1); updateHashtagDisplay(); console.log("해시태그 제거:", hashtag, "현재 해시태그들:", hashtagsArray); } } function updateHashtagDisplay() { const container = $("#hashtag-container"); container.empty(); if (hashtagsArray.length === 0) { container.html('
추가된 해시태그가 없습니다.
'); return; } hashtagsArray.forEach(function (hashtag) { const hashtagElement = $(` ${hashtag.startsWith("#") ? hashtag : "#" + hashtag} × `); container.append(hashtagElement); }); } function setHashtags(tags) { // 기존 게시글 수정 시 해시태그 설정 hashtagsArray = tags || []; updateHashtagDisplay(); } function getHashtags() { // 해시태그 배열 반환 (저장 시 사용) return hashtagsArray; } const Validator = { /** * 필드 값이 비어있는지 확인하고, 오류 시 메시지를 표시/포커스 이동합니다. * @param {string} value - 검사할 값 * @param {string} errorSelector - 오류 메시지 요소의 CSS 선택자 * @param {HTMLElement} [focusTarget] - 오류 시 포커스를 이동할 요소 (선택적) * @returns {boolean} - 유효성 검사 성공 시 true, 실패 시 false */ validateEmptyField: function (value, errorSelector, focusTarget) { if (!value || value.trim() === "") { $(errorSelector).show(); if (focusTarget) focusTarget.focus(); return false; // 유효성 실패 } else { $(errorSelector).hide(); return true; // 유효성 성공 } }, /** * 특정 조건이 '오류 조건'을 만족하는지 확인합니다. * 주의: 이 함수는 오류 상황(condition === true)일 때 false를 반환하도록 표준화되어야 합니다. * @param {boolean} condition - 오류로 간주될 조건 (true이면 오류) * @param {string} errorSelector - 오류 메시지 요소의 CSS 선택자 * @returns {boolean} - 유효성 검사 성공 시 true, 실패 시 false */ validateCondition: function (condition, errorSelector) { if (condition) { $(errorSelector).hide(); return true; } else { $(errorSelector).show(); return false; } } };