// ==UserScript==
// @name Sooplive Calendar Viewer
// @namespace https://sooplive-calendar-viewer.local
// @version 2.7
// @description Sooplive 즐겨찾기 그룹의 통합 일정을 캘린더 형태로 표시하는 스크립트
// @author 지창연구소
// @match https://www.sooplive.co.kr/*
// @grant GM_xmlhttpRequest
// @connect api-channel.sooplive.co.kr
// @connect myapi.sooplive.co.kr
// ==/UserScript==
(function() {
'use strict';
// 전역 변수
let anchorDate = new Date();
let currentUrl = window.location.href;
let fixedStreamer = null; // 고정된 멤버 정보 {id, nickname}
let clickedDay = null; // 클릭한 날짜 컬럼
// 상수 정의 (하드코딩 제거)
const TIMING = {
SCROLL_COMPLETE_WAIT: 600,
INDICATOR_SHOW_DELAY: 700,
INDICATOR_FADE_OUT: 200,
INDICATOR_FADE_IN: 10,
INIT_RETRY_DELAY: 2000,
URL_CHECK_INTERVAL: 1000,
SPA_NAVIGATION_DELAY: 100
};
const LAYOUT = {
SCROLL_MARGIN: 10,
INDICATOR_BOTTOM_OFFSET: 15,
SCROLL_POSITION_OFFSET: 5,
VISIBLE_AREA_PADDING: 50, // 보이는 영역의 패딩 (헤더 높이 고려)
OPTIMAL_POSITION_OFFSET: 30 // 최적 위치 오프셋
};
// 화살표 관련 유틸리티 함수들
function createScrollIndicator(position, titleText) {
const indicator = document.createElement('div');
indicator.className = `scroll-indicator scroll-indicator-${position}`;
// 위쪽/아래쪽 화살표 표시
indicator.innerHTML = position === 'top' ? '↑' : '↓';
indicator.title = titleText;
indicator.style.opacity = '0';
return indicator;
}
// 모든 강조 효과 및 화살표 제거
function clearAllHighlights() {
// 모든 강조 효과 제거
const allEventItems = document.querySelectorAll('.event-item');
allEventItems.forEach(eventItem => {
eventItem.classList.remove('highlight', 'fade');
});
// 빈 날짜 강조 효과 제거
const allDays = document.querySelectorAll('.calendar-day');
allDays.forEach(day => {
day.classList.remove('empty-day');
// 스크롤 인디케이터 제거 (더 확실하게)
const indicators = day.querySelectorAll('.scroll-indicator');
indicators.forEach(indicator => {
indicator.remove();
});
});
}
function removeScrollIndicator(container) {
const existingIndicator = container.querySelector('.scroll-indicator');
if (existingIndicator) {
existingIndicator.style.opacity = '0';
setTimeout(() => {
if (existingIndicator.parentNode) {
existingIndicator.remove();
}
}, TIMING.INDICATOR_FADE_OUT);
}
}
function showScrollIndicator(indicator, container, position) {
container.style.position = 'relative';
container.appendChild(indicator);
// 부드럽게 나타나기
setTimeout(() => {
indicator.classList.add('show');
}, 10);
}
// 테마 감지 함수 (Sooplive 실제 클래스 기반)
function detectTheme() {
const urlParams = new URLSearchParams(window.location.search);
const themeColor = urlParams.get('theme_color');
const htmlElement = document.documentElement;
const bodyElement = document.body;
// Sooplive의 실제 다크 테마 감지
const isDarkTheme =
themeColor === 'dark' ||
bodyElement.classList.contains('thema_dark') ||
htmlElement.getAttribute('dark') === 'true' ||
htmlElement.classList.contains('dark') ||
bodyElement.classList.contains('dark') ||
htmlElement.getAttribute('data-theme') === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDarkTheme ? 'dark' : 'light';
}
// CSS 스타일 추가 (테마 자동 감지)
const style = document.createElement('style');
style.textContent = `
#sooplive-calendar {
font-family: inherit;
border-radius: 12px;
transition: all 0.3s ease;
margin: 24px 0;
padding: 24px;
}
/* 라이트 테마 */
#sooplive-calendar {
background: #fff;
border: 1px solid #e1e5e9;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e1e5e9;
}
.calendar-header h3 {
margin: 0;
color: #1a1a1a;
font-size: 20px;
font-weight: 600;
}
.week-navigation {
display: flex;
align-items: center;
gap: 12px;
}
.week-navigation button {
background: #6366f1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.week-navigation button:hover {
background: #4f46e5;
transform: translateY(-1px);
}
.week-range {
font-weight: 500;
color: #374151;
font-size: 15px;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #f3f4f6;
border: 1px solid #e1e5e9;
border-radius: 12px;
overflow: hidden;
}
.calendar-day {
background: #fff;
min-height: 140px;
}
.day-header {
background: #f9fafb;
padding: 12px 8px;
text-align: center;
border-bottom: 1px solid #e1e5e9;
}
.day-name {
font-size: 12px;
color: #6b7280;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.day-number {
font-size: 17px;
color: #1a1a1a;
font-weight: 600;
margin-top: 4px;
}
.event-count {
font-size: 10px;
color: #6366f1;
font-weight: 600;
margin-top: 2px;
background: #f0f9ff;
border-radius: 8px;
padding: 2px 6px;
display: inline-block;
}
.calendar-credit {
text-align: right;
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid #e1e5e9;
font-size: 11px;
color: #9ca3af;
}
.credit-name {
color: #6366f1;
font-weight: 600;
}
.day-events {
padding: 12px 8px;
min-height: 100px;
max-height: 500px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #cbd5e0 #f7fafc;
}
.day-events::-webkit-scrollbar {
width: 4px;
}
.day-events::-webkit-scrollbar-track {
background: #f7fafc;
border-radius: 2px;
}
.day-events::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 2px;
}
.day-events::-webkit-scrollbar-thumb:hover {
background: #a0aec0;
}
.no-events {
color: #9ca3af;
font-size: 13px;
text-align: center;
margin-top: 30px;
font-style: italic;
}
.loading-message {
text-align: center;
padding: 20px;
color: #374151;
font-size: 15px;
font-weight: 500;
}
.event-item {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 8px;
margin-bottom: 6px;
font-size: 12px;
transition: all 0.2s ease;
cursor: pointer;
pointer-events: auto;
user-select: none;
position: relative;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.event-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
border-color: #7dd3fc;
}
.event-content {
flex: 1;
min-width: 0;
}
.event-actions {
margin-left: 8px;
flex-shrink: 0;
}
.station-link-btn {
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 3px;
padding: 3px;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
width: 16px;
height: 16px;
}
.station-link-btn:hover {
background: rgba(99, 102, 241, 0.2);
border-color: rgba(99, 102, 241, 0.5);
opacity: 1;
transform: scale(1.1);
color: #4f46e5;
}
/* 특정 멤버 강조 효과 */
.event-item.highlight {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important;
border: 2px solid #f59e0b !important;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3) !important;
transform: scale(1.02) !important;
z-index: 10;
position: relative;
}
.event-item.fade {
opacity: 0.3;
filter: grayscale(50%);
}
.calendar-day.empty-day {
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%) !important;
border: 2px solid #fca5a5 !important;
box-shadow: 0 0 0 1px #f87171 !important;
}
.calendar-day.empty-day .day-header {
background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%) !important;
border-bottom: 2px solid #f87171 !important;
}
.calendar-day.empty-day .day-number {
color: #dc2626 !important;
font-weight: 700 !important;
}
.calendar-day .scroll-indicator {
position: absolute;
left: 50%;
transform: translateX(-50%);
background: #6366f1;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
z-index: 10;
animation: pulse 1.5s infinite;
opacity: 0;
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.calendar-day .scroll-indicator-top {
top: 15px;
}
.calendar-day .scroll-indicator-bottom {
bottom: 15px;
}
.calendar-day .scroll-indicator.show {
opacity: 1;
transform: translateX(-50%) scale(1);
}
@keyframes pulse {
0%, 100% { transform: translateX(-50%) scale(1); opacity: 1; }
50% { transform: translateX(-50%) scale(1.1); opacity: 0.8; }
}
.event-time {
color: #0369a1;
font-weight: 600;
margin-bottom: 4px;
font-size: 11px;
}
.event-type-badge {
display: inline-block;
background: #3b82f6;
color: white;
font-size: 9px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
margin-left: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.event-type-badge.방송 {
background: #3b82f6;
}
.event-type-badge.방송예정 {
background: #06b6d4;
}
.event-type-badge.합방 {
background: #10b981;
}
.event-type-badge.휴방 {
background: #f59e0b;
}
.event-type-badge.기타 {
background: #6b7280;
}
.event-title {
color: #1e293b;
font-size: 12px;
line-height: 1.3;
margin-bottom: 3px;
font-weight: 500;
}
/* 주말 스타일 - 라이트 */
.calendar-day:nth-child(7) {
background: #fafbfc;
}
.calendar-day:nth-child(7) .day-header {
background: #f3f4f6;
}
/* 주말 날짜 숫자 색상 - 라이트 */
.calendar-day:nth-child(7) .day-number {
color: #dc2626; /* 일요일 - 빨간색 */
}
/* 오늘 날짜 강조 - 라이트 */
.calendar-day.today {
background: #f8fafc;
border: 2px solid #e2e8f0;
}
.calendar-day.today .day-header {
background: #f1f5f9;
}
/* 다크 테마 스타일 - Sooplive 실제 클래스 */
body.thema_dark #sooplive-calendar,
html[dark=true] #sooplive-calendar,
.dark #sooplive-calendar,
html.dark #sooplive-calendar,
body.dark #sooplive-calendar,
[data-theme="dark"] #sooplive-calendar {
background: #1f2937;
border: 1px solid #374151;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
body.thema_dark .calendar-header,
html[dark=true] .calendar-header,
.dark .calendar-header,
html.dark .calendar-header,
body.dark .calendar-header,
[data-theme="dark"] .calendar-header {
border-bottom: 1px solid #374151;
}
body.thema_dark .calendar-header h3,
html[dark=true] .calendar-header h3,
.dark .calendar-header h3,
html.dark .calendar-header h3,
body.dark .calendar-header h3,
[data-theme="dark"] .calendar-header h3 {
color: #f9fafb;
}
body.thema_dark .week-range,
html[dark=true] .week-range,
.dark .week-range,
html.dark .week-range,
body.dark .week-range,
[data-theme="dark"] .week-range {
color: #d1d5db;
}
body.thema_dark .calendar-grid,
html[dark=true] .calendar-grid,
.dark .calendar-grid,
html.dark .calendar-grid,
body.dark .calendar-grid,
[data-theme="dark"] .calendar-grid {
background: #374151;
border: 1px solid #4b5563;
}
body.thema_dark .calendar-day,
html[dark=true] .calendar-day,
.dark .calendar-day,
html.dark .calendar-day,
body.dark .calendar-day,
[data-theme="dark"] .calendar-day {
background: #1f2937;
}
body.thema_dark .day-header,
html[dark=true] .day-header,
.dark .day-header,
html.dark .day-header,
body.dark .day-header,
[data-theme="dark"] .day-header {
background: #374151;
border-bottom: 1px solid #4b5563;
}
body.thema_dark .day-name,
html[dark=true] .day-name,
.dark .day-name,
html.dark .day-name,
body.dark .day-name,
[data-theme="dark"] .day-name {
color: #9ca3af;
}
body.thema_dark .day-number,
html[dark=true] .day-number,
.dark .day-number,
html.dark .day-number,
body.dark .day-number,
[data-theme="dark"] .day-number {
color: #f9fafb;
}
body.thema_dark .event-count,
html[dark=true] .event-count,
.dark .event-count,
html.dark .event-count,
body.dark .event-count,
[data-theme="dark"] .event-count {
color: #93c5fd;
background: #1e3a8a;
}
body.thema_dark .no-events,
html[dark=true] .no-events,
.dark .no-events,
html.dark .no-events,
body.dark .no-events,
[data-theme="dark"] .no-events {
color: #6b7280;
}
body.thema_dark .loading-message,
html[dark=true] .loading-message,
.dark .loading-message,
html.dark .loading-message,
body.dark .loading-message,
[data-theme="dark"] .loading-message {
color: #d1d5db;
}
body.thema_dark .event-item,
html[dark=true] .event-item,
.dark .event-item,
html.dark .event-item,
body.dark .event-item,
[data-theme="dark"] .event-item {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
border: 1px solid #3b82f6;
}
body.thema_dark .event-item:hover,
html[dark=true] .event-item:hover,
.dark .event-item:hover,
html.dark .event-item:hover,
body.dark .event-item:hover,
[data-theme="dark"] .event-item:hover {
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
border-color: #60a5fa;
}
body.thema_dark .station-link-btn,
html[dark=true] .station-link-btn,
.dark .station-link-btn,
html.dark .station-link-btn,
body.dark .station-link-btn,
[data-theme="dark"] .station-link-btn {
background: rgba(99, 102, 241, 0.2);
border-color: rgba(99, 102, 241, 0.4);
color: #a5b4fc;
}
body.thema_dark .station-link-btn:hover,
html[dark=true] .station-link-btn:hover,
.dark .station-link-btn:hover,
html.dark .station-link-btn:hover,
body.dark .station-link-btn:hover,
[data-theme="dark"] .station-link-btn:hover {
background: rgba(99, 102, 241, 0.3);
border-color: rgba(99, 102, 241, 0.6);
color: #c7d2fe;
}
/* 다크 테마 특정 멤버 강조 효과 */
body.thema_dark .event-item.highlight,
html[dark=true] .event-item.highlight,
.dark .event-item.highlight,
html.dark .event-item.highlight,
body.dark .event-item.highlight,
[data-theme="dark"] .event-item.highlight {
background: linear-gradient(135deg, #451a03 0%, #78350f 100%) !important;
border: 2px solid #f59e0b !important;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4) !important;
}
body.thema_dark .event-item.fade,
html[dark=true] .event-item.fade,
.dark .event-item.fade,
html.dark .event-item.fade,
body.dark .event-item.fade,
[data-theme="dark"] .event-item.fade {
opacity: 0.2;
filter: grayscale(70%);
}
body.thema_dark .calendar-day.empty-day,
html[dark=true] .calendar-day.empty-day,
.dark .calendar-day.empty-day,
html.dark .calendar-day.empty-day,
body.dark .calendar-day.empty-day,
[data-theme="dark"] .calendar-day.empty-day {
background: linear-gradient(135deg, #3d1f1f 0%, #4d2626 100%) !important;
border: 2px solid #ef4444 !important;
box-shadow: 0 0 0 1px #fca5a5 !important;
}
body.thema_dark .calendar-day.empty-day .day-header,
html[dark=true] .calendar-day.empty-day .day-header,
.dark .calendar-day.empty-day .day-header,
html.dark .calendar-day.empty-day .day-header,
body.dark .calendar-day.empty-day .day-header,
[data-theme="dark"] .calendar-day.empty-day .day-header {
background: linear-gradient(135deg, #4d2626 0%, #5d2d2d 100%) !important;
border-bottom: 2px solid #ef4444 !important;
}
body.thema_dark .calendar-day.empty-day .day-number,
html[dark=true] .calendar-day.empty-day .day-number,
.dark .calendar-day.empty-day .day-number,
html.dark .calendar-day.empty-day .day-number,
body.dark .calendar-day.empty-day .day-number,
[data-theme="dark"] .calendar-day.empty-day .day-number {
color: #f87171 !important;
font-weight: 700 !important;
}
body.thema_dark .calendar-day .scroll-indicator,
html[dark=true] .calendar-day .scroll-indicator,
.dark .calendar-day .scroll-indicator,
html.dark .calendar-day .scroll-indicator,
body.dark .calendar-day .scroll-indicator,
[data-theme="dark"] .calendar-day .scroll-indicator {
background: #4f46e5;
color: #e5e7eb;
}
body.thema_dark .event-time,
html[dark=true] .event-time,
.dark .event-time,
html.dark .event-time,
body.dark .event-time,
[data-theme="dark"] .event-time {
color: #93c5fd;
}
body.thema_dark .event-type-badge,
html[dark=true] .event-type-badge,
.dark .event-type-badge,
html.dark .event-type-badge,
body.dark .event-type-badge,
[data-theme="dark"] .event-type-badge {
color: white;
font-weight: 700;
}
body.thema_dark .event-type-badge.방송,
html[dark=true] .event-type-badge.방송,
.dark .event-type-badge.방송,
html.dark .event-type-badge.방송,
body.dark .event-type-badge.방송,
[data-theme="dark"] .event-type-badge.방송 {
background: #2563eb;
}
body.thema_dark .event-type-badge.방송예정,
html[dark=true] .event-type-badge.방송예정,
.dark .event-type-badge.방송예정,
html.dark .event-type-badge.방송예정,
body.dark .event-type-badge.방송예정,
[data-theme="dark"] .event-type-badge.방송예정 {
background: #0891b2;
}
body.thema_dark .event-type-badge.합방,
html[dark=true] .event-type-badge.합방,
.dark .event-type-badge.합방,
html.dark .event-type-badge.합방,
body.dark .event-type-badge.합방,
[data-theme="dark"] .event-type-badge.합방 {
background: #059669;
}
body.thema_dark .event-type-badge.휴방,
html[dark=true] .event-type-badge.휴방,
.dark .event-type-badge.휴방,
html.dark .event-type-badge.휴방,
body.dark .event-type-badge.휴방,
[data-theme="dark"] .event-type-badge.휴방 {
background: #d97706;
}
body.thema_dark .event-type-badge.기타,
html[dark=true] .event-type-badge.기타,
.dark .event-type-badge.기타,
html.dark .event-type-badge.기타,
body.dark .event-type-badge.기타,
[data-theme="dark"] .event-type-badge.기타 {
background: #4b5563;
}
body.thema_dark .event-title,
html[dark=true] .event-title,
.dark .event-title,
html.dark .event-title,
body.dark .event-title,
[data-theme="dark"] .event-title {
color: #e5e7eb;
}
/* 다크 테마 스크롤바 */
body.thema_dark .day-events,
html[dark=true] .day-events,
.dark .day-events,
html.dark .day-events,
body.dark .day-events,
[data-theme="dark"] .day-events {
scrollbar-color: #4a5568 #2d3748;
}
body.thema_dark .day-events::-webkit-scrollbar-track,
html[dark=true] .day-events::-webkit-scrollbar-track,
.dark .day-events::-webkit-scrollbar-track,
html.dark .day-events::-webkit-scrollbar-track,
body.dark .day-events::-webkit-scrollbar-track,
[data-theme="dark"] .day-events::-webkit-scrollbar-track {
background: #2d3748;
}
body.thema_dark .day-events::-webkit-scrollbar-thumb,
html[dark=true] .day-events::-webkit-scrollbar-thumb,
.dark .day-events::-webkit-scrollbar-thumb,
html.dark .day-events::-webkit-scrollbar-thumb,
body.dark .day-events::-webkit-scrollbar-thumb,
[data-theme="dark"] .day-events::-webkit-scrollbar-thumb {
background: #4a5568;
}
body.thema_dark .day-events::-webkit-scrollbar-thumb:hover,
html[dark=true] .day-events::-webkit-scrollbar-thumb:hover,
.dark .day-events::-webkit-scrollbar-thumb:hover,
html.dark .day-events::-webkit-scrollbar-thumb:hover,
body.dark .day-events::-webkit-scrollbar-thumb:hover,
[data-theme="dark"] .day-events::-webkit-scrollbar-thumb:hover {
background: #718096;
}
/* 다크 테마 크레딧 */
body.thema_dark .calendar-credit,
html[dark=true] .calendar-credit,
.dark .calendar-credit,
html.dark .calendar-credit,
body.dark .calendar-credit,
[data-theme="dark"] .calendar-credit {
color: #6b7280;
border-top: 1px solid #374151;
text-align: right;
}
body.thema_dark .credit-name,
html[dark=true] .credit-name,
.dark .credit-name,
html.dark .credit-name,
body.dark .credit-name,
[data-theme="dark"] .credit-name {
color: #93c5fd;
}
/* 주말 스타일 - 다크 */
body.thema_dark .calendar-day:nth-child(7),
html[dark=true] .calendar-day:nth-child(7),
.dark .calendar-day:nth-child(7),
html.dark .calendar-day:nth-child(7),
body.dark .calendar-day:nth-child(7),
[data-theme="dark"] .calendar-day:nth-child(7) {
background: #111827;
}
body.thema_dark .calendar-day:nth-child(7) .day-header,
html[dark=true] .calendar-day:nth-child(7) .day-header,
.dark .calendar-day:nth-child(7) .day-header,
html.dark .calendar-day:nth-child(7) .day-header,
body.dark .calendar-day:nth-child(7) .day-header,
[data-theme="dark"] .calendar-day:nth-child(7) .day-header {
background: #374151;
}
/* 주말 날짜 숫자 색상 - 다크 */
body.thema_dark .calendar-day:nth-child(7) .day-number,
html[dark=true] .calendar-day:nth-child(7) .day-number,
.dark .calendar-day:nth-child(7) .day-number,
html.dark .calendar-day:nth-child(7) .day-number,
body.dark .calendar-day:nth-child(7) .day-number,
[data-theme="dark"] .calendar-day:nth-child(7) .day-number {
color: #ef4444; /* 일요일 - 빨간색 */
}
/* 휴일 날짜 숫자 색상 - 라이트 */
.calendar-day.holiday .day-number {
color: #dc2626 !important; /* 휴일 - 빨간색 */
}
/* 휴일 날짜 숫자 색상 - 다크 */
body.thema_dark .calendar-day.holiday .day-number,
html[dark=true] .calendar-day.holiday .day-number,
.dark .calendar-day.holiday .day-number,
html.dark .calendar-day.holiday .day-number,
body.dark .calendar-day.holiday .day-number,
[data-theme="dark"] .calendar-day.holiday .day-number {
color: #ef4444 !important; /* 휴일 - 빨간색 */
}
/* 오늘 날짜 강조 - 다크 */
body.thema_dark .calendar-day.today,
html[dark=true] .calendar-day.today,
.dark .calendar-day.today,
html.dark .calendar-day.today,
body.dark .calendar-day.today,
[data-theme="dark"] .calendar-day.today {
background: #0f172a;
border: 2px solid #475569;
box-shadow: 0 0 0 1px #64748b;
}
body.thema_dark .calendar-day.today .day-header,
html[dark=true] .calendar-day.today .day-header,
.dark .calendar-day.today .day-header,
html.dark .calendar-day.today .day-header,
body.dark .calendar-day.today .day-header,
[data-theme="dark"] .calendar-day.today .day-header {
background: #1e293b;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.calendar-grid {
grid-template-columns: repeat(7, 1fr);
gap: 0;
}
.calendar-day {
min-height: 100px;
}
.day-events {
padding: 8px 4px;
min-height: 70px;
}
.week-navigation {
gap: 8px;
}
.week-navigation button {
padding: 6px 12px;
font-size: 13px;
}
}
`;
document.head.appendChild(style);
// URL에서 groupId 추출
function getGroupId() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('groupId') || '';
}
// 주간 시작일 계산 (월요일 기준)
function startOfWeek(date) {
const d = new Date(date);
const day = d.getDay();
const diff = day === 0 ? -6 : 1 - day;
d.setDate(d.getDate() + diff);
d.setHours(0, 0, 0, 0);
return d;
}
// 휴일 체크 함수 (주말만 체크)
function isHoliday(date) {
return date.getDay() === 0;
}
// 날짜 문자열 포맷팅 함수
function formatDateString(date) {
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
}
// 오늘 날짜 문자열
function getTodayString() {
const today = new Date();
return formatDateString(today);
}
// 일정 아이템 HTML 생성 함수
function createEventItemHTML(event) {
const streamerId = event.userId || event.streamerId;
const streamerUrl = streamerId ? `https://www.sooplive.co.kr/station/${streamerId}` : '#';
const streamerNickname = event.streamerNickname || '';
return `
<div class="event-item"
data-streamer-id="${streamerId || ''}"
data-streamer-nickname="${streamerNickname}">
<div class="event-content">
<div class="event-time">
${event.eventTime}
<span class="event-type-badge ${event.calendarTypeName || '기타'}">${event.calendarTypeName || '기타'}</span>
</div>
<div class="event-title">${event.title}</div>
</div>
<div class="event-actions">
<button class="station-link-btn"
onclick="event.stopPropagation(); window.open('${streamerUrl}', '_blank')"
title="방송국으로 이동">
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M4 0L0 3v5h2V4h4v4h2V3L4 0z"/>
</svg>
</button>
</div>
</div>
`;
}
// 현재 페이지의 쿠키와 인증 헤더 가져오기
function getAuthHeaders() {
const headers = {
'Accept': 'application/json',
'User-Agent': navigator.userAgent,
'Referer': window.location.href,
'Origin': window.location.origin
};
if (document.cookie) {
headers['Cookie'] = document.cookie;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ||
document.querySelector('input[name="_token"]')?.value ||
document.querySelector('meta[name="csrf"]')?.content;
if (csrfToken) {
headers['X-CSRF-TOKEN'] = csrfToken;
headers['X-Requested-With'] = 'XMLHttpRequest';
}
return headers;
}
// API 호출
function fetchJSON(url) {
return new Promise((resolve, reject) => {
const headers = getAuthHeaders();
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: headers,
onload: function(response) {
if (response.status === 403) {
resolve({
ok: false,
data: response.responseText,
status: response.status,
statusText: response.statusText,
error: '인증이 필요합니다. sooplive.co.kr에 로그인되어 있는지 확인해주세요.'
});
return;
}
try {
const data = JSON.parse(response.responseText);
resolve({
ok: response.status >= 200 && response.status < 300,
data,
status: response.status,
statusText: response.statusText
});
} catch (e) {
resolve({
ok: false,
data: response.responseText,
status: response.status,
statusText: response.statusText,
parseError: e.message
});
}
},
onerror: function(error) {
reject(new Error(`네트워크 오류: ${error.error || 'Unknown error'}`));
},
ontimeout: function() {
reject(new Error('요청 시간 초과'));
},
timeout: 10000
});
});
}
// 즐겨찾기 스트리머 목록
async function getStreamers(groupId) {
try {
const response = await fetchJSON(`https://myapi.sooplive.co.kr/api/favorite/${groupId}`);
if (!response.ok) {
if (response.status === 403) {
throw new Error(`인증 오류 (403): sooplive.co.kr에 로그인되어 있는지 확인해주세요.`);
}
throw new Error(`HTTP ${response.status}: ${response.data || response.error || 'Unknown error'}`);
}
const items = Array.isArray(response.data) ? response.data : (response.data?.data || []);
if (items.length === 0) {
return [];
}
const streamers = items.map(item => {
const userId = item?.user_id;
const nickname = item?.user_nick;
const isLive = item?.is_live || false;
return { userId, nickname, isLive };
}).filter(streamer => streamer.userId && streamer.nickname);
return streamers;
} catch (error) {
throw error;
}
}
// 캘린더 이벤트
async function getEvents(userId, weekStart) {
try {
// 이번주 전체를 검색 (weekStart 기준)
const year = weekStart.getFullYear();
const month = weekStart.getMonth() + 1;
const day = weekStart.getDate();
const response = await fetchJSON(`https://api-channel.sooplive.co.kr/v1.1/channel/${userId}/calendar?view=week&year=${year}&month=${month}&day=${day}&userId=${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const events = [];
if (response.data.days) {
for (const day of response.data.days) {
if (day.events) {
for (const event of day.events) {
if (event.eventDate && event.eventTime) {
// 이벤트 날짜는 그대로 유지
events.push({
title: event.title || '제목없음',
eventDate: event.eventDate,
eventTime: event.eventTime,
calendarTypeName: event.calendarTypeName || '일정'
});
}
}
}
}
}
return events;
} catch (error) {
return [];
}
}
// 캘린더 HTML 생성
function createCalendarHTML(events, weekStart) {
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const eventsByDate = {};
for (const event of events) {
if (!eventsByDate[event.eventDate]) {
eventsByDate[event.eventDate] = [];
}
eventsByDate[event.eventDate].push(event);
}
const weekDates = [];
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(date.getDate() + i);
weekDates.push(date);
}
let html = `
<div class="calendar-container">
<div class="calendar-header">
<h3>통합 일정</h3>
<div class="week-navigation">
<button id="prev-week-btn">← 이전 주</button>
<span class="week-range">${weekStart.toLocaleDateString('ko-KR')} ~ ${weekEnd.toLocaleDateString('ko-KR')}</span>
<button id="next-week-btn">다음 주 →</button>
</div>
</div>
<div class="calendar-grid">
`;
// 오늘 날짜
const todayStr = getTodayString();
for (const date of weekDates) {
// 이벤트 날짜는 그대로 사용
const dateStr = formatDateString(date);
const dayEvents = eventsByDate[dateStr] || [];
dayEvents.sort((a, b) => a.eventTime.localeCompare(b.eventTime));
// 오늘 날짜인지 확인
const isToday = dateStr === todayStr;
const todayClass = isToday ? ' today' : '';
// 휴일인지 확인
const isHolidayDate = isHoliday(date);
const holidayClass = isHolidayDate ? ' holiday' : '';
html += `
<div class="calendar-day${todayClass}${holidayClass}">
<div class="day-header">
<div class="day-name">${date.toLocaleDateString('ko-KR', { weekday: 'short' })}</div>
<div class="day-number">${date.getDate()}</div>
<div class="event-count">${dayEvents.length}개</div>
</div>
<div class="day-events">
`;
if (dayEvents.length === 0) {
html += '<div class="no-events">일정 없음</div>';
} else {
// 모든 일정 표시 (스크롤로 확인)
for (const event of dayEvents) {
html += createEventItemHTML(event);
}
}
html += `
</div>
</div>
`;
}
html += `
</div>
<div class="calendar-credit">
made by <span class="credit-name">지창연구소</span>
</div>
</div>
`;
return html;
}
// 마우스 오버 이벤트 처리 함수
function addHoverEvents() {
const eventItems = document.querySelectorAll('.event-item');
eventItems.forEach(item => {
item.addEventListener('mouseenter', function() {
// 고정된 멤버가 있으면 마우스 오버 무시
if (fixedStreamer) return;
const streamerId = this.getAttribute('data-streamer-id');
const streamerNickname = this.getAttribute('data-streamer-nickname');
// 마우스 오버한 날 찾기
const hoveredDay = this.closest('.calendar-day');
// 같은 스트리머의 모든 일정 강조
const allEventItems = document.querySelectorAll('.event-item');
allEventItems.forEach(eventItem => {
const itemStreamerId = eventItem.getAttribute('data-streamer-id');
const itemStreamerNickname = eventItem.getAttribute('data-streamer-nickname');
if (itemStreamerId === streamerId && itemStreamerNickname === streamerNickname) {
eventItem.classList.add('highlight');
} else {
eventItem.classList.add('fade');
}
});
// 해당 스트리머의 일정이 없는 날들을 빨갛게 강조
highlightEmptyDays(streamerId, streamerNickname);
// 화살표 표시 및 스크롤 실행
showScrollIndicatorsForStreamer(streamerId, streamerNickname);
// 마우스 오버한 날짜를 제외한 다른 날짜들의 해당 멤버 첫 번째 일정을 가운데로 스크롤
scrollToFirstEventInOtherDays(streamerId, streamerNickname, hoveredDay);
});
item.addEventListener('mouseleave', function() {
// 고정된 멤버가 없을 때만 강조 효과 제거
if (!fixedStreamer) {
clearAllHighlights();
}
});
// 클릭 이벤트 추가 (멤버 고정)
item.addEventListener('click', function(e) {
// 방송국 버튼 클릭이 아닐 때만 멤버 고정
if (!e.target.classList.contains('station-link-btn')) {
const streamerId = this.getAttribute('data-streamer-id');
const streamerNickname = this.getAttribute('data-streamer-nickname');
// 클릭한 날짜 저장
clickedDay = this.closest('.calendar-day');
// 이미 고정된 멤버와 같으면 고정 해제
if (fixedStreamer && fixedStreamer.id === streamerId && fixedStreamer.nickname === streamerNickname) {
fixedStreamer = null;
clearAllHighlights();
} else {
// 다른 멤버 클릭 시 기존 고정 해제 후 새 멤버로 고정 변경
clearAllHighlights();
fixedStreamer = { id: streamerId, nickname: streamerNickname };
applyFixedStreamerHighlight();
}
}
});
});
}
// 고정된 멤버 강조 적용 함수
function applyFixedStreamerHighlight() {
if (!fixedStreamer) return;
const allEventItems = document.querySelectorAll('.event-item');
allEventItems.forEach(eventItem => {
const itemStreamerId = eventItem.getAttribute('data-streamer-id');
const itemStreamerNickname = eventItem.getAttribute('data-streamer-nickname');
if (itemStreamerId === fixedStreamer.id && itemStreamerNickname === fixedStreamer.nickname) {
eventItem.classList.add('highlight');
} else {
eventItem.classList.add('fade');
}
});
// 해당 스트리머의 일정이 없는 날들을 빨갛게 강조
highlightEmptyDays(fixedStreamer.id, fixedStreamer.nickname);
// 화살표 표시
showScrollIndicatorsForStreamer(fixedStreamer.id, fixedStreamer.nickname);
// 다른 날짜들의 해당 멤버 첫 번째 일정을 맨 위로 스크롤
scrollToFirstEventInOtherDays(fixedStreamer.id, fixedStreamer.nickname);
}
// 스크롤 완료 대기 함수
function waitForScrollComplete(container, callback) {
const startTime = Date.now();
let lastScrollTop = container.scrollTop;
let isScrolling = false;
const checkScroll = () => {
const currentScrollTop = container.scrollTop;
const currentTime = Date.now();
if (Math.abs(currentScrollTop - lastScrollTop) > 1) {
isScrolling = true;
lastScrollTop = currentScrollTop;
} else if (isScrolling && (currentTime - startTime) > 200) {
// 스크롤이 멈췄고 충분한 시간이 지났으면 완료
callback();
return;
}
requestAnimationFrame(checkScroll);
};
requestAnimationFrame(checkScroll);
}
// 스크롤 완료 후 화살표 인디케이터 업데이트
function updateScrollIndicatorsAfterScroll(streamerId, streamerNickname, excludeDay = null) {
const allDays = document.querySelectorAll('.calendar-day');
allDays.forEach(day => {
if (excludeDay === day) return;
const dayEventsContainer = day.querySelector('.day-events');
if (!dayEventsContainer) return;
// 해당 스트리머의 일정들 찾기
const streamerEvents = Array.from(day.querySelectorAll('.event-item')).filter(event => {
const eventStreamerId = event.getAttribute('data-streamer-id');
const eventStreamerNickname = event.getAttribute('data-streamer-nickname');
return eventStreamerId === streamerId && eventStreamerNickname === streamerNickname;
});
if (streamerEvents.length === 0) return;
// 현재 보이는 영역 확인
const containerRect = dayEventsContainer.getBoundingClientRect();
const containerTop = containerRect.top;
const containerBottom = containerRect.bottom;
// 위쪽/아래쪽에 숨겨진 일정이 있는지 확인
let hasHiddenEventsAbove = false;
let hasHiddenEventsBelow = false;
streamerEvents.forEach(event => {
const eventRect = event.getBoundingClientRect();
if (eventRect.bottom < containerTop) {
hasHiddenEventsAbove = true;
} else if (eventRect.top > containerBottom) {
hasHiddenEventsBelow = true;
}
});
// 기존 화살표 제거
removeScrollIndicator(day);
// 새로운 화살표 표시
if (hasHiddenEventsAbove) {
const topIndicator = createScrollIndicator('top', '위쪽에 더 많은 일정이 있습니다');
showScrollIndicator(topIndicator, day, 'top');
}
if (hasHiddenEventsBelow) {
const bottomIndicator = createScrollIndicator('bottom', '아래쪽에 더 많은 일정이 있습니다');
showScrollIndicator(bottomIndicator, day, 'bottom');
}
});
}
// 개선된 스크롤 함수 - 다른 날짜들의 해당 멤버 첫 번째 일정을 가운데로 스크롤
function scrollToFirstEventInOtherDays(streamerId, streamerNickname, excludeDay = null) {
// 강조 효과 적용 후 레이아웃 안정화를 위해 지연
setTimeout(() => {
const allDays = document.querySelectorAll('.calendar-day');
let scrollCount = 0;
let totalScrolls = 0;
allDays.forEach(day => {
const dayEventsContainer = day.querySelector('.day-events');
if (!dayEventsContainer) return;
// 제외할 날짜 체크 (클릭한 날짜 또는 마우스 오버한 날짜)
if (clickedDay === day || excludeDay === day) return;
// 해당 스트리머의 첫 번째 일정 찾기
const streamerEvents = Array.from(day.querySelectorAll('.event-item')).filter(event => {
const eventStreamerId = event.getAttribute('data-streamer-id');
const eventStreamerNickname = event.getAttribute('data-streamer-nickname');
return eventStreamerId === streamerId && eventStreamerNickname === streamerNickname;
});
if (streamerEvents.length === 0) return;
// 시간순 정렬 후 첫 번째 일정
const firstEvent = streamerEvents.sort((a, b) => {
const timeA = a.querySelector('.event-time')?.textContent || '';
const timeB = b.querySelector('.event-time')?.textContent || '';
return timeA.localeCompare(timeB);
})[0];
if (firstEvent) {
// 스크롤이 필요한지 확인
if (dayEventsContainer.scrollHeight > dayEventsContainer.clientHeight) {
totalScrolls++;
// 더 정확한 스크롤 위치 계산
const containerHeight = dayEventsContainer.clientHeight;
const eventHeight = firstEvent.offsetHeight;
// getBoundingClientRect를 사용한 정확한 위치 계산
const containerRect = dayEventsContainer.getBoundingClientRect();
const eventRect = firstEvent.getBoundingClientRect();
const relativeTop = eventRect.top - containerRect.top + dayEventsContainer.scrollTop;
// 위쪽에 하나 정도 더 보일 정도로 스크롤하는 위치 계산
const scrollPosition = relativeTop - (eventHeight * 1.5) + (eventHeight / 2);
// 부드러운 스크롤 적용
dayEventsContainer.scrollTo({
top: Math.max(0, scrollPosition),
behavior: 'smooth'
});
// 스크롤 완료 후 화살표 업데이트
waitForScrollComplete(dayEventsContainer, () => {
scrollCount++;
if (scrollCount === totalScrolls) {
// 모든 스크롤이 완료되면 화살표 업데이트
updateScrollIndicatorsAfterScroll(streamerId, streamerNickname, excludeDay);
}
});
}
}
});
// 스크롤이 필요한 컨테이너가 없으면 즉시 화살표 업데이트
if (totalScrolls === 0) {
updateScrollIndicatorsAfterScroll(streamerId, streamerNickname, excludeDay);
}
}, 150); // 150ms 지연으로 레이아웃 안정화
}
// 빈 날짜 강조 함수 (휴방일 포함)
function highlightEmptyDays(streamerId, streamerNickname) {
const allDays = document.querySelectorAll('.calendar-day');
allDays.forEach(day => {
const dayEvents = day.querySelectorAll('.event-item');
let hasStreamerNormalEvent = false;
// 해당 스트리머의 일반 일정(휴방 제외)이 있는지 확인
dayEvents.forEach(event => {
const eventStreamerId = event.getAttribute('data-streamer-id');
const eventStreamerNickname = event.getAttribute('data-streamer-nickname');
const eventType = event.querySelector('.event-type-badge')?.textContent;
if (eventStreamerId === streamerId && eventStreamerNickname === streamerNickname) {
// 휴방이 아닌 일반 일정이 있는지 확인
if (eventType !== '휴방') {
hasStreamerNormalEvent = true;
}
}
});
// 해당 스트리머의 일반 일정이 없는 날은 빨갛게 강조
if (!hasStreamerNormalEvent) {
day.classList.add('empty-day');
}
});
}
// 해당 멤버의 스크롤 영역 밖에 일정이 있으면 화살표 표시 (스크롤 전 초기 표시용)
function showScrollIndicatorsForStreamer(streamerId, streamerNickname) {
const allDays = document.querySelectorAll('.calendar-day');
allDays.forEach(day => {
const dayEvents = day.querySelectorAll('.event-item');
const dayEventsContainer = day.querySelector('.day-events');
if (!dayEventsContainer) return;
// 해당 스트리머의 일정들 찾기
const streamerEvents = Array.from(dayEvents).filter(event => {
const eventStreamerId = event.getAttribute('data-streamer-id');
const eventStreamerNickname = event.getAttribute('data-streamer-nickname');
return eventStreamerId === streamerId && eventStreamerNickname === streamerNickname;
});
if (streamerEvents.length === 0) return;
// 스크롤 영역의 높이와 스크롤 위치 확인
const containerHeight = dayEventsContainer.clientHeight;
const scrollHeight = dayEventsContainer.scrollHeight;
// 스크롤이 가능한지 확인 (내용이 컨테이너보다 높을 때)
if (scrollHeight > containerHeight) {
const containerRect = dayEventsContainer.getBoundingClientRect();
// 위쪽/아래쪽에 숨겨진 일정이 있는지 확인
let hasHiddenEventsAbove = false;
let hasHiddenEventsBelow = false;
// 해당 멤버의 일정들을 시간순으로 정렬
const sortedStreamerEvents = streamerEvents.sort((a, b) => {
const timeA = a.querySelector('.event-time')?.textContent || '';
const timeB = b.querySelector('.event-time')?.textContent || '';
return timeA.localeCompare(timeB);
});
// 첫 번째와 마지막 일정 확인
const firstEvent = sortedStreamerEvents[0];
const lastEvent = sortedStreamerEvents[sortedStreamerEvents.length - 1];
if (firstEvent && lastEvent) {
const firstEventRect = firstEvent.getBoundingClientRect();
const lastEventRect = lastEvent.getBoundingClientRect();
// 첫 번째 일정이 위쪽에 숨겨져 있는지 확인
hasHiddenEventsAbove = firstEventRect.bottom < containerRect.top;
// 마지막 일정이 아래쪽에 숨겨져 있는지 확인
hasHiddenEventsBelow = lastEventRect.top > containerRect.bottom;
}
// 기존 화살표 제거
removeScrollIndicator(day);
// 화살표 표시
if (hasHiddenEventsAbove) {
const topIndicator = createScrollIndicator('top', '위쪽에 더 많은 일정이 있습니다');
showScrollIndicator(topIndicator, day, 'top');
}
if (hasHiddenEventsBelow) {
const bottomIndicator = createScrollIndicator('bottom', '아래쪽에 더 많은 일정이 있습니다');
showScrollIndicator(bottomIndicator, day, 'bottom');
}
}
});
}
// 주간 변경
window.changeWeek = function(days) {
anchorDate.setDate(anchorDate.getDate() + days);
loadCalendar();
};
// 캘린더 로드
window.loadCalendar = async function() {
const container = document.getElementById('sooplive-calendar');
if (!container) return;
container.innerHTML = '<div class="loading-message">일정을 불러오는 중...</div>';
try {
const groupId = getGroupId();
if (!groupId) {
container.innerHTML = '<div style="color: red; padding: 20px;">groupId를 찾을 수 없습니다.</div>';
return;
}
const streamers = await getStreamers(groupId);
if (streamers.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 20px;">즐겨찾기 스트리머가 없습니다.</div>';
return;
}
// 현재 날짜를 기준으로 주 계산
const weekStart = startOfWeek(anchorDate);
const allEvents = [];
for (const streamer of streamers) {
const { userId, nickname } = streamer;
const events = await getEvents(userId, weekStart);
for (const event of events) {
allEvents.push({
...event,
streamerNickname: nickname,
userId: userId,
streamerId: userId,
title: `${nickname}:${event.title}`
});
}
}
container.innerHTML = createCalendarHTML(allEvents, weekStart);
// 이벤트 리스너 추가
const prevBtn = document.getElementById('prev-week-btn');
const nextBtn = document.getElementById('next-week-btn');
if (prevBtn) {
prevBtn.addEventListener('click', () => changeWeek(-7));
}
if (nextBtn) {
nextBtn.addEventListener('click', () => changeWeek(7));
}
// 마우스 오버 이벤트 추가
addHoverEvents();
// 고정된 멤버가 있으면 강조 적용
if (fixedStreamer) {
applyFixedStreamerHighlight();
}
} catch (error) {
let errorMessage = `오류: ${error.message}`;
let troubleshooting = '';
if (error.message.includes('Failed to fetch') || error.message.includes('네트워크 오류')) {
errorMessage = '네트워크 연결 오류가 발생했습니다.';
troubleshooting = `
<div style="margin-top: 10px; padding: 10px; background: #fff3cd; border-radius: 4px; font-size: 12px;">
<strong>해결 방법:</strong><br>
1. 인터넷 연결을 확인해주세요<br>
2. sooplive.co.kr에 로그인되어 있는지 확인해주세요<br>
3. 페이지를 새로고침해보세요
</div>
`;
} else if (error.message.includes('403') || error.message.includes('인증 오류')) {
errorMessage = '인증이 필요합니다. 로그인 상태를 확인해주세요.';
troubleshooting = `
<div style="margin-top: 10px; padding: 10px; background: #f8d7da; border-radius: 4px; font-size: 12px;">
<strong>403 Forbidden 오류 해결 방법:</strong><br>
1. <strong>sooplive.co.kr에 로그인</strong>되어 있는지 확인<br>
2. 로그인 세션이 만료되었다면 <strong>다시 로그인</strong><br>
3. 브라우저 쿠키가 활성화되어 있는지 확인
</div>
`;
}
container.innerHTML = `
<div style="color: red; padding: 20px;">
${errorMessage}
${troubleshooting}
</div>
`;
}
};
// 캘린더 컨테이너 추가
function addCalendarContainer() {
if (document.getElementById('sooplive-calendar')) {
return;
}
// groupId가 없으면 캘린더를 표시하지 않음
const groupId = getGroupId();
if (!groupId) {
return;
}
let strmArea = document.querySelector('.strm_area');
if (!strmArea) {
setTimeout(addCalendarContainer, TIMING.INIT_RETRY_DELAY);
return;
}
const calendarContainer = document.createElement('div');
calendarContainer.id = 'sooplive-calendar';
strmArea.parentNode.insertBefore(calendarContainer, strmArea.nextSibling);
// 캘린더 전체 영역에서 마우스가 벗어났을 때 모든 효과 제거
calendarContainer.addEventListener('mouseleave', function() {
clearAllHighlights();
});
// 문서 전체에서 마우스가 캘린더 영역을 벗어났을 때도 효과 제거
document.addEventListener('mouseleave', function(e) {
const calendar = document.getElementById('sooplive-calendar');
if (calendar && !calendar.contains(e.relatedTarget)) {
clearAllHighlights();
}
});
loadCalendar();
}
// URL 변경 감지
function checkUrlChange() {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
// 기존 캘린더 제거
const existingCalendar = document.getElementById('sooplive-calendar');
if (existingCalendar) {
existingCalendar.remove();
}
// 새 캘린더 추가
setTimeout(addCalendarContainer, TIMING.SPA_NAVIGATION_DELAY * 5);
}
}
// 초기화
function init() {
addCalendarContainer();
}
// 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
setTimeout(init, TIMING.INIT_RETRY_DELAY);
// URL 변경 감지 시작 (SPA 대응)
setInterval(checkUrlChange, TIMING.URL_CHECK_INTERVAL);
// popstate 이벤트 리스너 (뒤로가기/앞으로가기 대응)
window.addEventListener('popstate', () => {
setTimeout(() => {
checkUrlChange();
}, TIMING.SPA_NAVIGATION_DELAY);
});
})();