$(document).ready(function () { 'use strict'; // ========================= // Config & Selectors // ========================= const API_BASE_URL = `${typeof hostURL !== 'undefined' ? hostURL : ''}/ajax/api.php`; const SELECTOR = { viewList: '#viewList', viewWrite: '#viewWrite', viewDetail: '#viewDetail', postList: '#postList', writeForm: '#writeForm', categorySelect: '#categorySelect', boardPagination: '#boardPagination', boardCategory: '#board_catagory' }; const urlParams = new URLSearchParams(window.location.search); const modeParam = urlParams.get('mode'); const idParam = urlParams.get('id'); const currentPageFileName = window.location.pathname.split('/').pop(); if (currentPageFileName.includes('manager')) { var current_page_manager = currentPageFileName; var current_page = currentPageFileName.replace('_manager', ''); } else { var current_page = currentPageFileName; var current_page_manager = currentPageFileName.replace('.html', '_manager.html'); } // 페이지 관련 변수 let currentDetailRaw = null; const PAGE_SIZE = 20; let currentPostData = []; let currentPage = 1; let totalPageCount = 1; let totalPostCount = 0; let currentSearchType = null; let currentSearchKeyword = null; let currentCategory = null; var rowListCount = 20; var rowImageCount = 4; // 파일 업로드 관련 상태 let dt = new DataTransfer(); let existingFiles = []; let deletedFileIds = []; // DOM 캐싱 const $dropZone = $('#file-upload-box'); const $fileInput = $('#fileInput'); const $fileListContainer = $('#fileListContainer'); const $fileItemsList = $('#fileItemsList'); const $fileCount = $('#fileCount'); const $fileTotalSize = $('#fileTotalSize'); // ========================= // Init & Event Binding // ========================= initPage(); bindEvents(); // ========================= // Core & Page Init // ========================= function initPage() { if ($(SELECTOR.viewList).length) { loadPosts(); } if ($(SELECTOR.viewWrite).length || $(SELECTOR.viewDetail).length) { if (modeParam === 'detail' && idParam) { showPostDetail(idParam); } else if (modeParam === 'edit' && idParam) { loadCategoryOptions().then(() => loadPostForEdit(idParam)); } else { loadCategoryOptions().then(() => { resetWriteForm(); showWriteView(); }); } } } // ========================= // Core & Event Init // ========================= function bindEvents() { $('#btnGoWrite').on('click', () => { location.href = `${current_page_manager}?mode=write`; }); $('#btnSearch').on('click', onClickSearch); $('#searchForm').on('submit', onSubmitSearch); // 체크박스 & 선택 삭제 $('#checkAll').on('change', onChangeCheckAll); $(document).on('change', '.row-check', onChangeRowCheck); $('#btnDeleteSelected').on('click', onClickDeleteSelected); // 분류 추가/설정 모달 $('#categoryForm').on('submit', onSubmitCategoryForm); $('#boardSetupModal').on('show.bs.modal', loadboardSetup); $('#btnSaveBoardSetup').on('click', onClickSaveBoardSetup); // 이벤트 위임 $(document) .on('click', '#postList .title-cell', onClickTitleCell) .on('click', '#board_catagory .category-pill-btn', onClickCategory) .on('click', '#boardPagination .page-link', onClickPageBtn) .on('click', '#btnBackToList', () => { location.href = `${current_page}`; }) .on('click', '#btnDeleteCurrentPost', onClickDeleteCurrent) .on('click', '#btnEditCurrentPost', onClickEditCurrent); // 작성 폼 관련 $('#btnCancelWrite, #btnCancelWriteBottom').on('click', onClickCancelWrite); $(SELECTOR.writeForm).on('submit', onSubmitWriteForm); // 드래그 앤 드롭 & 파일 인풋 $fileInput.on('change', function () { if (this.files?.length) appendFiles(this.files); }); $dropZone .on('dragover dragenter', (e) => { e.preventDefault(); e.stopPropagation(); $dropZone.addClass('is-dragover'); }) .on('dragleave dragend drop', (e) => { e.preventDefault(); e.stopPropagation(); $dropZone.removeClass('is-dragover'); }) .on('drop', (e) => { const droppedFiles = e.originalEvent.dataTransfer.files; if (droppedFiles?.length) appendFiles(droppedFiles); }); $fileItemsList .on('click', '.btn-remove-file', onRemoveNewFile) .on('click', '.btn-remove-existing-file', onRemoveExistingFile); $('#btnClearAllFiles').on('click', onClearAllFiles); } function getBoardName() { return $('#board_name').val() || 'post_notice'; } // ========================= // Util Functions // ========================= function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function resolveFileUrl(rawPath) { if (!rawPath) return ''; if (/^(https?:)?\/\//i.test(rawPath)) return rawPath; const base = (typeof hostURL !== 'undefined' ? hostURL : '').replace(/\/+$/, ''); return `${base}/${String(rawPath).replace(/^\/+/, '')}`; } function extractFirstContentImageUrl(content) { if (!content) return ''; try { const doc = new DOMParser().parseFromString(content, 'text/html'); const img = doc.querySelector('img'); const src = img ? (img.getAttribute('src') || '') : ''; return src ? resolveFileUrl(src) : ''; } catch (e) { return ''; } } function getGalleryThumbnailUrl(item) { if (item.first_image_attachment?.file_path) { return resolveFileUrl(item.first_image_attachment.file_path); } return extractFirstContentImageUrl(item.content); } // ========================= // File Management // ========================= function updateFileListUI() { const newFiles = dt.files; const totalCount = existingFiles.length + newFiles.length; if (totalCount === 0) { $fileItemsList.empty(); $fileListContainer.addClass('d-none'); $fileInput[0].files = dt.files; return; } $fileListContainer.removeClass('d-none'); $fileCount.text(totalCount); let totalBytes = 0; let htmlBuffer = ''; // 1. 기존 파일 렌더링 existingFiles.forEach((file, index) => { const size = Number(file.file_size || file.size || 0); totalBytes += size; const fileName = file.origin_name || file.original_name || file.name || '첨부파일'; htmlBuffer += `
${escapeHtml(fileName)} 기존 파일 (${formatBytes(size)})
`; }); // 2. 신규 파일 렌더링 Array.from(newFiles).forEach((file, index) => { totalBytes += file.size; htmlBuffer += `
${escapeHtml(file.name)} 신규 (${formatBytes(file.size)})
`; }); $fileItemsList.html(htmlBuffer); $fileTotalSize.text(formatBytes(totalBytes)); $fileInput[0].files = dt.files; } function appendFiles(newFiles) { const existingMap = new Set(Array.from(dt.files).map(f => `${f.name}_${f.size}`)); for (const file of newFiles) { if (!existingMap.has(`${file.name}_${file.size}`)) { dt.items.add(file); existingMap.add(`${file.name}_${file.size}`); } } updateFileListUI(); } function onRemoveNewFile(e) { e.stopPropagation(); const removeIdx = $(this).data('index'); const newDt = new DataTransfer(); Array.from(dt.files).forEach((file, idx) => { if (idx !== removeIdx) newDt.items.add(file); }); dt = newDt; updateFileListUI(); } function onRemoveExistingFile(e) { e.stopPropagation(); const removeIdx = $(this).data('index'); const removedItem = existingFiles.splice(removeIdx, 1)[0]; const fileId = removedItem?.attachment_id || removedItem?.file_id || removedItem?.id; if (fileId) deletedFileIds.push(fileId); updateFileListUI(); } function onClearAllFiles(e) { e.stopPropagation(); existingFiles.forEach(f => { const fileId = f.attachment_id || f.file_id || f.id; if (fileId) deletedFileIds.push(fileId); }); existingFiles = []; dt = new DataTransfer(); updateFileListUI(); } function loadExistingFilesToDataTransfer(attachments = []) { existingFiles = attachments.map(item => ({ ...item, fileUrl: resolveFileUrl(item.download_url || item.file_path || '') })); deletedFileIds = []; updateFileListUI(); } // ========================= // Category & Settings Logic // ========================= function loadCategoryOptions() { return $.ajax({ url: `${API_BASE_URL}?action=category_list&board_name=${encodeURIComponent(getBoardName())}`, method: 'GET', dataType: 'json' }).then(res => { const list = res.boardInfo?.category_list; const categories = (typeof list === 'string' ? list.split(',') : (list || [])) .map(c => c.trim()) .filter(Boolean); const $select = $(SELECTOR.categorySelect); if ($select.length) { let options = ''; categories.forEach(cat => { options += ``; }); $select.html(options); } return categories; }).catch(err => { console.error('카테고리 로드 실패:', err); return []; }); } function onClickSaveBoardSetup() { $.ajax({ url: `${API_BASE_URL}?action=save_board_setup`, method: 'POST', data: $('#boardSetupForm').serialize(), dataType: 'json', success(res) { if (!res.success) { alert(res.message || '게시판 설정 저장에 실패했습니다.'); return; } alert('게시판 설정이 저장되었습니다.'); $('#boardSetupModal').modal('hide'); }, error: () => alert('게시판 설정 저장 중 통신 오류가 발생했습니다.') }); } function loadboardSetup() { $.ajax({ url: `${API_BASE_URL}?action=board_setup&board_name=${encodeURIComponent(getBoardName())}`, method: 'GET', dataType: 'json', success(res) { if (!res.success || !res.boardInfo) return; const info = res.boardInfo; const $form = $('#boardSetupForm'); ['board_id', 'board_name', 'category_list', 'function_settings', 'display_settings', 'file_attachment_policy'].forEach(name => { $form.find(`input[name="${name}"]`).val(info[name] || ''); }); ['is_active', 'use_comment_yn', 'use_category_yn', 'file_upload_yn'].forEach(name => { $form.find(`input[name="${name}"]`).prop('checked', info[name] === 'Y'); }); }, error: () => alert('게시판 설정 조회 중 오류가 발생했습니다.') }); } function onSubmitCategoryForm(e) { e.preventDefault(); $.ajax({ url: `${API_BASE_URL}?action=category_update`, method: 'POST', data: { board_name: getBoardName(), category_list: $('#newCategoryName').val() || '' }, dataType: 'json', success(res) { if (res.success) { alert('분류가 업데이트되었습니다.'); location.reload(); } else { alert(res.message || '분류 업데이트에 실패했습니다.'); } }, error: () => alert('서버와 통신 중 오류가 발생했습니다.') }); } // ========================= // View Switching & Post Edit // ========================= function showWriteView() { $(SELECTOR.viewDetail).hide(); $(SELECTOR.viewWrite).show(); $('#wr_board_name').val(getBoardName()); window.scrollTo(0, 0); } function showDetailView() { $(SELECTOR.viewWrite).hide(); $(SELECTOR.viewDetail).show(); window.scrollTo(0, 0); } function setWriteMode(mode, postId = '') { const $form = $(SELECTOR.writeForm); $form.attr({ 'data-mode': mode, 'data-post-id': postId }); $('#write_postid').val(postId); const isEdit = mode === 'edit'; $('#write_title').text(isEdit ? '게시글 수정' : '게시글 작성'); $('#btnSubmit').html(` ${isEdit ? '수정' : '저장'} 완료`); } function resetWriteForm() { if (typeof $.fn.summernote !== 'undefined' && $('#post_content').length) { initSummernote(); } const $form = $(SELECTOR.writeForm); if ($form.length) { $form[0].reset(); const $content = $('#post_content'); $content.data('summernote') ? $content.summernote('reset') : $content.val(''); } setWriteMode('create', ''); } async function populateEditForm(data, postId) { resetWriteForm(); setWriteMode('edit', postId); await loadCategoryOptions(); $('input[name="title"]').val(data.title || ''); $('input[name="author"]').val(data.author || ''); const $content = $('#post_content'); const postContent = data.content || ''; if ($content.data('summernote')) { $content.summernote('code', postContent); } else { $content.val(postContent); } if (data.category) { $('#categorySelect').val(data.category); } loadExistingFilesToDataTransfer(data.attachments || []); showWriteView(); } // ========================= // List & Detail Logic // ========================= // 게시글 목록 조회 function loadPosts(searchType, searchKeyword, category, page) { // 검색/분류/페이지 상태를 보관해 페이지 전환 시 동일한 조건으로 재조회 const currentSearchType = searchKeyword ? (searchType || 'title') : null; const currentSearchKeyword = searchKeyword || null; const currentCategory = (category && category !== 'all') ? category : null; const params = { action: 'list', board_name: getBoardName(), page: page || 1, page_size: PAGE_SIZE }; if (currentSearchKeyword) { params.search_type = currentSearchType; params.search_keyword = currentSearchKeyword; } if (currentCategory) { params.category = currentCategory; } $.ajax({ url: API_BASE_URL, method: 'GET', data: params, dataType: 'json', success(res) { if (!res.success) { alert(res.message || '목록 조회에 실패했습니다.'); return; } currentPostData = res.data || []; currentPage = res.currentPage || params.page; totalPageCount = res.totalPages || 1; totalPostCount = res.totalCount || currentPostData.length; renderPostList(currentPostData); if (res.boardInfo) renderBoardInfo(res.boardInfo, category || 'all'); }, error: () => alert('목록 조회 중 통신 오류가 발생했습니다.') }); } function renderBoardInfo(boardInfo, activeCategory = 'all') { const $categoryContainer = $(SELECTOR.boardCategory); if (!$categoryContainer.length) return; const categories = (boardInfo.category_list || '').split(',').map(c => c.trim()).filter(Boolean); const counts = boardInfo.category_counts || {}; let buttons = ` `; categories.forEach(cat => { const isActive = activeCategory === cat; buttons += ` `; }); $categoryContainer.html(buttons); } function renderPostList(data) { // 서버에서 이미 현재 페이지분만 조회해 내려주므로 클라이언트에서 추가로 자르지 않는다 currentPostData = data || []; const $list = $(SELECTOR.postList).empty(); $('#checkAll').prop('checked', false); if (!currentPostData.length) { $list.html(''); renderPagination(totalPostCount, currentPage, totalPageCount); return; } const cards = currentPostData.map(item => { const category = item.category ? escapeHtml(item.category) : '전체'; const title = escapeHtml(item.title || '제목 없음'); const author = escapeHtml(item.author || '작성자 미상'); const date = item.created_at ? item.created_at.substring(0, 10) : '-'; const thumb = getGalleryThumbnailUrl(item); return ` `; }).join(''); $list.html(cards); renderPagination(totalPostCount, currentPage, totalPageCount); } // 페이지 네비게이션 렌더링 함수 function renderPagination(totalCount, page, totalPages) { const $nav =$(SELECTOR.boardPagination); // 페이지 네비게이션이 필요 없는 경우 처리 if (!$nav.length || !totalCount || totalPages <= 1) { $nav.empty(); return; } // 한 그룹에 표시할 페이지 수 const GROUP_SIZE = 5; // 현재 그룹의 시작 페이지 번호 계산 const groupStart = Math.floor((page - 1) / GROUP_SIZE) * GROUP_SIZE + 1; // 현재 그룹의 끝 페이지 번호 계산 const groupEnd = Math.min(groupStart + GROUP_SIZE - 1, totalPages); // Bootstrap 표준 구조: li.page-item > a.page-link let pageButtons = ''; for (let p = groupStart; p <= groupEnd; p++) { const isActive = p === page ? ' active' : ''; pageButtons += `
  • ${p}
  • `; } const isFirst = page === 1; const isLast = page === totalPages; $nav.html(`
    Total ${totalCount} pages
    `); } function showPostDetail(postId) { if (!postId) return; $.ajax({ url: `${API_BASE_URL}?action=detail&id=${postId}&board_name=${encodeURIComponent(getBoardName())}`, method: 'GET', dataType: 'json', success(res) { if (!res.success) { alert(res.message || '게시글을 불러올 수 없습니다.'); location.href = `${current_page}`; return; } const data = res.data; currentDetailRaw = data; $('#detailTitle').text(data.title || ''); $('#detailCategory').text(data.category || ''); $('#detailAuthor').text(data.author || ''); $('#detailDate').text(data.created_at ? data.created_at.substring(0, 10) : '-'); $('#detailViews').text(data.view_count || 0); $('#detailPostId').text(postId); $('#write_postid').val(postId); $('#detailContent').html(data.content || ''); const attachments = data.attachments || []; const $attachmentArea = $('#detailAttachmentArea'); const $attachmentList = $('#detailAttachments').empty(); if (attachments.length) { const listItems = attachments.map(file => { const fileName = file.origin_name || file.original_name || file.name || '첨부파일'; const fileUrl = resolveFileUrl(file.download_url || file.file_path || ''); const fileSize = file.size_formatted || (file.size ? `${(file.size / 1024).toFixed(1)} KB` : ''); return `
  • ${escapeHtml(fileName)}
    ${fileSize ? `${escapeHtml(fileSize)}` : ''}
  • `; }).join(''); $attachmentList.html(listItems); $attachmentArea.show(); } else { $attachmentArea.hide(); } showDetailView(); }, error: () => { alert('게시글 조회 중 오류가 발생했습니다.'); location.href = `${current_page}`; } }); } function loadPostForEdit(postId) { $.ajax({ url: `${API_BASE_URL}?action=detail&id=${postId}&board_name=${encodeURIComponent(getBoardName())}`, method: 'GET', dataType: 'json', success(res) { if (!res.success) { alert('게시글 정보를 가져오지 못했습니다.'); return; } currentDetailRaw = res.data; populateEditForm(res.data, postId); } }); } // ========================= // Action Events // ========================= function onSubmitWriteForm(e) { e.preventDefault(); const $content = $('#post_content'); if ($content.data('summernote')) { $content.val($content.summernote('code')); } const postId = $('#write_postid').val(); const mode = postId ? 'update' : 'create'; const formData = new FormData(this); formData.set('board_name', getBoardName()); if (mode === 'update') { formData.set('write_postid', postId); formData.set('post_id', postId); formData.set('id', postId); if (deletedFileIds.length) { formData.set('delete_attach_ids', JSON.stringify(deletedFileIds)); } } formData.delete('attachments[]'); Array.from(dt.files).forEach(file => formData.append('attachments[]', file)); const $submitBtn = $('#btnSubmit') .prop('disabled', true) .html(' 처리 중...'); $.ajax({ url: `${API_BASE_URL}?action=${mode}`, method: 'POST', data: formData, processData: false, contentType: false, dataType: 'json', success(res) { if (res.success) { alert('저장되었습니다.'); location.href = `${current_page}`; } else { alert(res.message || '게시글 저장에 실패했습니다.'); } }, error: () => alert('서버와 통신 중 오류가 발생했습니다.'), complete: () => $submitBtn.prop('disabled', false) }); } function deletePosts(ids, onSuccess) { $.ajax({ url: `${API_BASE_URL}?action=delete&board_name=${encodeURIComponent(getBoardName())}`, method: 'POST', contentType: 'application/json', data: JSON.stringify({ ids }), dataType: 'json', success(res) { if (res.success) { onSuccess?.(); } else { alert(res.message || '삭제에 실패했습니다.'); } }, error: () => alert('통신 중 오류가 발생했습니다.') }); } function onClickDeleteSelected() { const selected = $('.row-check:checked').map(function () { return $(this).val(); }).get(); if (!selected.length) { alert('삭제할 게시글을 하나 이상 선택해주세요.'); return; } if (!confirm(`선택한 ${selected.length}건의 게시글을 삭제하시겠습니까?`)) return; deletePosts(selected, () => loadPosts()); } function onClickDeleteCurrent() { const postId = $('#detailPostId').text().trim(); if (!postId || !confirm('이 게시글을 삭제하시겠습니까?')) return; deletePosts([postId], () => { alert('삭제되었습니다.'); location.href = `${current_page}`; }); } function onClickEditCurrent() { const postId = $('#detailPostId').text().trim(); if (!postId) return; currentDetailRaw ? populateEditForm(currentDetailRaw, postId) : loadPostForEdit(postId); } function onClickCancelWrite() { const titleVal = $('input[name="title"]').val()?.trim(); if (titleVal && !confirm('작성 중인 내용이 저장되지 않습니다. 취소하시겠습니까?')) { return; } location.href = `${current_page}`; } function onClickCategory(e) { e.preventDefault(); const category = String($(this).data('category') || 'all').trim(); if (!category) return; $('#board_catagory .category-pill-btn').removeClass('active'); $(this).addClass('active'); loadPosts(null, null, category); } function onClickPageBtn(e) { e.preventDefault(); const $li = $(this).closest('.page-item'); if ($li.hasClass('disabled') || $li.hasClass('active')) return; const targetPage = parseInt($(this).data('page'), 10); if (!targetPage || targetPage === currentPage) return; loadPosts(currentSearchType, currentSearchKeyword, currentCategory, targetPage); document.getElementById('viewList')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function onClickTitleCell() { const postId = $(this).closest('[data-post-id]').data('post-id'); location.href = `${current_page_manager}?mode=detail&id=${postId}`; } function onClickSearch() { const modalEl = document.getElementById('searchModal'); if (modalEl && typeof bootstrap !== 'undefined') { bootstrap.Modal.getOrCreateInstance(modalEl).show(); $('#searchKeyword').trigger('focus'); } } function onSubmitSearch(e) { e.preventDefault(); const keyword = $('#searchKeyword').val().trim(); if (!keyword) { alert('검색어를 입력해주세요.'); $('#searchKeyword').trigger('focus'); return; } loadPosts($('#searchType').val(), keyword); const modalEl = document.getElementById('searchModal'); if (modalEl && typeof bootstrap !== 'undefined') { bootstrap.Modal.getOrCreateInstance(modalEl).hide(); } } // 체크박스 전체 선택 및 개별 선택 처리 function onChangeCheckAll() { $('.row-check').prop('checked', $(this).is(':checked')); //$('.row-check').prop('checked',true); } function onChangeRowCheck() { const total = $('.row-check').length; const checked = $('.row-check:checked').length; $('#checkAll').prop('checked', total > 0 && total === checked); } // ========================= // Summernote Editor // ========================= function initSummernote() { $('#post_content').summernote({ placeholder: '본문 내용을 입력하세요...', height: 380, lang: 'ko-KR', toolbar: [ ['style', ['style']], ['font', ['bold', 'underline', 'clear']], ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], ['insert', ['link', 'picture', 'video']], ['view', ['fullscreen', 'codeview', 'help']] ], callbacks: { onImageUpload(files) { Promise.all(Array.from(files).map(uploadSummernoteImage)); }, onMediaDelete($target) { const imageUrl = $target.attr('src'); if (imageUrl) deleteSummernoteImage(imageUrl); } } }); } function uploadSummernoteImage(file) { const data = new FormData(); data.append('file', file); data.append('board_name', getBoardName()); return $.ajax({ url: `${API_BASE_URL}?action=upload_editor_image`, method: 'POST', data, contentType: false, processData: false, dataType: 'json' }).then(res => { if (res.success && res.url) { $('#post_content').summernote('insertImage', resolveFileUrl(res.url)); } else { alert(res.message || '이미지 업로드에 실패했습니다.'); } }).catch(() => { alert('이미지 업로드 중 서버 통신 오류가 발생했습니다.'); }); } function deleteSummernoteImage(imageUrl) { $.ajax({ url: `${API_BASE_URL}?action=delete_editor_image`, method: 'POST', data: { image_url: imageUrl, board_name: getBoardName() }, dataType: 'json', error: () => console.error('이미지 파일 삭제 중 오류 발생') }); } });