//////////////////////////////////////////////////////////////////////////////////////// // A-RMS Patch Note Page JavaScript - FF14 Pure Style with Infinite Scroll //////////////////////////////////////////////////////////////////////////////////////// var globalAuthorUserName = ""; //////////////////////////////////////////////////////////////////////////////////////// // 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"], ["./css/patchnote/patch-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 globalUpdateNoteId = new URLSearchParams(window.location.search).get("id"); function initializePatchEditor() { // 패치에디터 페이지 제목 초기화 function initializeBaseUI() { $("#patch-editor-title").text(isEditMode() ? "업데이트 노트 수정" : "신규 업데이트 노트 등록"); $("#footer-publish-btn #publish-text").text(isEditMode() ? "수정" : "등록"); $("#footer-publish-btn").addClass(isEditMode() ? "btn-success" : "btn-primary"); // CKEditor 스크립트 로드 onLoadCKEditor(); } // 수정 모드일 경우 기존 패치 데이터 로드 function initializePatchNoteDetail() { if (!isEditMode()) return; var patchId = globalUpdateNoteId; if (!patchId) { showFullScreenError("not-found"); return; } function renderPatchnoteDetail(patchData) { if (!patchData) { showFullScreenError("not-found"); return; } const { c_id, c_patchnote_author_id, c_patchnote_title, c_patchnote_subtitle, c_patchnote_thumbnail_url, c_patchnote_contents } = patchData; $("#patch-id").val(c_id); $("#patch-title").val(c_patchnote_title); $("#patch-subtitle").val(c_patchnote_subtitle); $("#patch-author-id").val(c_patchnote_author_id); if (c_patchnote_thumbnail_url) { const imgElement = document.createElement("img"); imgElement.src = c_patchnote_thumbnail_url; $("#preview").append(imgElement); } let editor_instance_wait = setInterval(function () { try { var editor_instance = CKEDITOR.instances['editor']; if (editor_instance) { CKEDITOR.instances.editor.setData(c_patchnote_contents); CKEDITOR.instances.editor.setReadOnly(false); clearInterval(editor_instance_wait); } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 재시도 중..."); } }, 313 /*milli*/); } $.ajax({ url: "/auth-anon/api/arms/patchnote/getNode.do", data: { c_id: patchId }, method: "GET", dataType: "json", success: function (patchData) { renderPatchnoteDetail(patchData); }, error: function (xhr, status, error) { console.error("패치노트 데이터 로드 중 오류 발생:", error); } }); } // 이벤트 핸들러 등록 function defineEvents() { function onClickSumbit() { const ENDPOINT = "/auth-user/api/arms/patchnote/" + (isEditMode() ? "updatePatchnote.do" : "addPatchnote.do"); const METHOD = isEditMode() ? "PUT" : "POST"; const requestBody = { c_id: $("#patch-id").val(), c_patchnote_author_id: $("#patch-author-id").val() || globalAuthorUserName, c_patchnote_title: $("#patch-title").val(), c_patchnote_subtitle: $("#patch-subtitle").val() || editor.getData().substring(0, 20), c_patchnote_thumbnail_url: $("#preview img").attr("src") || "", c_patchnote_contents: editor.getData() }; const isTitleValid = Validator.validateEmptyField( requestBody.c_patchnote_title, "#patchnote-title-error", $("#patch-title") ); const isContentValid = Validator.validateEmptyField(requestBody.c_patchnote_contents, "#patchnote-content-error"); if (!isTitleValid || !isContentValid) return; $.ajax({ url: ENDPOINT, method: METHOD, contentType: "application/json", data: JSON.stringify(requestBody), success: function (response) { jSuccess("패치노트가 성공적으로 " + (isEditMode() ? "수정" : "등록") + "되었습니다."); setTimeout(() => { window.location.href = "/cover/template.html?page=patchnoteDetail&id=" + response.c_id; }, 1500); }, error: function (xhr, status, error) { jError("요청을 실패했습니다. 다시 시도하거나 관리자에게 문의해주세요."); } }); } function onChangeThumbnail(e) { const file = e.target.files[0]; if (file) { if (!Validator.validateCondition(isImage(file), "#patchnote-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 onConfirmCancel() { let routeUrl = "/cover/template.html?page=patchnote"; if (isEditMode()) routeUrl += "Detail&id=" + globalUpdateNoteId; $("#confirmModal").modal("hide"); window.location.href = routeUrl; } $("#footer-cancel-btn").on("click", () => $("#confirmModal").modal("show")); $("#cancel-confirm-yes").on("click", onConfirmCancel); $("#footer-publish-btn").on("click", onClickSumbit); $("input[name='thumbnail']").on("change", onChangeThumbnail); } // call initializeBaseUI(); initializePatchNoteDetail(); defineEvents(); } /** * 유틸성, 여러 곳에서 사용되는 함수는 가장 바깥에 분리, 나머지는 함수 내부로 이동. */ function onLoadCKEditor() { var waitCKEDITOR = setInterval(function () { try { if (window.CKEDITOR) { if (window.CKEDITOR.status === "loaded") { editor = CKEDITOR.replace("editor", { contentsCss: "./css/contents.css" }); clearInterval(waitCKEDITOR); } } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 초기화 재시도 중..."); } }, 313 /*milli*/); } function isEditMode() { var patchId = new URLSearchParams(window.location.search).get("id"); return Boolean(patchId); } function updateAuthorId(authorUserName) { if ($("#patch-author-id").val().trim() === "") { $("#patch-author-id").val(authorUserName); } globalAuthorUserName = authorUserName; } function isImage(file) { return file && file.type.startsWith("image/"); } function showFullScreenError(errorCase, targetArgs = $(".patch-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 validateAuthorization() { function valid(json) { // admin 아이디를 등록자 아이디로 세팅 updateAuthorId(json.preferred_username); initializePatchEditor(); } function invalid() { showFullScreenError("not-authorized"); } function error() { showFullScreenError("default"); } validateAdminRole(valid, invalid, error); } 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; } } };