// 외부 jQuery 및 플러그인 로드 함수 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-detail.css" ]); $(".widget").widgster(); $("#sidebar").hide(); $(".wrap").css("margin-left", 0); $("#footer").load("/cover/html/template/landing-footer.html"); }) .catch(function (error) { console.error("플러그인 로드 중 오류 발생"); console.error(error); }); } // 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='취소', okType='primary' // 'primary', 'danger', 'success' } = {}) { 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; // 기존 클래스 제거 후 새 클래스 추가 okBtn.className = ''; okBtn.classList.add('modal-btn-ok', `modal-btn-${okType}`); 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 () { 'use strict'; // 전역 변수 var isAdmin = false; // --- 유틸리티 및 헬퍼 함수 --- function getParam(name) { return new URL(location.href).searchParams.get(name); } function escapeHTML(s) { return String(s || "").replace(/[&<>"']/g, m => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[m])); } function buildPocHref() { const url = new URL(location.href); if (url.searchParams.get('page')) { url.searchParams.set('page', 'poc'); url.searchParams.delete('id'); return url.toString(); } return 'poc.html'; } function redirectToExperienceList() { const url = new URL(location.href); if (url.searchParams.get('page')) { url.searchParams.set('page', 'experience'); url.searchParams.delete('id'); window.location.href = url.toString(); } else { window.location.href = 'experience.html'; } } /* ---------- 권한 체크 ---------- */ function experienceDetailAuthCheck() { return $.ajax({ url: "/auth-user/me", type: "GET", timeout: 7313, global: false, statusCode: { 200: function (json) { console.log("[ experienceDetail :: authCheck ] userName = " + json.preferred_username); console.log("[ experienceDetail :: authCheck ] roles = " + json.realm_access.roles); const hasAdminPermission = json.realm_access.roles.includes("ROLE_ADMIN"); if (hasAdminPermission) { isAdmin = true; showAdminButtons(); } else { isAdmin = false; hideAdminButtons(); } }, 401: function () { // 비로그인 사용자 console.log("비로그인 사용자 - 수정/삭제 버튼 숨김"); isAdmin = false; hideAdminButtons(); } } }); } function showAdminButtons() { $("#btn-modify-experience").show(); $("#btn-delete-experience").show(); console.log("관리자 권한 확인 - 수정/삭제 버튼 표시"); } function hideAdminButtons() { $("#btn-modify-experience").hide(); $("#btn-delete-experience").hide(); console.log("수정/삭제 버튼 숨김"); } // --- HTML 템플릿 함수 --- function shareBlockHtml() { return `
`; } function heroBlockHtml(src) { const safeSrc = src || '/cover/img/logo.png'; return `
썸네일
`; } // 이미지 비율에 따라 object-fit 조정 function adjustDetailImageObjectFit() { $('.detail-hero-image').each(function() { const img = this; if (img.complete && img.naturalWidth > 0) { applyDetailObjectFit(img); } else { $(img).on('load', function() { applyDetailObjectFit(this); }); } }); } function applyDetailObjectFit(img) { const ratio = img.naturalWidth / img.naturalHeight; // 16:10 비율 = 1.6, 허용 범위 1.4 ~ 1.8 (16:9 ≈ 1.78 포함) if (ratio >= 1.4 && ratio <= 1.8) { // 가로로 긴 이미지: cover 사용 img.style.objectFit = 'cover'; img.style.maxHeight = 'none'; } else { // 세로로 긴 이미지나 특이한 비율: contain img.style.objectFit = 'contain'; img.style.maxHeight = '730px'; } } // --- 소셜 미디어 공유 함수 --- async function shareToSocial(platform, title, url, description, thumbnailUrl) { const encodedUrl = encodeURIComponent(url); const encodedTitle = encodeURIComponent(title); const encodedDesc = encodeURIComponent(description || ''); let shareUrl = ''; switch(platform) { case 'facebook': // Facebook Share Dialog shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`; break; case 'twitter': // Twitter/X Share const twitterText = encodeURIComponent(`${title}`); shareUrl = `https://twitter.com/intent/tweet?text=${twitterText}&url=${encodedUrl}`; break; case 'linkedin': // LinkedIn Share (제목, 설명, 출처 포함) const source = encodeURIComponent('313devgrp'); shareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}&summary=${encodedDesc}&source=${source}`; break; case 'link': // 링크 복사 navigator.clipboard.writeText(url).then(() => { jSuccess('링크가 복사되었습니다!'); }).catch(() => { jError('링크 복사에 실패했습니다.'); }); return; default: return; } if (shareUrl) { window.open(shareUrl, '_blank'); } } // --- API 호출 함수 --- function fetchExperienceById(id) { return $.ajax({ url: `/auth-anon/api/cover/experience/getExperience/${encodeURIComponent(id)}`, type: 'GET' }).then(function(res){ if (res && res.success && res.response) { return res.response; } }); } function removeExperienceById(id) { return $.ajax({ url: `/auth-anon/api/cover/experience/removeExperience/${encodeURIComponent(id)}`, type: 'DELETE' }); } // --- Open Graph 태그 업데이트 함수 --- function updateOpenGraphTags(title, description, imageUrl, url) { // 기존 OG 태그 제거 $('meta[property^="og:"]').remove(); $('meta[name^="twitter:"]').remove(); // 이미지 URL 절대 경로로 변환 let absoluteImageUrl = imageUrl; if (imageUrl && !imageUrl.startsWith('http')) { absoluteImageUrl = window.location.origin + imageUrl; } // 새 OG 태그 추가 const ogTags = [ { property: 'og:type', content: 'article' }, { property: 'og:title', content: title }, { property: 'og:description', content: description }, { property: 'og:image', content: absoluteImageUrl }, { property: 'og:image:width', content: '1200' }, { property: 'og:image:height', content: '630' }, { property: 'og:url', content: url }, { property: 'og:site_name', content: '313devgrp' }, { property: 'og:locale', content: 'ko_KR' }, { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:title', content: title }, { name: 'twitter:description', content: description }, { name: 'twitter:image', content: absoluteImageUrl }, { name: 'author', content: '313devgrp' } ]; ogTags.forEach(tag => { const meta = $(''); if (tag.property) meta.attr('property', tag.property); if (tag.name) meta.attr('name', tag.name); meta.attr('content', tag.content); $('head').append(meta); }); } // --- 렌더링 함수 --- function renderDetail($root, entity) { const id = entity.c_id; const title = entity.c_clientcase_title; const subtitle = entity.c_clientcase_subtitle || ''; const category = entity.c_clientcase_category || 'client_case'; const rawDateObject = entity.c_clientcase_created; let date = ''; // 생성일자 if (rawDateObject && typeof rawDateObject === 'object' && rawDateObject.year) { const year = rawDateObject.year; const month = String(rawDateObject.monthValue).padStart(2, '0'); const day = String(rawDateObject.dayOfMonth).padStart(2, '0'); date = `${year}-${month}-${day}`; } const thumb = entity.c_clientcase_thumbnail_image; const contents = entity.c_clientcase_contents || ''; // 설명 텍스트 추출 (HTML 태그 제거) const tempDiv = $('
').html(contents); const description = tempDiv.text().substring(0, 200).trim() + '...'; // Open Graph 태그 업데이트 updateOpenGraphTags( title, description, thumb || '/cover/img/logo.png', window.location.href ); const header = `
${escapeHTML(title || "")}
${subtitle ? `
${escapeHTML(subtitle)}
` : ''}
${escapeHTML(category)}
|
${escapeHTML(date)}
${shareBlockHtml()}
`; const body = ` ${heroBlockHtml(thumb)}
${contents}
`; const footer = `
글이 마음에 드셨나요? 공유하기
${shareBlockHtml()}
A-RMS가 궁금하다면? 지금 무료로 체험해 보세요
`; $root .data('currentItem', { id, title, description, thumbnail: thumb }) .html(header + body + footer); // 이미지 로드 후 비율에 따라 object-fit 조정 adjustDetailImageObjectFit(); } // --- 자동 마운트 --- $(function () { console.log('experienceDetail] Init'); // DOM이 완전히 로드될 때까지 대기 function waitForElement() { const $host = $('#experience-detail'); if (!$host.length) { console.log("experienceDetail not found ==> retrying"); setTimeout(waitForElement, 100); // 100ms 후 재시도 return; } const id = parseInt(getParam('id'), 10); if (!id) { $host.html('
잘못된 접근입니다. (id 누락)
'); return; } hideAdminButtons(); // 권한 체크 먼저 실행 experienceDetailAuthCheck().always(function() { // 권한 체크 완료 후 데이터 로드 fetchExperienceById(id) .then(entity => { if (!entity || !entity.c_id) { $host.html('
해당 게시물을 찾을 수 없습니다.
'); return; } renderDetail($host, entity); }) .catch(err => { console.error(err); $host.html('
콘텐츠를 불러오지 못했습니다.
'); }); }); } // 초기 실행 waitForElement(); // 이벤트 리스너: 공유 버튼 $(document).on('click', '.share-btn', function(e) { e.preventDefault(); const platform = $(this).data('platform'); const $host = $('#experience-detail'); const item = $host.data('currentItem'); if (!item) { jError('공유할 콘텐츠 정보를 찾을 수 없습니다.'); return; } const currentUrl = window.location.href; const title = item.title || 'A-RMS 경험 공유'; const description = item.description || 'A-RMS의 프로젝트 경험을 확인해보세요.'; const thumbnailUrl = item.thumbnail; shareToSocial(platform, title, currentUrl, description, thumbnailUrl); }); // 이벤트 리스너: 목록으로 버튼 $(document).on('click', '#btn-back-to-list', function () { redirectToExperienceList(); }); // 이벤트 리스너: 수정 버튼 $(document).on('click', '#btn-modify-experience', function () { if (!isAdmin) { jError('권한이 없습니다.'); return; } const $host = $('#experience-detail'); const item = $host.data('currentItem'); if (!item) { jError('콘텐츠 정보를 찾을 수 없습니다.'); return; } sessionStorage.setItem('experienceEditDraft', JSON.stringify({ id: item.id, title: item.title || '' })); const url = new URL(location.href); url.searchParams.set('page', 'experienceEditor'); url.searchParams.set('id', String(item.id)); location.href = url.toString(); }); // 이벤트 리스너: 삭제 버튼 $(document).on('click', '#btn-delete-experience', async function () { if (!isAdmin) { jError('권한이 없습니다.'); return; } const $btn = $(this); const $host = $('#experience-detail'); const item = $host.data('currentItem'); if (!item) { jError('콘텐츠 정보를 찾을 수 없습니다.'); return; } const ok = await tinyConfirm({ title: '삭제 확인', message: '정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.', okText: '삭제', cancelText: '취소', okType: 'danger' }); if (!ok) return; $btn.prop('disabled', true); try { const res = await removeExperienceById(item.id); if (res?.success) { jSuccess('삭제되었습니다.'); redirectToExperienceList(); } else { jError('삭제는 되었지만 응답 포맷을 확인하세요.'); } } catch (err) { console.error(err); jError('삭제 중 오류가 발생했습니다.'); } finally { $btn.prop('disabled', false); } }); // 이벤트 리스너: 카테고리 클릭 $(document).on('click', '.meta-cat', function(e) { e.preventDefault(); redirectToExperienceList(); }); //이벤트 리스너 : poc이동하기 버튼 $(document).on('click', '#btn-apply', function(e) { e.preventDefault(); window.location.href = buildPocHref(); }); }); })();