Files
edu/admin/js/content_upload.js

1185 lines
47 KiB
JavaScript

// 페이지가 완전히 로딩된 후 실행되는 이벤트 리스너입니다.
document.addEventListener('DOMContentLoaded', () => {
const catSelect = document.getElementById('category_code'); // 카테고리(대분류) 셀렉트 박스
const blocks = document.querySelectorAll('.cat-block'); // 카테고리별로 동적으로 보여질 입력 폼 블록들
const catGroup = document.getElementById('category_group'); // 카테고리 구분(중분류) 셀렉트 박스
const goalBtn = document.getElementById('btn-goal'); // 학습목표 등록 모달창을 여는 통계/목표 버튼
/**
* 사용자가 선택한 카테고리에 맞춰 하단 입력 폼 영역을 동적으로 노출하거나 숨기는 함수입니다.
*/
function applyToggle() {
let name = '마이클래스'; // 기본 카테고리는 마이클래스로 설정합니다.
// 카테고리 셀렉트가 있을 경우 현재 선택된 옵션의 data-name 속성을 읽어옵니다.
if (catSelect) {
const opt = catSelect.options[catSelect.selectedIndex];
name = opt && opt.dataset && opt.dataset.name ? opt.dataset.name : name;
}
// 준비된 폼 블록들(.cat-block)을 순회하며, 카테고리 이름과 일치하는 블록만 보여줍니다.
blocks.forEach(b => {
const cat = b.getAttribute('data-cat') || '';
const cats = cat.split(',');
if (cats.includes(name)) b.classList.remove('hidden'); else b.classList.add('hidden');
});
const isIS = (name === '인사이트');
const isLD = (name === '리더십');
const isContainer = document.getElementById('issue_type_is_container');
const ldContainer = document.getElementById('issue_type_ld_container');
const isSelect = document.getElementById('issue_type_code_is');
const ldSelect = document.getElementById('issue_type_code_ld');
const offerIdContainer = document.getElementById('offer_id_container');
if (isContainer && ldContainer && isSelect && ldSelect) {
if (isIS) {
isContainer.classList.remove('hidden');
ldContainer.classList.add('hidden');
isSelect.disabled = false;
ldSelect.disabled = true;
} else if (isLD) {
isContainer.classList.add('hidden');
ldContainer.classList.remove('hidden');
isSelect.disabled = true;
ldSelect.disabled = false;
}
}
if (offerIdContainer) {
if (isIS) {
offerIdContainer.classList.remove('hidden');
} else {
offerIdContainer.classList.add('hidden');
}
}
const sortOrderContainer = document.getElementById('sort_order_container');
if (sortOrderContainer) {
if (['CA10001', 'CA10002', 'CA10003'].includes(catSelect.value)) {
sortOrderContainer.style.display = 'block';
} else {
sortOrderContainer.style.display = 'none';
}
}
const memoBtn = document.getElementById('btn-memo-open');
if (memoBtn) {
if (catSelect && catSelect.value === 'CA10001') {
memoBtn.classList.remove('hidden');
} else {
memoBtn.classList.add('hidden');
}
}
}
function toggleImageUpload() {
const isBizTrend = (catSelect && catSelect.value === 'CA10006');
const isPart1 = (catGroup && catGroup.value === 'CA200B01');
const imgContainer = document.getElementById('image_upload_container');
if (imgContainer) {
if (isBizTrend && isPart1) {
imgContainer.classList.remove('hidden');
} else {
imgContainer.classList.add('hidden');
document.getElementById('image_name_input').value = '';
document.getElementById('image_name_label').textContent = '대표이미지등록(N)';
}
}
// 사내도서여부: 대표이미지등록과 동일 조건(비즈트렌드 + 1부)일 때만 표시
const bookYnContainer = document.getElementById('book_yn_container');
if (bookYnContainer) {
if (isBizTrend && isPart1) {
bookYnContainer.classList.remove('hidden');
} else {
bookYnContainer.classList.add('hidden');
const bookYnChk = document.getElementById('book_yn');
if (bookYnChk) bookYnChk.checked = false;
}
}
}
window.previewImageUpload = function(input) {
const label = document.getElementById('image_name_label');
if (input.files && input.files[0]) {
const file = input.files[0];
if (file.size > 4 * 1024 * 1024) {
alert("용량이 4MB 이하인 이미지 파일만 등록 가능합니다.");
input.value = '';
label.textContent = '대표이미지등록(N)';
return;
}
if (!file.type.startsWith('image/')) {
alert("이미지 파일만 등록 가능합니다.");
input.value = '';
label.textContent = '대표이미지등록(N)';
return;
}
label.textContent = '대표이미지등록(Y)';
} else {
label.textContent = '대표이미지등록(N)';
}
};
/**
* 대분류 카테고리가 변경되었을 때, 해당하는 중분류 값을 서버에서 불러와 셀렉트 박스에 세팅하는 함수입니다.
*/
async function loadCategoryGroup() {
if (!catSelect || !catGroup) return; // 요소가 없으면 실행하지 않습니다.
const code = catSelect.value;
try {
// 서버에서 카테고리 그룹(중분류) API 데이터를 비동기로 통신하여 가져옵니다.
const res = await fetch(`../bbs/code_list.php?type=category_group&desc01=${encodeURIComponent(code)}`);
const data = await res.json();
// 기존 옵션 초기화 후 기본 '선택' 옵션을 추가합니다.
catGroup.innerHTML = '';
const def = document.createElement('option');
def.value = '';
def.textContent = '선택';
catGroup.appendChild(def);
// 받아온 데이터 리스트를 옵션 태그로 추가합니다.
(data.items || []).forEach(it => {
const o = document.createElement('option');
o.value = it.code;
o.textContent = it.name;
catGroup.appendChild(o);
});
} catch (e) {
// 오류 발생 시 '선택' 옵션만 노출되도록 초기화합니다.
catGroup.innerHTML = '<option value="">선택</option>';
}
}
/**
* 특정 기준년도에 등록된 활성 상태의 학습목표 목록을 불러와 학습목표코드 셀렉트 박스에 세팅하는 함수입니다.
*/
async function loadGoals() {
const yearInput = document.querySelector('input[name="base_year"]');
const goalSel = document.getElementById('goal_code');
if (!yearInput || !goalSel) return;
// 카테고리가 마이클래스가 아닐 때는 학습목표코드를 조회하지 않습니다.
if (catSelect) {
const opt = catSelect.options[catSelect.selectedIndex];
const catName = opt && opt.dataset && opt.dataset.name ? opt.dataset.name : '';
if (catName !== '마이클래스') {
goalSel.innerHTML = '<option value="">선택</option>';
return;
}
}
// 연도 필드가 비어있다면 올해 연도를 기본으로 사용합니다.
const year = yearInput.value || new Date().getFullYear();
// 카테고리구분(중분류)을 함께 기준으로 사용합니다. (분기 정보와 매핑)
const catGroupSelect = document.getElementById('category_group');
const categoryGroup = catGroupSelect ? (catGroupSelect.value || '') : '';
try {
const res = await fetch(`../bbs/code_list.php?type=learning_goal&year=${year}&category_group=${encodeURIComponent(categoryGroup)}`);
const data = await res.json();
goalSel.innerHTML = '<option value="">선택</option>';
(data.items || []).forEach(it => {
const o = document.createElement('option');
o.value = it.code;
o.textContent = it.name;
goalSel.appendChild(o);
});
} catch (e) {
goalSel.innerHTML = '<option value="">선택</option>';
}
}
const yearInput = document.querySelector('input[name="base_year"]');
if (yearInput) {
yearInput.addEventListener('change', loadGoals);
yearInput.addEventListener('blur', loadGoals);
}
// 카테고리구분이 변경되면 학습목표 코드 리스트도 재조회합니다.
if (catGroup) {
catGroup.addEventListener('change', () => {
loadGoals();
toggleImageUpload();
});
}
if (catSelect) {
catSelect.addEventListener('change', () => {
applyToggle();
loadCategoryGroup().then(() => {
loadGoals();
toggleImageUpload();
});
});
applyToggle();
loadCategoryGroup().then(() => {
loadGoals();
toggleImageUpload();
});
}
const offerIdInput = document.querySelector('input[name="offer_id"]');
const isOfferCheck = document.querySelector('input[name="is_offer"]');
if (offerIdInput && isOfferCheck) {
offerIdInput.addEventListener('input', () => {
isOfferCheck.checked = offerIdInput.value.trim() !== '';
});
}
const editBtns = document.querySelectorAll('.btn-edit');
const modalTitle = document.querySelector('#upload-modal h3');
const modalForm = document.querySelector('#upload-modal form');
const contentIdInput = document.querySelector('input[name="content_id"]');
// 수정 버튼들을 순회하며 이벤트 핸들러를 등록합니다.
editBtns.forEach(btn => {
btn.addEventListener('click', async () => {
// 버튼에 바인딩된 해당 행의 JSON 데이터를 가져옵니다.
const row = JSON.parse(btn.getAttribute('data-row'));
const contentId = row && row.content_id ? row.content_id : '';
if (!contentId) {
alert('콘텐츠 ID가 없습니다.');
return;
}
// content_id 기준으로 DB에서 최신 데이터 조회
let detail = null;
try {
const dRes = await fetch(`../bbs/code_list.php?type=content_detail&content_id=${encodeURIComponent(contentId)}`);
const dData = await dRes.json();
if (!dData.success || !dData.item) {
alert('콘텐츠 조회 실패: ' + (dData.message || '알 수 없는 오류'));
return;
}
detail = dData.item;
} catch (e) {
alert('콘텐츠 조회 중 오류가 발생했습니다.');
return;
}
// 모달 제목을 '콘텐츠 수정'으로 변경합니다.
if (modalTitle) modalTitle.textContent = '콘텐츠 수정';
// 콘텐츠 등록/수정 모달을 보여줍니다.
document.getElementById('upload-modal').classList.remove('hidden');
// 삭제 버튼 보여주기
const deleteBtn = document.getElementById('btn-delete');
if (deleteBtn) deleteBtn.classList.remove('hidden');
// 선택된 행의 카테고리 정보로 폼을 세팅합니다.
if (catSelect) {
catSelect.value = detail.category_code || row.category_code;
catSelect.disabled = true; // 카테고리 수정 불가 (읽기 모드)
document.getElementById('category_code_hidden').value = row.category_code;
applyToggle(); // 폼 요소를 토글합니다.
await loadCategoryGroup(); // 해당하는 중분류 목록을 불러옵니다.
if (catGroup) {
catGroup.value = detail.category_group || row.category_group || '';
}
toggleImageUpload(); // 카테고리 구분이 세팅된 후 다시 노출 제어 실행
}
// 동적 노출 폼에 데이터를 바인딩합니다.
const catName = catSelect.options[catSelect.selectedIndex]?.dataset.name || '';
if (catName === '마이클래스') {
const yearInput = document.querySelector('input[name="base_year"]');
if (yearInput && (detail.base_year || row.base_year)) {
yearInput.value = detail.base_year || row.base_year;
}
await loadGoals();
const goalCodeSelect = document.getElementById('goal_code');
if (goalCodeSelect && (detail.goal_code || row.goal_code)) {
goalCodeSelect.value = detail.goal_code || row.goal_code;
}
} else if (catName === '법정교육') {
const yearLaw = document.querySelector('input[name="base_year_law"]');
if (yearLaw && (detail.base_year || row.base_year)) yearLaw.value = detail.base_year || row.base_year;
} else if (catName === '비즈트렌드') {
const sDateBzt = document.querySelector('.cat-block[data-cat="비즈트렌드"] input[name="start_date_bzt"]');
if (sDateBzt && (detail.start_date || row.start_date)) sDateBzt.value = detail.start_date || row.start_date;
// 사내도서여부 체크 상태 복원 (비즈트렌드+1부 조건에서만 표시됨)
const bookYnChk = document.getElementById('book_yn');
if (bookYnChk) bookYnChk.checked = ((detail.book_yn ?? row.book_yn) === '1');
} else if (catName === '인사이트' || catName === '리더십') {
const isIS = (catName === '인사이트');
const isContainer = document.getElementById('issue_type_is_container');
const ldContainer = document.getElementById('issue_type_ld_container');
const isSelect = document.getElementById('issue_type_code_is');
const ldSelect = document.getElementById('issue_type_code_ld');
const offerIdContainer = document.getElementById('offer_id_container');
if (isIS) {
if (isContainer) isContainer.classList.remove('hidden');
if (ldContainer) ldContainer.classList.add('hidden');
if (isSelect) { isSelect.disabled = false; isSelect.value = detail.issue_type_code || ''; }
if (ldSelect) ldSelect.disabled = true;
if (offerIdContainer) offerIdContainer.classList.remove('hidden');
} else {
if (isContainer) isContainer.classList.add('hidden');
if (ldContainer) ldContainer.classList.remove('hidden');
if (isSelect) isSelect.disabled = true;
if (ldSelect) { ldSelect.disabled = false; ldSelect.value = detail.issue_type_code || ''; }
if (offerIdContainer) offerIdContainer.classList.add('hidden');
}
const isOffer = document.querySelector('input[name="is_offer"]');
if (isOffer) isOffer.checked = (detail.is_offer === '1');
const offerId = document.querySelector('input[name="offer_id"]');
if (offerId) offerId.value = detail.offer_id || '';
const desc3In = document.querySelector('textarea[name="description3"]');
if (desc3In) desc3In.value = detail.description3 || row.description3 || '';
}
// 콘텐츠 기본 폼 속성들에 값을 채워넣습니다.
if (contentIdInput) contentIdInput.value = detail.content_id || row.content_id;
const titleIn = document.querySelector('input[name="title"]');
if (titleIn) titleIn.value = detail.title || row.title || '';
const descIn = document.querySelector('textarea[name="description"]');
if (descIn) descIn.value = detail.description || row.description || '';
const desc1In = document.querySelector('input[name="description1"]');
if (desc1In) desc1In.value = detail.description1 || row.description1 || '';
const desc2In = document.querySelector('input[name="description2"]');
if (desc2In) desc2In.value = detail.description2 || row.description2 || '';
const sortOrderIn = document.querySelector('input[name="sort_order"]');
if (sortOrderIn) sortOrderIn.value = detail.sort_order || row.sort_order || '';
const urlIn = document.querySelector('input[name="content_url"]');
if (urlIn) urlIn.value = detail.content_url || row.content_url || '';
// content_tm (영상 길이) 복원
const contentTmIn = document.getElementById('content_tm_input');
if (contentTmIn) {
const tm = detail.content_tm || row.content_tm || '';
contentTmIn.value = tm;
// 길이 표시 업데이트
updateDurationDisplay(tm ? parseInt(tm) : 0);
}
const imageLabel = document.getElementById('image_name_label');
if (imageLabel) {
if (detail.image_name || row.image_name) {
imageLabel.textContent = '대표이미지등록(Y)';
} else {
imageLabel.textContent = '대표이미지등록(N)';
}
}
// Keyword binding using API
const kwBtns = document.querySelectorAll('.kw-btn');
const kwCountDisplay = document.getElementById('keyword-count-display');
if (kwBtns) {
kwBtns.forEach(btn => {
btn.dataset.active = 'false';
const iconWrap = btn.querySelector('.kw-icon');
if (iconWrap) iconWrap.innerHTML = '<i class="fa-solid fa-plus text-xs"></i>';
});
try {
const res = await fetch(`../bbs/code_list.php?type=content_keywords&content_id=${encodeURIComponent(row.content_id)}`);
const data = await res.json();
const kws = data.items || [];
kws.forEach(k => {
const b = Array.from(kwBtns).find(btn => btn.dataset.code === k);
if (b) {
b.dataset.active = 'true';
const iconWrap = b.querySelector('.kw-icon');
if (iconWrap) iconWrap.innerHTML = '<i class="fa-solid fa-minus text-xs"></i>';
}
});
if (kwCountDisplay) kwCountDisplay.textContent = kws.length;
updateKeywordInput();
} catch (e) {
console.error('Failed to load keywords', e);
if (kwCountDisplay) kwCountDisplay.textContent = '0';
updateKeywordInput();
}
}
});
});
const goalForm = document.getElementById('goal-form');
if (goalForm) {
goalForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(goalForm);
try {
const resp = await fetch(goalForm.action, {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
alert('저장되었습니다.');
loadGoalGrid();
resetGoalForm();
} else {
alert('저장 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (err) {
alert('오류가 발생했습니다.');
}
});
}
// 콘텐츠 수정 모달 AJAX Submit (저장 후 모달 유지)
const contentForm = document.querySelector('#upload-modal form');
if (contentForm) {
contentForm.addEventListener('submit', async (e) => {
e.preventDefault(); // 기본 페이지 이동 방지
const formData = new FormData(contentForm);
const isNew = !formData.get('content_id'); // content_id가 없으면 새 콘텐츠
try {
const resp = await fetch(contentForm.action, {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
alert('저장되었습니다.');
if (isNew) {
// 새 콘텐츠 추가 시 모달 닫기
document.getElementById('upload-modal').classList.add('hidden');
location.reload(); // 페이지 새로고침으로 그리드 업데이트
}
// 수정 시 모달 유지 (새로고침 없이)
} else {
alert('저장 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (err) {
alert('저장에 실패했습니다.');
}
});
}
// 삭제 버튼 이벤트
const deleteBtn = document.getElementById('btn-delete');
if (deleteBtn) {
deleteBtn.addEventListener('click', async () => {
if (!confirm('정말로 이 콘텐츠를 삭제하시겠습니까?')) return;
const contentId = document.querySelector('input[name="content_id"]').value;
if (!contentId) return;
try {
const resp = await fetch('../bbs/content_delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `content_id=${encodeURIComponent(contentId)}`
});
const data = await resp.json();
if (data.success) {
alert('삭제되었습니다.');
document.getElementById('upload-modal').classList.add('hidden');
location.reload(); // 페이지 새로고침으로 그리드 업데이트
} else {
alert('삭제 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (err) {
alert('삭제에 실패했습니다.');
}
});
}
// Keyword Selection Logic
const kwBtns = document.querySelectorAll('.kw-btn');
const kwInput = document.getElementById('kw_selected'); // Updated ID for modal hidden input
function updateKeywordInput() {
const activeKws = Array.from(kwBtns)
.filter(btn => btn.dataset.active === 'true')
.map(btn => btn.dataset.code);
if (kwInput) {
kwInput.value = activeKws.join(',');
}
}
kwBtns.forEach(btn => {
btn.addEventListener('click', () => {
const isActive = btn.dataset.active === 'true';
btn.dataset.active = !isActive;
const iconWrap = btn.querySelector('.kw-icon');
if (iconWrap) {
iconWrap.innerHTML = !isActive
? '<i class="fa-solid fa-minus text-xs"></i>'
: '<i class="fa-solid fa-plus text-xs"></i>';
}
updateKeywordInput();
});
});
// content_url 입력 필드 blur 시 자동 영상 정보 조회
const contentUrlInput = document.getElementById('content_url_input');
if (contentUrlInput) {
contentUrlInput.addEventListener('blur', () => {
const val = contentUrlInput.value.trim();
if (val) fetchYouTubeInfo();
});
}
});
/**
* 신규 콘텐츠 등록 모달을 여는 함수입니다. (기존 데이터 초기화)
*/
function openNewModal() {
document.getElementById('upload-modal').classList.remove('hidden');
const modalTitle = document.querySelector('#upload-modal h3');
if (modalTitle) modalTitle.textContent = '새 콘텐츠 등록';
const modalForm = document.querySelector('#upload-modal form');
if (modalForm) modalForm.reset();
const contentIdInput = document.querySelector('input[name="content_id"]');
if (contentIdInput) contentIdInput.value = '';
const imageInput = document.getElementById('image_name_input');
if (imageInput) imageInput.value = '';
const imageLabel = document.getElementById('image_name_label');
if (imageLabel) imageLabel.textContent = '대표이미지등록(N)';
// content_tm 및 YouTube 상태 초기화
const contentTmIn = document.getElementById('content_tm_input');
if (contentTmIn) contentTmIn.value = '';
const ytStatus = document.getElementById('yt-fetch-status');
if (ytStatus) ytStatus.classList.add('hidden');
const durDisplay = document.getElementById('yt-duration-display');
if (durDisplay) durDisplay.classList.add('hidden');
// 삭제 버튼 숨기기
const deleteBtn = document.getElementById('btn-delete');
if (deleteBtn) deleteBtn.classList.add('hidden');
const kwBtns = document.querySelectorAll('.kw-btn');
const kwCountDisplay = document.getElementById('keyword-count-display');
if (kwBtns) {
kwBtns.forEach(btn => {
btn.dataset.active = 'false';
const iconWrap = btn.querySelector('.kw-icon');
if (iconWrap) iconWrap.innerHTML = '<i class="fa-solid fa-plus text-xs"></i>';
});
}
if (kwCountDisplay) kwCountDisplay.textContent = '0';
const kwSelected = document.getElementById('kw_selected');
if (kwSelected) kwSelected.value = '';
// 카테고리를 다시 선택 상태로 트리거하여 폼의 동적 노출 상태를 초기화합니다.
const catSelect = document.getElementById('category_code');
if (catSelect) {
catSelect.disabled = false; // 새 콘텐츠 등록 시에는 카테고리 선택 가능
document.getElementById('category_code_hidden').value = catSelect.value;
catSelect.dispatchEvent(new Event('change'));
}
}
/**
* 학습목표 등록(및 리스트 조회) 모달을 여는 함수입니다.
*/
function openGoalModal() {
document.getElementById('goal-modal').classList.remove('hidden');
loadGoalGrid();
resetGoalForm();
}
/**
* 학습목표 모달을 닫는 함수입니다.
*/
function closeGoalModal() {
document.getElementById('goal-modal').classList.add('hidden');
}
/**
* 학습목표 목록을 Ajax로 불러와 표(Grid) 형식으로 그려주는 함수입니다.
*/
async function loadGoalGrid() {
const yearInput = document.getElementById('goal_search_year');
const quarterInput = document.getElementById('goal_search_quarter');
if (!yearInput) return;
const year = yearInput.value || new Date().getFullYear();
const quarter = quarterInput ? quarterInput.value : '';
const gridBody = document.getElementById('goal-grid-body');
if (!gridBody) return;
// 데이터를 불러오기 전 로딩 상태 표시
gridBody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-gray-500">로딩 중...</td></tr>';
try {
// API에 기준년도와 분기 데이터를 포함해 호출합니다.
const res = await fetch(`../bbs/code_list.php?type=learning_goal_all&year=${year}&quarter=${encodeURIComponent(quarter)}`);
const data = await res.json();
gridBody.innerHTML = '';
// 결과 데이터가 없는 경우
if (!data.items || data.items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-gray-500">검색된 학습목표가 없습니다.</td></tr>';
return;
}
// 결과가 있는 경우 행 단위로 HTML을 생성하여 테이블을 그립니다.
data.items.forEach((it, idx) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-teal-50/30 transition';
const isActiveHtml = it.is_active === '1'
? '<i class="fa-solid fa-check text-teal-600"></i>'
: '<span class="text-gray-300">-</span>';
tr.innerHTML = `
<td class="p-3 text-center text-gray-400">${idx + 1}</td>
<td class="p-3 font-bold text-gray-700">${escapeHtml(it.quarter_name || it.quarter || '-')}</td>
<td class="p-3 font-bold text-gray-700">${escapeHtml(it.goal_no_name || it.goal_no || '-')}</td>
<td class="p-3 font-mono text-xs text-gray-500">${it.goal_code}</td>
<td class="p-3 font-bold text-gray-800">${escapeHtml(it.title || '')}</td>
<td class="p-3 text-center">${isActiveHtml}</td>
<td class="p-3 text-gray-500 truncate max-w-[12rem]">${escapeHtml(it.remarks || '')}</td>
<td class="p-3 text-center">
<!-- 수정 버튼 클릭 시 현재 행의 데이터를 폼에 로드합니다 -->
<button class="px-3 py-1 bg-teal-800 text-white rounded text-xs hover:bg-teal-900 transition"
onclick='editGoalRow(${JSON.stringify(it).replace(/'/g, "&#39;")})'>수정</button>
</td>
`;
gridBody.appendChild(tr);
});
} catch (e) {
gridBody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-red-500">오류가 발생했습니다.</td></tr>';
}
}
function editGoalRow(row) {
document.getElementById('goal_form_code').value = row.goal_code;
document.getElementById('goal_form_title').value = row.title || '';
const _titleLen = document.getElementById('goal_title_len');
if (_titleLen) _titleLen.textContent = (row.title || '').length;
document.getElementById('goal_form_quarter').value = row.quarter || '';
const goalNoSel = document.getElementById('goal_form_goal_no');
if (goalNoSel) goalNoSel.value = row.goal_no || '';
document.getElementById('goal_form_remarks').value = row.remarks || '';
document.getElementById('goal_form_active').checked = (row.is_active === '1');
const sort = document.getElementById('goal_form_sort');
if (sort) sort.value = row.sort_order || '';
const searchYear = document.getElementById('goal_search_year').value;
document.getElementById('goal_form_year').value = searchYear;
document.getElementById('btn-goal-delete').classList.remove('hidden');
document.getElementById('btn-goal-new').classList.remove('hidden');
}
function resetGoalForm() {
document.getElementById('goal_form_code').value = '';
document.getElementById('goal_form_title').value = '';
const _titleLenR = document.getElementById('goal_title_len');
if (_titleLenR) _titleLenR.textContent = '0';
document.getElementById('goal_form_quarter').value = '';
const goalNoSel = document.getElementById('goal_form_goal_no');
if (goalNoSel) goalNoSel.value = '';
document.getElementById('goal_form_remarks').value = '';
document.getElementById('goal_form_active').checked = true;
const sort = document.getElementById('goal_form_sort');
if (sort) sort.value = '';
const searchYear = document.getElementById('goal_search_year').value;
document.getElementById('goal_form_year').value = searchYear;
document.getElementById('btn-goal-delete').classList.add('hidden');
document.getElementById('btn-goal-new').classList.add('hidden');
}
async function deleteGoal() {
const code = document.getElementById('goal_form_code').value;
if (!code) return;
if (!confirm('정말 삭제하시겠습니까?')) return;
const form = document.getElementById('goal-form');
const formData = new FormData(form);
try {
await fetch('../bbs/goal_delete.php', {
method: 'POST',
body: formData
});
alert('삭제되었습니다.');
loadGoalGrid();
resetGoalForm();
} catch (err) {
alert('오류가 발생했습니다.');
}
}
function escapeHtml(unsafe) {
return (unsafe || '').toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function openKeywordModal() {
const contentIdInput = document.querySelector('input[name="content_id"]');
if (!contentIdInput || !contentIdInput.value) {
alert("새 콘텐츠를 저장 후 키워드를 등록해주세요");
return;
}
const kwContentId = document.getElementById('kw_content_id');
if (kwContentId) kwContentId.value = contentIdInput.value;
// Update hidden input before opening
const kwInput = document.getElementById('kw_selected');
const kwBtns = document.querySelectorAll('.kw-btn');
if (kwInput) {
kwInput.value = Array.from(kwBtns).filter(btn => btn.dataset.active === 'true').map(btn => btn.dataset.code).join(',');
}
document.getElementById('keyword-modal').classList.remove('hidden');
}
function closeKeywordModal() {
document.getElementById('keyword-modal').classList.add('hidden');
}
/**
* 키워드 모달에서 저장 버튼 클릭 시 호출되는 함수입니다.
* 선택된 키워드 코드 목록을 AJAX로 서버에 저장합니다.
*/
async function saveKeywords(event) {
if (event) event.preventDefault();
const form = document.getElementById('keyword-form');
if (!form) return;
// 현재 활성화된 키워드 코드를 수집하여 hidden input에 세팅합니다.
const kwBtns = document.querySelectorAll('.kw-btn');
const kwInput = document.getElementById('kw_selected');
if (kwInput) {
kwInput.value = Array.from(kwBtns)
.filter(btn => btn.dataset.active === 'true')
.map(btn => btn.dataset.code)
.join(',');
}
const formData = new FormData(form);
try {
const res = await fetch('../bbs/keyword_save.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
// 메인 모달의 키워드 카운트 표시를 업데이트합니다.
const kwCountDisplay = document.getElementById('keyword-count-display');
const activeCount = Array.from(kwBtns).filter(btn => btn.dataset.active === 'true').length;
if (kwCountDisplay) kwCountDisplay.textContent = activeCount;
alert('키워드가 저장되었습니다.');
closeKeywordModal();
} else {
alert('저장 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (e) {
alert('오류가 발생했습니다.');
}
}
function openMemoModal() {
const contentIdInput = document.querySelector('input[name="content_id"]');
if (!contentIdInput || !contentIdInput.value) {
alert('콘텐츠 수정에서만 사용 가능합니다.');
return;
}
const contentId = contentIdInput.value;
const idLabel = document.getElementById('memo-content-id');
const memoContentId = document.getElementById('memo_form_content_id');
const memoSeq = document.getElementById('memo_form_seq');
const memoTitle = document.getElementById('memo_form_title');
const memoActive = document.getElementById('memo_form_active');
const delBtn = document.getElementById('btn-memo-delete');
const gridBody = document.getElementById('memo-grid-body');
if (idLabel) idLabel.textContent = contentId;
if (memoContentId) memoContentId.value = contentId;
if (memoSeq) memoSeq.value = '';
if (memoTitle) memoTitle.value = '';
if (memoActive) memoActive.checked = true;
if (delBtn) delBtn.classList.add('hidden');
if (gridBody) gridBody.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-gray-500">로딩 중...</td></tr>';
document.getElementById('memo-modal').classList.remove('hidden');
loadMemoGrid();
}
function closeMemoModal() {
document.getElementById('memo-modal').classList.add('hidden');
}
async function loadMemoGrid() {
const contentId = document.getElementById('memo_form_content_id')?.value || '';
const gridBody = document.getElementById('memo-grid-body');
if (!contentId || !gridBody) return;
gridBody.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-gray-500">로딩 중...</td></tr>';
try {
const res = await fetch(`../bbs/code_list.php?type=content_memos&content_id=${encodeURIComponent(contentId)}`);
const data = await res.json();
if (!data.success) {
gridBody.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-red-500">조회에 실패했습니다.</td></tr>';
return;
}
const items = data.items || [];
if (items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-gray-500">등록된 포스트잇이 없습니다.</td></tr>';
return;
}
gridBody.innerHTML = '';
items.forEach(it => {
const tr = document.createElement('tr');
const activeHtml = it.is_active === '1'
? '<i class="fa-solid fa-check text-teal-600"></i>'
: '<span class="text-gray-300">-</span>';
tr.innerHTML = `
<td class="p-3 text-center text-gray-700 font-bold">${it.seq ?? ''}</td>
<td class="p-3 text-gray-700">${escapeHtml(it.title || '')}</td>
<td class="p-3 text-center">${activeHtml}</td>
<td class="p-3 text-center">
<button type="button"
class="px-3 py-1 bg-teal-800 text-white rounded text-xs hover:bg-teal-900 transition"
onclick='editMemo(${JSON.stringify(it).replace(/'/g, "&#39;")})'>수정</button>
</td>
`;
gridBody.appendChild(tr);
});
} catch (e) {
gridBody.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-red-500">오류가 발생했습니다.</td></tr>';
}
}
function newMemo() {
const memoSeq = document.getElementById('memo_form_seq');
const memoTitle = document.getElementById('memo_form_title');
const memoActive = document.getElementById('memo_form_active');
const delBtn = document.getElementById('btn-memo-delete');
if (memoSeq) memoSeq.value = '';
if (memoTitle) memoTitle.value = '';
if (memoActive) memoActive.checked = true;
if (delBtn) delBtn.classList.add('hidden');
}
function editMemo(row) {
const memoSeq = document.getElementById('memo_form_seq');
const memoTitle = document.getElementById('memo_form_title');
const memoActive = document.getElementById('memo_form_active');
const delBtn = document.getElementById('btn-memo-delete');
if (memoSeq) memoSeq.value = row.seq ?? '';
if (memoTitle) memoTitle.value = row.title || '';
if (memoActive) memoActive.checked = (row.is_active === '1');
if (delBtn) delBtn.classList.remove('hidden');
}
async function saveMemo() {
const form = document.getElementById('memo-form');
if (!form) return;
const formData = new FormData(form);
try {
const res = await fetch('../bbs/memo_save.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
alert('저장되었습니다.');
await loadMemoGrid();
// 저장 후에는 수정 상태로 전환(삭제 버튼 활성)
const memoSeq = document.getElementById('memo_form_seq');
if (memoSeq && !memoSeq.value && data.seq) memoSeq.value = data.seq;
const delBtn = document.getElementById('btn-memo-delete');
if (delBtn) delBtn.classList.remove('hidden');
} else {
alert('저장 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (e) {
alert('오류가 발생했습니다.');
}
}
async function deleteMemo() {
const contentId = document.getElementById('memo_form_content_id')?.value || '';
const seq = document.getElementById('memo_form_seq')?.value || '';
if (!contentId || !seq) return;
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const fd = new FormData();
fd.set('content_id', contentId);
fd.set('seq', seq);
const res = await fetch('../bbs/memo_delete.php', { method: 'POST', body: fd });
const data = await res.json();
if (data.success) {
alert('삭제되었습니다.');
await loadMemoGrid();
newMemo();
} else {
alert('삭제 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (e) {
alert('오류가 발생했습니다.');
}
}
function openRecommendModal() {
const goalCode = document.getElementById('goal_form_code')?.value || '';
if (!goalCode) {
alert('학습목표를 선택 후 사용 가능합니다.');
return;
}
const goalCodeLabel = document.getElementById('recommend-goal-code');
const recommendGoalCode = document.getElementById('recommend_form_goal_code');
const recommendSeq = document.getElementById('recommend_form_seq');
const recommendTitle = document.getElementById('recommend_form_title');
const recommendActive = document.getElementById('recommend_form_active');
const delBtn = document.getElementById('btn-recommend-delete');
const gridBody = document.getElementById('recommend-grid-body');
if (goalCodeLabel) goalCodeLabel.textContent = goalCode;
if (recommendGoalCode) recommendGoalCode.value = goalCode;
if (recommendSeq) recommendSeq.value = '';
if (recommendTitle) recommendTitle.value = '';
const recommendTitle2b = document.getElementById('recommend_form_title2');
if (recommendTitle2b) recommendTitle2b.value = '';
if (recommendActive) recommendActive.checked = true;
if (delBtn) delBtn.classList.add('hidden');
if (gridBody) gridBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">로딩 중...</td></tr>';
document.getElementById('recommend-modal').classList.remove('hidden');
loadRecommendGrid();
}
function closeRecommendModal() {
document.getElementById('recommend-modal').classList.add('hidden');
}
async function loadRecommendGrid() {
const goalCode = document.getElementById('recommend_form_goal_code')?.value || '';
const gridBody = document.getElementById('recommend-grid-body');
if (!goalCode || !gridBody) return;
gridBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">로딩 중...</td></tr>';
try {
const res = await fetch(`../bbs/code_list.php?type=goal_recommends&goal_code=${encodeURIComponent(goalCode)}`);
const data = await res.json();
if (!data.success) {
gridBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-red-500">조회에 실패했습니다.</td></tr>';
return;
}
const items = data.items || [];
if (items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-gray-500">등록된 추천이유가 없습니다.</td></tr>';
return;
}
gridBody.innerHTML = '';
items.forEach(it => {
const tr = document.createElement('tr');
const activeHtml = it.is_active === '1'
? '<i class="fa-solid fa-check text-teal-600"></i>'
: '<span class="text-gray-300">-</span>';
tr.innerHTML = `
<td class="p-3 text-center text-gray-700 font-bold">${it.seq ?? ''}</td>
<td class="p-3 text-gray-700">${escapeHtml(it.title || '')}</td>
<td class="p-3 text-gray-700">${escapeHtml(it.title2 || '')}</td>
<td class="p-3 text-center">${activeHtml}</td>
<td class="p-3 text-center">
<button type="button"
class="px-3 py-1 bg-teal-800 text-white rounded text-xs hover:bg-teal-900 transition"
onclick='editRecommend(${JSON.stringify(it).replace(/'/g, "&#39;")})'>수정</button>
</td>
`;
gridBody.appendChild(tr);
});
} catch (e) {
gridBody.innerHTML = '<tr><td colspan="5" class="p-4 text-center text-red-500">오류가 발생했습니다.</td></tr>';
}
}
function newRecommend() {
const recommendSeq = document.getElementById('recommend_form_seq');
const recommendTitle = document.getElementById('recommend_form_title');
const recommendTitle2 = document.getElementById('recommend_form_title2');
const recommendActive = document.getElementById('recommend_form_active');
const delBtn = document.getElementById('btn-recommend-delete');
if (recommendSeq) recommendSeq.value = '';
if (recommendTitle) recommendTitle.value = '';
if (recommendTitle2) recommendTitle2.value = '';
if (recommendActive) recommendActive.checked = true;
if (delBtn) delBtn.classList.add('hidden');
}
function editRecommend(row) {
const recommendSeq = document.getElementById('recommend_form_seq');
const recommendTitle = document.getElementById('recommend_form_title');
const recommendTitle2 = document.getElementById('recommend_form_title2');
const recommendActive = document.getElementById('recommend_form_active');
const delBtn = document.getElementById('btn-recommend-delete');
if (recommendSeq) recommendSeq.value = row.seq ?? '';
if (recommendTitle) recommendTitle.value = row.title || '';
if (recommendTitle2) recommendTitle2.value = row.title2 || '';
if (recommendActive) recommendActive.checked = (row.is_active === '1');
if (delBtn) delBtn.classList.remove('hidden');
}
async function saveRecommend() {
const form = document.getElementById('recommend-form');
if (!form) return;
const formData = new FormData(form);
try {
const res = await fetch('../bbs/recommend_save.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
alert('저장되었습니다.');
await loadRecommendGrid();
// 저장 후에는 수정 상태로 전환(삭제 버튼 활성)
const recommendSeq = document.getElementById('recommend_form_seq');
if (recommendSeq && !recommendSeq.value && data.seq) recommendSeq.value = data.seq;
const delBtn = document.getElementById('btn-recommend-delete');
if (delBtn) delBtn.classList.remove('hidden');
} else {
alert('저장 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (e) {
alert('오류가 발생했습니다.');
}
}
async function deleteRecommend() {
const goalCode = document.getElementById('recommend_form_goal_code')?.value || '';
const seq = document.getElementById('recommend_form_seq')?.value || '';
if (!goalCode || !seq) return;
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const fd = new FormData();
fd.set('goal_code', goalCode);
fd.set('seq', seq);
const res = await fetch('../bbs/recommend_delete.php', { method: 'POST', body: fd });
const data = await res.json();
if (data.success) {
alert('삭제되었습니다.');
await loadRecommendGrid();
newRecommend();
} else {
alert('삭제 실패: ' + (data.message || '알 수 없는 오류'));
}
} catch (e) {
alert('오류가 발생했습니다.');
}
}
/**
* YouTube URL 또는 영상 ID를 입력하면 서버 프록시(youtube_info.php)를 통해
* 영상 설명(요약)과 길이(초)를 가져와 폼에 자동 세팅합니다.
*/
async function fetchYouTubeInfo() {
const urlInput = document.getElementById('content_url_input');
const descInput = document.getElementById('description_input');
const contentTmIn = document.getElementById('content_tm_input');
const fetchBtn = document.getElementById('btn-yt-fetch');
if (!urlInput) return;
const raw = urlInput.value.trim();
if (!raw) {
showYtStatus('유튜브 URL 또는 영상 ID를 입력해주세요.', 'error');
return;
}
// 버튼 로딩 상태
if (fetchBtn) {
fetchBtn.disabled = true;
fetchBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 불러오는 중...';
}
showYtStatus('영상 정보를 불러오는 중...', 'loading');
try {
const res = await fetch(`../bbs/youtube_info.php?video_id=${encodeURIComponent(raw)}`);
const data = await res.json();
if (!data.success) {
showYtStatus('⚠ ' + (data.message || '영상 정보를 가져올 수 없습니다.'), 'error');
return;
}
// 콘텐츠설명 자동 입력 (기존 값이 있으면 확인 후 덮어쓰기)
if (descInput) {
const existing = descInput.value.trim();
if (existing) {
if (confirm('콘텐츠설명이 이미 입력되어 있습니다.\n영상 정보로 덮어쓰시겠습니까?')) {
descInput.value = data.description || '';
}
} else {
descInput.value = data.description || '';
}
}
// content_tm (영상 길이, 초 단위) hidden input 세팅
if (contentTmIn) {
contentTmIn.value = data.content_tm || 0;
}
// 콘텐츠설명 라벨 옆 영상 길이 표시
updateDurationDisplay(data.content_tm || 0);
// 성공 메시지
const ytTitle = data.yt_title ? ` — ${data.yt_title}` : '';
showYtStatus(`✓ 로드 완료${ytTitle} (영상 길이: ${data.duration_fmt || ''})`, 'success');
} catch (e) {
showYtStatus('⚠ 서버 통신 오류가 발생했습니다.', 'error');
} finally {
if (fetchBtn) {
fetchBtn.disabled = false;
fetchBtn.innerHTML = '<i class="fa-brands fa-youtube"></i>영상 정보 가져오기';
}
}
}
/**
* 영상 길이(초)를 '분:초' 형식으로 콘텐츠설명 라벨 옆에 표시합니다.
*/
function updateDurationDisplay(seconds) {
const display = document.getElementById('yt-duration-display');
const text = document.getElementById('yt-duration-text');
if (!display || !text) return;
if (seconds && seconds > 0) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
text.textContent = `${m}${String(s).padStart(2, '0')}초`;
display.classList.remove('hidden', 'text-gray-300', 'font-normal');
display.classList.add('text-teal-600', 'font-bold');
} else {
display.classList.add('hidden');
display.classList.remove('text-teal-600', 'font-bold');
}
}
/**
* YouTube API 상태 메시지를 표시합니다.
* type: 'success' | 'error' | 'loading'
*/
function showYtStatus(msg, type) {
const div = document.getElementById('yt-fetch-status');
const span = document.getElementById('yt-fetch-msg');
if (!div || !span) return;
span.textContent = msg;
div.classList.remove('hidden', 'text-green-600', 'text-red-500', 'text-gray-500');
if (type === 'success') div.classList.add('text-green-600');
else if (type === 'error') div.classList.add('text-red-500');
else div.classList.add('text-gray-500');
}