// ==UserScript==
// @name Zod.kr 편의성 스크립트
// @namespace http://tampermonkey.net/
// @version 1.61
// @description 서명 확장, 단축키 이동, 이미지 작게 보기(돋보기 포함), Shift + 마우스휠로 Swiper 이동, 즐겨찾기 단축키 등 편의 기능 제공
// @match https://zod.kr/*
// @match https://*.zod.kr/*
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// jQuery 로드 대기
function waitForjQuery(callback) {
if (typeof unsafeWindow.jQuery === 'undefined') {
setTimeout(function() { waitForjQuery(callback); }, 100);
} else {
callback(unsafeWindow.jQuery);
}
}
function main($) {
$(document).ready(function() {
// ---------------------------------
// 0. 서명 확장/축소
// ---------------------------------
let signaturesExpanded = false;
let expandButtons = [];
function initSignatureExpand() {
expandButtons = [];
$('.app-article-signature__profile-body').each(function() {
var signature = $(this);
var contentDiv = signature.find('div[style*="max-height:100px"]');
if (contentDiv.length) {
const expandButton = $('<button>서명 확장</button>');
expandButton.css({
position: 'relative',
backgroundColor: '#3F9DFF',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
padding: '5px 10px',
marginTop: '10px',
display: 'block'
});
expandButton.on('click', function() {
signaturesExpanded = !signaturesExpanded;
expandButtons.forEach(function(item) {
if (signaturesExpanded) {
item.contentDiv.css({ 'max-height': '100%', 'height': 'auto', 'overflow': 'visible' });
item.button.text('서명 축소');
} else {
item.contentDiv.css({ 'max-height': '100px', 'height': '', 'overflow': 'hidden' });
item.button.text('서명 확장');
}
});
});
signature.append(expandButton);
expandButtons.push({ button: expandButton, contentDiv: contentDiv });
}
});
}
initSignatureExpand();
// 해시 변경 시 서명 확장 기능 재초기화
window.addEventListener('hashchange', function() {
initSignatureExpand();
});
// ---------------------------------
// 1. 여러 단축키(이동) 및 토글 기능
// ---------------------------------
let isNavigationEnabled = localStorage.getItem('isNavigationEnabled') !== 'false'; // 기본값 true
let isFavNavigationEnabled = localStorage.getItem('isFavNavigationEnabled') !== 'false'; // 기본값 true
let keyDownTimes = {};
const navigationKeys = {
'z': 'https://zod.kr/', // ZOD 메인화면
'a': 'https://zod.kr/all', // 전체글보기
'n': 'https://zod.kr/news_all', // 뉴스 모아보기 게시판
'r': 'https://zod.kr/review', // 리뷰 모아보기 게시판
'b': 'https://zod.kr/benchmark', // 리뷰 > 벤치마크 게시판
'c': 'https://zod.kr/community', // 커뮤니티 모아보기 게시판
'f': 'https://zod.kr/free', // 커뮤니티 > 자유게시판
'g': 'https://zod.kr/game', // 커뮤니티 > 게임게시판
'h': 'https://zod.kr/hardware', // PC하드웨어 모아보기 게시판
'1': 'https://zod.kr/cpu', // PC하드웨어 > CPU / 메인보드 / 램
'2': 'https://zod.kr/gpu', // PC하드웨어 > 그래픽카드
'3': 'https://zod.kr/case', // PC하드웨어 > 케이스 / 쿨링
'4': 'https://zod.kr/ssd', // PC하드웨어 > 저장장치
'5': 'https://zod.kr/psu', // PC하드웨어 > 파워서플라이
'6': 'https://zod.kr/display', // PC하드웨어 > 디스플레이
'7': 'https://zod.kr/keyma', // PC하드웨어 > 키보드 / 마우스
'8': 'https://zod.kr/audio', // PC하드웨어 > 오디오
'9': 'https://zod.kr/general', // PC하드웨어 > PC 일반
'0': 'https://zod.kr/pcbuild', // PC하드웨어 > 조립 / 견적
'm': 'https://zod.kr/device', // 모바일 모아보기 게시판
't': 'https://zod.kr/all_tips', // 정보 모아보기 게시판
'u': 'https://zod.kr/user_review', // 정보 > 유저리뷰 게시판
'd': 'https://zod.kr/deal', // 특가 모아보기 게시판
'q': 'https://zod.kr/qna', // 문의/버그신고 게시판
'`': 'https://zod.kr/member/notifications', // 내 알림 목록 보기
'x': 'https://zod.kr/notice' // 공지사항 게시판
};
// 즐겨찾기 항목 가져오기
function getFavoriteLinks() {
const favItems = document.querySelectorAll('#zod-user-fav ul.app-custom-scroll-horizon li[data-mid] a');
return Array.from(favItems).map(item => item.href);
}
// 단축키 토글 버튼 추가
const dropdownMenu = document.querySelector('.app-dropdown-menu.app-right');
if (dropdownMenu) {
const ul = dropdownMenu.querySelector('ul.app-dropdown-menu-list');
if (ul) {
// 이미지 작게 보기 토글
const imageLi = document.createElement('li');
imageLi.className = 'tw-flex tw-p-4 tw-items-center';
imageLi.innerHTML = `
<p class="tw-text-sm">이미지 작게 보기</p>
<div class="tw-flex-1"></div>
<button class="app-button app-button-xs tw-p-3" id="toggle-image-size">${localStorage.getItem('isImageSmall') === 'true' ? 'ON' : 'OFF'}</button>
`;
ul.appendChild(imageLi);
const imageToggleButton = document.getElementById('toggle-image-size');
if (imageToggleButton) {
if (localStorage.getItem('isImageSmall') === 'true') imageToggleButton.style.backgroundColor = '#3f9dff';
imageToggleButton.addEventListener('click', toggleImageSize);
}
// 사이트 이동 단축키 토글
const navLi = document.createElement('li');
navLi.className = 'tw-flex tw-p-4 tw-items-center';
navLi.innerHTML = `
<p class="tw-text-sm">사이트 이동 단축키</p>
<div class="tw-flex-1"></div>
<button class="app-button app-button-xs tw-p-3" id="toggle-navigation">${isNavigationEnabled ? 'ON' : 'OFF'}</button>
`;
ul.appendChild(navLi);
const navToggleButton = document.getElementById('toggle-navigation');
if (navToggleButton) {
if (isNavigationEnabled) navToggleButton.style.backgroundColor = '#3f9dff';
navToggleButton.addEventListener('click', function() {
isNavigationEnabled = !isNavigationEnabled;
localStorage.setItem('isNavigationEnabled', isNavigationEnabled);
navToggleButton.textContent = isNavigationEnabled ? 'ON' : 'OFF';
navToggleButton.style.backgroundColor = isNavigationEnabled ? '#3f9dff' : '';
});
}
// 즐겨찾기 단축키 사용 토글
const favLi = document.createElement('li');
favLi.className = 'tw-flex tw-p-4 tw-items-center';
favLi.innerHTML = `
<p class="tw-text-sm">즐겨찾기 단축키</p>
<div class="tw-flex-1"></div>
<button class="app-button app-button-xs tw-p-3" id="toggle-fav-navigation">${isFavNavigationEnabled ? 'ON' : 'OFF'}</button>
`;
ul.appendChild(favLi);
const favToggleButton = document.getElementById('toggle-fav-navigation');
if (favToggleButton) {
if (isFavNavigationEnabled) favToggleButton.style.backgroundColor = '#3f9dff';
favToggleButton.addEventListener('click', function() {
isFavNavigationEnabled = !isFavNavigationEnabled;
localStorage.setItem('isFavNavigationEnabled', isFavNavigationEnabled);
favToggleButton.textContent = isFavNavigationEnabled ? 'ON' : 'OFF';
favToggleButton.style.backgroundColor = isFavNavigationEnabled ? '#3f9dff' : '';
});
}
// 즐겨찾기 단축키 설명
const favDescLi = document.createElement('li');
favDescLi.className = 'tw-p-4';
favDescLi.innerHTML = `<p style="font-size: 9px; color: #666;">즐겨찾기 단축키 적용 시, 1 ~ 0 단축키는 즐겨찾기로 대체됩니다.</p>`;
ul.appendChild(favDescLi);
}
}
// ---------------------------------
// 2. 검색창 관련
// ---------------------------------
const mobileSearchBtn = document.querySelector('.app-board-container--only-mobile .app-icon-button');
const MOBILE_SEARCH_INPUT_SELECTOR = 'input[name="search_keyword"].app-input.app-input-expand';
const overlaySearchToggleBtn = document.querySelector('a.app-header-item.app-icon-button.app-icon-button-gray.app-search-toggle');
const OVERLAY_SEARCH_INPUT_SELECTOR = 'input.app-search-form__input[name="search_keyword"]';
// 검색창이 열릴 때 ESC로 닫히도록 설정
function setupSearchCloseOnEsc() {
const appSearch = document.querySelector('#app-search');
if (appSearch && appSearch.classList.contains('app-search--active')) {
$(document).on('keydown.searchClose', function(e) {
if (e.key === 'Escape') {
const closeButton = appSearch.querySelector('.app-search__close');
if (closeButton) closeButton.click();
}
});
} else {
$(document).off('keydown.searchClose');
}
}
// 검색창 토글 시 ESC 이벤트 설정
if (overlaySearchToggleBtn) {
overlaySearchToggleBtn.addEventListener('click', function() {
setTimeout(setupSearchCloseOnEsc, 100);
});
}
// 페이지 로드 시 검색창이 열려 있으면 ESC 설정
setupSearchCloseOnEsc();
// ---------------------------------
// 3. keydown 핸들러
// ---------------------------------
$(document).on('keydown', function(e) {
const key = e.key.toLowerCase();
const isInputFocused = $(':focus').is('input, textarea, [contenteditable="true"]'); // 포커스 상태 미리 확인
// --- Alt+S 처리 (이 단축키는 입력 중에도 필요할 수 있으므로 먼저 처리) ---
if (key === 's' && e.altKey) {
e.preventDefault();
if (overlaySearchToggleBtn) {
overlaySearchToggleBtn.click();
setTimeout(() => {
const overlaySearchInput = document.querySelector(OVERLAY_SEARCH_INPUT_SELECTOR);
if (overlaySearchInput) {
overlaySearchInput.value = '';
overlaySearchInput.focus();
}
}, 100);
}
return; // Alt+S 처리 후 종료
}
// --- ✨ 핵심 수정: 입력 필드에 포커스가 있다면 Escape키만 처리하고 즉시 종료 ---
if (isInputFocused) {
if (e.key === 'Escape') {
const appSearch = document.querySelector('#app-search');
const closeButton = appSearch?.querySelector('.app-search__close');
const closeButtonSmall = document.querySelector('.app-dialog-close');
if (appSearch && appSearch.classList.contains('app-search--active') && closeButton) {
closeButton.click();
}
if (closeButtonSmall && closeButtonSmall.offsetHeight > 0) {
closeButtonSmall.click();
}
}
return; // 입력 중이면 다른 단축키 실행 방지
}
// --- 입력 상태가 아닐 때만 실행될 단축키들 ---
if (!e.altKey && !e.ctrlKey) { // Alt, Ctrl 조합이 아닌 경우
if (key === 'e') { // 'e' 키 처리 (서명 확장/축소)
signaturesExpanded = !signaturesExpanded;
expandButtons.forEach(function(item) {
if (signaturesExpanded) {
item.contentDiv.css({ 'max-height': '100%', 'height': 'auto', 'overflow': 'visible' });
item.button.text('서명 축소');
} else {
item.contentDiv.css({ 'max-height': '100px', 'height': '', 'overflow': 'hidden' });
item.button.text('서명 확장');
}
});
} else if (key === 's') { // 's' 키 처리 (모바일 검색)
if (mobileSearchBtn) {
mobileSearchBtn.click();
setTimeout(() => {
const mobileSearchInput = document.querySelector(MOBILE_SEARCH_INPUT_SELECTOR);
if (mobileSearchInput) {
mobileSearchInput.value = '';
mobileSearchInput.focus();
}
}, 100);
}
} else if (key === '\\') { // '\' 키 처리 (설정 메뉴 토글)
e.preventDefault();
const configToggle = document.querySelector('.app-dropdown.zod-app--header-config .app-dropdown-toggle');
if (configToggle) {
configToggle.click();
}
} else { // 네비게이션 단축키 처리 (a, b, c, ..., 1, 2, 3, ...)
const favLinks = getFavoriteLinks();
const isNumberKey = /^[0-9]$/.test(key); // 숫자 0-9 확인
if (isNumberKey && isFavNavigationEnabled && favLinks.length > 0) {
const index = key === '0' ? 9 : parseInt(key) - 1;
if (index < favLinks.length) {
window.location.href = favLinks[index];
}
} else if (isNavigationEnabled && navigationKeys.hasOwnProperty(key)) {
keyDownTimes[key] = Date.now();
}
}
}
});
// ---------------------------------
// 4. keyup 핸들러
// ---------------------------------
$(document).on('keyup', function(e) {
if ($(':focus').is('input, textarea, [contenteditable="true"]') || e.altKey || e.ctrlKey) return;
const key = e.key.toLowerCase();
if (isNavigationEnabled && navigationKeys.hasOwnProperty(key) && keyDownTimes[key]) {
let duration = Date.now() - keyDownTimes[key];
if (duration >= 80) window.location.href = navigationKeys[key];
delete keyDownTimes[key];
}
});
// ---------------------------------
// 5. Alt+Enter, Ctrl+Enter, Alt+Ctrl+Enter => 등록 / 추천+등록
// ---------------------------------
function addAltEnterFeature() {
function addAltEnterListener(textarea) {
if (textarea.dataset.altEnterListenerAdded === 'true') return;
textarea.dataset.altEnterListenerAdded = 'true';
textarea.addEventListener('keydown', function(event) {
if ((event.key === 'Enter' || event.keyCode === 13) && (event.altKey || event.ctrlKey)) {
event.preventDefault();
var form = textarea.closest('form');
if (form) {
var submitButtons = form.querySelectorAll('button[type="submit"]');
var targetButton = null;
if (event.altKey && event.ctrlKey) {
submitButtons.forEach(function(button) {
if (button.textContent.trim() === '추천+등록') targetButton = button;
});
} else {
submitButtons.forEach(function(button) {
if (button.textContent.trim() === '등록') targetButton = button;
});
}
if (targetButton) {
targetButton.click();
setTimeout(() => {
textarea.blur();
document.activeElement.blur();
}, 100);
}
}
}
});
}
var textareas = document.querySelectorAll('textarea.app-textarea');
textareas.forEach(addAltEnterListener);
var altEnterObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
if (node.matches('textarea.app-textarea')) {
addAltEnterListener(node);
} else {
node.querySelectorAll('textarea.app-textarea').forEach(addAltEnterListener);
}
}
});
});
});
altEnterObserver.observe(document.body, { childList: true, subtree: true });
}
addAltEnterFeature();
// ---------------------------------
// 6. 이미지 작게 보기 기능 추가 (돋보기 버튼 포함)
// ---------------------------------
const styleTag = document.createElement('style');
styleTag.textContent = `
.small-images .rhymix_content img:not(.original-size):not(.zod-link-preview img):not(.zod-sticker--item img) { max-width: 50%; }
.original-size { max-width: 100% !important; }
.magnifier-button {
position: absolute;
z-index: 10;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
borderRadius: 3px;
padding: 2px 6px;
cursor: pointer;
}
`;
document.head.appendChild(styleTag);
let isImageSmall = localStorage.getItem('isImageSmall') === 'true';
if (isImageSmall) document.body.classList.add('small-images');
function createMagnifierButton(image) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'magnifier-button';
button.innerHTML = '🔍';
button.setAttribute('aria-label', 'View actual size');
button.addEventListener('click', () => {
image.classList.toggle('original-size');
updateButtonPosition(image, button);
});
return button;
}
function updateButtonPosition(image, button) {
const imageRect = image.getBoundingClientRect();
const parentRect = image.parentElement.getBoundingClientRect();
const isOriginalSize = image.classList.contains('original-size');
button.style.top = `${imageRect.bottom - parentRect.top - button.offsetHeight}px`;
button.style.left = `${imageRect.right - parentRect.left - (isOriginalSize ? button.offsetWidth : 0)}px`;
}
function addMagnifierButtonToImage(image) {
if (image.naturalWidth <= 360 || image.parentElement.querySelector('.magnifier-button')) return;
const button = createMagnifierButton(image);
image.parentElement.style.position = 'relative';
image.parentElement.appendChild(button);
updateButtonPosition(image, button);
new ResizeObserver(() => updateButtonPosition(image, button)).observe(image);
}
function addMagnifierButtons() {
const images = document.querySelectorAll('.rhymix_content img:not(.zod-link-preview img):not(.zod-sticker--item img)');
images.forEach(image => {
if (image.complete) {
addMagnifierButtonToImage(image);
} else {
image.addEventListener('load', () => addMagnifierButtonToImage(image), { once: true });
}
});
}
function removeMagnifierButtons() {
document.querySelectorAll('.magnifier-button').forEach(button => button.remove());
}
function toggleImageSize() {
isImageSmall = !isImageSmall;
localStorage.setItem('isImageSmall', isImageSmall);
const button = document.getElementById('toggle-image-size');
if (isImageSmall) {
document.body.classList.add('small-images');
addMagnifierButtons();
if (button) {
button.textContent = 'ON';
button.style.backgroundColor = '#3f9dff';
}
} else {
document.body.classList.remove('small-images');
removeMagnifierButtons();
if (button) {
button.textContent = 'OFF';
button.style.backgroundColor = '';
}
}
}
if (isImageSmall) {
document.addEventListener('DOMContentLoaded', addMagnifierButtons);
window.addEventListener('load', addMagnifierButtons);
addMagnifierButtons();
}
const imageObserver = new MutationObserver(() => {
if (isImageSmall) addMagnifierButtons();
});
imageObserver.observe(document.body, { childList: true, subtree: true });
// ---------------------------------
// 7. Shift + 마우스휠로 Swiper 페이지 이동
// ---------------------------------
function initSwiperShiftScroll() {
// 대상 Swiper 컨테이너들을 모두 선택
const swiperContainers = document.querySelectorAll('.zod-widgets--review, #zod-recent-popular-main.swiper');
swiperContainers.forEach((swiperContainer) => {
// 각 컨테이너 내부의 페이지네이션 요소를 찾음
// '.zod-widgets--review' 안에는 '.swiper-pagination'
// '#zod-recent-popular-main' 안에는 '.pagination.zod-swiper-pagination'
const paginationEl = swiperContainer.matches('.zod-widgets--review')
? swiperContainer.querySelector('.swiper-pagination')
: swiperContainer.querySelector('.pagination.zod-swiper-pagination');
// 페이지네이션 요소가 없거나 이미 초기화된 경우 건너뜀
if (!paginationEl || paginationEl.getAttribute('data-shift-scroll-initialized') === 'true') return;
// Swiper 컨테이너에 'wheel' 이벤트 리스너 추가
swiperContainer.addEventListener('wheel', function(event) {
if (event.shiftKey) {
event.preventDefault();
const bullets = paginationEl.querySelectorAll('.swiper-pagination-bullet');
if (!bullets || bullets.length === 0) return;
let activeIndex = -1;
for (let i = 0; i < bullets.length; i++) {
if (bullets[i].classList.contains('swiper-pagination-bullet-active') || bullets[i].getAttribute('aria-current') === 'true') {
activeIndex = i;
break;
}
}
if (activeIndex === -1) return;
if (event.deltaY > 0) { // 아래로 스크롤 (다음)
if (activeIndex < bullets.length - 1) bullets[activeIndex + 1].click();
} else { // 위로 스크롤 (이전)
if (activeIndex > 0) bullets[activeIndex - 1].click();
}
}
}, { passive: false });
paginationEl.setAttribute('data-shift-scroll-initialized', 'true');
//console.log('Shift+Scroll initialized for swiper:', swiperContainer.className || swiperContainer.id);
});
}
initSwiperShiftScroll();
const swiperObserver = new MutationObserver(() => initSwiperShiftScroll());
swiperObserver.observe(document.body, { childList: true, subtree: true });
// 즐겨찾기 앞쪽 별표 제거
const favLabel = document.querySelector('#zod-user-fav li.fav-label');
if (favLabel) {
favLabel.remove();
}
});
}
waitForjQuery(function($) { main($); });
})();