您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
유튜브 댓글의 다양한 날짜 형식을 "YYYY년 MM월 DD일 오전/오후 HH:MM"으로 변환됩니다. 오래된 댓글의 경우, 변환되는 날짜와 시간은 유튜브 정보의 한계로 인해 실제와 다를 수 있습니다.
// ==UserScript== // @name YouTube 댓글 날짜 변환기(컴퓨터 로컬 타임 기반) // @namespace YouTube 댓글 날짜 변환기(컴퓨터 로컬 타임 기반) // @version 0.1 // @description 유튜브 댓글의 다양한 날짜 형식을 "YYYY년 MM월 DD일 오전/오후 HH:MM"으로 변환됩니다. 오래된 댓글의 경우, 변환되는 날짜와 시간은 유튜브 정보의 한계로 인해 실제와 다를 수 있습니다. // @match *://*.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com // @author mickey90427 <[email protected]> // @license MIT // ==/UserScript== (function() { 'use strict'; // #region 보조 함수 (날짜 변환) /** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 주의: 이 스크립트에서 변환되는 모든 날짜는 당신의 로컬 시간(컴퓨터/브라우저에 설정된 시간)을 기준으로 합니다. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 주어진 날짜 문자열 (상대적 또는 절대적)을 목표하는 절대 날짜 형식으로 변환합니다. * @param {string} dateString 유튜브에서 가져온 날짜 문자열 (예: "1개월 전", "어제", "2024년 5월 19일"). * @returns {string|null} 형식화된 절대 날짜 문자열 (예: "2025년 06월 19일 오후 04:52") 또는 변환 실패 시 null. * * 중요: 유튜브의 "X개월 전", "Y년 전"과 같은 상대적 날짜는 정확한 시작 시점을 알 수 없습니다. * 따라서 이 함수는 스크립트가 실행되는 '현재 시점'을 기준으로 역산하여 날짜를 '추정'합니다. * 특히 1일 이상의 상대적 날짜(예: 1주 전, 1개월 전, 1년 전)는 실제 댓글 작성 날짜와 반드시 다를 수밖에 없습니다. * 이는 유튜브가 제공하는 정보 자체가 정밀하지 않기 때문이며, 스크립트의 한계가 아닙니다. */ function convertToTargetDateFormat(dateString) { const now = new Date(); // 스크립트 실행 시점의 현재 날짜와 시간 let targetDate = new Date(now.getTime()); // 상대적 날짜 변환의 기준이 될 날짜 (기본값: 현재 시간) const trimmedText = dateString.trim(); // 앞뒤 공백 제거 // 1. 상대적 날짜 처리 (예: "방금 전", "오늘", "어제", "1개월 전") if (trimmedText === '방금 전') { // '방금 전'은 현재 시점이므로 targetDate를 조정할 필요 없음 } else if (trimmedText === '오늘') { // '오늘'도 현재 시점이므로 targetDate를 조정할 필요 없음 (시간 정보는 현재 시간 유지) } else if (trimmedText === '어제') { targetDate.setDate(targetDate.getDate() - 1); // 현재 날짜에서 하루 전으로 설정 } else { // '숫자 단위 전' 형식 (예: "1개월 전", "2일 전") const relativeMatch = trimmedText.match(/^(\d+)\s*(년|개월|주|일|시간|분|초)\s*전$/); if (relativeMatch) { const value = parseInt(relativeMatch[1]); // 숫자 값 (예: 1, 2) const unit = relativeMatch[2]; // 단위 (예: '년', '개월') switch (unit) { case '년': targetDate.setFullYear(targetDate.getFullYear() - value); break; case '개월': targetDate.setMonth(targetDate.getMonth() - value); break; case '주': targetDate.setDate(targetDate.getDate() - (value * 7)); // 1주는 7일 break; case '일': targetDate.setDate(targetDate.getDate() - value); break; case '시간': targetDate.setHours(targetDate.getHours() - value); break; case '분': targetDate.setMinutes(targetDate.getMinutes() - value); break; case '초': targetDate.setSeconds(targetDate.getSeconds() - value); break; default: // 알 수 없는 상대적 시간 단위인 경우 변환하지 않고 null 반환 return null; } } else { // 2. 이미 존재하는 절대 날짜 처리 (예: "2024년 5월 19일", "2023. 12. 31.") let parsedDate = null; // 다양한 절대 날짜 형식 파싱 시도 let absMatch = trimmedText.match(/(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일/); // "YYYY년 MM월 DD일" if (!absMatch) { absMatch = trimmedText.match(/(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\./); // "YYYY. MM. DD." } if (!absMatch) { // YYYY-MM-DD absMatch = trimmedText.match(/(\d{4})-(\d{1,2})-(\d{1,2})/); // "YYYY-MM-DD" } if (absMatch) { const year = parseInt(absMatch[1]); const month = parseInt(absMatch[2]) - 1; // JavaScript의 월은 0부터 시작 (0:1월 ~ 11:12월) const day = parseInt(absMatch[3]); // 절대 날짜에 시간 정보가 없는 경우, 현재 시간으로 기본 설정 parsedDate = new Date(year, month, day, now.getHours(), now.getMinutes(), now.getSeconds()); } else { // 위의 정규식에 일치하지 않으면 표준 JavaScript Date 파서로 시도 (예: "May 19, 2024" 같은 영어 형식) try { const tempDate = new Date(trimmedText); if (!isNaN(tempDate.getTime())) { // 유효한 날짜인지 확인 parsedDate = tempDate; } } catch (e) { // 파싱 오류는 무시 } } if (parsedDate) { targetDate = parsedDate; } else { // 상대적 날짜도 아니고 인식할 수 있는 절대 날짜 형식도 아닌 경우, null 반환 return null; } } } // 최종 목표 형식: "YYYY년 MM월 DD일 오전/오후 HH:MM"으로 포맷 const year = targetDate.getFullYear(); const month = String(targetDate.getMonth() + 1).padStart(2, '0'); // 1월부터 시작하도록 +1, 두 자리로 채움 const day = String(targetDate.getDate()).padStart(2, '0'); // 두 자리로 채움 let hours = targetDate.getHours(); const minutes = String(targetDate.getMinutes()).padStart(2, '0'); // 두 자리로 채움 const ampm = hours >= 12 ? '오후' : '오전'; // 오전/오후 결정 hours = hours % 12; // 24시간제를 12시간제로 변환 hours = hours ? hours : 12; // 0시는 12시로 표시 const finalHours = String(hours); // 한 자리 숫자에는 앞에 0을 붙이지 않음 (예: 오전 9시) return `${year}년 ${month}월 ${day}일 ${ampm} ${finalHours}:${minutes}`; } // #endregion 보조 함수 // #region 핵심 처리 로직 // 목표하는 날짜 형식 (예: "2025년 06월 19일 오후 4:52")을 판별하는 정규식 // 이 정규식에 맞는 형식은 변환을 건너뛰도록 합니다. const TARGET_FORMAT_REGEX = /^\d{4}년\s*\d{2}월\s*\d{2}일\s*(오전|오후)\s*\d{1,2}:\d{2}$/; /** * 하나의 댓글 날짜 요소를 처리합니다. * @param {HTMLElement} element 날짜 텍스트를 포함하는 <a> 요소. */ function processSingleDateElement(element) { const originalText = element.textContent.trim(); // 요소의 원본 텍스트 가져오기 // 1. 이 스크립트에 의해 이미 변환된 요소인지 확인합니다. // MutationObserver의 무한 루프를 방지합니다. if (element.dataset.convertedDate === originalText) { return; // 이미 변환된 것이면 처리 건너뛰기 } // 2. 현재 텍스트가 이미 우리가 원하는 "YYYY년 MM월 DD일 오전/오후 HH:MM" 형식인지 확인합니다. if (TARGET_FORMAT_REGEX.test(originalText)) { // 이미 목표 형식이라면, 변환된 것으로 표시하고 건너뜁니다. element.dataset.convertedDate = originalText; return; } // 3. 아직 변환되지 않았고, 목표 형식도 아니라면 변환을 시도합니다. const newDateText = convertToTargetDateFormat(originalText); // 새로운 날짜 텍스트가 유효하고, 원본 텍스트와 다를 경우에만 업데이트합니다. if (newDateText && element.textContent !== newDateText) { element.textContent = newDateText; // 요소의 텍스트를 새 날짜로 변경 element.dataset.convertedDate = newDateText; // 변환되었음을 표시 // console.log(`[YouTube 날짜 변환기] 변환 완료: "${originalText}" -> "${newDateText}"`); // 디버깅용 로그 } } /** * 주어진 컨테이너 내의 모든 날짜 요소를 찾아 처리합니다. * @param {HTMLElement} container 검색할 DOM 요소 (예: document.body 또는 새로 추가된 댓글 스레드). */ function processAllDateElementsInContainer(container) { // 댓글 날짜 텍스트를 포함하는 <a> 태그를 모두 선택합니다. // span#published-time-text 아래의 yt-simple-endpoint 클래스를 가진 <a> 태그를 찾습니다. const dateElements = container.querySelectorAll('span#published-time-text > a.yt-simple-endpoint'); // console.log(`[YouTube 날짜 변환기] 컨테이너에서 ${dateElements.length}개의 날짜 요소 발견.`); // 디버깅용 로그 dateElements.forEach(processSingleDateElement); // 각 날짜 요소를 개별적으로 처리 } // #endregion 핵심 처리 로직 // #region 실시간 관찰 (옵저버) // DOM 변화를 감지하여 동적으로 로드되는 콘텐츠를 처리하기 위한 메인 옵저버 const commentObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // 자식 노드 목록에 변화가 있고, 추가된 노드가 있는 경우 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { // 추가된 노드가 HTML 요소인 경우에만 처리 if (node.nodeType === Node.ELEMENT_NODE) { // 추가된 노드 자체가 댓글 스레드이거나 댓글 뷰 모델이거나 댓글 섹션이거나, // 또는 그 안에 댓글 스레드나 댓글 뷰 모델이 포함되어 있는 경우 if (node.matches('ytd-comment-thread-renderer, ytd-comment-view-model, ytd-comments#comments') || node.querySelector('ytd-comment-thread-renderer, ytd-comment-view-model')) { // 해당 컨테이너 내의 모든 날짜 요소를 처리 processAllDateElementsInContainer(node); } } }); } }); }); // body 요소의 변화를 관찰하기 시작합니다. // childList: 자식 노드의 추가/제거 감지 // subtree: 자식 노드뿐만 아니라 모든 하위 트리의 변화 감지 (매우 중요) commentObserver.observe(document.body, { childList: true, subtree: true }); console.log('[YouTube 날짜 변환기] 실시간 옵저버 시작.'); // 페이지 로드 시 이미 존재하는 댓글을 초기 변환하기 위한 설정 // 'load' 이벤트: 페이지의 모든 리소스(이미지 등)가 로드된 후 window.addEventListener('load', () => { console.log('[YouTube 날짜 변환기] window.load 시점 초기 스캔.'); processAllDateElementsInContainer(document.body); }); // 'DOMContentLoaded' 이벤트: HTML 문서가 완전히 로드되고 파싱된 후 (리소스 로드 전) document.addEventListener('DOMContentLoaded', () => { console.log('[YouTube 날짜 변환기] DOMContentLoaded 시점 초기 스캔.'); processAllDateElementsInContainer(document.body); }); // #endregion 실시간 관찰 })();