Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
/**
* 마이페이지 전용 스크립트
* =====================
* 이 파일은 mypage.html에서만 사용됩니다.
* 초보자도 이해하기 쉽도록 주석을 많이 달았습니다.
*/
(function () {
"use strict";
// ------------------------------
// 1. 컨텐츠 제안 영역
// ------------------------------
// URL과 추천이유가 모두 입력되면 "제안 보내기" 버튼이 활성화됩니다.
var suggestUrl = document.querySelector(".suggest-section .suggest-url");
var suggestReason = document.querySelector(".suggest-section .suggest-reason");
var suggestBtn = document.querySelector(".suggest-section .btn-primary");
function checkSuggestForm() {
if (!suggestBtn) return;
var hasUrl = suggestUrl && suggestUrl.value.trim().length > 0;
var hasReason = suggestReason && suggestReason.value.trim().length > 0;
suggestBtn.disabled = !(hasUrl && hasReason);
}
if (suggestUrl) suggestUrl.addEventListener("input", checkSuggestForm);
if (suggestReason) suggestReason.addEventListener("input", checkSuggestForm);
// ------------------------------
// 2. 제안현황 아코디언
// ------------------------------
// "제안현황" 버튼을 클릭하면 목록이 펼쳐지거나 접힙니다.
var accordion = document.querySelector(".suggest-status.accordion");
if (accordion) {
var accordionTrigger = accordion.querySelector(".accordion-trigger");
var accordionContent = accordion.querySelector(".accordion-content");
accordionTrigger.addEventListener("click", function () {
// is-open 클래스가 있으면 열림, 없으면 닫힘
var isOpen = accordion.classList.toggle("is-open");
// 접근성: 스크린 리더가 열림/닫힘 상태를 인식할 수 있게 함
accordionTrigger.setAttribute("aria-expanded", isOpen);
accordionContent.hidden = !isOpen;
});
}
// ------------------------------
// 3. 콘텐츠 탭 전환 (+ 모바일 화살표/스와이프와 동일 상태)
// ------------------------------
var contentTabs = document.querySelectorAll(".content-tab");
var contentPanels = document.querySelectorAll(".content-panel");
var mobileTabContent = document.getElementById("mobileTabContent");
var mobileTabReview = document.getElementById("mobileTabReview");
var contentTabWrap = document.querySelector(".mylist-area .content-tab-wrap");
var reviewSection = document.querySelector(".mylist-area .review-section");
var contentPanelNames = ["watching", "completed", "saved"];
var contentPanelLabels = ["시청중", "시청완료", "저장"];
function panelIndexFromId(id) {
if (!id) return 0;
var i = contentPanelNames.indexOf(id);
return i === -1 ? 0 : i;
}
var activeTabOnLoad = document.querySelector(".mylist-area .content-tab.active") ||
document.querySelector(".content-tab.active");
var currentPanelIndex = 0;
if (activeTabOnLoad) {
currentPanelIndex = panelIndexFromId(activeTabOnLoad.getAttribute("data-tab"));
} else {
var activePanelOnLoad = document.querySelector(".mylist-area .content-panel.active") ||
document.querySelector(".content-panel.active");
currentPanelIndex = panelIndexFromId(activePanelOnLoad && activePanelOnLoad.getAttribute("data-panel"));
}
function updateMobileContentLabel() {
var label = mobileTabContent && mobileTabContent.querySelector(".mobile-tab-label");
if (label) label.textContent = contentPanelLabels[currentPanelIndex];
var activeName = contentPanelNames[currentPanelIndex];
contentPanels.forEach(function (panel) {
var panelId = panel.getAttribute("data-panel");
panel.classList.toggle("active", panelId === activeName);
});
contentTabs.forEach(function (tab) {
var tabId = tab.getAttribute("data-tab");
var isOn = tabId === activeName;
tab.classList.toggle("active", isOn);
tab.setAttribute("aria-selected", isOn ? "true" : "false");
});
}
// add_mypage.js의 목록 AJAX는 .content-tab 클릭에만 연결되어 있음.
// 모바일(화살표·스와이프)은 DOM의 active만 바꾸므로, 해당 탭에 클릭을 한 번 보내 데이터를 맞춘다.
function triggerContentTabDataSync() {
var name = contentPanelNames[currentPanelIndex];
var tabBtn = document.querySelector(".mylist-area .content-tab[data-tab=\"" + name + "\"]") ||
document.querySelector(".content-tab[data-tab=\"" + name + "\"]");
if (tabBtn) tabBtn.click();
}
contentTabs.forEach(function (tab) {
tab.addEventListener("click", function () {
var targetTab = this.getAttribute("data-tab");
if (!targetTab) return;
var ix = contentPanelNames.indexOf(targetTab);
if (ix === -1) return;
currentPanelIndex = ix;
updateMobileContentLabel();
});
});
// ------------------------------
// 4. 모바일: 학습 활동 모달
// ------------------------------
var mobileActivityBtn = document.getElementById("mobileActivityBtn");
var mobileActivityModal = document.getElementById("mobileActivityModal");
var mobileActivityClose = document.getElementById("mobileActivityClose");
var mobileActivityBackdrop = document.getElementById("mobileActivityBackdrop");
function openActivityModal() {
if (!mobileActivityModal) return;
// 버튼 위치 기준으로 모달 패널 위치 계산
var panel = mobileActivityModal.querySelector(".modal-panel");
if (panel && mobileActivityBtn) {
var rect = mobileActivityBtn.getBoundingClientRect();
var gap = 8;
var panelWidth = Math.min(window.innerWidth - 32, 420);
// 버튼 왼쪽 기준 정렬, 화면 밖으로 나가지 않도록 보정
var left = rect.left;
if (left + panelWidth > window.innerWidth - 16) {
left = window.innerWidth - panelWidth - 16;
}
if (left < 16) left = 16;
panel.style.top = (rect.bottom + gap) + "px";
panel.style.left = left + "px";
panel.style.width = panelWidth + "px";
}
mobileActivityModal.hidden = false;
document.body.style.overflow = "hidden";
}
function closeActivityModal() {
if (!mobileActivityModal) return;
mobileActivityModal.hidden = true;
document.body.style.overflow = "";
}
if (mobileActivityBtn) mobileActivityBtn.addEventListener("click", openActivityModal);
if (mobileActivityClose) mobileActivityClose.addEventListener("click", closeActivityModal);
if (mobileActivityBackdrop) mobileActivityBackdrop.addEventListener("click", closeActivityModal);
// ------------------------------
// 5. 모바일: 컨텐츠 제안 바텀시트
// ------------------------------
var mobileSuggestBtn = document.getElementById("mobileSuggestBtn");
var mobileSuggestSheet = document.getElementById("mobileSuggestSheet");
var mobileSuggestBackdrop = document.getElementById("mobileSuggestBackdrop");
var btnSheetConfirm = document.getElementById("btnSheetConfirm");
var sheetUrl = document.getElementById("sheet-url");
var sheetReason = document.getElementById("sheet-reason");
function openSuggestSheet() {
if (!mobileSuggestSheet) return;
mobileSuggestSheet.hidden = false;
document.body.style.overflow = "hidden";
}
function closeSuggestSheet() {
if (!mobileSuggestSheet) return;
mobileSuggestSheet.hidden = true;
document.body.style.overflow = "";
}
function checkSheetForm() {
if (!btnSheetConfirm) return;
var hasUrl = sheetUrl && sheetUrl.value.trim().length > 0;
var hasReason = sheetReason && sheetReason.value.trim().length > 0;
btnSheetConfirm.disabled = !(hasUrl && hasReason);
}
if (mobileSuggestBtn) mobileSuggestBtn.addEventListener("click", openSuggestSheet);
if (mobileSuggestBackdrop) mobileSuggestBackdrop.addEventListener("click", closeSuggestSheet);
if (sheetUrl) sheetUrl.addEventListener("input", checkSheetForm);
if (sheetReason) sheetReason.addEventListener("input", checkSheetForm);
// ------------------------------
// 6. 모바일: 2탭 전환 (콘텐츠 ↔ 한줄 소감)
// ------------------------------
function switchMobileTab(target) {
// target: "content" | "review"
if (!mobileTabContent || !mobileTabReview) return;
if (target === "content") {
mobileTabContent.classList.add("active");
mobileTabContent.setAttribute("aria-selected", "true");
mobileTabReview.classList.remove("active");
mobileTabReview.setAttribute("aria-selected", "false");
if (contentTabWrap) contentTabWrap.classList.remove("is-mobile-hidden");
if (reviewSection) reviewSection.classList.remove("is-mobile-active");
} else {
mobileTabReview.classList.add("active");
mobileTabReview.setAttribute("aria-selected", "true");
mobileTabContent.classList.remove("active");
mobileTabContent.setAttribute("aria-selected", "false");
if (contentTabWrap) contentTabWrap.classList.add("is-mobile-hidden");
if (reviewSection) reviewSection.classList.add("is-mobile-active");
}
}
if (mobileTabContent) {
// 탭 본체 클릭 (화살표 제외)
mobileTabContent.addEventListener("click", function (e) {
var prev = e.target.closest(".mobile-tab-prev");
var next = e.target.closest(".mobile-tab-next");
if (prev) {
// 이전 패널
currentPanelIndex = (currentPanelIndex - 1 + contentPanelNames.length) % contentPanelNames.length;
updateMobileContentLabel();
triggerContentTabDataSync();
switchMobileTab("content");
e.stopPropagation();
} else if (next) {
// 다음 패널
currentPanelIndex = (currentPanelIndex + 1) % contentPanelNames.length;
updateMobileContentLabel();
triggerContentTabDataSync();
switchMobileTab("content");
e.stopPropagation();
} else {
switchMobileTab("content");
}
});
}
if (mobileTabReview) {
mobileTabReview.addEventListener("click", function () {
switchMobileTab("review");
});
}
// 초기 레이블 설정
updateMobileContentLabel();
// ------------------------------
// 7. 모바일: content-panels 스와이프로 탭 전환
// ------------------------------
var contentPanelsEl = document.querySelector(".content-panels");
var swipeStartX = 0;
var swipeStartY = 0;
var swipeGestureStarted = false;
if (contentPanelsEl) {
contentPanelsEl.addEventListener("touchstart", function (e) {
swipeGestureStarted = true;
swipeStartX = e.touches[0].clientX;
swipeStartY = e.touches[0].clientY;
}, { passive: true });
contentPanelsEl.addEventListener("touchcancel", function () {
swipeGestureStarted = false;
}, { passive: true });
contentPanelsEl.addEventListener("touchend", function (e) {
if (!swipeGestureStarted) return;
swipeGestureStarted = false;
// 모바일 환경에서만 동작 (터치 지원 기기)
var deltaX = e.changedTouches[0].clientX - swipeStartX;
var deltaY = e.changedTouches[0].clientY - swipeStartY;
// 수직 스크롤보다 수평 이동이 클 때만 탭 전환
if (Math.abs(deltaX) < 50 || Math.abs(deltaX) < Math.abs(deltaY)) return;
if (deltaX < 0) {
// 왼쪽 스와이프 → 다음 탭
currentPanelIndex = (currentPanelIndex + 1) % contentPanelNames.length;
} else {
// 오른쪽 스와이프 → 이전 탭
currentPanelIndex = (currentPanelIndex - 1 + contentPanelNames.length) % contentPanelNames.length;
}
updateMobileContentLabel();
triggerContentTabDataSync();
switchMobileTab("content");
}, { passive: true });
}
})();