/** * ===================== * 사용페이지 : 마이페이지 * 작 성 자 : ERP기획팀 moon * 최초작성일자: 26.03.20. * ===================== */ (function () { /** * 콘텐츠 탭별 페이지 상태 관리 * - page: 현재까지 불러온 페이지 번호 * - loading: 현재 요청 중 여부 * - isLast: 마지막 페이지 여부 */ const contentListState = { watching: { page: 1, loading: false, isLast: false }, completed: { page: 1, loading: false, isLast: false }, saved: { page: 1, loading: false, isLast: false } }; /** * 특정 탭의 페이지 상태를 초기화하는 함수 * - 탭을 새로 조회할 때 page, loading, isLast 값을 초기화한다. */ function resetContentListState(tab) { if (!contentListState[tab]) return; contentListState[tab].page = 1; contentListState[tab].loading = false; contentListState[tab].isLast = false; } /** * 마이페이지 초기 진입 시 실행 * - 연도 변경 이벤트 연결 * - 학습활동 데이터 로드 * - 콘텐츠 탭 클릭 시 재렌더링 이벤트 연결 */ document.addEventListener("DOMContentLoaded", function () { // 연도 select 변경 이벤트 연결 bindYearSelectChange(); // 학습활동 초기 데이터 조회 loadMypageInit(); // 콘텐츠 탭 클릭 시 리스트 재조회 이벤트 연결 bindContentTabReload(); //초기 실행에 무한스크롤 바인딩 추가 bindInfiniteScroll(); // 콘텐츠 카드 삭제 버튼 이벤트 연결 bindRemoveCardAction(); // 북마크 체크박스 이벤트 연결 bindBookmarkToggleAction(); // [제안하기]PC용 전용 폼 검증/제출 연결 bindOfferFormValidation(); bindOfferFormSubmit(); // [제안하기]모바일 전용 bindMobileOfferFormValidation(); bindMobileOfferSubmit(); // ★ 초기 선택년도 기준 한줄소감 / 학습레벨 조회 const yearSelectEl = document.getElementById('mypage-year-select'); const initialYear = yearSelectEl ? (yearSelectEl.value || '') : ''; loadReviewList(initialYear); loadLevelData(initialYear); }); /** * 모든 리소스 로드 완료 후 실행 * - 퍼블리싱용 mypage.js 초기화가 끝난 뒤 * 현재 활성 탭 클릭을 한 번 강제 실행하고, * 3개 콘텐츠 탭을 모두 미리 조회한다. */ window.addEventListener("load", function () { setTimeout(function () { // 현재 활성 탭 강제 클릭 triggerInitialActiveTabClick(); // 3개 탭 목록 모두 미리 조회 preloadAllContentLists(); }, 200); }); /** * 최초 로드시 현재 활성 탭 버튼을 강제로 한 번 클릭시키는 함수 * - 퍼블리싱용 mypage.js의 탭 전환 로직을 그대로 사용한다. * - 이후 bindContentTabReload()에 연결된 재렌더링 로직이 함께 실행된다. */ function triggerInitialActiveTabClick() { // 현재 active 상태의 탭 버튼 찾기 const activeTab = document.querySelector('.content-tab.active'); if (!activeTab) return; console.log('[초기 활성 탭 강제 클릭]', activeTab.getAttribute('data-tab')); // 실제 클릭 이벤트 발생 activeTab.click(); } /** * 초기 콘텐츠 리스트 렌더링 안정화 함수 * - 최초 로드 시 mypage.js가 DOM을 다시 건드리는 경우가 있어 * 현재 활성 탭 기준으로 일정 횟수 재렌더링을 수행한다. * - 카드가 정상적으로 들어오면 자동 중단한다. */ /* function stabilizeInitialContentRendering() { let tryCount = 0; const maxTry = 8; const timer = setInterval(function () { tryCount++; // 시청중 리스트에 카드가 실제로 들어왔는지 확인 const watchingList = document.getElementById('mypage-watching-list'); const cardCount = watchingList ? watchingList.querySelectorAll('.content-card').length : 0; console.log('[초기 복구 시도]', tryCount, 'watching card count:', cardCount); // 카드가 정상 삽입되었으면 중단 if (cardCount > 0) { clearInterval(timer); } // 최대 시도 횟수 초과 시 중단 if (tryCount >= maxTry) { clearInterval(timer); } }, 250); } */ /** * 마이페이지 초기 데이터 로드 함수 * - 서버에서 학습활동 관련 데이터를 가져온다 * - 연도 select, 총 학습시간, 전체평균, 카테고리별 수치를 화면에 반영한다. */ function loadMypageInit(year = '') { fetch('/edu/ajax/get_init_data_for_mypage.php?year=' + year) .then(function (res) { return res.json(); }) .then(function (data) { // 실패 시 종료 if (!data.success) return; // 1. 연도 select 구성 renderYearSelect(data.year_list, data.selected_year); // 2. 총 학습시간 표시 renderMyTotalMinutes(data.my_total_minutes); // 3. 모바일 총 학습시간 표시 renderMobileMyTotalMinutes(data.my_total_minutes); // 4. 전체평균 표시 renderAvgTotalMinutes(data.avg_total_minutes); // 5. 상단 총 학습시간 게이지 표시 renderMyTotalGauge(data.my_total_minutes); // 6. 마이클래스 항목 표시 renderMyClassActivity(data.activity_items); // 7. 온보딩 학습활동 데이터를 화면에 반영 renderOnboardingActivity(data.activity_items); // 8. 법정교육 학습활동 데이터를 화면에 반영 renderLegalEducationActivity(data.activity_items); // 9. 리더십 학습활동 데이터를 화면에 반영 renderLeadershipActivity(data.activity_items); // 10. 인사이트 학습활동 데이터를 화면에 반영 renderInsightActivity(data.activity_items); // 11. 비즈트렌드 학습활동 데이터를 화면에 반영 renderBizTrendActivity(data.activity_items); }) .catch(function (error) { console.error('학습활동 데이터를 불러오지 못했습니다.', error); }); } /** * 연도 select 옵션을 구성하는 함수 * - 서버에서 받은 year_list를 기준으로 option 생성 * - selectedYear와 일치하는 값을 selected 처리 */ function renderYearSelect(yearList, selectedYear) { const select = document.getElementById('mypage-year-select'); if (!select) return; select.innerHTML = ''; yearList.forEach(function (year) { const option = document.createElement('option'); option.value = year; option.textContent = year + '년'; if (year == selectedYear) { option.selected = true; } select.appendChild(option); }); } /** * 총 학습시간(분)을 화면에 표시하는 함수 * - 서버에서 받은 my_total_minutes 값을 * #mypage-total-minutes 영역에 표시한다. * - 숫자는 천단위 콤마 포맷 적용 */ function renderMyTotalMinutes(totalMinutes) { const el = document.getElementById('mypage-total-minutes'); if (!el) return; const value = parseInt(totalMinutes, 10) || 0; el.textContent = value.toLocaleString(); } /** * 모바일 총 학습시간(분)을 화면에 표시하는 함수 * - 서버에서 받은 my_total_minutes 값을 * #mobile-mypage-total-minutes 영역에 표시한다. * - 숫자는 천단위 콤마 포맷 적용 */ function renderMobileMyTotalMinutes(totalMinutes) { const el = document.getElementById('mobile-mypage-total-minutes'); if (!el) return; const value = parseInt(totalMinutes, 10) || 0; el.textContent = value.toLocaleString(); } /** * 전체평균 학습시간(분)을 화면에 표시하는 함수 * - 서버에서 받은 avg_total_minutes 값을 * #mypage-avg-minutes 영역에 표시한다. * - 숫자는 천단위 콤마 포맷 적용 */ function renderAvgTotalMinutes(avgTotalMinutes) { const el = document.getElementById('mypage-avg-minutes'); if (!el) return; const value = parseInt(avgTotalMinutes, 10) || 0; el.textContent = value.toLocaleString(); } /** * 총 학습시간 게이지를 화면에 반영하는 함수 * - 서버에서 받은 my_total_minutes 값을 기준으로 * #mypage-total-gauge의 width 값을 변경한다. * - 최대 기준은 40시간(2400분)으로 계산한다. * - 2400분을 초과해도 게이지는 100%를 넘지 않도록 제한한다. */ function renderMyTotalGauge(totalMinutes) { const el = document.getElementById('mypage-total-gauge'); if (!el) return; const value = parseInt(totalMinutes, 10) || 0; const maxMinutes = 2400; let percent = Math.round((value / maxMinutes) * 100); if (percent < 0) percent = 0; if (percent > 100) percent = 100; el.style.width = percent + '%'; } /** * 연도 select 변경 이벤트를 연결하는 함수 * - 사용자가 연도를 변경하면 * 학습활동, 한줄소감, 학습레벨, 콘텐츠 탭을 함께 다시 조회한다. */ function bindYearSelectChange() { const select = document.getElementById('mypage-year-select'); if (!select) return; select.addEventListener('change', function () { const selectedYear = this.value || ''; // ★ 학습활동 재조회 loadMypageInit(selectedYear); // ★ 한줄소감 재조회 loadReviewList(selectedYear); // ★ 학습레벨 재조회 loadLevelData(selectedYear); // ★ 콘텐츠 탭 상태 초기화 resetContentListState('watching'); resetContentListState('completed'); resetContentListState('saved'); // ★ 콘텐츠 3개 탭 재조회 loadWatchingContentList(1); loadCompletedContentList(1); loadSavedContentList(1); }); } /** * 마이클래스(CA10001) 학습활동 데이터를 화면에 반영하는 함수 * - activity_items 배열에서 마이클래스 항목만 찾아서 * 해당 li[data-category="CA10001"] 영역의 수치를 갱신한다. * - 학습시간, 퍼센트, 게이지, 전체 콘텐츠 개수를 반영한다. */ function renderMyClassActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10001'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10001"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalCount = parseInt(item.master_total_cnt, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분 (' + percent + '%)'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalCount.toLocaleString() + '개'; } } /** * 온보딩(CA10002) 학습활동 데이터를 화면에 반영하는 함수 * - 학습시간, 퍼센트, 게이지, 분모를 표시 * - 온보딩 기간일 경우에만 "필수 시청 날짜"를 노출한다. */ function renderOnboardingActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10002'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10002"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalCount = parseInt(item.master_total_cnt, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분 (' + percent + '%)'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalCount.toLocaleString() + '개'; } const noteEl = li.querySelector('.activity-note'); const dateEl = li.querySelector('.onboarding-due-date'); if (noteEl && dateEl) { if (item.onboarding_due_date) { const d = new Date(item.onboarding_due_date); if (!isNaN(d.getTime())) { const yy = String(d.getFullYear()).slice(2); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); dateEl.textContent = yy + '.' + mm + '.' + dd; } } if (item.is_onboarding_period === 'Y') { noteEl.style.display = ''; } else { noteEl.style.display = 'none'; } } } /** * 법정교육(CA10003) 학습활동 데이터를 화면에 반영하는 함수 * - 학습시간, 퍼센트, 게이지, 분모를 표시한다. * - activity_items 배열에서 법정교육 항목만 찾아 * 해당 li[data-category="CA10003"] 영역을 갱신한다. */ function renderLegalEducationActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10003'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10003"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalCount = parseInt(item.master_total_cnt, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분 (' + percent + '%)'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalCount.toLocaleString() + '개'; } } /** * 리더십(CA10004) 학습활동 데이터를 화면에 반영하는 함수 * - 학습시간, 게이지, 분모(분)를 표시한다. * - activity_items 배열에서 리더십 항목만 찾아 * 해당 li[data-category="CA10004"] 영역을 갱신한다. */ function renderLeadershipActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10004'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10004"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalContentTm = parseInt(item.total_content_tm, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalContentTm.toLocaleString() + '분'; } } /** * 인사이트(CA10005) 학습활동 데이터를 화면에 반영하는 함수 * - 학습시간, 게이지, 분모(분)를 표시한다. * - activity_items 배열에서 인사이트 항목만 찾아 * 해당 li[data-category="CA10005"] 영역을 갱신한다. */ function renderInsightActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10005'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10005"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalContentTm = parseInt(item.total_content_tm, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalContentTm.toLocaleString() + '분'; } } /** * 비즈트렌드(CA10006) 학습활동 데이터를 화면에 반영하는 함수 * - 학습시간, 게이지, 분모(분)를 표시한다. * - activity_items 배열에서 비즈트렌드 항목만 찾아 * 해당 li[data-category="CA10006"] 영역을 갱신한다. */ function renderBizTrendActivity(activityItems) { if (!Array.isArray(activityItems)) return; const item = activityItems.find(function (row) { return row.category_code === 'CA10006'; }); if (!item) return; const li = document.querySelector('.activity-list li[data-category="CA10006"]'); if (!li) return; const totalWatchTm = parseInt(item.total_watch_tm, 10) || 0; const percent = parseInt(item.percent, 10) || 0; const totalContentTm = parseInt(item.total_content_tm, 10) || 0; const valueEl = li.querySelector('.activity-value'); if (valueEl) { valueEl.innerHTML = '' + totalWatchTm.toLocaleString() + '분'; } const gaugeEl = li.querySelector('.gauge-fill'); if (gaugeEl) { gaugeEl.style.width = percent + '%'; } const endEl = li.querySelector('.gauge-end'); if (endEl) { endEl.textContent = totalContentTm.toLocaleString() + '분'; } } /** * 콘텐츠 탭 클릭 이벤트 감지 후 * 해당 탭에 맞는 데이터를 다시 렌더링하는 함수 * * - mypage.js가 탭 전환을 처리한 이후 실행된다 * - 각 탭(watching / completed / saved)에 따라 * AJAX 호출을 다시 수행하여 리스트를 덮어쓴다 */ function bindContentTabReload() { const tabs = document.querySelectorAll('.content-tab'); tabs.forEach(function (tabBtn) { tabBtn.addEventListener('click', function () { const tab = this.getAttribute('data-tab'); setTimeout(function () { // 탭 전환 시 페이지 상태 초기화 resetContentListState(tab); if (tab === 'watching') { loadWatchingContentList(1); } else if (tab === 'completed') { loadCompletedContentList(1); } else if (tab === 'saved') { loadSavedContentList(1); } console.log('[탭 재렌더링]', tab); }, 100); }); }); } /** * 현재 활성화된 콘텐츠 탭을 기준으로 * 해당 탭의 리스트를 다시 렌더링하는 함수 * * - 최초 페이지 진입 시 사용 * - mypage.js가 탭/패널 초기화를 끝낸 뒤 실행되어야 한다 */ /* function reloadActiveContentTab() { const activeTab = document.querySelector('.content-tab.active'); if (!activeTab) return; const tab = activeTab.getAttribute('data-tab'); if (tab === 'watching') { loadWatchingContentList(); } else if (tab === 'completed') { loadCompletedContentList(); } else if (tab === 'saved') { loadSavedContentList(); } console.log('[초기 활성 탭 재렌더링]', tab); } */ /** * 시청중인 콘텐츠 목록을 서버에서 조회하여 화면에 표시하는 함수 * - page=1이면 목록 전체를 교체한다. * - page>1이면 기존 목록 아래에 이어붙인다. * - 선택된 년도 기준으로 데이터를 조회한다. */ function loadWatchingContentList(page = 1) { const listEl = document.getElementById('mypage-watching-list'); if (!listEl) return; const state = contentListState.watching; if (state.loading || state.isLast) return; // ★ 요청 시작 상태 반영 state.loading = true; // ★ 현재 선택년도 수집 const selectedYear = getSelectedMypageYear(); fetch('/edu/ajax/get_video_list_for_mypage.php?tab=watching&page=' + page + '&selected_year=' + encodeURIComponent(selectedYear)) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; // ★ page 1이면 전체 교체, 그 외는 이어붙이기 if (page === 1) { if ((data.html || '').trim() === '') { renderEmptyContentMessage(listEl); } else { listEl.innerHTML = data.html || ''; } } else { listEl.insertAdjacentHTML('beforeend', data.html || ''); } // ★ 탭 개수 갱신 renderContentTabCounts(data.counts); // ★ 상태 갱신 state.page = page; state.isLast = !!data.is_last; console.log('[watching] 렌더링 완료:', page, state.isLast); }) .catch(function (error) { console.error('시청중 콘텐츠 목록을 불러오지 못했습니다.', error); }) .finally(function () { state.loading = false; }); } /** * 시청완료 콘텐츠 목록 로드 * - page=1이면 목록 전체를 교체한다. * - page>1이면 기존 목록 아래에 이어붙인다. * - 선택된 년도 기준으로 데이터를 조회한다. */ function loadCompletedContentList(page = 1) { const listEl = document.querySelector('#panel-completed .content-grid'); if (!listEl) return; const state = contentListState.completed; if (state.loading || state.isLast) return; state.loading = true; // ★ 현재 선택년도 수집 const selectedYear = getSelectedMypageYear(); fetch('/edu/ajax/get_video_list_for_mypage.php?tab=completed&page=' + page + '&selected_year=' + encodeURIComponent(selectedYear)) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; if (page === 1) { if ((data.html || '').trim() === '') { renderEmptyContentMessage(listEl); } else { listEl.innerHTML = data.html || ''; } } else { listEl.insertAdjacentHTML('beforeend', data.html || ''); } renderContentTabCounts(data.counts); state.page = page; state.isLast = !!data.is_last; console.log('[completed] 렌더링 완료:', page, state.isLast); }) .catch(function (error) { console.error('시청완료 콘텐츠 목록을 불러오지 못했습니다.', error); }) .finally(function () { state.loading = false; }); } /** * 저장한 콘텐츠 목록 로드 * - page=1이면 목록 전체를 교체한다. * - page>1이면 기존 목록 아래에 이어붙인다. * - 선택된 년도 기준으로 데이터를 조회한다. */ function loadSavedContentList(page = 1) { const listEl = document.querySelector('#panel-saved .content-grid'); if (!listEl) return; const state = contentListState.saved; if (state.loading || state.isLast) return; state.loading = true; // ★ 현재 선택년도 수집 const selectedYear = getSelectedMypageYear(); fetch('/edu/ajax/get_video_list_for_mypage.php?tab=saved&page=' + page + '&selected_year=' + encodeURIComponent(selectedYear)) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; if (page === 1) { if ((data.html || '').trim() === '') { renderEmptyContentMessage(listEl); } else { listEl.innerHTML = data.html || ''; } } else { listEl.insertAdjacentHTML('beforeend', data.html || ''); } renderContentTabCounts(data.counts); state.page = page; state.isLast = !!data.is_last; console.log('[saved] 렌더링 완료:', page, state.isLast); }) .catch(function (error) { console.error('저장한 콘텐츠 목록을 불러오지 못했습니다.', error); }) .finally(function () { state.loading = false; }); } /** * 콘텐츠 탭의 개수(count)를 화면에 반영하는 함수 * - 서버에서 받은 counts 값을 * 시청중 / 시청완료 / 저장한 콘텐츠 탭에 표시한다. */ function renderContentTabCounts(counts) { // counts 객체가 없으면 종료 if (!counts) return; // 각 탭 버튼의 count 영역 찾기 const watchingCountEl = document.querySelector('#tab-watching .count'); const completedCountEl = document.querySelector('#tab-completed .count'); const savedCountEl = document.querySelector('#tab-saved .count'); // 숫자 변환 및 예외 방어 const watchingCount = parseInt(counts.watching, 10) || 0; const completedCount = parseInt(counts.completed, 10) || 0; const savedCount = parseInt(counts.saved, 10) || 0; // 화면 반영 if (watchingCountEl) { watchingCountEl.textContent = watchingCount.toLocaleString(); } if (completedCountEl) { completedCountEl.textContent = completedCount.toLocaleString(); } if (savedCountEl) { savedCountEl.textContent = savedCount.toLocaleString(); } } /** * 콘텐츠 3개 탭의 목록을 최초 로드 시 미리 조회하는 함수 * - 시청중 / 시청완료 / 저장한 콘텐츠를 모두 한 번씩 조회한다. * - 이후 사용자가 탭을 눌렀을 때는 이미 최신 구조가 반영된 상태가 된다. */ function preloadAllContentLists() { // 시청중 콘텐츠 조회 loadWatchingContentList(); // 시청완료 콘텐츠 조회 loadCompletedContentList(); // 저장한 콘텐츠 조회 loadSavedContentList(); } /** * 현재 활성 탭 기준으로 무한스크롤을 처리하는 함수 * - 화면 하단 근처까지 내려오면 * 현재 활성 탭의 다음 페이지를 조회한다. */ function bindInfiniteScroll() { window.addEventListener('scroll', function () { // 현재 문서 스크롤 위치 계산 const scrollTop = window.scrollY || document.documentElement.scrollTop; const windowHeight = window.innerHeight; const docHeight = document.documentElement.scrollHeight; // 하단 200px 전 도달 시 동작 if (scrollTop + windowHeight < docHeight - 200) return; // 현재 활성 탭 찾기 const activeTab = document.querySelector('.content-tab.active'); if (!activeTab) return; const tab = activeTab.getAttribute('data-tab'); if (tab === 'watching') { loadWatchingContentList(contentListState.watching.page + 1); } else if (tab === 'completed') { loadCompletedContentList(contentListState.completed.page + 1); } else if (tab === 'saved') { loadSavedContentList(contentListState.saved.page + 1); } }); } /** * 콘텐츠 카드의 삭제 버튼 클릭 이벤트를 연결하는 함수 * - 동적으로 생성된 카드에도 동작하도록 이벤트 위임 방식으로 처리한다. * - 삭제 성공 시 현재 탭 목록을 1페이지부터 다시 조회한다. */ function bindRemoveCardAction() { // 문서 전체에 클릭 이벤트 연결 document.addEventListener('click', function (event) { // 삭제 버튼이 아니면 종료 const removeBtn = event.target.closest('.btn-remove-card'); if (!removeBtn) return; // 기본 동작 차단 event.preventDefault(); event.stopPropagation(); // 카드 요소 찾기 const cardEl = removeBtn.closest('.content-card'); if (!cardEl) return; // 콘텐츠 ID 가져오기 const contentId = cardEl.getAttribute('data-content-id'); if (!contentId) { alert('콘텐츠 ID를 찾을 수 없습니다.'); return; } // 현재 활성 탭 확인 const activeTab = document.querySelector('.content-tab.active'); const tab = activeTab ? activeTab.getAttribute('data-tab') : 'watching'; // 사용자 확인 const isConfirmed = window.confirm('해당 콘텐츠를 목록에서 제거하시겠습니까?'); if (!isConfirmed) return; // 삭제 요청 실행 removeContentCard(contentId, tab); }); } /** * 콘텐츠 삭제 요청 함수 * - 서버에 content_id와 현재 탭 정보를 전달하여 삭제 처리한다. * - 삭제 성공 시 해당 탭의 목록을 1페이지부터 다시 조회한다. */ function removeContentCard(contentId, tab) { fetch('/edu/ajax/delete_mypage_content_item.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content_id: contentId, tab: tab }) }) .then(function (res) { return res.json(); }) .then(function (data) { // 실패 시 종료 if (!data.success) { alert(data.message || '삭제에 실패했습니다.'); return; } // 현재 탭 상태 초기화 resetContentListState(tab); // 현재 탭 목록 다시 조회 if (tab === 'watching') { loadWatchingContentList(1); } else if (tab === 'completed') { loadCompletedContentList(1); } else if (tab === 'saved') { loadSavedContentList(1); } console.log('[삭제 완료]', contentId, tab); }) .catch(function (error) { console.error('삭제 중 오류가 발생했습니다.', error); alert('삭제 중 오류가 발생했습니다.'); }); } /** * 콘텐츠 카드의 북마크 체크박스 변경 이벤트를 연결하는 함수 * - 동적으로 생성된 카드에도 동작하도록 이벤트 위임 방식으로 처리한다. * - 체크 시 저장, 체크 해제 시 저장 취소 요청을 보낸다. */ function bindBookmarkToggleAction() { // 문서 전체에 change 이벤트 연결 document.addEventListener('change', function (event) { // 북마크 체크박스가 아니면 종료 const checkbox = event.target.closest('.bookmark input[type="checkbox"]'); if (!checkbox) return; // 카드 요소 찾기 const cardEl = checkbox.closest('.content-card'); if (!cardEl) return; // 콘텐츠 ID 가져오기 const contentId = cardEl.getAttribute('data-content-id'); if (!contentId) { console.error('content_id가 없습니다.'); checkbox.checked = !checkbox.checked; return; } // 현재 체크 상태 확인 const isChecked = checkbox.checked; // 서버 저장/해제 요청 toggleBookmark(contentId, isChecked, checkbox); }); } /** * 북마크 저장/해제 요청 함수 * - 체크 여부에 따라 서버에 저장 또는 해제 요청을 보낸다. * - 실패 시 체크 상태를 원복한다. * - saved 탭에서 체크 해제 시에는 현재 카드를 즉시 화면에서 제거한다. */ function toggleBookmark(contentId, isChecked, checkboxEl) { fetch('/edu/ajax/toggle_content_wishlist.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content_id: contentId, is_checked: isChecked ? 'Y' : 'N' }) }) .then(function (res) { return res.json(); }) .then(function (data) { // 실패 시 체크 상태 원복 if (!data.success) { alert(data.message || '북마크 처리에 실패했습니다.'); checkboxEl.checked = !isChecked; return; } // 탭 개수 갱신 if (data.counts) { renderContentTabCounts(data.counts); } //추가된 핵심 syncBookmarkState(contentId, isChecked); // 현재 활성 탭 확인 const activeTab = document.querySelector('.content-tab.active'); const currentTab = activeTab ? activeTab.getAttribute('data-tab') : ''; // saved 탭에서 체크 해제한 경우: // 현재 카드만 즉시 제거하여 UX 개선 if (currentTab === 'saved' && isChecked === false) { const cardEl = checkboxEl.closest('.content-card'); if (cardEl) { cardEl.remove(); } // 리스트가 비었으면 안내 문구 표시 const savedListEl = document.querySelector('#panel-saved .content-grid'); if (savedListEl && savedListEl.querySelectorAll('.content-card').length === 0) { renderEmptyContentMessage( savedListEl); } } console.log('[북마크 처리 완료]', contentId, isChecked ? 'save' : 'remove'); }) .catch(function (error) { console.error('북마크 처리 중 오류가 발생했습니다.', error); checkboxEl.checked = !isChecked; alert('북마크 처리 중 오류가 발생했습니다.'); }); } /** * 콘텐츠 리스트가 비었는지 확인하고, * 비어 있으면 안내 문구를 표시하는 함수 * - 현재는 saved 탭에서 북마크 해제 후 마지막 카드 제거 상황에 사용한다. */ function renderEmptyContentMessageIfNeeded(tab) { let listEl = null; if (tab === 'watching') { listEl = document.getElementById('mypage-watching-list'); } else if (tab === 'completed') { listEl = document.querySelector('#panel-completed .content-grid'); } else if (tab === 'saved') { listEl = document.querySelector('#panel-saved .content-grid'); } if (!listEl) return; const cardCount = listEl.querySelectorAll('.content-card').length; if (cardCount > 0) return; listEl.innerHTML = '
  • 표시할 콘텐츠가 없습니다.
  • '; } /** * 동일한 content_id를 가진 모든 카드의 북마크 상태를 동기화하는 함수 * - 한 탭에서 체크/해제 시 다른 탭의 카드에도 즉시 반영한다. */ function syncBookmarkState(contentId, isChecked) { // 동일 content_id를 가진 모든 카드 찾기 const cards = document.querySelectorAll('.content-card[data-content-id="' + contentId + '"]'); cards.forEach(function (cardEl) { const checkbox = cardEl.querySelector('.bookmark input[type="checkbox"]'); if (!checkbox) return; checkbox.checked = isChecked; }); console.log('[북마크 동기화]', contentId, isChecked); } /** * PC 제안하기 폼 제출 이벤트를 연결하는 함수 * - 기본 submit을 막고 AJAX로 제안 등록을 처리한다. * - 저장 성공 시 입력값 초기화 및 버튼 비활성화를 수행한다. */ function bindOfferFormSubmit() { const formEl = document.getElementById('offerForm'); if (!formEl) return; formEl.addEventListener('submit', function (event) { // 기본 submit 막기 event.preventDefault(); // AJAX 제출 실행 submitOfferFormAjax(); }); } /** * PC 제안하기 폼의 입력값 상태에 따라 버튼 활성화 여부를 제어하는 함수 * - URL, 추천이유가 모두 입력된 경우에만 버튼을 활성화한다. */ function bindOfferFormValidation() { const formEl = document.getElementById('offerForm'); if (!formEl) return; const urlEl = formEl.querySelector('#suggest-url'); const reasonEl = formEl.querySelector('#suggest-reason'); const submitBtn = formEl.querySelector('button[type="submit"]'); if (!urlEl || !reasonEl || !submitBtn) return; function updateOfferSubmitButtonState() { const hasUrl = (urlEl.value || '').trim() !== ''; const hasReason = (reasonEl.value || '').trim() !== ''; submitBtn.disabled = !(hasUrl && hasReason); } // 입력 이벤트 연결 urlEl.addEventListener('input', updateOfferSubmitButtonState); reasonEl.addEventListener('input', updateOfferSubmitButtonState); // 초기 상태 반영 updateOfferSubmitButtonState(); } /** * 제안하기 폼을 AJAX로 제출하는 함수 * - FormData를 사용하여 insert_offer.php로 전송한다. * - 성공 시 입력값 초기화, 버튼 비활성화, 제안현황 갱신을 수행한다. */ function submitOfferFormAjax() { const formEl = document.getElementById('offerForm'); if (!formEl) return; const submitBtn = formEl.querySelector('button[type="submit"]'); const urlEl = formEl.querySelector('#suggest-url'); const reasonEl = formEl.querySelector('#suggest-reason'); if (!submitBtn || !urlEl || !reasonEl) return; // ★ 중복 클릭 방지 submitBtn.disabled = true; const formData = new FormData(formEl); fetch('/edu/ajax/insert_offer.php', { method: 'POST', body: formData }) .then(function (res) { // ★ JSON 파싱 전에 원문 확인 return res.text(); }) .then(function (text) { console.log('[insert_offer.php 응답 원문]', text); let data = null; try { data = JSON.parse(text); } catch (e) { throw new Error('JSON 파싱 실패: ' + text); } if (!data.success) { alert(data.message || '제안 등록에 실패했습니다.'); submitBtn.disabled = false; return; } // 성공 안내 alert(data.message || '제안이 등록되었습니다.'); // 입력값 초기화 urlEl.value = ''; reasonEl.value = ''; // 버튼 비활성화 submitBtn.disabled = true; // 제안현황 다시 조회 loadOfferHistory(); }) .catch(function (error) { console.error('제안 등록 중 오류가 발생했습니다.', error); alert('제안 등록 중 오류가 발생했습니다.'); submitBtn.disabled = false; }); } /** * 제안현황 목록을 다시 조회하여 화면에 반영하는 함수 * - 제안 등록 성공 후 최신 제안현황을 다시 불러온다. */ function loadOfferHistory() { const listEl = document.querySelector('.suggest-status-list'); if (!listEl) return; fetch('/edu/ajax/get_offer_history.php') .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; listEl.innerHTML = data.html || ''; }) .catch(function (error) { console.error('제안현황을 불러오지 못했습니다.', error); }); } /** * 모바일 제안하기 입력값 상태에 따라 확인 버튼 활성화 여부를 제어하는 함수 * - URL, 추천이유가 모두 입력된 경우에만 버튼을 활성화한다. */ function bindMobileOfferFormValidation() { const urlEl = document.getElementById('sheet-url'); const reasonEl = document.getElementById('sheet-reason'); const confirmBtn = document.getElementById('btnSheetConfirm'); if (!urlEl || !reasonEl || !confirmBtn) return; function updateMobileOfferButtonState() { const hasUrl = (urlEl.value || '').trim() !== ''; const hasReason = (reasonEl.value || '').trim() !== ''; confirmBtn.disabled = !(hasUrl && hasReason); } urlEl.addEventListener('input', updateMobileOfferButtonState); reasonEl.addEventListener('input', updateMobileOfferButtonState); updateMobileOfferButtonState(); } /** * 모바일 제안하기 내용을 AJAX로 저장하는 함수 * - sheet-url, sheet-reason 값을 insert_offer.php로 전송한다. * - 성공 시 입력값 초기화, 버튼 비활성화, 바텀시트 닫기, 제안현황 갱신을 수행한다. */ function submitMobileOfferAjax() { const urlEl = document.getElementById('sheet-url'); const reasonEl = document.getElementById('sheet-reason'); const confirmBtn = document.getElementById('btnSheetConfirm'); if (!urlEl || !reasonEl || !confirmBtn) return; const referenceUrl = (urlEl.value || '').trim(); const reason = (reasonEl.value || '').trim(); if (referenceUrl === '') { alert('URL을 입력해주세요.'); return; } if (reason === '') { alert('추천이유를 입력해주세요.'); return; } confirmBtn.disabled = true; const formData = new FormData(); formData.append('type_code', 'OF10001'); formData.append('status_code', 'OF10002'); formData.append('reference_url', referenceUrl); formData.append('reason', reason); fetch('/edu/ajax/insert_offer.php', { method: 'POST', body: formData }) .then(function (res) { return res.text(); }) .then(function (text) { console.log('[모바일 insert_offer.php 응답 원문]', text); let data = null; try { data = JSON.parse(text); } catch (e) { throw new Error('JSON 파싱 실패: ' + text); } if (!data.success) { alert(data.message || '제안 등록에 실패했습니다.'); confirmBtn.disabled = false; return; } alert(data.message || '제안이 등록되었습니다.'); // 입력값 초기화 urlEl.value = ''; reasonEl.value = ''; // 버튼 비활성화 confirmBtn.disabled = true; // 제안현황 다시 조회 loadOfferHistory(); // 모바일 바텀시트 닫기 closeMobileSuggestSheet(); }) .catch(function (error) { console.error('모바일 제안 등록 중 오류가 발생했습니다.', error); alert('제안 등록 중 오류가 발생했습니다.'); confirmBtn.disabled = false; }); } /** * 모바일 제안하기 확인 버튼 클릭 이벤트를 연결하는 함수 * - 클릭 시 AJAX 저장을 수행한다. */ function bindMobileOfferSubmit() { const confirmBtn = document.getElementById('btnSheetConfirm'); if (!confirmBtn) return; confirmBtn.addEventListener('click', function (event) { event.preventDefault(); submitMobileOfferAjax(); }); } /** * 모바일 제안하기 바텀시트를 닫는 함수 * - hidden 속성을 다시 설정하여 닫힘 상태로 만든다. */ function closeMobileSuggestSheet() { const sheetEl = document.getElementById('mobileSuggestSheet'); if (!sheetEl) return; sheetEl.hidden = true; } /** * 콘텐츠 리스트가 비어 있을 때 빈 상태 문구를 렌더링하는 함수 * - 탭 종류에 따라 적절한 안내 문구를 표시한다. */ /* function renderEmptyContentMessage(tab, listEl) { if (!listEl) return; let message = '표시할 콘텐츠가 없습니다.'; if (tab === 'watching') { message = '시청중인 콘텐츠가 없습니다.'; } else if (tab === 'completed') { message = '시청완료 콘텐츠가 없습니다.'; } else if (tab === 'saved') { message = '저장한 콘텐츠가 없습니다.'; } listEl.innerHTML = '
  • ' + message + '
  • '; } */ /** * 콘텐츠 리스트가 비어 있을 때 빈 상태 문구를 렌더링하는 함수 * - 현재 활성 탭 기준으로 메시지를 자동 결정한다. */ function renderEmptyContentMessage(listEl) { if (!listEl) return; // ★ 현재 활성 탭 가져오기 const activeTab = document.querySelector('.content-tab.active'); const tab = activeTab ? activeTab.getAttribute('data-tab') : ''; let message = '표시할 콘텐츠가 없습니다.'; if (tab === 'watching') { message = '시청중인 콘텐츠가 없습니다.'; } else if (tab === 'completed') { message = '시청완료 콘텐츠가 없습니다.'; } else if (tab === 'saved') { message = '저장한 콘텐츠가 없습니다.'; } listEl.innerHTML = '
  • ' + message + '
  • '; } /** * 한줄소감 리스트를 서버에서 조회하여 화면에 반영하는 함수 * - 선택년도 기준으로 한줄소감 HTML 조각과 건수를 받아온다. */ function loadReviewList(selectedYear = '') { const listEl = document.getElementById('mypage-review-list'); if (!listEl) return; fetch('/edu/ajax/get_review_list_for_mypage.php?selected_year=' + encodeURIComponent(selectedYear)) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; // ★ 리스트 HTML 반영 listEl.innerHTML = data.html || ''; // ★ 건수 반영 renderReviewCount(data.total_count); console.log('[한줄소감] 렌더링 완료:', data.total_count); }) .catch(function (error) { console.error('한줄소감 리스트를 불러오지 못했습니다.', error); }); } /** * 한줄소감 건수를 화면에 반영하는 함수 * - 제목 우측 count 값을 변경한다. */ function renderReviewCount(totalCount) { const countEl = document.getElementById('mypage-review-count'); if (!countEl) return; const value = parseInt(totalCount, 10) || 0; countEl.textContent = value.toLocaleString(); } /** * 학습레벨 정보를 서버에서 조회하여 화면에 반영하는 함수 * - 선택년도 기준으로 학습레벨 정보를 가져온다. */ function loadLevelData(selectedYear = '') { fetch('/edu/ajax/get_level_data_for_mypage.php?selected_year=' + encodeURIComponent(selectedYear)) .then(function (res) { return res.json(); }) .then(function (data) { if (!data.success) return; renderLevelData(data); console.log('[학습레벨] 렌더링 완료:', data.learning_level); }) .catch(function (error) { console.error('학습레벨 정보를 불러오지 못했습니다.', error); }); } /** * 학습레벨 정보를 화면에 반영하는 함수 * - wrapper class, 아이콘, 레벨 텍스트를 변경한다. */ function renderLevelData(data) { const boxEl = document.getElementById('mypage-level-box'); const iconEl = document.getElementById('mypage-level-icon'); const labelEl = document.getElementById('mypage-level-label'); if (!boxEl || !iconEl || !labelEl) return; const levelClass = (data.level_class || 'rookie').toString().trim(); const levelIcon = (data.level_icon || '/edu/img/ico/ico_level_rookie.svg').toString().trim(); const levelLabel = (data.learning_level || 'Rookie').toString().trim(); // ★ badge-level 기본 클래스 유지 + 레벨 클래스만 교체 boxEl.className = 'badge-level ' + levelClass; // ★ 아이콘 교체 iconEl.src = levelIcon; // ★ 텍스트 교체 labelEl.textContent = levelLabel; } /** * 현재 선택된 년도를 반환하는 함수 */ function getSelectedMypageYear() { const yearEl = document.getElementById('mypage-year-select'); return yearEl ? (yearEl.value || '') : ''; } })();