/* ---------- CSS 동적 로드 ---------- */ function loadFunctionCSS(hrefList){ hrefList.forEach(function (href) { var cssLink = $("").attr({ type: "text/css", rel: "stylesheet", href: href }); $("head").append(cssLink); }); } /* ---------- 문서 로드시 ---------- */ 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"] ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { loadFunctionCSS([ "/cover/css/function/layout.css", "/cover/css/experience/experience-common.css", "/cover/css/experience/experience-editor.css" ]); $(".widget").widgster(); $("#sidebar").hide(); $(".wrap").css("margin-left", 0); $("#footer").load("/cover/html/template/landing-footer.html"); $("#modal-root").load("/cover/html/experience/components/confirm-modal.html"); //권한 체크 checkAdminPermissionAndInit(); }) .catch(function (error) { console.error("플러그인 로드 중 오류 발생"); console.error(error); }); } /* ---------- 권한 체크 및 에디터 초기화 ---------- */ function checkAdminPermissionAndInit() { $.ajax({ url: "/auth-user/me", type: "GET", timeout: 7313, global: false, statusCode: { 200: function (json) { console.log("[ experienceEditor :: authCheck ] 로그인 사용자:", json.preferred_username); console.log("[ experienceEditor :: authCheck ] 권한:", json.realm_access.roles); const hasAdminPermission = json.realm_access.roles.includes("ROLE_ADMIN"); if (hasAdminPermission) { initCustomerPostEditor(); } else { console.log("권한 없음. 목록으로 이동") jError("접근 권한이 없습니다."); goToExperienceList(); } }, 401: function () { console.log("비로그인") jError("로그인이 필요합니다."); goToExperienceList(); } }, error: function(xhr) { jError("권한 확인 중 오류가 발생했습니다."); goToExperienceList(); } }); } /* ---------- 목록으로 이동 ---------- */ function goToExperienceList() { const url = new URL(location.href); if (url.searchParams.get('page')) { url.searchParams.set('page', 'experience'); url.searchParams.delete('id'); location.href = url.toString(); } else { location.href = 'experience.html'; } } /* ---------- Tiny Confirm Modal 유틸 ---------- */ function ensureTinyModalLoaded() { return new Promise((resolve) => { const ready = () => resolve(true); if (document.getElementById('tiny-modal')) return ready(); if (!document.getElementById('modal-root')) { const div = document.createElement('div'); div.id = 'modal-root'; document.body.appendChild(div); } $('#modal-root').load('/cover/html/experience/components/confirm-modal.html', () => ready()); }); } async function tinyConfirm({ title = '확인', message = '진행하시겠습니까?', okText = '확인', cancelText = '취소' } = {}) { await ensureTinyModalLoaded(); const modal = document.getElementById('tiny-modal'); const titleEl = document.getElementById('tiny-modal-title'); const msgEl = document.getElementById('tiny-modal-message'); const okBtn = document.getElementById('tiny-modal-ok'); const cancelBtn = document.getElementById('tiny-modal-cancel'); const closeBtn = document.getElementById('tiny-modal-close'); const backdrop = modal.querySelector('.tiny-modal-backdrop'); titleEl.textContent = title; msgEl.innerHTML = message; okBtn.textContent = okText; cancelBtn.textContent = cancelText; modal.classList.add('is-open'); document.body.style.overflow = 'hidden'; return new Promise((resolve) => { const close = (result) => { modal.classList.remove('is-open'); document.body.style.overflow = ''; cleanup(); resolve(result); }; const onOk = () => close(true); const onCancel = () => close(false); const onEsc = (e) => { if (e.key === 'Escape') close(false); }; const onBackdrop = (e) => { if (e.target === backdrop) close(false); }; function cleanup() { okBtn.removeEventListener('click', onOk); cancelBtn.removeEventListener('click', onCancel); closeBtn.removeEventListener('click', onCancel); document.removeEventListener('keydown', onEsc); backdrop.removeEventListener('click', onBackdrop); } okBtn.addEventListener('click', onOk); cancelBtn.addEventListener('click', onCancel); closeBtn.addEventListener('click', onCancel); document.addEventListener('keydown', onEsc); backdrop.addEventListener('click', onBackdrop); }); } /* ============================================================ * 고객사례 등록 에디터 * ============================================================ */ function initCustomerPostEditor() { //페이지 초기 로딩 시 숨김 $('.detail-page').css('opacity', '0'); let updateTitleCounter, updateSubtitleCounter; // CKEditor 초기화 함수 호출 initEditor(); // CKEditor가 준비되면 데이터를 로드 CKEDITOR.on('instanceReady', function(evt) { if (evt.editor.name === 'contents') { // UI 모드 설정 및 데이터 로드 const { isEditing, editingId } = setupEditorModeUI(); if (isEditing) { //수정 모드: 데이터 로드 완료 후 화면 표시 loadEditData(editingId).then(() => { //모든 데이터 로드 완료 후 표시 $('.detail-page').animate({ opacity: 1 }, 300); }); } else { //등록 모드: 바로 표시 $('.detail-page').animate({ opacity: 1 }, 300); } } }); // ======== UI 모드 세팅 ======== function setupEditorModeUI() { const url = new URL(location.href); const editingId = url.searchParams.get('id'); const isEditing = !!editingId; const $title = $('.hero-title'); const $save = $('#btn-save'); if (isEditing) { $title.text('고객사례 수정'); $save.text('수정하기').removeClass('btn-primary').addClass('btn-warning'); document.title = '고객사례 수정'; } else { $title.text('고객사례 등록'); $save.text('등록하기').removeClass('btn-warning').addClass('btn-primary'); document.title = '고객사례 등록'; } return { isEditing, editingId }; } // ======== 단건 불러와 에디터 채우기(수정 모드만) ======== function loadEditData(editingId) { return $.ajax({ url: `/auth-anon/api/cover/experience/getExperience/${encodeURIComponent(editingId)}`, type: 'GET', success: function(res) { const entity = res?.success ? res.response : res; if (!entity || !entity.c_id) return; $('#post-title').val(entity.c_clientcase_title || ''); $('#post-subtitle').val(entity.c_clientcase_subtitle || ''); // 데이터를 읽어온 후 카운터 업데이트 if (updateTitleCounter) updateTitleCounter(); if (updateSubtitleCounter) updateSubtitleCounter(); // CKEDITOR 인스턴스가 존재하는지 확인후 설정 if (CKEDITOR.instances['contents']) { CKEDITOR.instances['contents'].setData(entity.c_clientcase_contents || ''); } else { console.error("CKEditor 인스턴스가 아직 준비되지 않았습니다."); } const thumb = entity.c_clientcase_thumbnail_image || ''; const hidden = document.getElementById('post-thumb-url'); const img = document.getElementById('thumb-img'); const empty = document.querySelector('.thumb-empty'); if (hidden) { hidden.value = thumb; if (thumb && img) { img.src = thumb; img.style.display = 'block'; if (empty) empty.style.display = 'none'; } } }, error: function(xhr) { console.error(xhr); $('.detail-page').animate({ opacity: 1 }, 300); } }); } // --- 썸네일 피커 --- function initThumbnailPicker(){ const file = document.getElementById('thumb-file'); const img = document.getElementById('thumb-img'); const empty = document.querySelector('.thumb-empty'); const hidden = document.getElementById('post-thumb-url'); const preview = document.getElementById('thumb-preview'); const pop = document.getElementById('thumb-pop'); if (!preview || !file || !hidden) return; if (pop) pop.hidden = true; preview.addEventListener('click', (e) => { e.stopPropagation(); if (!hidden.value) { file.click(); return; } if (!pop) return; const r = preview.getBoundingClientRect(); const margin = 8; const popW = 160; pop.style.left = `${Math.min(r.right - popW, window.innerWidth - popW - margin)}px`; pop.style.top = `${r.top + margin}px`; pop.hidden = false; }); pop?.addEventListener('click', (e)=>{ const act = e.target?.dataset?.act; if (!act) return; if (act === 'change') file.click(); if (act === 'remove') clearThumb(); if (act === 'close') closePop(); }); const closePop = ()=> { if (pop && !pop.hidden) pop.hidden = true; }; document.addEventListener('click', closePop); window.addEventListener('scroll', closePop, { passive:true }); window.addEventListener('resize', closePop); file.addEventListener('change', (e) => { const f = e.target.files?.[0]; if (!f) return; if (!/^image\//.test(f.type)) { jError('이미지 파일을 선택하세요.'); return; } // 원본 이미지를 Base64로 변환 const reader = new FileReader(); reader.onload = (e) => { setThumb(e.target.result); }; reader.onerror = () => { jError('파일을 읽을 수 없습니다.'); file.value = ''; }; reader.readAsDataURL(f); closePop(); }); function setThumb(src){ if (img){ img.src = src; img.style.display = 'block'; } if (empty) empty.style.display = 'none'; hidden.value = src; } function clearThumb(){ file.value = ''; if (img){ img.src = ''; img.style.display = 'none'; } if (empty) empty.style.display = 'block'; hidden.value = ''; closePop(); } } /* ---------- 저장/취소 ---------- */ document.getElementById("btn-cancel")?.addEventListener("click", async ()=>{ const ok = await tinyConfirm({ title: '작성 취소', message: '작성 중인 내용을 취소하고 나가시겠습니까?', okText: '나가기', cancelText: '계속 작성' }); if (!ok) return; goToExperienceList(); }); /* ---------- 저장 버튼(#btn-save) ---------- */ function bindExperienceSave() { const $saveBtn = $("#btn-save"); if ($saveBtn.length === 0) return; $saveBtn.on("click", function (e) { e.preventDefault(); const title = $("#post-title").val() || ""; const subtitle = $("#post-subtitle").val() || ""; const contents = CKEDITOR.instances['contents'].getData(); const thumbnail_image = $("#post-thumb-url").val() || ""; if (!title.trim()) { jError("제목을 입력하세요."); return; } if (title.length > 255) { jError("제목은 최대 255자까지 입력 가능합니다."); return; } if (!subtitle.trim()) { jError("설명을 입력하세요."); return; } if (subtitle.length > 255) { jError("설명은 최대 255자까지 입력 가능합니다."); return; } if (!contents.trim()){ jError("내용을 입력하세요."); return; } if (!thumbnail_image.trim()){ jError("썸네일을 지정해주세요."); return; } const basePayload = { c_clientcase_title: title, c_clientcase_contents: contents, c_clientcase_category: "고객사례", c_clientcase_visibility: "PUBLIC", c_clientcase_subtitle: subtitle, c_clientcase_thumbnail_image: thumbnail_image, c_clientcase_desc: "" }; const urlObj = new URL(location.href); const editingId = urlObj.searchParams.get('id'); let url, method, payload; if (editingId) { url = "/auth-anon/api/cover/experience/updateExperience"; method = "PUT"; payload = { ...basePayload, c_id: editingId }; } else { url = "/auth-anon/api/cover/experience/addExperience"; method = "POST"; payload = { ...basePayload, c_title: title.substring(0, 255), ref: 2, c_type: "default" }; } const orig = $saveBtn.text(); $saveBtn.prop("disabled", true).text("저장 중..."); $.ajax({ url, type: method, data: payload, success: function(res){ $saveBtn.prop("disabled", false).text(orig); if (res?.success) { jSuccess(editingId ? "수정되었습니다." : "등록되었습니다."); goToExperienceList(); } else { jError("요청은 처리됐지만 응답 포맷을 확인하세요."); console.log(res); } }, error: function(xhr){ $saveBtn.prop("disabled", false).text(orig); console.error(xhr); jError("요청 중 오류가 발생했습니다."); } }); }); } function initEditor() { console.log("ckEditor") var waitCKEDITOR = setInterval(function () { try { if (window.CKEDITOR) { if (window.CKEDITOR.status == "loaded") { CKEDITOR.replace("contents", { contentsCss: "/cover/css/contents.css" }); clearInterval(waitCKEDITOR); } } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 초기화 재시도 중..."); } }, 313); } // ======== 글자 수 카운터 ======== function initCharacterCounter() { const $title = $("#post-title"); const $subtitle = $("#post-subtitle"); const $titleCounter = $("#title-counter"); const $subtitleCounter = $("#subtitle-counter"); updateTitleCounter = function() { const len = $title.val().length; $titleCounter.text(`${len}/255`); $titleCounter.removeClass('danger'); if (len > 255) { $titleCounter.addClass('danger'); } }; updateSubtitleCounter = function() { const len = $subtitle.val().length; $subtitleCounter.text(`${len}/255`); $subtitleCounter.removeClass('danger'); if (len > 255) { $subtitleCounter.addClass('danger'); } }; $title.on('input', updateTitleCounter); $subtitle.on('input', updateSubtitleCounter); // 초기 카운터 표시 updateTitleCounter(); updateSubtitleCounter(); } initThumbnailPicker(); bindExperienceSave(); initCharacterCounter(); }