Files
edu/js/apply_page/back/add_mypage_260323_1940.js
T

1517 lines
50 KiB
JavaScript

/**
* =====================
* 사용페이지 : 마이페이지
* 작 성 자 : 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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분 (' + 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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분 (' + 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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분 (' + 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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분';
}
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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분';
}
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 = '<em>' + totalWatchTm.toLocaleString() + '</em>분';
}
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 = '<li class="content-empty">표시할 콘텐츠가 없습니다.</li>';
}
/**
* 동일한 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 = '<li class="content-empty">' + message + '</li>';
}
*/
/**
* 콘텐츠 리스트가 비어 있을 때 빈 상태 문구를 렌더링하는 함수
* - 현재 활성 탭 기준으로 메시지를 자동 결정한다.
*/
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 = '<li class="content-empty">' + message + '</li>';
}
/**
* 한줄소감 리스트를 서버에서 조회하여 화면에 반영하는 함수
* - 선택년도 기준으로 한줄소감 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 || '') : '';
}
})();