Files
edu/js/apply_page/add_mypage.js
T

2085 lines
69 KiB
JavaScript

/**
* =====================
* 사용페이지 : 마이페이지
* 작 성 자 : ERP기획팀 moon
* 최초작성일자: 26.03.20.
* 2차수정일자: 26.04.21. (영상 모달 로직 추가)
* =====================
*/
console.log('[add_mypage.js 로드]');
(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 }
};
// Task B: 탭별 구조화 영상 데이터 저장소(content_id -> videoData)
const mypageVideoStore = {
watching: new Map(),
completed: new Map(),
saved: new Map()
};
const MOBILE_CONTENT_TAB_ORDER = ['watching', 'completed', 'saved'];
// 디버깅/브리지 연동용 전역 노출
window._mypageVideoStore = mypageVideoStore;
let mypageModalManager = null;
/**
* 특정 탭의 페이지 상태를 초기화하는 함수
* - 탭을 새로 조회할 때 page, loading, isLast 값을 초기화한다.
*/
function resetContentListState(tab) {
if (!contentListState[tab]) return;
contentListState[tab].page = 1;
contentListState[tab].loading = false;
contentListState[tab].isLast = false;
if (mypageVideoStore[tab]) {
mypageVideoStore[tab].clear();
}
}
function normalizeMypageVideo(video) {
if (!video || typeof video !== 'object') return null;
const contentId = String(video.content_id || video.id || '').trim();
if (!contentId) return null;
return {
...video,
id: String(video.id || contentId),
content_id: contentId,
category_code: String(video.category_code || ''),
category_name: String(video.category_name || ''),
category: String(video.category_name || video.category || ''),
subcate: String(video.subcate || ''),
title: String(video.title || ''),
description: String(video.description || ''),
description2: String(video.description2 || ''),
content_url: String(video.content_url || video.url || ''),
url: String(video.url || video.content_url || ''),
thumbnail: String(video.thumbnail || ''),
watch_tm: Number(video.watch_tm || 0),
content_tm: Number(video.content_tm || 0),
all_tm: Number(video.all_tm || 0),
type: String(video.type || 'main'),
bookmark: video.bookmark === true || video.is_bookmarked === true,
is_bookmarked: video.bookmark === true || video.is_bookmarked === true,
keywords: Array.isArray(video.keywords) ? video.keywords : []
};
}
function mergeVideosIntoStore(tab, videos, page) {
if (!mypageVideoStore[tab]) return;
if (page === 1) {
mypageVideoStore[tab].clear();
}
(Array.isArray(videos) ? videos : []).forEach(function (video) {
const normalized = normalizeMypageVideo(video);
if (!normalized) return;
mypageVideoStore[tab].set(normalized.content_id, normalized);
});
syncMypageModalManagerVideos();
}
function getAllMypageVideos() {
const merged = new Map();
['watching', 'completed', 'saved'].forEach(function (tab) {
mypageVideoStore[tab].forEach(function (video, contentId) {
if (!merged.has(contentId)) {
merged.set(contentId, video);
}
});
});
return Array.from(merged.values());
}
function ensureMypageModalManager() {
if (mypageModalManager) {
return mypageModalManager;
}
if (typeof VideoModalManager === 'undefined') {
return null;
}
mypageModalManager = new VideoModalManager({
videos: getAllMypageVideos()
});
return mypageModalManager;
}
function syncMypageModalManagerVideos() {
const manager = ensureMypageModalManager();
if (!manager || !manager.config) {
return;
}
manager.config.videos = getAllMypageVideos();
}
function getVideoByTabAndContentId(tab, contentId) {
const normalizedTab = String(tab || '').trim();
const id = String(contentId || '').trim();
if (!id) return null;
if (mypageVideoStore[normalizedTab] && mypageVideoStore[normalizedTab].has(id)) {
return mypageVideoStore[normalizedTab].get(id);
}
const allVideos = getAllMypageVideos();
return allVideos.find(function (video) {
return String(video.content_id || '') === id;
}) || null;
}
function toBridgeInfo(videoData) {
const description = [
String(videoData?.description || '').trim(),
String(videoData?.description2 || '').trim(),
].filter(Boolean).join('\n');
return {
contentId: String(videoData?.content_id || videoData?.id || ''),
videoUrl: String(videoData?.content_url || videoData?.url || ''),
title: String(videoData?.title || ''),
description: description,
categoryName: String(videoData?.category_name || videoData?.category || ''),
subCategory: String(videoData?.subcate || ''),
watchTm: Number(videoData?.watch_tm || 0),
contentTm: Number(videoData?.content_tm || 0),
bookmark: videoData?.bookmark === true || videoData?.is_bookmarked === true,
};
}
async function tryBridgeOpen(openFn, bridgeInfo, bridgeName) {
if (typeof openFn !== 'function') {
return false;
}
try {
await openFn(bridgeInfo);
return true;
} catch (error) {
console.warn('[mypage modal] bridge failed:', bridgeName, error?.message || error);
return false;
}
}
async function openMypageVideoByCategory(videoData) {
const categoryCode = String(videoData?.category_code || '').toUpperCase();
const bridgeInfo = toBridgeInfo(videoData);
if (categoryCode === 'CA10006') {
const opened = await tryBridgeOpen(window._biztrendOpenModal, bridgeInfo, '_biztrendOpenModal');
if (opened) return;
}
if (categoryCode === 'CA10005') {
const opened = await tryBridgeOpen(window._insightOpenModal, bridgeInfo, '_insightOpenModal');
if (opened) return;
}
if (categoryCode === 'CA10004') {
const opened = await tryBridgeOpen(window._leadershipOpenModal, bridgeInfo, '_leadershipOpenModal');
if (opened) return;
}
if (categoryCode === 'CA10003') {
const opened = await tryBridgeOpen(window._learningOpenModal, bridgeInfo, '_learningOpenModal');
if (opened) return;
}
if (categoryCode === 'CA10002') {
const opened = await tryBridgeOpen(window._onboardingOpenModal, bridgeInfo, '_onboardingOpenModal');
if (opened) return;
}
const manager = ensureMypageModalManager();
if (!manager || typeof manager.openVideo !== 'function') {
throw new Error('VideoModalManager is not ready');
}
syncMypageModalManagerVideos();
await manager.openVideo(String(videoData?.content_id || videoData?.id || ''));
}
function bindContentCardOpenAction() {
document.addEventListener('click', function (event) {
const cardLink = event.target.closest('.mylist-area .content-card .card-link');
if (!cardLink) return;
// 북마크/삭제 버튼은 카드 오픈 이벤트에서 제외
if (event.target.closest('.bookmark') || event.target.closest('.btn-remove-card')) {
return;
}
event.preventDefault();
event.stopPropagation();
const cardEl = cardLink.closest('.content-card');
if (!cardEl) return;
const contentId = String(cardEl.getAttribute('data-content-id') || '').trim();
if (!contentId) return;
const panelEl = cardEl.closest('.content-panel');
const tab = panelEl ? String(panelEl.getAttribute('data-panel') || '') : '';
const videoData = getVideoByTabAndContentId(tab, contentId);
if (!videoData) {
console.warn('[mypage modal] videoData not found:', tab, contentId);
return;
}
openMypageVideoByCategory(videoData).catch(function (error) {
console.warn('[mypage modal] open by category failed:', error?.message || error);
});
});
}
/**
* 마이페이지 초기 진입 시 실행
* - 연도 변경 이벤트 연결
* - 학습활동 데이터 로드
* - 콘텐츠 탭 클릭 시 재렌더링 이벤트 연결
*/
document.addEventListener("DOMContentLoaded", function () {
console.log('[DOMContentLoaded 진입]');
// 연도 select 변경 이벤트 연결
bindYearSelectChange();
// 학습활동 초기 데이터 조회
loadMypageInit();
// 콘텐츠 탭 클릭 시 리스트 재조회 이벤트 연결
bindContentTabReload();
// 모바일 리스트 탭(콘텐츠/한줄소감 + 화살표) 이벤트 연결
bindMobileMylistTabs();
//초기 실행에 무한스크롤 바인딩 추가
bindInfiniteScroll();
// 콘텐츠 카드 삭제 버튼 이벤트 연결
bindRemoveCardAction();
// 콘텐츠 카드 클릭 모달 오픈 이벤트 연결
bindContentCardOpenAction();
// 북마크 체크박스 이벤트 연결
bindBookmarkToggleAction();
// [제안하기]PC용 전용 폼 검증/제출 연결
bindOfferFormValidation();
bindOfferFormSubmit();
// [제안하기]모바일 전용
bindMobileOfferFormValidation();
bindMobileOfferSubmit();
// ★ 초기 선택년도 기준 한줄소감 / 학습레벨 조회
const yearSelectEl = document.getElementById('mypage-year-select');
const initialYear = yearSelectEl ? (yearSelectEl.value || '') : '';
loadReviewList(initialYear);
loadLevelData(initialYear);
//프로필이미지 연동
bindProfileUpload();
});
/**
* 모든 리소스 로드 완료 후 실행
* - 퍼블리싱용 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('/ajax/get_init_data_for_mypage.php?year=' + year)
.then(function (res) {
return res.json();
})
.then(function (data) {
console.log('loadMypageInit');
console.log(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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(value / 60);
el.textContent = totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(value / 60);
el.textContent = totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(value / 60);
el.textContent = totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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;
// 2. 분 단위 변환 (소수점 버림 처리 예시)
const totalWatchTmMin = Math.floor(totalWatchTm / 60);
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>' + totalWatchTmMin.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);
});
});
}
function getActiveContentTabName() {
const activeTab = document.querySelector('.content-tab.active');
return activeTab ? (activeTab.getAttribute('data-tab') || 'completed') : 'completed';
}
function activateContentTabUI(tab) {
const tabs = document.querySelectorAll('.content-tab');
const panels = document.querySelectorAll('.content-panel');
tabs.forEach(function (btn) {
const isActive = btn.getAttribute('data-tab') === tab;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
panels.forEach(function (panel) {
const isActive = panel.getAttribute('data-panel') === tab;
panel.classList.toggle('active', isActive);
});
}
function loadContentTabFirstPage(tab) {
resetContentListState(tab);
if (tab === 'watching') {
loadWatchingContentList(1);
} else if (tab === 'completed') {
loadCompletedContentList(1);
} else if (tab === 'saved') {
loadSavedContentList(1);
}
}
function getMobileContentLabel(tab) {
if (tab === 'watching') return '시청중';
if (tab === 'saved') return '저장한 콘텐츠';
return '시청완료';
}
function renderMobileContentLabel(tab) {
const labelEl = document.querySelector('#mobileTabContent .mobile-tab-label');
if (!labelEl) return;
labelEl.textContent = getMobileContentLabel(tab);
}
function setMobileListMode(mode) {
const contentTabWrapEl = document.querySelector('.mypage .mylist-area .content-tab-wrap');
const reviewSectionEl = document.querySelector('.mypage .mylist-area .review-section');
const mobileTabContentEl = document.getElementById('mobileTabContent');
const mobileTabReviewEl = document.getElementById('mobileTabReview');
if (!contentTabWrapEl || !reviewSectionEl || !mobileTabContentEl || !mobileTabReviewEl) {
return;
}
const isReview = mode === 'review';
contentTabWrapEl.classList.toggle('is-mobile-hidden', isReview);
reviewSectionEl.classList.toggle('is-mobile-active', isReview);
mobileTabContentEl.classList.toggle('active', !isReview);
mobileTabContentEl.setAttribute('aria-selected', !isReview ? 'true' : 'false');
mobileTabReviewEl.classList.toggle('active', isReview);
mobileTabReviewEl.setAttribute('aria-selected', isReview ? 'true' : 'false');
}
function moveMobileContentTab(step) {
const currentTab = getActiveContentTabName();
const currentIdx = MOBILE_CONTENT_TAB_ORDER.indexOf(currentTab);
const safeCurrentIdx = currentIdx >= 0 ? currentIdx : 1;
const nextIdx = (safeCurrentIdx + step + MOBILE_CONTENT_TAB_ORDER.length) % MOBILE_CONTENT_TAB_ORDER.length;
const nextTab = MOBILE_CONTENT_TAB_ORDER[nextIdx];
activateContentTabUI(nextTab);
renderMobileContentLabel(nextTab);
loadContentTabFirstPage(nextTab);
console.log('[모바일 콘텐츠 탭 전환]', currentTab, '->', nextTab);
}
function bindMobileMylistTabs() {
const mobileTabContentEl = document.getElementById('mobileTabContent');
const mobileTabReviewEl = document.getElementById('mobileTabReview');
if (!mobileTabContentEl || !mobileTabReviewEl) {
return;
}
renderMobileContentLabel(getActiveContentTabName());
mobileTabContentEl.addEventListener('click', function (event) {
const prevArrow = event.target.closest('.mobile-tab-prev');
const nextArrow = event.target.closest('.mobile-tab-next');
const label = event.target.closest('.mobile-tab-label');
let direction = 0;
if (prevArrow || nextArrow) {
event.preventDefault();
event.stopPropagation();
direction = prevArrow ? -1 : 1;
} else if (!label) {
const rect = mobileTabContentEl.getBoundingClientRect();
const clickX = Number(event.clientX || 0) - rect.left;
// 화살표 영역 탭 인식 보강: span이 아닌 버튼 타겟으로 잡혀도 동작시킨다.
if (clickX <= rect.width * 0.28) {
direction = -1;
} else if (clickX >= rect.width * 0.72) {
direction = 1;
}
}
setMobileListMode('content');
if (direction !== 0) {
moveMobileContentTab(direction);
}
});
mobileTabReviewEl.addEventListener('click', function () {
setMobileListMode('review');
});
// 코드 다른 위치에서 content-tab이 바뀌면 모바일 라벨도 동기화
document.querySelectorAll('.content-tab').forEach(function (tabBtn) {
tabBtn.addEventListener('click', function () {
const tab = this.getAttribute('data-tab') || 'completed';
renderMobileContentLabel(tab);
});
});
}
/**
* 현재 활성화된 콘텐츠 탭을 기준으로
* 해당 탭의 리스트를 다시 렌더링하는 함수
*
* - 최초 페이지 진입 시 사용
* - 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();
console.log('[watching] fetch 시작', { page: page, selectedYear: selectedYear });
fetch('/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) {
console.warn('[watching] API 실패', data);
renderEmptyContentMessage(listEl);
return;
}
console.log('[watching] API 성공', {
total_count: data.total_count,
html_length: (data.html || '').length,
is_last: data.is_last,
});
mergeVideosIntoStore('watching', data.videos, page);
// ★ 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.getElementById('mypage-completed-list');
if (!listEl) return;
const state = contentListState.completed;
if (state.loading || state.isLast) return;
state.loading = true;
const selectedYear = getSelectedMypageYear();
console.log('[completed] fetch 시작', { page: page, selectedYear: selectedYear });
fetch('/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) {
console.warn('[completed] API 실패', data);
renderEmptyContentMessage(listEl);
return;
}
console.log('[completed] API 성공', {
total_count: data.total_count,
html_length: (data.html || '').length,
is_last: data.is_last,
});
mergeVideosIntoStore('completed', data.videos, page);
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) {
console.log('[saved] 함수진입, page =', page);
const listEl = document.getElementById('mypage-saved-list');
console.log('[saved] listEl =', listEl);
const state = contentListState.saved;
console.log('[saved] state before =', JSON.stringify(state));
if (!listEl) {
console.log('[saved] 중단: listEl 없음');
return;
}
if (state.loading || state.isLast) {
console.log('[saved] 중단: loading/isLast', state.loading, state.isLast);
return;
}
state.loading = true;
console.log('[saved] fetch 시작');
const selectedYear = getSelectedMypageYear();
fetch('/ajax/get_video_list_for_mypage.php?tab=saved&page=' + page + '&selected_year=' + encodeURIComponent(selectedYear))
.then(function (res) {
console.log('[saved] fetch 응답 도착', res.status);
return res.json();
})
.then(function (data) {
console.log('[saved] 응답 data =', data);
if (!data.success) {
console.warn('[saved] API 실패', data);
renderEmptyContentMessage(listEl);
return;
}
console.log('[saved] API 성공', {
total_count: data.total_count,
html_length: (data.html || '').length,
is_last: data.is_last,
});
mergeVideosIntoStore('saved', data.videos, page);
if (page === 1) {
if ((data.html || '').trim() === '') {
console.log('[saved] html 비어있음');
renderEmptyContentMessage(listEl);
} else {
console.log('[saved] html 길이 =', (data.html || '').length);
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('[saved] catch:', error);
})
.finally(function () {
state.loading = false;
console.log('[saved] finally, loading=false');
});
}
/**
* 콘텐츠 탭의 개수(count)를 화면에 반영하는 함수
* - 전달된 값만 갱신하고, 없는 값은 기존 화면값을 유지한다.
*/
function renderContentTabCounts(counts) {
if (!counts) return;
const watchingCountEl = document.querySelector('#tab-watching .count');
const completedCountEl = document.querySelector('#tab-completed .count');
const savedCountEl = document.querySelector('#tab-saved .count');
if (watchingCountEl && counts.watching !== undefined) {
const watchingCount = parseInt(counts.watching, 10) || 0;
watchingCountEl.textContent = watchingCount.toLocaleString();
}
if (completedCountEl && counts.completed !== undefined) {
const completedCount = parseInt(counts.completed, 10) || 0;
completedCountEl.textContent = completedCount.toLocaleString();
}
if (savedCountEl && counts.saved !== undefined) {
const savedCount = parseInt(counts.saved, 10) || 0;
savedCountEl.textContent = savedCount.toLocaleString();
}
}
/**
* 콘텐츠 3개 탭의 목록을 최초 로드 시 미리 조회하는 함수
* - 시청중 / 시청완료 / 저장한 콘텐츠를 모두 한 번씩 조회한다.
* - 이후 사용자가 탭을 눌렀을 때는 이미 최신 구조가 반영된 상태가 된다.
*/
function preloadAllContentLists() {
// 시청중 콘텐츠 조회
loadWatchingContentList();
// 시청완료 콘텐츠 조회
loadCompletedContentList();
// 저장한 콘텐츠 조회
loadSavedContentList();
}
/**
* 현재 활성 탭 기준으로 무한스크롤을 처리하는 함수
* - 각 탭의 리스트(ul) 자체에서 scroll 이벤트를 감지한다.
*/
function bindInfiniteScroll() {
console.log('bindInfiniteScroll');
const watchingEl = document.getElementById('mypage-watching-list');
const completedEl = document.getElementById('mypage-completed-list');
const savedEl = document.getElementById('mypage-saved-list');
[watchingEl, completedEl, savedEl].forEach(function (listEl) {
if (!listEl) return;
listEl.addEventListener('scroll', function () {
const scrollTop = listEl.scrollTop;
const clientHeight = listEl.clientHeight;
const scrollHeight = listEl.scrollHeight;
console.log('[list scroll]', listEl.id, scrollTop, clientHeight, scrollHeight);
// 하단 100px 전 도달 시 동작
if (scrollTop + clientHeight < scrollHeight - 100) return;
const activeTab = document.querySelector('.content-tab.active');
if (!activeTab) return;
const tab = activeTab.getAttribute('data-tab');
const state = contentListState[tab];
if (!state || state.loading || state.isLast) {
console.log('[무한스크롤 차단]', tab, state);
return;
}
console.log('[무한스크롤 실행]', tab, state.page + 1);
if (tab === 'watching') {
loadWatchingContentList(state.page + 1);
} else if (tab === 'completed') {
loadCompletedContentList(state.page + 1);
} else if (tab === 'saved') {
loadSavedContentList(state.page + 1);
}
});
});
}
/**
* 현재 활성 탭 기준으로 무한스크롤을 처리하는 함수
* - 화면 하단 근처까지 내려오면
* 현재 활성 탭의 다음 페이지를 조회한다.
*/
function bindInfiniteScroll_old() {
console.log('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('/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() {
document.addEventListener('change', function (event) {
// ★ 최소 수정 핵심
const checkbox = event.target;
if (!checkbox || checkbox.type !== 'checkbox') return;
// 카드 요소 찾기
const card = checkbox.closest('.content-card');
if (!card) return;
// 콘텐츠 ID 가져오기
const contentId = card.getAttribute('data-content-id');
if (!contentId) return;
// 현재 체크 상태 확인
const isChecked = checkbox.checked;
// 서버 저장/해제 요청
toggleBookmark(contentId, isChecked, checkbox);
});
}
/**
* 북마크 저장/해제 요청 함수 (최종 안정 버전)
* - 서버 기준으로 항상 동기화
* - saved 탭 상태 꼬임 방지
*/
function toggleBookmark(contentId, isChecked, checkboxEl) {
fetch('/ajax/toggle_content_wishlist.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content_id: contentId,
is_checked: isChecked ? '1' : '0'
})
})
.then(function (res) {
// ★ 오픈 전까지 응답 원문 확인 가능하도록 text로 먼저 받음
return res.text();
})
.then(function (text) {
console.log('[북마크 응답 원문]', text);
let data = null;
try {
data = JSON.parse(text);
} catch (e) {
console.error('[북마크 JSON 파싱 실패]', text);
throw e;
}
// 실패 시 종료
if (!data.success) {
alert(data.message || '북마크 처리에 실패했습니다.');
return;
}
// 탭 개수 갱신
if (data.counts) {
renderContentTabCounts(data.counts);
}
// 동일 content_id 카드의 체크상태 동기화
syncBookmarkState(contentId, isChecked);
// 현재 활성 탭 확인
const activeTab = document.querySelector('.content-tab.active');
const currentTab = activeTab ? activeTab.getAttribute('data-tab') : '';
// ★ 최소 수정 핵심:
// saved 탭은 카드 제거하지 말고 상태 초기화 후 서버 재조회
if (currentTab === 'saved') {
resetContentListState('saved');
loadSavedContentList(1);
}
console.log('[북마크 처리 완료]', contentId, isChecked ? 'save' : 'remove');
})
.catch(function (error) {
console.error('북마크 처리 중 오류가 발생했습니다.', error);
// ★ 최소 수정 핵심:
// DB는 이미 반영됐을 수 있으므로 체크상태를 강제로 원복하지 않음
// 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.getElementById('mypage-completed-list');
} else if (tab === 'saved') {
listEl = document.getElementById('mypage-saved-list');
}
if (!listEl) return;
const count = listEl.querySelectorAll('.content-card').length;
if (count === 0) {
listEl.innerHTML = '<li class="content-empty">표시할 콘텐츠가 없습니다.</li>';
}
}
document.addEventListener('change', function (event) {
console.log('change 발생:', event.target);
});
/**
* 동일한 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('/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('/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('/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('/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('/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 || '/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 || '') : '';
}
function bindProfileUpload() {
const trigger = document.getElementById('profile-upload-trigger');
const fileInput = document.getElementById('profile-file-input');
const img = document.getElementById('profile-image');
if (!trigger || !fileInput) return;
trigger.addEventListener('click', function () {
fileInput.click();
});
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
if (!file) return;
console.log('[프로필] 파일 선택:', file.name, file.size);
// 1차 검사: 프런트에서 3MB 제한
const maxSize = 2 * 1024 * 1024; // 3MB
if (file.size > maxSize) {
alert('2MB 이하의 이미지 파일만 업로드 가능합니다.');
fileInput.value = '';
return;
}
const formData = new FormData();
formData.append('profile_image', file);
fetch('/ajax/upload_profile_image.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
console.log('[프로필 응답]', data);
if (!data.success) {
alert(data.message);
return;
}
img.src = data.image_url + '?t=' + Date.now();
})
.catch(err => {
console.error('[프로필] 오류:', err);
alert('업로드 오류');
});
});
}
})();
console.log('[add_mypage.js 끝까지로드]');