// ==UserScript==
// @name Chzzk_L&V: Dal.wiki Viewer
// @namespace Chzzk_L&V: Dal.wiki Viewer
// @version 1.0.5
// @description 치지직 합방 일정과 대회/이벤트를 확인가능한 Dal.wiki를 치지직 방송을 보면서도 확인하고자 만들었습니다.
// @author DOGJIP
// @match *://chzzk.naver.com/*
// @grant none
// @run-at document-idle
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com
// ==/UserScript==
(function () {
'use strict';
// 치지직 도메인이 아니거나, iframe 내부라면 스크립트를 실행하지 않음
if (!location.hostname.includes('chzzk.naver.com')) return;
if (window.top !== window.self) return;
const topicPath = '/topic/%EC%B9%98%EC%A7%80%EC%A7%81%20%ED%95%A9%EB%B0%A9%2F%EB%8C%80%ED%9A%8C%2F%EC%BD%98%ED%85%90%EC%B8%A0%20%EC%9D%BC%EC%A0%95';
let buttonContainer, calendarBtn, streamerBtn, dayBtn, monthBtn, iframe, closeBtn, opacitySlider, rememberOpacityCheckbox, checkbox;
let viewMode = 'agenda'; // 기본 보기 모드는 일간 (month or agenda)
let currentOpacity = 1.0;
const STORAGE_KEY = 'dalwiki_opacity';
const fixedPosition = { top: 16, left: 150 };
const iframeDefaultWidth = 1000;
const iframeDefaultHeight = 800;
function applyButtonPosition() {
Object.assign(buttonContainer.style, {
position: 'fixed',
top: `${fixedPosition.top}px`,
left: `${fixedPosition.left}px`,
zIndex: 2147483647,
display: 'flex',
gap: '4px'
});
}
function centerIframe() {
const maxW = window.innerWidth * 0.9;
const maxH = window.innerHeight * 0.9;
const w = Math.min(iframeDefaultWidth, maxW);
const h = Math.min(iframeDefaultHeight, maxH);
const top = (window.innerHeight - h) / 2;
const left = (window.innerWidth - w) / 2;
Object.assign(iframe.style, {
width: w + 'px',
height: h + 'px',
top: top + 'px',
left: left + 'px',
display: 'block',
position: 'fixed',
border: '2px solid #ccc',
borderRadius: '8px',
boxShadow: '0 0 12px rgba(0,0,0,0.4)',
background: 'white',
zIndex: 2147483646
});
Object.assign(closeBtn.style, {
position: 'fixed',
top: (top - 30) + 'px',
left: (left + w - 30) + 'px',
display: 'block',
background:'crimson',
color: 'white',
border: 'none',
padding: '4px 8px',
borderRadius: '50%',
cursor: 'pointer',
fontSize: '12px',
zIndex: 2147483647
});
// 1) 슬라이더 위치: closeBtn 왼쪽 (기존 동일)
const sliderRight = parseFloat(closeBtn.style.left) - 110;
opacitySlider.style.top = closeBtn.style.top;
opacitySlider.style.left = `${sliderRight}px`;
// 2) 체크박스 위치: 슬라이더 왼쪽 (슬라이더 너비 100px + 여백 8px)
const checkboxLeft = sliderRight - 100 - 8;
rememberOpacityCheckbox.style.top = closeBtn.style.top;
rememberOpacityCheckbox.style.left = `${checkboxLeft}px`;
streamerBtn.style.top = rememberOpacityCheckbox.style.top;
streamerBtn.style.left = iframe.style.left;
streamerBtn.style.display = 'block';
// 일(日) 버튼: streamerBtn 폭 + 8px 여백 만큼 띄우기
dayBtn.style.top = rememberOpacityCheckbox.style.top;
const streamerWidth = streamerBtn.offsetWidth;
const gap = 8; // 원하는 여백(px)
dayBtn.style.left = `${parseFloat(iframe.style.left) + streamerWidth + gap}px`;
dayBtn.style.display = 'block';
// 월(月) 버튼
monthBtn.style.top = rememberOpacityCheckbox.style.top;
// 일 버튼 바로 오른쪽으로 위치 (4px 여백)
const dayWidth = dayBtn.offsetWidth;
const smallGap = 4;
monthBtn.style.left = `${parseFloat(dayBtn.style.left) + dayWidth + smallGap}px`;
monthBtn.style.display = 'block';
}
function createUI() {
buttonContainer = document.createElement('div');
document.body.appendChild(buttonContainer);
// 스트리머 일정 버튼
streamerBtn = document.createElement('button');
streamerBtn.textContent = '🎥 스트리머 일정';
Object.assign(streamerBtn.style, {
display: 'none',
position: 'fixed',
zIndex: 2147483647,
padding: '4px 8px',
fontSize: '12px',
background: '#444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
document.body.appendChild(streamerBtn);
// 1) 일정 보기 버튼
calendarBtn = document.createElement('button');
calendarBtn.textContent = '📅 일정 보기';
Object.assign(calendarBtn.style, {
padding: '6px 8px',
zIndex: 2147483647,
background: '#333333',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '12px'
});
buttonContainer.appendChild(calendarBtn);
// 2-1) 일(日) 버튼
dayBtn = document.createElement('button');
dayBtn.textContent = '일(日)';
Object.assign(dayBtn.style, {
display: 'none',
position: 'fixed',
zIndex: 2147483647,
padding: '4px 8px',
background: '#555555',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
marginLeft: '4px'
});
document.body.appendChild(dayBtn);
// 2-2) 월(月) 버튼
monthBtn = document.createElement('button');
monthBtn.textContent = '월(月)';
Object.assign(monthBtn.style, {
display: 'none',
position: 'fixed',
zIndex: 2147483647,
padding: '4px 8px',
background: '#555555',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
marginLeft: '4px'
});
document.body.appendChild(monthBtn);
// 3) iframe
iframe = document.createElement('iframe');
Object.assign(iframe.style, { display: 'none' });
document.body.appendChild(iframe);
// 4) 닫기 버튼
closeBtn = document.createElement('button');
closeBtn.textContent = '✖';
Object.assign(closeBtn.style, { display: 'none' });
document.body.appendChild(closeBtn);
applyButtonPosition();
// 5) 투명도 조절 슬라이더
opacitySlider = document.createElement('input');
opacitySlider.type = 'range';
opacitySlider.min = '0.3';
opacitySlider.max = '1';
opacitySlider.step = '0.01';
opacitySlider.value = '1';
Object.assign(opacitySlider.style, {
display: 'none',
position: 'fixed',
zIndex: 2147483647,
width: '100px',
cursor: 'pointer'
});
document.body.appendChild(opacitySlider);
// 6) 투명도 기억 여부 체크박스
rememberOpacityCheckbox = document.createElement('label');
rememberOpacityCheckbox.style.position = 'fixed';
rememberOpacityCheckbox.style.zIndex = 2147483647;
rememberOpacityCheckbox.style.top = '50px';
rememberOpacityCheckbox.style.transform = 'translateX(0)';
rememberOpacityCheckbox.style.fontSize = '12px';
rememberOpacityCheckbox.style.color = '#fff';
rememberOpacityCheckbox.style.background = 'rgba(34, 34, 34, 0.5)'; // 50% 투명한 검정색
rememberOpacityCheckbox.style.padding = '4px 6px';
rememberOpacityCheckbox.style.borderRadius = '4px';
checkbox = document.createElement('input'); // <-- 전역 변수 사용
checkbox.type = 'checkbox';
checkbox.style.marginRight = '4px';
rememberOpacityCheckbox.appendChild(checkbox);
rememberOpacityCheckbox.appendChild(document.createTextNode('투명도 기억'));
document.body.appendChild(rememberOpacityCheckbox);
rememberOpacityCheckbox.style.display = 'none';
}
function openIframe() {
const dt = new Date();
const yyyy = dt.getFullYear();
const mm = String(dt.getMonth()+1).padStart(2,'0');
const dd = String(dt.getDate()).padStart(2,'0');
iframe.src = `https://dal.wiki${topicPath}/${viewMode}?date=${yyyy}-${mm}-${dd}`;
centerIframe();
// 투명도 적용
iframe.style.opacity = currentOpacity;
opacitySlider.value = String(currentOpacity);
opacitySlider.style.display = 'block';
rememberOpacityCheckbox.style.display = 'block';
checkbox.checked = !!localStorage.getItem(STORAGE_KEY);
}
function bindEvents() {
// 일정 보기
calendarBtn.addEventListener('click', () => {
if (iframe.style.display === 'block') {
iframe.style.display = 'none';
closeBtn.style.display = 'none';
opacitySlider.style.display = 'none';
rememberOpacityCheckbox.style.display = 'none';
streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
} else {
openIframe();
}
});
// 닫기 버튼
closeBtn.addEventListener('click', () => {
iframe.style.display = 'none';
closeBtn.style.display = 'none';
opacitySlider.style.display = 'none';
rememberOpacityCheckbox.style.display = 'none';
streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
});
// 화면 리사이즈 대응
window.addEventListener('resize', () => {
if (iframe.style.display === 'block') centerIframe();
});
// 슬라이더 변경 시, 저장 옵션이 체크되어 있으면 localStorage에 저장
opacitySlider.addEventListener('input', () => {
currentOpacity = parseFloat(opacitySlider.value);
iframe.style.opacity = currentOpacity;
if (checkbox.checked) {
localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
}
});
// 체크박스 토글: 체크 해제 시 저장값 제거
checkbox.addEventListener('change', () => {
if (!checkbox.checked) {
localStorage.removeItem(STORAGE_KEY);
} else {
localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
}
});
streamerBtn.addEventListener('click', () => {
const dt = new Date();
const yyyy= dt.getFullYear();
const mm = String(dt.getMonth()+1).padStart(2,'0');
const dd = String(dt.getDate()).padStart(2,'0');
iframe.src = `https://dal.wiki/topic/%EC%B9%98%EC%A7%80%EC%A7%81%20%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%A8%B8%20%EC%9D%BC%EC%A0%95/agenda?date=${yyyy}-${mm}-${dd}`;
});
// 日 버튼 클릭 시 일간(agenda) 보기
dayBtn.addEventListener('click', () => {
const dt = new Date();
const yyyy = dt.getFullYear();
const mm = String(dt.getMonth()+1).padStart(2,'0');
const dd = String(dt.getDate()).padStart(2,'0');
iframe.src = `https://dal.wiki${topicPath}/agenda?date=${yyyy}-${mm}-${dd}`;
});
// 月 버튼 클릭 시 월간(month) 보기
monthBtn.addEventListener('click', () => {
const dt = new Date();
const yyyy = dt.getFullYear();
const mm = String(dt.getMonth()+1).padStart(2,'0');
const dd = String(dt.getDate()).padStart(2,'0');
iframe.src = `https://dal.wiki${topicPath}/month?date=${yyyy}-${mm}-${dd}`;
});
}
function onReady(fn) {
if (document.body) {
// 초기 투명도 로드
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
currentOpacity = parseFloat(saved);
}
return fn();
}
new MutationObserver((obs) => {
if (document.body) {
obs.disconnect();
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
currentOpacity = parseFloat(saved);
}
fn();
}
}).observe(document.documentElement, { childList: true });
}
function observeBodyStyle() {
const body = document.body;
const observer = new MutationObserver(() => {
const style = window.getComputedStyle(body);
const wide = style.overflow === 'hidden' && style.position === 'fixed';
buttonContainer.style.display = wide ? 'none' : 'flex';
});
observer.observe(body, { attributes: true, attributeFilter: ['style'] });
// 초기 상태 설정
const initStyle = window.getComputedStyle(body);
const isWide = initStyle.overflow === 'hidden' && initStyle.position === 'fixed';
buttonContainer.style.display = isWide ? 'none' : 'flex';
}
onReady(() => {
createUI();
bindEvents();
observeBodyStyle();
});
})();