Sooplive Calendar Viewer

Sooplive 즐겨찾기 그룹의 통합 일정을 캘린더 형태로 표시하는 스크립트

目前為 2025-09-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Sooplive Calendar Viewer
// @namespace    https://sooplive-calendar-viewer.local
// @version      2.6
// @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;

    // 테마 감지 함수 (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: 400px;
            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;
        }
        
        .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-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%);
        }
        
        .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 .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 .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}"
                 onclick="window.open('${streamerUrl}', '_blank')" 
                 style="cursor: pointer; pointer-events: auto;">
                <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>
        `;
    }

    // 현재 페이지의 쿠키와 인증 헤더 가져오기
    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() {
                const streamerId = this.getAttribute('data-streamer-id');
                const streamerNickname = this.getAttribute('data-streamer-nickname');
                
                // 같은 스트리머의 모든 일정 강조
                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');
                    }
                });
            });
            
            item.addEventListener('mouseleave', function() {
                // 모든 강조 효과 제거
                const allEventItems = document.querySelectorAll('.event-item');
                allEventItems.forEach(eventItem => {
                    eventItem.classList.remove('highlight', 'fade');
                });
            });
        });
    }

    // 주간 변경
    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();
        } 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, 2000);
            return;
        }

        const calendarContainer = document.createElement('div');
        calendarContainer.id = 'sooplive-calendar';

        strmArea.parentNode.insertBefore(calendarContainer, strmArea.nextSibling);
        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, 500);
        }
    }

    // 초기화
    function init() {
        addCalendarContainer();
    }

    // 실행
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    setTimeout(init, 2000);

    // URL 변경 감지 시작 (SPA 대응)
    setInterval(checkUrlChange, 1000);
    
    // popstate 이벤트 리스너 (뒤로가기/앞으로가기 대응)
    window.addEventListener('popstate', () => {
        setTimeout(() => {
            checkUrlChange();
        }, 100);
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址