// ==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);
});
})();