dcinside shortcut

디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.

2025-02-28 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.0.5
// @description  디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
//               - 글 목록에 번호 추가 (1~100번까지 표시)
//               - 숫자 키 (1~9, 0)로 해당 번호의 글로 이동 (0은 10번 글)
//               - ` or . + 숫자 입력 + ` or .으로 특정 번호의 글로 이동 (1~100번)
//               - ALT + 숫자 (1~9, 0): 즐겨찾는 갤러리 등록/이동
//               - ALT + `: 즐겨찾는 갤러리 목록 표시/숨기기
//               - W: 글쓰기 페이지로 이동
//               - C: 댓글 입력창으로 커서 이동
//               - D: 댓글 새로고침 및 스크롤
//               - R: 페이지 새로고침
//               - Q: 페이지 최상단으로 스크롤
//               - E: 글 목록으로 스크롤
//               - F: 전체글 보기로 이동
//               - G: 개념글 보기로 이동
//               - A: 이전 페이지로 이동
//               - S: 다음 페이지로 이동
//               - Z: 이전 글로 이동
//               - X: 다음 글로 이동
// @author       노노하꼬
// @match        *://gall.dcinside.com/*
// @match        *://www.dcinside.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @supportURL   https://gallog.dcinside.com/nonohako/guestbook
// ==/UserScript==

