Chzzk_L&V: Dal.wiki Viewer

치지직에서 Dal.wiki 일정 확인 및 추가기능

이 스크립트 설치?
작성자 추천 스크립트

Chzzk_L&V: Chatting Plus 스크립트도 사용해 보세요.

이 스크립트 설치
// ==UserScript==
// @name         Chzzk_L&V: Dal.wiki Viewer
// @namespace    Chzzk_L&V: Dal.wiki Viewer
// @version      1.0.6
// @description  치지직에서 Dal.wiki 일정 확인 및 추가기능
// @author       DOGJIP
// @match        *://chzzk.naver.com/*
// @match        https://dal.wiki/*
// @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';

  const isChzzk = location.hostname.includes('chzzk.naver.com');
  const isDalWiki = location.hostname.includes('dal.wiki');

  // ========================= CHZZK: 일정 뷰어 =========================
  if (isChzzk && window.top === window.self) {
    let buttonContainer, calendarBtn, streamerBtn, dayBtn, monthBtn, iframe, closeBtn, opacitySlider, rememberOpacityCheckbox, checkbox;
    let viewMode = '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
      });

      const sliderRight = parseFloat(closeBtn.style.left) - 110;
      opacitySlider.style.top = closeBtn.style.top;
      opacitySlider.style.left = `${sliderRight}px`;

      const checkboxLeft = sliderRight - 100 - 8;
      rememberOpacityCheckbox.style.top = closeBtn.style.top;
      rememberOpacityCheckbox.style.left = `${checkboxLeft}px`;

      streamerBtn.style.top = closeBtn.style.top;
      streamerBtn.style.left = iframe.style.left;
      streamerBtn.style.display = 'block';

      dayBtn.style.top = closeBtn.style.top;
      const streamerWidth = streamerBtn.offsetWidth;
      const gap = 8;
      dayBtn.style.left = `${parseFloat(iframe.style.left) + streamerWidth + gap}px`;
      dayBtn.style.display = 'block';

      const dayWidth = dayBtn.offsetWidth;
      monthBtn.style.top = closeBtn.style.top;
      monthBtn.style.left = `${parseFloat(dayBtn.style.left) + dayWidth + 4}px`;
      monthBtn.style.display = 'block';
    }

    function createUI() {
      buttonContainer = document.createElement('div');
      document.body.appendChild(buttonContainer);
      calendarBtn = document.createElement('button');
      calendarBtn.textContent = '📅 일정 보기';
      Object.assign(calendarBtn.style, { padding: '6px 8px', zIndex: 2147483647, background: '#333', color: 'white', borderRadius: '6px', border: 'none', fontSize: '12px' });
      buttonContainer.appendChild(calendarBtn);

      streamerBtn = document.createElement('button');
      streamerBtn.textContent = '🎥 스트리머 일정';
      dayBtn = document.createElement('button');
      monthBtn = document.createElement('button');
      [streamerBtn, dayBtn, monthBtn].forEach(btn => {
        btn.style.display = 'none';
        btn.style.position = 'fixed';
        btn.style.zIndex = '2147483647';
        btn.style.padding = '4px 8px';
        btn.style.background = '#555';
        btn.style.color = 'white';
        btn.style.borderRadius = '4px';
        btn.style.border = 'none';
        btn.style.cursor = 'pointer';
        btn.style.fontSize = '12px';
        document.body.appendChild(btn);
      });
      dayBtn.textContent = '일(日)';
      monthBtn.textContent = '월(月)';

      iframe = document.createElement('iframe');
      Object.assign(iframe.style, { display: 'none' });
      document.body.appendChild(iframe);

      closeBtn = document.createElement('button');
      closeBtn.textContent = '✖';
      Object.assign(closeBtn.style, { display: 'none' });
      document.body.appendChild(closeBtn);

      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' });
      document.body.appendChild(opacitySlider);

      rememberOpacityCheckbox = document.createElement('label');
      checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.style.marginRight = '4px';
      rememberOpacityCheckbox.appendChild(checkbox);
      rememberOpacityCheckbox.appendChild(document.createTextNode('투명도 기억'));
      Object.assign(rememberOpacityCheckbox.style, { display: 'none', position: 'fixed', zIndex: 2147483647, fontSize: '12px', color: '#fff', background: 'rgba(34,34,34,0.5)', padding: '4px 6px', borderRadius: '4px' });
      document.body.appendChild(rememberOpacityCheckbox);

      applyButtonPosition();
    }

    function openIframe(mode = viewMode) {
      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');

      let topicPath = '';
      let page = '';
      switch (mode) {
        case 'agenda':
          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';
          page = 'agenda';
          break;
        case 'month':
          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';
          page = 'month';
          break;
        case 'streamer':
          topicPath = '/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';
          page = 'agenda';
          break;
        default:
          return;
      }

      iframe.src = `https://dal.wiki${topicPath}/${page}?date=${yyyy}-${mm}-${dd}`;
      centerIframe();
      iframe.style.opacity = currentOpacity;
      opacitySlider.value = currentOpacity.toString();
      opacitySlider.style.display = 'block';
      rememberOpacityCheckbox.style.display = 'block';
      checkbox.checked = !!localStorage.getItem(STORAGE_KEY);
    }

    function bindEvents() {
      calendarBtn.onclick = () => {
        if (iframe.style.display === 'block') {
          iframe.style.display = closeBtn.style.display = opacitySlider.style.display = rememberOpacityCheckbox.style.display = 'none';
          streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
        } else {
          openIframe();
        }
      };
      closeBtn.onclick = () => {
        iframe.style.display = closeBtn.style.display = opacitySlider.style.display = rememberOpacityCheckbox.style.display = 'none';
        streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
      };
      window.addEventListener('resize', () => iframe.style.display === 'block' && centerIframe());
      opacitySlider.oninput = () => {
        currentOpacity = parseFloat(opacitySlider.value);
        iframe.style.opacity = currentOpacity;
        if (checkbox.checked) localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
      };
      checkbox.onchange = () => !checkbox.checked ? localStorage.removeItem(STORAGE_KEY) : localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
      streamerBtn.onclick = () => openIframe('streamer');
      dayBtn.onclick = () => openIframe('agenda');
      monthBtn.onclick = () => openIframe('month');
    }

    function observeBodyStyle() {
      const observer = new MutationObserver(() => {
        const style = window.getComputedStyle(document.body);
        const hidden = style.overflow === 'hidden' && style.position === 'fixed';
        buttonContainer.style.display = hidden ? 'none' : 'flex';
      });
      observer.observe(document.body, { attributes: true, attributeFilter: ['style'] });
    }

    function init() {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (saved) currentOpacity = parseFloat(saved);
      createUI();
      bindEvents();
      observeBodyStyle();
    }

    if (document.body) init();
    else new MutationObserver((obs) => document.body && (obs.disconnect(), init())).observe(document.documentElement, { childList: true });
  }

  // ========================= DAL.WIKI 내 고정 레이아웃 =========================
  if (isDalWiki) {
    const observer = new MutationObserver(() => {
      const aside = document.querySelector('aside.md\\:w-\\[176px\\]');
      if (!aside || aside.dataset.stickyApplied === 'true') return;

      const addButtonAnchor = aside.querySelector('a[href*="/editor"]');
      const scrollWrapper = aside.querySelector('[style*="--radix-scroll-area-corner-width"]');
      const viewport = scrollWrapper?.querySelector('[data-radix-scroll-area-viewport]');
      if (!addButtonAnchor || !scrollWrapper || !viewport) return;

      aside.dataset.stickyApplied = 'true';
      viewport.style.minHeight = viewport.offsetHeight + 'px';
      scrollWrapper.style.maxHeight = '50vh';
      scrollWrapper.style.overflowY = 'auto';

      const stickyContainer = document.createElement('div');
      stickyContainer.style.position = 'sticky';
      stickyContainer.style.top = '0';
      stickyContainer.style.background = 'white';
      stickyContainer.style.zIndex = '50';
      stickyContainer.style.paddingBottom = '12px';

      const config = { attributes: true, childList: false, subtree: false };
      observer.disconnect();

      stickyContainer.appendChild(addButtonAnchor);
      stickyContainer.appendChild(scrollWrapper);
      aside.insertBefore(stickyContainer, aside.firstChild);

      observer.observe(document.body, config);
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }
})();