(function() {
    'use strict';

    // 즐겨찾는 갤러리 저장 키
    const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';

    // Tampermonkey API 사용 가능 여부 확인
    const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';

    // 즐겨찾는 갤러리 목록 가져오기
    async function getFavoriteGalleries() {
        let favorites = {};

        if (isTampermonkey) {
            // Tampermonkey API 사용
            favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
        } else {
            try {
                // localStorage에서 확인
                const data = localStorage.getItem(FAVORITE_GALLERIES_KEY);
                if (data) {
                    favorites = JSON.parse(data);
                } else {
                    // 쿠키에서 확인
                    const cookieFavorites = document.cookie.split('; ').find(row => row.startsWith(FAVORITE_GALLERIES_KEY));
                    if (cookieFavorites) {
                        favorites = JSON.parse(decodeURIComponent(cookieFavorites.split('=')[1]));
                    }
                }
            } catch (error) {
                console.error('즐겨찾기 데이터를 가져오는데 실패했습니다:', error);
            }
        }

        return favorites;
    }

    // 즐겨찾는 갤러리 목록 저장
    function saveFavoriteGalleries(favorites) {
        try {
            if (isTampermonkey) {
                // Tampermonkey API 사용
                GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
            } else {
                // localStorage에 저장
                localStorage.setItem(FAVORITE_GALLERIES_KEY, JSON.stringify(favorites));

                // 쿠키에도 저장 (도메인 간 공유를 위해)
                const expirationDate = new Date();
                expirationDate.setFullYear(expirationDate.getFullYear() + 1);
                document.cookie = `${FAVORITE_GALLERIES_KEY}=${encodeURIComponent(JSON.stringify(favorites))}; expires=${expirationDate.toUTCString()}; path=/; domain=.dcinside.com`;
            }
        } catch (error) {
            console.error('즐겨찾기 데이터를 저장하는데 실패했습니다:', error);
            alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
        }
    }

    // 즐겨찾는 갤러리 목록 UI 표시
    async function showFavoriteGalleries() {
        const favorites = await getFavoriteGalleries();
        const container = document.createElement('div');
        container.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #ffffff;
            padding: 20px;
            border-radius: 16px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            z-index: 10000;
            width: 360px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: 'Roboto', sans-serif;
            border: 1px solid #e0e0e0;
            transition: opacity 0.2s ease-in-out;
            opacity: 0;
        `;
        setTimeout(() => container.style.opacity = '1', 10); // 페이드인 효과

        // Google Roboto 폰트 로드
        if (!document.querySelector('link[href*="Roboto"]')) {
            const fontLink = document.createElement('link');
            fontLink.rel = 'stylesheet';
            fontLink.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap';
            document.head.appendChild(fontLink);
        }

        const title = document.createElement('h3');
        title.textContent = '즐겨찾는 갤러리';
        title.style.cssText = `
            font-size: 18px;
            font-weight: 700;
            color: #212121;
            margin: 0 0 15px 0;
            padding-bottom: 10px;
            border-bottom: 1px solid #e0e0e0;
        `;
        container.appendChild(title);

        const list = document.createElement('ul');
        list.style.cssText = `
            list-style: none;
            margin: 0;
            padding: 0;
            max-height: 50vh;
            overflow-y: auto;
        `;

        // Function to update the favorites list UI
        window.updateFavoritesList = async function() {
            list.innerHTML = '';
            const favorites = await getFavoriteGalleries();
            Object.entries(favorites).forEach(([key, gallery]) => {
                const item = document.createElement('li');
                item.style.cssText = `
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    padding: 12px 15px;
                    margin: 5px 0;
                    background-color: #fafafa;
                    border-radius: 10px;
                    transition: background-color 0.2s ease;
                    cursor: pointer;
                `;
                item.onmouseenter = () => {
                    item.style.backgroundColor = '#f0f0f0';
                };
                item.onmouseleave = () => {
                    item.style.backgroundColor = '#fafafa';
                };

                const galleryName = gallery.name || gallery.galleryId || 'Unknown Gallery';
                const nameSpan = document.createElement('span');
                nameSpan.textContent = `${key}: ${galleryName}`;
                nameSpan.style.cssText = `
                    font-size: 15px;
                    font-weight: 400;
                    color: #424242;
                    flex-grow: 1;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                `;
                item.appendChild(nameSpan);

                const removeButton = document.createElement('button');
                removeButton.textContent = '✕';
                removeButton.style.cssText = `
                    background-color: transparent;
                    color: #757575;
                    border: none;
                    border-radius: 50%;
                    width: 24px;
                    height: 24px;
                    font-size: 16px;
                    line-height: 1;
                    cursor: pointer;
                    transition: color 0.2s ease, background-color 0.2s ease;
                `;
                removeButton.onmouseenter = () => {
                    removeButton.style.color = '#d32f2f';
                    removeButton.style.backgroundColor = '#ffebee';
                };
                removeButton.onmouseleave = () => {
                    removeButton.style.color = '#757575';
                    removeButton.style.backgroundColor = 'transparent';
                };
                removeButton.onclick = async (e) => {
                    e.stopPropagation();
                    const currentFavorites = await getFavoriteGalleries();
                    delete currentFavorites[key];
                    saveFavoriteGalleries(currentFavorites);
                    // Update the list after removal
                    updateFavoritesList();
                };
                item.appendChild(removeButton);

                item.onclick = () => {
                    const { galleryType, galleryId } = favorites[key];
                    const url = galleryType === 'board' ?
                          `https://gall.dcinside.com/board/lists?id=${galleryId}` :
                        `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
                    window.location.href = url;
                };
                list.appendChild(item);
            });
        }

        // Initial population of favorites list
        await updateFavoritesList();
        container.appendChild(list);

        // 즐겨찾기 추가 UI 추가
        const addContainer = document.createElement('div');
        addContainer.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            margin: 15px 0;
            padding: 15px;
            background-color: #f5f5f5;
            border-radius: 10px;
        `;
        const numInput = document.createElement('input');
        numInput.type = 'text';
        numInput.placeholder = '0-9';
        numInput.style.cssText = `
            width: 45px;
            padding: 8px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            text-align: center;
            outline: none;
            transition: border-color 0.2s ease;
            background-color: #ffffff;
        `;
        numInput.onfocus = () => {
            numInput.style.borderColor = '#1976d2';
        };
        numInput.onblur = () => {
            numInput.style.borderColor = '#e0e0e0';
        };
        const addButton = document.createElement('button');
        addButton.textContent = '즐겨찾기 추가';
        addButton.style.cssText = `
            padding: 8px 16px;
            background-color: #1976d2;
            color: #ffffff;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.2s ease;
            flex-grow: 1;
        `;
        addButton.onmouseenter = () => {
            addButton.style.backgroundColor = '#1565c0';
        };
        addButton.onmouseleave = () => {
            addButton.style.backgroundColor = '#1976d2';
        };
        addButton.onclick = function(e) {
            e.stopPropagation();
            const digit = numInput.value.trim();
            if (!/^[0-9]$/.test(digit)) {
                alert('0부터 9까지의 숫자를 입력해주세요.');
                return;
            }
            handleAltNumberKey(digit);
            numInput.value = '';
            // 갱신된 즐겨찾기 목록 반영
            updateFavoritesList();
        };
        addContainer.appendChild(numInput);
        addContainer.appendChild(addButton);
        container.appendChild(addContainer);

        const closeButton = document.createElement('button');
        closeButton.textContent = '닫기';
        closeButton.style.cssText = `
            display: block;
            width: 100%;
            padding: 10px;
            margin-top: 15px;
            background-color: #1976d2;
            color: #ffffff;
            border: none;
            border-radius: 10px;
            font-size: 15px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.2s ease;
        `;
        closeButton.onmouseenter = () => closeButton.style.backgroundColor = '#1565c0';
        closeButton.onmouseleave = () => closeButton.style.backgroundColor = '#1976d2';
        closeButton.onclick = () => {
            container.style.opacity = '0';
            setTimeout(() => document.body.removeChild(container), 200); // 페이드아웃 후 제거
        };
        container.appendChild(closeButton);

        document.body.appendChild(container);
    }

    // 현재 페이지가 갤러리 메인 페이지인지 확인
    function isGalleryMainPage() {
        return window.location.href.includes('/lists') && window.location.href.includes('id=');
    }

    // 현재 갤러리 정보 가져오기
    function getCurrentGalleryInfo() {
        const url = window.location.href;
        if (!isGalleryMainPage()) {
            return { galleryType: '', galleryId: '', galleryName: '' };
        }

        const galleryType = url.includes('/person/') ? 'person' :
        url.includes('mgallery') ? 'mgallery' :
        url.includes('mini') ? 'mini' : 'board';
        const galleryId = (url.match(/id=([^&]+)/) || [])[1] || '';

        // 갤러리 이름 추출
        const galleryNameElement = document.querySelector('div.fl.clear h2 a');
        let galleryName = galleryId; // 기본값으로 galleryId 사용

        if (galleryNameElement) {
            // <div class="pagehead_titicon ngall sp_img"> 같은 요소를 제외하고 텍스트만 추출
            galleryName = Array.from(galleryNameElement.childNodes)
                .filter(node => node.nodeType === Node.TEXT_NODE)
                .map(node => node.textContent.trim())
                .join('')
                .trim();
        }

        return { galleryType, galleryId, galleryName };
    }

    // ALT+숫자 키 처리
    async function handleAltNumberKey(key) {
        const favorites = await getFavoriteGalleries();
        const galleryInfo = getCurrentGalleryInfo();

        if (favorites[key]) {
            // 이미 등록된 경우 해당 갤러리로 이동
            const { galleryType, galleryId } = favorites[key];
            let url = '';
            if (galleryType === 'person') {
                url = `https://gall.dcinside.com/person/board/lists/?id=${galleryId}`;
            } else if (galleryType === 'board') {
                url = `https://gall.dcinside.com/board/lists?id=${galleryId}`;
            } else {
                url = `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
            }
            window.location.href = url;
        } else if (isGalleryMainPage()) {
            favorites[key] = {
                galleryType: galleryInfo.galleryType,
                galleryId: galleryInfo.galleryId,
                name: galleryInfo.galleryName
            };
            saveFavoriteGalleries(favorites);

            // 커스텀 알림 표시
            const alertMessage = `${galleryInfo.galleryName}이(가) ${key}번에 등록되었습니다.`;
            const alertElement = document.createElement('div');
            alertElement.style.cssText = `
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background-color: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 15px 20px;
                border-radius: 8px;
                font-size: 14px;
                z-index: 10000;
                transition: opacity 0.3s ease;
            `;
            alertElement.textContent = alertMessage;
            document.body.appendChild(alertElement);

            setTimeout(() => {
                alertElement.style.opacity = '0';
                setTimeout(() => {
                    document.body.removeChild(alertElement);
                }, 300);
            }, 2000);

            // 즐겨찾기 목록 UI가 열려있다면 갱신
            const favoriteUI = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%;"]');
            if (favoriteUI) {
                await updateFavoritesList();
            }
        } else {
            alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
        }
    }

    // 키보드 이벤트 처리
    document.addEventListener('keydown', async event => {
        if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
            if (event.key >= '0' && event.key <= '9') {
                event.preventDefault();
                await handleAltNumberKey(event.key);
            } else if (event.key === '`') {
                event.preventDefault();
                const existingUI = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%;"]');
                if (existingUI) {
                    document.body.removeChild(existingUI);
                } else {
                    await showFavoriteGalleries();
                }
            }
        }
    });

    // URL 및 갤러리 정보 추출
    const url = window.location.href;
    const galleryType = url.includes('mgallery') ? 'mgallery' :
    url.includes('mini') ? 'mini' :
    url.includes('person') ? 'person' : 'board';
    const galleryId = (url.match(/id=([^&]+)/) || [])[1] || '';
    if (!galleryId) {
        console.warn('갤러리 ID가 없습니다.');
        return;
    }
        console.log('Gallery type:', galleryType);

    // 현재 페이지 번호 및 개념글 모드 확인
    const currentPage = parseInt((url.match(/page=(\d+)/) || [])[1]) || 1;
    const isRecommendMode = url.includes('exception_mode=recommend');

        // 기본 URL 구성
    let baseUrl = (galleryType === 'board') ?
        `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}` :
    `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
        if (isRecommendMode) {
            baseUrl += '&exception_mode=recommend';
        }

        // 숫자 입력 모드 관련 변수
    let numberInputMode = false, inputBuffer = '', numberInputTimeout = null, inputDisplay = null;

    // DOM 로드 후 안전하게 페이지 이동
        function navigateSafely(url) {
        const navigate = () => {
                console.log('페이지 이동:', url);
                window.location.href = url;
        };
        document.readyState === 'complete' ? navigate() : window.addEventListener('load', navigate, { once: true });
    }

    // 게시글 유효성 검사 함수들
    const isBlockedPost = numCell => {
            const row = numCell.closest('tr');
        return row && (row.classList.contains('block-disable') ||
                       row.classList.contains('list_trend') ||
                       row.style.display === 'none');
    };

    const isInvalidNumberCell = numCell => {
        const cleanedText = numCell.innerText.trim().replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');
        return ['AD', '공지', '설문'].includes(cleanedText) || isNaN(cleanedText);
    };

    const isInvalidTitleCell = titleCell => !!titleCell.querySelector('em.icon_notice');
    const isInvalidSubjectCell = subjectCell => {
        const text = subjectCell.innerText.trim();
        return ['AD', '공지', '설문'].some(keyword => text.includes(keyword));
    };

    const isValidPost = (numCell, titleCell, subjectCell) => {
        if (!numCell || !titleCell) return false;
        if (isBlockedPost(numCell) || isInvalidNumberCell(numCell) || isInvalidTitleCell(titleCell)) return false;
        if (subjectCell && isInvalidSubjectCell(subjectCell)) return false;
            return true;
    };

    // 유효한 게시글 목록 및 현재 게시글 인덱스 구하기
        function getValidPosts() {
        const rows = document.querySelectorAll('table.gall_list tbody tr');
            const validPosts = [];
            let currentPostIndex = -1;
        rows.forEach(row => {
                const numCell = row.querySelector('td.gall_num');
                const titleCell = row.querySelector('td.gall_tit');
                const subjectCell = row.querySelector('td.gall_subject');
                if (!isValidPost(numCell, titleCell, subjectCell)) return;
                if (numCell.querySelector('.sp_img.crt_icon')) {
                    currentPostIndex = validPosts.length;
                }
                const postLink = titleCell.querySelector('a:first-child');
                if (postLink) {
                    validPosts.push({ row, link: postLink });
                }
            });
            return { validPosts, currentPostIndex };
        }

    // 글 목록에 번호표 추가
        function addNumberLabels() {
            if (document.querySelector('.number-label')) {
                console.log('번호표가 이미 추가되어 있습니다.');
                return;
            }
            console.log('글 목록에 번호표 추가 중...');
        const allRows = document.querySelectorAll('table.gall_list tbody tr');
        const filteredRows = [];
        allRows.forEach(row => {
                const numCell = row.querySelector('td.gall_num');
                const titleCell = row.querySelector('td.gall_tit');
                const subjectCell = row.querySelector('td.gall_subject');
            if (!numCell || numCell.querySelector('.number-label') || numCell.querySelector('.sp_img.crt_icon')) return;
                if (!isValidPost(numCell, titleCell, subjectCell)) return;
            filteredRows.push(row);
            });

            const { validPosts, currentPostIndex } = getValidPosts();
        const maxPosts = Math.min(filteredRows.length, 100);
            for (let i = 0; i < maxPosts; i++) {
            const row = filteredRows[i];
                const numCell = row.querySelector('td.gall_num');
                const originalText = numCell.innerText.trim();
            let labelNumber = currentPostIndex !== -1
            ? (validPosts.findIndex(post => post.row === row) + 1)
            : (i + 1);
                if (!numCell.querySelector('.number-label')) {
                    const labelSpan = document.createElement('span');
                    labelSpan.className = 'number-label';
                labelSpan.style.cssText = 'color: #ff6600; font-weight: bold;';
                    labelSpan.textContent = `[${labelNumber}] `;
                    numCell.innerHTML = '';
                    numCell.appendChild(labelSpan);
                numCell.appendChild(document.createTextNode(originalText));
            }
        }
        console.log(`${maxPosts}개의 글에 번호표를 추가했습니다.`);
        }

    // 숫자 입력 모드 UI 업데이트
        function updateInputDisplay(text) {
            if (!inputDisplay) {
                inputDisplay = document.createElement('div');
            inputDisplay.style.cssText = 'position: fixed; top: 10px; right: 10px; background-color: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 5px; font-size: 16px; font-weight: bold; z-index: 9999;';
                document.body.appendChild(inputDisplay);
            }
            inputDisplay.textContent = text;
        }

        function exitNumberInputMode() {
            numberInputMode = false;
            inputBuffer = '';
            if (numberInputTimeout) {
                clearTimeout(numberInputTimeout);
                numberInputTimeout = null;
            }
            if (inputDisplay) {
                document.body.removeChild(inputDisplay);
                inputDisplay = null;
            }
            console.log('숫자 입력 모드 종료');
        }

        function navigateToPost(number) {
            const { validPosts } = getValidPosts();
        const targetIndex = parseInt(number, 10) - 1;
        console.log(`입력된 숫자: ${number}, 유효한 글 수: ${validPosts.length}`);
        if (targetIndex >= 0 && targetIndex < validPosts.length) {
            console.log(`${targetIndex + 1}번 글 클릭:`, validPosts[targetIndex].link.href);
            validPosts[targetIndex].link.click();
                return true;
            }
            return false;
        }

    // 페이지 로드 시 번호표 추가
        if (document.readyState === 'complete') {
            addNumberLabels();
        } else {
            window.addEventListener('load', addNumberLabels);
        }

    // MutationObserver를 통해 동적 변화 감시 (번호표 재추가)
    function setupMutationObserver(target) {
        if (!target) return null;
        const observer = new MutationObserver(() => setTimeout(addNumberLabels, 100));
        observer.observe(target, { childList: true, subtree: true, characterData: true });
                return observer;
            }
    let observer = setupMutationObserver(document.querySelector('table.gall_list tbody'));
        const bodyObserver = new MutationObserver(() => {
            if (!document.querySelector('.number-label')) {
                addNumberLabels();
            if (observer) observer.disconnect();
            observer = setupMutationObserver(document.querySelector('table.gall_list tbody'));
        }
    });
    bodyObserver.observe(document.body, { childList: true, subtree: true });

    // 키보드 이벤트 처리 (숫자 입력 모드 및 단축키)
    document.addEventListener('keydown', event => {
        if (!event || typeof event.key === 'undefined') return;
        const active = document.activeElement;
        if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable)) return;
        if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;

        // 백틱(`) 또는 마침표(.)로 숫자 입력 모드 전환
            if (event.key === '`' || event.key === '.') {
                event.preventDefault();
                if (numberInputMode && inputBuffer.length > 0) {
                    navigateToPost(inputBuffer);
                    exitNumberInputMode();
                    return;
                }
                numberInputMode = true;
                inputBuffer = '';
            if (numberInputTimeout) clearTimeout(numberInputTimeout);
                numberInputTimeout = setTimeout(exitNumberInputMode, 3000);
                updateInputDisplay('글 번호 입력: ');
                console.log('숫자 입력 모드 시작');
                return;
            }

        // 숫자 입력 모드: 숫자 키 처리
            if (numberInputMode && event.key >= '0' && event.key <= '9') {
                event.preventDefault();
                inputBuffer += event.key;
                updateInputDisplay(`글 번호 입력: ${inputBuffer}`);
            if (numberInputTimeout) clearTimeout(numberInputTimeout);
                numberInputTimeout = setTimeout(exitNumberInputMode, 3000);
                return;
            }

        // Enter: 입력 확정, Escape: 취소
            if (numberInputMode && event.key === 'Enter' && inputBuffer.length > 0) {
                event.preventDefault();
                navigateToPost(inputBuffer);
                exitNumberInputMode();
                return;
            }
            if (numberInputMode && event.key === 'Escape') {
                event.preventDefault();
                exitNumberInputMode();
                console.log('숫자 입력 모드 취소');
                return;
            }

        // 숫자 키 직접 입력 (숫자 입력 모드 아닐 때)
            if (!numberInputMode && event.key >= '0' && event.key <= '9') {
                const keyNumber = parseInt(event.key, 10);
            const targetIndex = keyNumber === 0 ? 9 : keyNumber - 1;
                const { validPosts } = getValidPosts();
                if (targetIndex >= 0 && targetIndex < validPosts.length) {
                    validPosts[targetIndex].link.click();
            }
            return;
            }

            // 기타 단축키 처리
                switch (event.key.toUpperCase()) {
            case 'W': { // 글쓰기
                const btn = document.querySelector('button#btn_write');
                if (btn) btn.click();
                        break;
            }
            case 'C': { // 댓글 입력창으로 이동
                        event.preventDefault();
                const textarea = document.querySelector('textarea[id^="memo_"]');
                if (textarea) textarea.focus();
                        break;
            }
            case 'D': { // 댓글 새로고침
                        event.preventDefault();
                const refresh = document.querySelector('button.btn_cmt_refresh');
                if (refresh) refresh.click();
                        break;
            }
            case 'R': { // 페이지 새로고침
                        location.reload();
                        break;
            }
            case 'Q': { // 최상단 스크롤
                        window.scrollTo(0, 0);
                        break;
            }
            case 'E': { // 글 목록으로 스크롤
                        event.preventDefault();
                const list = document.querySelector('table.gall_list');
                if (list) list.scrollIntoView({ block: 'start' });
                        break;
            }
            case 'F': { // 전체글 보기
                        event.preventDefault();
                const fullUrl = (galleryType === 'board') ?
                              `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}` :
                        `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
                navigateSafely(fullUrl);
                        break;
            }
            case 'G': { // 개념글 보기
                        event.preventDefault();
                const recUrl = (galleryType === 'board') ?
                              `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&exception_mode=recommend` :
                        `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&exception_mode=recommend`;
                navigateSafely(recUrl);
                        break;
            }
            case 'A': { // 이전 페이지
                event.preventDefault();
                if (currentPage > 1) {
                    const urlObj = new URL(window.location.href);
                    urlObj.searchParams.set('page', currentPage - 1);
                    navigateSafely(urlObj.toString());
                }
                break;
            }
            case 'S': { // 다음 페이지
                event.preventDefault();
                const urlObj = new URL(window.location.href);
                urlObj.searchParams.set('page', currentPage + 1);
                navigateSafely(urlObj.toString());
                break;
            }
            case 'Z': { // 이전 글
                        event.preventDefault();
                let prevLink = document.querySelector('a.prev');
                if (!prevLink) {
                    const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
                    if (crtIcon) {
                        let row = crtIcon.closest('tr')?.previousElementSibling;
                        while (row && (row.classList.contains('block-disable') || row.classList.contains('list_trend') || row.style.display === 'none')) {
                            row = row.previousElementSibling;
                        }
                        if (row) prevLink = row.querySelector('td.gall_tit a:first-child');
                    }
                }
                if (prevLink) navigateSafely(prevLink.href);
                break;
            }
            case 'X': { // 다음 글
                event.preventDefault();
                let nextLink = document.querySelector('a.next');
                if (!nextLink) {
                    const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
                    if (crtIcon) {
                        let row = crtIcon.closest('tr')?.nextElementSibling;
                        while (row && (row.classList.contains('block-disable') || row.classList.contains('list_trend') || row.style.display === 'none')) {
                            row = row.nextElementSibling;
                        }
                        if (row) nextLink = row.querySelector('td.gall_tit a:first-child');
                    }
                }
                if (nextLink) navigateSafely(nextLink.href);
                break;
                }
            }
        });
})();