dcinside shortcut

디시인사이드 갤러리 단축키: 글번호(1~100), ` or . + 숫자키 + ` or . 이동, ALT+숫자 즐겨찾기, W(글쓰기), C(댓글), D(새로고침), R(리로드), Q(최상단), E(목록), F(전체글), G(개념글), A/S(페이지), Z/X(글 이동)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description  디시인사이드 갤러리 단축키: 글번호(1~100), ` or . + 숫자키 + ` or . 이동, 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      CC BY-NC-SA 4.0
// @supportURL   https://gallog.dcinside.com/nonohako/guestbook
// ==/UserScript==
(function() {
    'use strict';

    // Constants
    const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';
    const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';
    const PAGE_NAVIGATION_MODE_KEY = 'dcinside_page_navigation_mode';
    const MACRO_Z_RUNNING_KEY = 'dcinside_macro_z_running';
    const MACRO_X_RUNNING_KEY = 'dcinside_macro_x_running';
    const MACRO_INTERVAL = 2500; // 2.5 seconds
    // Storage Module
    const Storage = {

        async getPageNavigationMode() {
            const defaultValue = 'ajax'; // 기본값은 AJAX 모드로 설정
            if (isTampermonkey) {
                return GM_getValue(PAGE_NAVIGATION_MODE_KEY, defaultValue);
            } else {
                const data = localStorage.getItem(PAGE_NAVIGATION_MODE_KEY) || this.getCookie(PAGE_NAVIGATION_MODE_KEY);
                return data !== null ? data : defaultValue; // 저장된 값이 없으면 기본값 반환
            }
        },

        savePageNavigationMode(mode) {
            try {
                if (mode !== 'ajax' && mode !== 'full') {
                    console.error('Invalid page navigation mode:', mode);
                    return;
                }
                if (isTampermonkey) {
                    GM_setValue(PAGE_NAVIGATION_MODE_KEY, mode);
                } else {
                    localStorage.setItem(PAGE_NAVIGATION_MODE_KEY, mode);
                    this.setCookie(PAGE_NAVIGATION_MODE_KEY, mode);
                }
            } catch (error) {
                console.error('Failed to save page navigation mode:', error);
            }
        },

        async getFavorites() {
            let favorites = {};
            try {
                if (isTampermonkey) {
                    favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
                } else {
                    const data = localStorage.getItem(FAVORITE_GALLERIES_KEY) ||
                          this.getCookie(FAVORITE_GALLERIES_KEY);
                    favorites = data ? JSON.parse(data) : {};
                }
            } catch (error) {
                console.error('Failed to retrieve favorites:', error);
            }
            return favorites;
        },

        saveFavorites(favorites) {
            try {
                const data = JSON.stringify(favorites);
                if (isTampermonkey) {
                    GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
                } else {
                    localStorage.setItem(FAVORITE_GALLERIES_KEY, data);
                    this.setCookie(FAVORITE_GALLERIES_KEY, data);
                }
            } catch (error) {
                console.error('Failed to save favorites:', error);
                alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
            }
        },

        getCookie(name) {
            const value = document.cookie.match(`(^|;)\\s*${name}=([^;]+)`);
            return value ? decodeURIComponent(value[2]) : null;
        },

        setCookie(name, value) {
            const date = new Date();
            date.setFullYear(date.getFullYear() + 1);
            document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; domain=.dcinside.com`;
        },

        async getAltNumberEnabled() {
            if (isTampermonkey) {
                return GM_getValue('altNumberEnabled', true); // 기본값: 활성화
            } else {
                const data = localStorage.getItem('altNumberEnabled') || this.getCookie('altNumberEnabled');
                return data !== null ? JSON.parse(data) : true;
            }
        },

        saveAltNumberEnabled(enabled) {
            try {
                const data = JSON.stringify(enabled);
                if (isTampermonkey) {
                    GM_setValue('altNumberEnabled', enabled);
                } else {
                    localStorage.setItem('altNumberEnabled', data);
                    this.setCookie('altNumberEnabled', data);
                }
            } catch (error) {
                console.error('Failed to save altNumberEnabled:', error);
            }
        },

        async getShortcutEnabled(key) {
            if (isTampermonkey) {
                return GM_getValue(key, true);
            } else {
                const data = localStorage.getItem(key) || this.getCookie(key);
                return data !== null ? JSON.parse(data) : true;
            }
        },

        saveShortcutEnabled(key, enabled) {
            try {
                const data = JSON.stringify(enabled);
                if (isTampermonkey) {
                    GM_setValue(key, enabled);
                } else {
                    localStorage.setItem(key, data);
                    this.setCookie(key, data);
                }
            } catch (error) {
                console.error(`Failed to save ${key}:`, error);
            }
        },

        async getShortcutKey(key) {
            if (isTampermonkey) {
                return GM_getValue(key, null);
            } else {
                const data = localStorage.getItem(key) || this.getCookie(key);
                return data !== null ? data : null;
            }
        },

        saveShortcutKey(key, value) {
            try {
                if (isTampermonkey) {
                    GM_setValue(key, value);
                } else {
                    localStorage.setItem(key, value);
                    this.setCookie(key, value);
                }
            } catch (error) {
                console.error(`Failed to save ${key}:`, error);
            }
        }
    };

    // UI Module
    const UI = {
        tooltipCSSInjected: false, // CSS 주입 여부 플래그

        // CSS를 페이지에 주입하는 함수
        injectTooltipCSS() {
            if (this.tooltipCSSInjected) return; // 이미 주입되었으면 실행 안 함

            const css = `
            /* 툴팁을 감싸는 컨테이너 (상대 위치 기준점) */
            .footnote-container {
                position: relative; /* 자식 tooltip의 absolute 위치 기준 */
                display: inline-block; /* span이면서 위치 기준이 되도록 */
                cursor: help;
            }

            /* 실제 툴팁 요소 */
            .footnote-tooltip {
                position: absolute;
                bottom: 115%; /* 트리거 요소 바로 위에 위치 */
                left: 50%;     /* 가로 중앙 정렬 시작점 */
                transform: translateX(-50%); /* 가로 중앙 정렬 완료 */
                background-color: rgba(0, 0, 0, 0.85); /* 반투명 검정 배경 */
                color: white;           /* 흰색 텍스트 */
                padding: 6px 10px;      /* 내부 여백 */
                border-radius: 5px;     /* 둥근 모서리 */
                font-size: 12px;        /* 작은 글씨 크기 */
                white-space: nowrap;    /* 줄바꿈 방지 */
                z-index: 10001;         /* 다른 요소 위에 표시되도록 */
                visibility: hidden;     /* 기본적으로 숨김 (visibility) */
                opacity: 0;             /* 기본적으로 투명 (opacity) */
                transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out; /* 부드러운 효과 */
                pointer-events: none;   /* 툴팁 자체가 마우스 이벤트 방해하지 않도록 */
            }

            /* 컨테이너에 마우스 호버 시 툴팁 표시 */
            .footnote-container:hover .footnote-tooltip {
                visibility: visible; /* 보이기 (visibility) */
                opacity: 1;          /* 불투명 (opacity) */
            }
        `;
            // style 태그를 만들어 head에 추가
            const styleElement = this.createElement('style', {}, { textContent: css });
            document.head.appendChild(styleElement);
            this.tooltipCSSInjected = true; // 주입 완료 플래그 설정
        },

        createPageNavigationModeSelector() {
            this.injectTooltipCSS(); // <<< 함수 시작 시 CSS 주입 함수 호출

            const container = this.createElement('div', {
                margin: '15px 0', padding: '10px', backgroundColor: '#f5f5f5',
                borderRadius: '10px', border: '1px solid #e0e0e0'
            });

            const title = this.createElement('div', {
                fontSize: '14px', fontWeight: '500', color: '#424242', marginBottom: '10px'
            }, { textContent: '페이지 이동 방식 (A/S 키)' });
            container.appendChild(title);

            const optionsContainer = this.createElement('div', {
                display: 'flex', justifyContent: 'space-around', alignItems: 'center' // alignItems 추가
            });

            const modes = [
                { value: 'ajax', text: '⚡ 빠른 이동 (AJAX)' },
                { value: 'full', text: '🔄 기본 이동 (새로고침)' }
            ];

            const tooltipText = "Refresher의 새로고침 기능과 충돌합니다. 둘 중에 하나만 사용하세요."; // 툴팁 텍스트

            Storage.getPageNavigationMode().then(currentMode => {
                modes.forEach(modeInfo => {
                    const label = this.createElement('label', {
                        display: 'flex', alignItems: 'center', cursor: 'pointer',
                        fontSize: '13px', color: '#555', gap: '5px' // 라디오 버튼과 텍스트 사이 간격
                    });
                    const radio = this.createElement('input', {
                        // marginRight: '5px' // gap으로 대체
                    }, {
                        type: 'radio', name: 'pageNavMode', value: modeInfo.value
                    });

                    if (modeInfo.value === currentMode) {
                        radio.checked = true;
                    }

                    radio.addEventListener('change', async (e) => {
                        if (e.target.checked) {
                            await Storage.savePageNavigationMode(e.target.value);
                            UI.showAlert(`페이지 이동 방식이 '${modeInfo.text.split(' ')[0]}' 모드로 변경되었습니다.`); // 텍스트 간소화
                        }
                    });

                    label.appendChild(radio);
                    label.appendChild(document.createTextNode(modeInfo.text)); // 라디오 버튼 텍스트 추가

                    // --- AJAX 옵션에만 각주 및 툴팁 추가 ---
                    if (modeInfo.value === 'ajax') {
                        // 1. 컨테이너 span 생성 (상대 위치 기준, 호버 타겟)
                        const footnoteContainer = this.createElement('span', {
                            // 스타일은 CSS 클래스로 이동
                        }, { className: 'footnote-container' });

                        // 2. 트리거 텍스트 '[주의]' span 생성
                        const footnoteTrigger = this.createElement('span', {
                            fontSize: '10px',
                            color: '#d32f2f',
                            fontWeight: 'bold',
                            verticalAlign: 'super',
                            marginLeft: '3px'
                            // title 속성 제거
                        }, { textContent: '[주의]' });

                        // 3. 실제 툴팁 내용 span 생성
                        const tooltipElement = this.createElement('span', {
                            // 스타일은 CSS 클래스로 이동
                        }, {
                            className: 'footnote-tooltip',
                            textContent: tooltipText
                        });

                        // 4. 컨테이너에 트리거와 툴팁 추가
                        footnoteContainer.appendChild(footnoteTrigger);
                        footnoteContainer.appendChild(tooltipElement);

                        // 5. 라벨에 최종 컨테이너 추가
                        label.appendChild(footnoteContainer);
                    }
                    // --- 커스텀 툴팁 적용 끝 ---

                    optionsContainer.appendChild(label);
                });
            });

            container.appendChild(optionsContainer);
            return container;
        },

        createElement(tag, styles, props = {}) {
            const el = document.createElement(tag);
            Object.assign(el.style, styles);
            Object.assign(el, props);
            return el;
        },

        async showFavorites() {
            const container = this.createElement('div', {
                position: 'fixed', top: '50%', left: '50%',
                transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
                padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
                zIndex: '10000', width: '360px', maxHeight: '80vh', overflowY: 'auto',
                fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
                transition: 'opacity 0.2s ease-in-out', opacity: '0'
            });
            setTimeout(() => container.style.opacity = '1', 10);

            this.loadRobotoFont();
            container.appendChild(this.createTitle());
            const list = this.createList();
            container.appendChild(list);
            container.appendChild(this.createAddContainer());
            container.appendChild(this.createToggleAltNumber()); // 새로 추가: 토글 버튼
            container.appendChild(this.createShortcutManagerButton());
            container.appendChild(this.createCloseButton(container));

            document.body.appendChild(container);
            await this.updateFavoritesList(list);
        },

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

        createToggleAltNumber() {
            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                margin: '15px 0', padding: '10px', backgroundColor: '#f5f5f5',
                borderRadius: '10px'
            });

            const label = this.createElement('span', {
                fontSize: '14px', fontWeight: '500', color: '#424242'
            }, { textContent: 'ALT + 숫자 단축키 사용' });

            const checkbox = this.createElement('input', {
                marginLeft: 'auto'
            }, { type: 'checkbox' });

            Storage.getAltNumberEnabled().then(enabled => {
                checkbox.checked = enabled;
            });

            checkbox.addEventListener('change', async () => {
                await Storage.saveAltNumberEnabled(checkbox.checked);
                UI.showAlert(`ALT + 숫자 단축키가 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);
            });

            container.appendChild(label);
            container.appendChild(checkbox);
            return container;
        },

        createShortcutManagerButton() {
            const button = this.createElement('button', {
                display: 'block', width: '100%', padding: '10px', marginTop: '15px',
                backgroundColor: '#4caf50', color: '#ffffff', border: 'none',
                borderRadius: '10px', fontSize: '15px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease'
            }, { textContent: '단축키 관리' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#388e3c');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#4caf50');
            button.addEventListener('click', () => this.showShortcutManager());
            return button;
        },

        showShortcutManager() {
            const container = this.createElement('div', {
                position: 'fixed', top: '50%', left: '50%',
                transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
                padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
                zIndex: '10000', width: '400px', maxHeight: '80vh', overflowY: 'auto',
                fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
                transition: 'opacity 0.2s ease-in-out', opacity: '0'
            });
            setTimeout(() => container.style.opacity = '1', 10);

            this.loadRobotoFont();
            container.appendChild(this.createTitle('단축키 관리'));
            container.appendChild(this.createPageNavigationModeSelector());

            // 단축키 활성화/비활성화 토글 추가
            container.appendChild(this.createShortcutToggle('W - 글쓰기', 'shortcutWEnabled'));
            container.appendChild(this.createShortcutToggle('C - 댓글 입력', 'shortcutCEnabled'));
            container.appendChild(this.createShortcutToggle('D - 댓글 새로고침', 'shortcutDEnabled'));
            container.appendChild(this.createShortcutToggle('R - 페이지 새로고침', 'shortcutREnabled'));
            container.appendChild(this.createShortcutToggle('Q - 최상단 스크롤', 'shortcutQEnabled'));
            container.appendChild(this.createShortcutToggle('E - 글 목록 스크롤', 'shortcutEEnabled'));
            container.appendChild(this.createShortcutToggle('F - 전체글 보기', 'shortcutFEnabled'));
            container.appendChild(this.createShortcutToggle('G - 개념글 보기', 'shortcutGEnabled'));
            container.appendChild(this.createShortcutToggle('A - 이전 페이지', 'shortcutAEnabled'));
            container.appendChild(this.createShortcutToggle('S - 다음 페이지', 'shortcutSEnabled'));
            container.appendChild(this.createShortcutToggle('Z - 이전 글', 'shortcutZEnabled'));
            container.appendChild(this.createShortcutToggle('X - 다음 글', 'shortcutXEnabled'));
            // --- Add Macro Toggles ---
            container.appendChild(this.createElement('div', { // 구분선
                height: '1px', backgroundColor: '#e0e0e0', margin: '15px 0'
            }));
            container.appendChild(this.createElement('div', { // 섹션 제목
                fontSize: '14px', fontWeight: '500', color: '#424242', marginBottom: '5px'
            }, { textContent: '자동 넘김 매크로 (ALT+키로 시작/중지)' }));

            // --- 레이블 수정 ---
            container.appendChild(this.createShortcutToggle('이전 글 자동 넘김', 'shortcutMacroZEnabled')); // "ALT+Z - " 제거
            container.appendChild(this.createShortcutToggle('다음 글 자동 넘김', 'shortcutMacroXEnabled')); // "ALT+X - " 제거
            // --- End Add Macro Toggles ---

            container.appendChild(this.createCloseButton(container));
            document.body.appendChild(container);
        },

        createShortcutToggle(label, enabledStorageKey) {
            const isMacroToggle = enabledStorageKey.startsWith('shortcutMacro');
            let displayLabel = label; // 표시될 최종 레이블

            // 매크로 토글인 경우, 레이블에서 단축키 정보 분리
            let prefix = '';
            if (isMacroToggle) {
                if (enabledStorageKey === 'shortcutMacroZEnabled') {
                    prefix = 'ALT+Z - ';
                    // displayLabel = "이전 글 자동 넘김"; // label 인수에서 분리된 부분
                } else if (enabledStorageKey === 'shortcutMacroXEnabled') {
                    prefix = 'ALT+X - ';
                    // displayLabel = "다음 글 자동 넘김"; // label 인수에서 분리된 부분
                }
                // label 인수 자체에 "ALT+Z - 이전 글..." 이 포함된 경우, prefix 제거
                if (displayLabel.startsWith(prefix)) {
                    displayLabel = displayLabel.substring(prefix.length).trim();
                }
            }


            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center',
                margin: '10px 0', padding: '10px', backgroundColor: '#f5f5f5',
                borderRadius: '10px', gap: '10px' // 요소 간 간격
            });

            // 접두사(ALT+Z/X)를 별도 span으로 처리하여 너비 고정 (선택적)
            if (prefix) {
                const prefixEl = this.createElement('span', {
                    fontSize: '14px', fontWeight: '500', color: '#666', // 약간 연한 색상
                    // fontFamily: 'monospace', // 고정폭 글꼴 사용 가능
                    minWidth: '60px', // 최소 너비 확보
                    textAlign: 'right', // 오른쪽 정렬 (선택적)
                    marginRight: '5px' // 레이블과의 간격
                }, { textContent: prefix });
                container.appendChild(prefixEl);
            }


            const labelEl = this.createElement('span', {
                fontSize: '14px', fontWeight: '500', color: '#424242',
                // width: '150px', // 고정 너비 제거 또는 조정
                flexGrow: '1', // 남은 공간 차지하도록 설정
                textOverflow: 'ellipsis', // 넘칠 때 ... 표시
                whiteSpace: 'nowrap' // 한 줄로 표시 강제
            }, { textContent: displayLabel }); // 수정된 displayLabel 사용

            const checkbox = this.createElement('input', {
                // marginLeft: 'auto' // flexGrow 사용 시 불필요할 수 있음, gap으로 대체됨
                flexShrink: 0 // 체크박스 크기 줄어들지 않도록
            }, { type: 'checkbox' });

            const keyInput = this.createElement('input', {
                width: '60px', padding: '5px',
                border: '1px solid #e0e0e0', borderRadius: '4px',
                fontSize: '12px', outline: 'none', textAlign: 'center',
                fontFamily: 'monospace',
                flexShrink: 0 // 키 입력 필드 크기 줄어들지 않도록
            }, { type: 'text', placeholder: '키 변경', maxLength: '1' });


            if (isMacroToggle) {
                keyInput.style.display = 'none'; // 매크로는 키 변경 불가
            }

            // --- 각주(툴팁) 추가 로직 (ALT+Z 매크로에만) ---
            if (enabledStorageKey === 'shortcutMacroZEnabled') {
                this.injectTooltipCSS(); // 툴팁 CSS 주입 보장

                const tooltipText = "AMD 아드레날린, 지포스 익스피리언스 단축키와 중복시 사용 불가";
                const footnoteContainer = this.createElement('span', {}, { className: 'footnote-container' });
                const footnoteTrigger = this.createElement('span', {
                    fontSize: '10px', color: '#d32f2f', fontWeight: 'bold',
                    verticalAlign: 'super', marginLeft: '3px', cursor: 'help'
                }, { textContent: '[주의]' });
                const tooltipElement = this.createElement('span', {}, {
                    className: 'footnote-tooltip', textContent: tooltipText
                });

                footnoteContainer.appendChild(footnoteTrigger);
                footnoteContainer.appendChild(tooltipElement);
                // labelEl 옆에 각주 컨테이너 추가
                labelEl.appendChild(footnoteContainer);
            }
            // --- 각주(툴팁) 추가 로직 끝 ---


            const keyStorageKey = enabledStorageKey.replace('Enabled', 'Key');
            const allCustomizableShortcutKeyStorageKeys = [ /* ... */ ];
            const defaultKey = isMacroToggle ? null : keyStorageKey.slice(-4, -3);

            if (!isMacroToggle) {
                Storage.getShortcutKey(keyStorageKey).then(savedKey => {
                    keyInput.placeholder = savedKey || defaultKey;
                });
            }

            // Checkbox logic (sessionStorage 클리어 포함)
            Storage.getShortcutEnabled(enabledStorageKey).then(enabled => {
                checkbox.checked = enabled;
                if (isMacroToggle) {
                    const storageKey = enabledStorageKey === 'shortcutMacroZEnabled' ? MACRO_Z_RUNNING_KEY : MACRO_X_RUNNING_KEY;
                    if (!enabled && sessionStorage.getItem(storageKey) === 'true') {
                        sessionStorage.setItem(storageKey, 'false');
                    }
                }
            });
            checkbox.addEventListener('change', async () => {
                await Storage.saveShortcutEnabled(enabledStorageKey, checkbox.checked);
                // 레이블 텍스트 조합하여 알림 메시지 생성
                const fullLabelForAlert = prefix ? `${prefix}${displayLabel}` : displayLabel;
                UI.showAlert(`${fullLabelForAlert} 기능이 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);

                if (!checkbox.checked && isMacroToggle) {
                    const storageKey = enabledStorageKey === 'shortcutMacroZEnabled' ? MACRO_Z_RUNNING_KEY : MACRO_X_RUNNING_KEY;
                    sessionStorage.setItem(storageKey, 'false');
                    console.log(`${prefix}Macro state cleared via UI toggle.`);
                }
            });

            // 키 입력 이벤트 리스너 (수정된 충돌 검사 로직 포함)
            if (!isMacroToggle) {
                keyInput.addEventListener('keydown', async (e) => { // async 추가
                    // 편집/탐색 키 허용
                    if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End'].includes(e.key)) {
                        return;
                    }
                    // 다른 모든 키 입력 시 기본 동작 방지
                    e.preventDefault();
                    e.stopPropagation();

                    const newKey = e.key.toUpperCase();

                    if (newKey.length === 1 && /^[A-Z]$/.test(newKey)) {
                        let isConflict = false;
                        let conflictingActionLabel = ''; // 충돌 기능 레이블 저장

                        // 충돌 검사 로직
                        for (const otherStorageKey of allCustomizableShortcutKeyStorageKeys) {
                            if (otherStorageKey === keyStorageKey) continue; // 자기 자신 제외

                            const otherDefault = otherStorageKey.slice(-4, -3);
                            const otherSavedKey = await Storage.getShortcutKey(otherStorageKey);
                            const currentlyAssignedKey = otherSavedKey || otherDefault;

                            if (currentlyAssignedKey === newKey) {
                                isConflict = true;
                                // 충돌된 기능의 레이블 찾기 (더 사용자 친화적인 메시지 위해)
                                // 이 부분은 실제 구현 시 단축키 목록과 레이블을 매핑하는 구조가 필요할 수 있음
                                // 여기서는 간단히 기본 키로 표시
                                conflictingActionLabel = `기본키 ${otherDefault}`;
                                break;
                            }
                        }

                        if (isConflict) {
                            UI.showAlert(`'${newKey}' 단축키는 이미 다른 기능(${conflictingActionLabel})에 할당되어 있습니다.`);
                            keyInput.value = '';
                        } else {
                            await Storage.saveShortcutKey(keyStorageKey, newKey);
                            UI.showAlert(`${label} 단축키가 '${newKey}'(으)로 변경되었습니다.`);
                            keyInput.placeholder = newKey;
                            keyInput.value = '';
                        }
                    } else if (newKey.length === 1) {
                        UI.showAlert("단축키는 영문 대문자(A-Z)만 가능합니다.");
                        keyInput.value = '';
                    }
                });

                // 포커스 이벤트 리스너
                keyInput.addEventListener('focus', () => { /* 필요 시 플래그 설정 */ });
                keyInput.addEventListener('blur', () => {
                });
            }

            // 요소들을 컨테이너에 추가
            container.appendChild(labelEl);
            container.appendChild(checkbox);
            if (!isMacroToggle) { // Only append input if not macro
                container.appendChild(keyInput);
            }
            return container;
        },

        createTitle() {
            return this.createElement('h3', {
                fontSize: '18px', fontWeight: '700', color: '#212121',
                margin: '0 0 15px 0', paddingBottom: '10px', borderBottom: '1px solid #e0e0e0'
            }, { textContent: '즐겨찾는 갤러리' });
        },

        createList() {
            return this.createElement('ul', {
                listStyle: 'none', margin: '0', padding: '0',
                maxHeight: '50vh', overflowY: 'auto'
            });
        },

        async updateFavoritesList(list) {
            list.innerHTML = '';
            const favorites = await Storage.getFavorites();
            Object.entries(favorites).forEach(([key, gallery]) => {
                list.appendChild(this.createFavoriteItem(key, gallery));
            });
        },

        createFavoriteItem(key, gallery) {
            const item = this.createElement('li', {
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '10px 15px', margin: '5px 0', backgroundColor: '#fafafa',
                borderRadius: '10px', transition: 'background-color 0.2s ease', cursor: 'pointer'
            });

            item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f0f0f0');
            item.addEventListener('mouseleave', () => item.style.backgroundColor = '#fafafa');
            item.addEventListener('click', () => this.navigateToGallery(gallery));

            // Ensure we display the gallery name properly
            const name = gallery.name || gallery.galleryName || gallery.galleryId || 'Unknown Gallery';
            item.appendChild(this.createElement('span', {
                fontSize: '15px', fontWeight: '400', color: '#424242',
                flexGrow: '1', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
            }, { textContent: `${key}: ${name}` }));

            item.appendChild(this.createRemoveButton(key));
            return item;
        },

        createRemoveButton(key) {
            const button = this.createElement('button', {
                backgroundColor: 'transparent', color: '#757575', border: 'none',
                borderRadius: '50%', width: '24px', height: '24px', fontSize: '16px',
                lineHeight: '1', cursor: 'pointer', transition: 'color 0.2s ease, background-color 0.2s ease'
            }, { textContent: '✕' });

            button.addEventListener('mouseenter', () => {
                button.style.color = '#d32f2f';
                button.style.backgroundColor = '#ffebee';
            });
            button.addEventListener('mouseleave', () => {
                button.style.color = '#757575';
                button.style.backgroundColor = 'transparent';
            });
            button.addEventListener('click', async (e) => {
                e.stopPropagation();
                const favorites = await Storage.getFavorites();
                delete favorites[key];
                Storage.saveFavorites(favorites);
                await this.updateFavoritesList(button.closest('ul'));
            });
            return button;
        },

        createAddContainer() {
            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                gap: '8px', margin: '15px 0', padding: '15px', backgroundColor: '#f5f5f5',
                borderRadius: '10px'
            });

            const input = this.createElement('input', {
                width: '45px', padding: '8px', border: '1px solid #e0e0e0',
                borderRadius: '8px', fontSize: '14px', textAlign: 'center',
                outline: 'none', transition: 'border-color 0.2s ease', backgroundColor: '#ffffff'
            }, { type: 'text', placeholder: '0-9' });

            input.addEventListener('focus', () => input.style.borderColor = '#1976d2');
            input.addEventListener('blur', () => input.style.borderColor = '#e0e0e0');

            const button = this.createElement('button', {
                padding: '8px 16px', backgroundColor: '#1976d2', color: '#ffffff',
                border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease', flexGrow: '1'
            }, { textContent: '즐겨찾기 추가' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                const digit = input.value.trim();
                if (!/^[0-9]$/.test(digit)) {
                    alert('0부터 9까지의 숫자를 입력해주세요.');
                    return;
                }
                Gallery.handleFavoriteKey(digit);
                input.value = '';
            });

            container.appendChild(input);
            container.appendChild(button);
            return container;
        },

        createCloseButton(container) {
            const button = this.createElement('button', {
                display: 'block', width: '100%', padding: '10px', marginTop: '15px',
                backgroundColor: '#1976d2', color: '#ffffff', border: 'none',
                borderRadius: '10px', fontSize: '15px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease'
            }, { textContent: 'Close' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
            button.addEventListener('click', () => {
                container.style.opacity = '0';
                setTimeout(() => document.body.removeChild(container), 200);
            });
            return button;
        },

        navigateToGallery(gallery) {
            const url = gallery.galleryType === 'board'
            ? `https://gall.dcinside.com/board/lists?id=${gallery.galleryId}`
                : `https://gall.dcinside.com/${gallery.galleryType}/board/lists?id=${gallery.galleryId}`;
            window.location.href = url;
        },

        showAlert(message) {
            const alert = this.createElement('div', {
                position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)',
                backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '15px 20px',
                borderRadius: '8px', fontSize: '14px', zIndex: '10000', transition: 'opacity 0.3s ease'
            }, { textContent: message });

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

    // Gallery Module
    const Gallery = {
        isMainPage() {
            const { href } = window.location;
            return href.includes('/lists') && href.includes('id=');
        },

        getInfo() {
            if (!this.isMainPage()) return { galleryType: '', galleryId: '', galleryName: '' };

            const { href } = window.location;
            const galleryType = href.includes('/person/') ? 'person' :
            href.includes('mgallery') ? 'mgallery' :
            href.includes('mini') ? 'mini' : 'board';
            const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
            const nameEl = document.querySelector('div.fl.clear h2 a');
            const galleryName = nameEl
            ? Array.from(nameEl.childNodes)
            .filter(node => node.nodeType === Node.TEXT_NODE)
            .map(node => node.textContent.trim())
            .join('') || galleryId
            : galleryId;

            return { galleryType, galleryId, galleryName };
        },

        async handleFavoriteKey(key) {
            const favorites = await Storage.getFavorites();
            const info = this.getInfo();

            if (favorites[key]) {
                UI.navigateToGallery(favorites[key]);
            } else if (this.isMainPage()) {
                // Ensure galleryName is saved as 'name' for UI compatibility
                favorites[key] = {
                    galleryType: info.galleryType,
                    galleryId: info.galleryId,
                    name: info.galleryName
                };
                Storage.saveFavorites(favorites);
                UI.showAlert(`${info.galleryName}이(가) ${key}번에 등록되었습니다.`);
                const list = document.querySelector('ul[style*="max-height: 50vh"]');
                if (list) await UI.updateFavoritesList(list);
            } else {
                alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
            }
        },

        getPageInfo() {
            const { href } = window.location;
            const galleryType = href.includes('mgallery') ? 'mgallery' :
            href.includes('mini') ? 'mini' :
            href.includes('person') ? 'person' : 'board';
            const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
            const currentPage = parseInt(href.match(/page=(\d+)/)?.[1] || '1', 10);
            const isRecommendMode = href.includes('exception_mode=recommend');

            return { galleryType, galleryId, currentPage, isRecommendMode };
        }
    };

    // Post Navigation Module
    const Posts = {
        isValidPost(numCell, titleCell, subjectCell) {
            if (!numCell || !titleCell) return false;
            const row = numCell.closest('tr');
            if (row?.classList.contains('block-disable') ||
                row?.classList.contains('list_trend') ||
                row?.style.display === 'none') return false;

            const numText = numCell.textContent.trim().replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');
            if (['AD', '공지', '설문', 'Notice'].includes(numText) || isNaN(numText)) return false;
            if (titleCell.querySelector('em.icon_notice')) return false;
            if (subjectCell?.textContent.trim().match(/AD|공지|설문|뉴스|고정|이슈/)) return false;
            return true;
        },

        getValidPosts() {
            const rows = document.querySelectorAll('table.gall_list tbody tr');
            const validPosts = [];
            let currentIndex = -1;

            rows.forEach((row, index) => {
                const numCell = row.querySelector('td.gall_num');
                const titleCell = row.querySelector('td.gall_tit');
                const subjectCell = row.querySelector('td.gall_subject');
                if (!this.isValidPost(numCell, titleCell, subjectCell)) return;

                const link = titleCell.querySelector('a:first-child');
                if (link) {
                    validPosts.push({ row, link });
                    if (numCell.querySelector('.sp_img.crt_icon')) currentIndex = validPosts.length - 1;
                }
            });

            return { validPosts, currentIndex };
        },

        addNumberLabels() {
            // 1. 페이지 전체에서 유효한 게시글 목록을 가져옵니다.
            //    getValidPosts는 내부적으로 관련 테이블들을 찾아 처리해야 합니다.
            const { validPosts } = this.getValidPosts();
            // console.log(`addNumberLabels: Found ${validPosts.length} total valid posts.`); // 디버깅

            // 2. 찾은 모든 유효 게시글에 대해 라벨 추가 시도 (최대 100개)
            validPosts.slice(0, 100).forEach((post, i) => {
                const numCell = post.row.querySelector('td.gall_num');
                if (!numCell) return; // 번호 셀 없으면 건너뛰기

                // --- 중요: 중복 라벨 방지 체크 ---
                // 이 번호 셀('numCell') 내부에 이미 '.number-label'이 있는지 확인합니다.
                if (numCell.querySelector('span.number-label')) {
                    // console.log("Skipping already labeled cell:", numCell.textContent); // 디버깅
                    return; // 이미 라벨이 있으면 이 셀에 대한 작업 중단
                }
                // --- 체크 끝 ---

                // 현재 보고 있는 글 아이콘이 있으면 라벨 추가 안 함
                if (numCell.querySelector('.sp_img.crt_icon')) return;

                // 라벨 생성 및 추가
                const label = UI.createElement('span', {
                    color: '#ff6600', fontWeight: 'bold', marginRight: '3px'
                }, { className: 'number-label', textContent: `[${i + 1}]` });
                numCell.prepend(label);
            });
        },

        navigate(number) {
            const { validPosts } = this.getValidPosts();
            const index = parseInt(number, 10) - 1;
            if (index >= 0 && index < validPosts.length) {
                validPosts[index].link.click();
                return true;
            }
            return false;
        },
        formatDates() {
            // 오늘 날짜를 'YYYY-MM-DD' 형식으로 가져옵니다.
            const today = new Date();
            const todayDateString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;

            // 아직 포맷되지 않은 '.gall_date' 셀들을 선택합니다.
            const dateCells = document.querySelectorAll('td.gall_date:not(.date-formatted)');

            dateCells.forEach(dateCell => {
                // title 속성에 전체 타임스탬프가 있는지 확인합니다.
                if (dateCell.title) {
                    const fullTimestamp = dateCell.title; // 예: "2025-04-08 17:03:17"

                    // 정규 표현식을 사용하여 년, 월, 일, 시, 분을 추출합니다.
                    const match = fullTimestamp.match(/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):\d{2}/);
                    // match[1]: YYYY, match[2]: MM, match[3]: DD, match[4]: HH, match[5]: mm

                    if (match) {
                        const postYear = match[1];
                        const postMonth = match[2];
                        const postDay = match[3];
                        const postHour = match[4];
                        const postMinute = match[5];
                        const postDateString = `${postYear}-${postMonth}-${postDay}`; // 게시글 작성일 (YYYY-MM-DD)

                        let formattedDate = ''; // 최종 표시될 문자열 초기화

                        // 게시글 작성일과 오늘 날짜 비교
                        if (postDateString === todayDateString) {
                            // 오늘 작성된 글이면 시간만 표시 (HH:MM)
                            formattedDate = `${postHour}:${postMinute}`;
                        } else {
                            // 오늘 이전에 작성된 글이면 날짜와 시간 표시 (MM.DD HH:MM)
                            formattedDate = `${postMonth}.${postDay} ${postHour}:${postMinute}`;
                        }

                        // 셀의 텍스트 내용을 결정된 형식으로 업데이트합니다.
                        // 단, 이미 원하는 형식과 동일하다면 DOM 조작 최소화를 위해 변경하지 않습니다.
                        if (dateCell.textContent !== formattedDate) {
                            dateCell.textContent = formattedDate;
                        }
                        // 처리 완료 표시 클래스를 추가하여 중복 작업을 방지합니다.
                        dateCell.classList.add('date-formatted');
                    }
                } else {
                    // title 속성이 없는 경우에도 처리된 것으로 표시합니다.
                    dateCell.classList.add('date-formatted');
                }
            });
        },
        // == 레이아웃 조정 함수 ==
        adjustColgroupWidths() {
            const colgroup = document.querySelector('table.gall_list colgroup');
            if (!colgroup) return;

            const cols = colgroup.querySelectorAll('col');
            let targetWidths = null; // 적용할 너비 배열 초기화

            // col 개수에 따라 다른 너비 배열 설정
            if (cols.length === 8) {
                // 8개인 경우 (예: 체크박스, 말머리, 아이콘, 제목, 글쓴이, 작성일, 조회, 추천)
                targetWidths = ['25px', '9%', '51px', null, '15%', '8%', '6%', '6%'];
                // console.log("말머리 + 아이콘 O (8 cols) 레이아웃 적용");
            } else if (cols.length === 7) {
                // 7개인 경우 (예: 번호, 말머리, 제목, 글쓴이, 작성일, 조회, 추천)
                targetWidths = ['9%', '51px', null, '15%', '8%', '6%', '6%'];
                // console.log("말머리 O (7 cols) 레이아웃 적용");
            } else if (cols.length === 6) {
                // 6개인 경우 (예: 번호, 제목, 글쓴이, 작성일, 조회, 추천)
                targetWidths = ['9%', null, '15%', '8%', '6%', '6%'];
                // console.log("말머리 X (6 cols) 레이아웃 적용");
            } else {
                // 예상과 다른 개수일 경우 경고 로그 남기고 종료
                console.warn("Colgroup 내 col 개수가 6, 7 또는 8이 아닙니다:", cols.length);
                return;
            }
            // 선택된 너비 배열(targetWidths)을 사용하여 스타일 적용
            cols.forEach((col, index) => {
                // targetWidths 배열 길이를 초과하는 인덱스는 무시 (안전 장치)
                if (index >= targetWidths.length) return;

                const targetWidth = targetWidths[index];

                if (targetWidth !== null) { // null이 아닌 경우 (너비 지정 필요)
                    // 현재 너비와 목표 너비가 다를 경우에만 변경
                    if (col.style.width !== targetWidth) {
                        col.style.width = targetWidth;
                    }
                } else { // null인 경우 (너비 지정 불필요, 브라우저 자동 계산)
                    // 기존에 width 스타일이 있었다면 제거
                    if (col.style.width) {
                        col.style.width = ''; // 또는 col.style.removeProperty('width');
                    }
                }
            });
        }
    };

    // Event Handlers
    const Events = {
        async triggerMacroNavigation() {
            const shouldRunZ = sessionStorage.getItem(MACRO_Z_RUNNING_KEY) === 'true';
            const shouldRunX = sessionStorage.getItem(MACRO_X_RUNNING_KEY) === 'true';

            if (shouldRunZ) {
                const enabled = await Storage.getShortcutEnabled('shortcutMacroZEnabled');
                if (enabled) {
                    console.log('Z Macro: Triggering previous post navigation after delay.');
                    // Add a small visual indicator that the macro is about to run
                    UI.showAlert('자동 이전 글 (2.5초 후)', 500); // Show brief alert
                    setTimeout(async () => {
                        // Double-check state *before* navigation, in case user cancelled
                        if (sessionStorage.getItem(MACRO_Z_RUNNING_KEY) === 'true') {
                            await this.navigatePrevPost();
                        } else {
                            console.log('Z Macro: Cancelled before navigation.');
                        }
                    }, MACRO_INTERVAL);
                } else {
                    // Feature disabled in UI, stop the macro state
                    console.log('Z Macro: Feature disabled, stopping.');
                    sessionStorage.setItem(MACRO_Z_RUNNING_KEY, 'false');
                }
            } else if (shouldRunX) {
                const enabled = await Storage.getShortcutEnabled('shortcutMacroXEnabled');
                if (enabled) {
                    console.log('X Macro: Triggering next post navigation after delay.');
                    UI.showAlert('자동 다음 글 (2.5초 후)', 500); // Show brief alert
                    setTimeout(async () => {
                        // Double-check state *before* navigation
                        if (sessionStorage.getItem(MACRO_X_RUNNING_KEY) === 'true') {
                            await this.navigateNextPost();
                        } else {
                            console.log('X Macro: Cancelled before navigation.');
                        }
                    }, MACRO_INTERVAL);
                } else {
                    // Feature disabled in UI, stop the macro state
                    console.log('X Macro: Feature disabled, stopping.');
                    sessionStorage.setItem(MACRO_X_RUNNING_KEY, 'false');
                }
            }
        },

        async toggleZMacro() {
            const enabled = await Storage.getShortcutEnabled('shortcutMacroZEnabled');
            if (!enabled) {
                UI.showAlert('이전 글 자동 넘김 기능이 비활성화 상태입니다.');
                sessionStorage.setItem(MACRO_Z_RUNNING_KEY, 'false'); // Ensure state is off
                return;
            }

            const isCurrentlyRunning = sessionStorage.getItem(MACRO_Z_RUNNING_KEY) === 'true';

            if (isCurrentlyRunning) {
                // Stop Z Macro
                sessionStorage.setItem(MACRO_Z_RUNNING_KEY, 'false');
                console.log('Z Macro stopped via toggle.');
                UI.showAlert('이전 글 자동 넘김 중지');
                // Clear any pending navigation timeout if the user stops it quickly
                // (This requires storing the timeout ID, slightly more complex. Let's omit for now)

            } else {
                // Start Z Macro
                sessionStorage.setItem(MACRO_Z_RUNNING_KEY, 'true');
                sessionStorage.setItem(MACRO_X_RUNNING_KEY, 'false'); // Ensure X is off
                console.log('Z Macro started via toggle. Navigating now...');
                UI.showAlert('이전 글 자동 넘김 시작 (ALT+Z로 중지)');
                await this.navigatePrevPost(); // Navigate *immediately* on start
            }
        },

        async toggleXMacro() {
            const enabled = await Storage.getShortcutEnabled('shortcutMacroXEnabled');
            if (!enabled) {
                UI.showAlert('다음 글 자동 넘김 기능이 비활성화 상태입니다.');
                sessionStorage.setItem(MACRO_X_RUNNING_KEY, 'false'); // Ensure state is off
                return;
            }

            const isCurrentlyRunning = sessionStorage.getItem(MACRO_X_RUNNING_KEY) === 'true';

            if (isCurrentlyRunning) {
                // Stop X Macro
                sessionStorage.setItem(MACRO_X_RUNNING_KEY, 'false');
                console.log('X Macro stopped via toggle.');
                UI.showAlert('다음 글 자동 넘김 중지');
                // Clear any pending navigation timeout if the user stops it quickly (optional enhancement)

            } else {
                // Start X Macro
                sessionStorage.setItem(MACRO_X_RUNNING_KEY, 'true');
                sessionStorage.setItem(MACRO_Z_RUNNING_KEY, 'false'); // Ensure Z is off
                console.log('X Macro started via toggle. Navigating now...');
                UI.showAlert('다음 글 자동 넘김 시작 (ALT+X로 중지)');
                await this.navigateNextPost(); // Navigate *immediately* on start
            }
        },
        // --- End NEW Macro Control Functions ---

        getFirstValidPostLink(doc) {
            // 특정 갤러리 목록을 감싸는 컨테이너 찾기
            const galleryListWrap = doc.querySelector('.gall_listwrap'); // <<< 범위 제한 추가
            if (!galleryListWrap) {
                console.error("Could not find gallery list container (.gall_listwrap) in fetched document.");
                return null;
            }

            // 찾은 컨테이너 내부의 tbody 안의 tr 만을 대상으로 함
            const rows = galleryListWrap.querySelectorAll('tbody tr'); // <<< 범위 제한 추가

            for (const row of rows) {
                // isValidPost 검사는 동일
                if (Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                    const link = row.querySelector('td.gall_tit a:first-child');
                    if (link && link.href) {
                        // 절대 URL 반환 로직은 동일
                        return new URL(link.getAttribute('href'), doc.baseURI).href;
                    }
                }
            }
            return null; // 유효한 링크 못 찾음
        },

        findPaginationLink(direction = 'next') { // 'next' 또는 'prev'
            let targetLinkElement = null;
            let targetPagingBox = null;

            // 페이징 박스 찾기 (기존 A/S 로직과 동일)
            const exceptionPagingWrap = document.querySelector('.bottom_paging_wrapre');
            if (exceptionPagingWrap) {
                targetPagingBox = exceptionPagingWrap.querySelector('.bottom_paging_box');
            } else {
                const normalPagingWraps = document.querySelectorAll('.bottom_paging_wrap');
                if (normalPagingWraps.length > 1) {
                    targetPagingBox = normalPagingWraps[1]?.querySelector('.bottom_paging_box');
                } else if (normalPagingWraps.length === 1) {
                    targetPagingBox = normalPagingWraps[0]?.querySelector('.bottom_paging_box');
                }
            }

            if (targetPagingBox) {
                const currentPageElement = targetPagingBox.querySelector('em');

                if (direction === 'prev') {
                    // 이전 링크 찾기
                    if (currentPageElement) {
                        const prevSibling = currentPageElement.previousElementSibling;
                        if (prevSibling?.tagName === 'A' && prevSibling.hasAttribute('href')) {
                            targetLinkElement = prevSibling;
                        }
                    } else { // em 없는 경우 (검색 등)
                        targetLinkElement = targetPagingBox.querySelector('a.search_prev[href]');
                    }
                } else { // direction === 'next'
                    // 다음 링크 찾기
                    if (currentPageElement) {
                        const nextSibling = currentPageElement.nextElementSibling;
                        if (nextSibling?.tagName === 'A' && nextSibling.hasAttribute('href')) {
                            targetLinkElement = nextSibling;
                        }
                    } else { // em 없는 경우 (검색 등)
                        targetLinkElement = targetPagingBox.querySelector('a.search_next[href]');
                    }
                }
            }
            return targetLinkElement; // 찾은 링크 요소 또는 null 반환
        },

        saveScrollPosition() {
            sessionStorage.setItem('dcinsideShortcut_scrollPos', window.scrollY);
        },

        numberInput: { mode: false, buffer: '', timeout: null, display: null },

        async handleKeydown(event) {
            // --- 디버깅 로그 추가 ---
            // console.log(`Keydown: Key='${event.key}', Code='${event.code}', Alt=${event.altKey}`);

            // Check for Macro Toggles FIRST
            if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
                // --- 디버깅 로그 추가 ---
                // console.log(`Alt key pressed: Code='${event.code}'`);

                // --- event.code 사용으로 변경 ---
                if (event.code === 'KeyZ') { // 'KeyZ'는 대부분의 표준 키보드에서 Z키의 코드입니다.
                    // console.log("Alt+Z (event.code) condition met!"); // 디버깅 확인용
                    event.preventDefault(); // 기본 동작 및 다른 리스너로의 전파 방지
                    event.stopPropagation(); // 이벤트 버블링 중단 (혹시 모를 상위 요소 리스너 방지)
                    await this.toggleZMacro();
                    return; // 처리 완료
                }
                if (event.code === 'KeyX') { // 'KeyX'는 X키의 코드입니다.
                    // console.log("Alt+X (event.code) condition met!"); // 디버깅 확인용
                    event.preventDefault();
                    event.stopPropagation();
                    await this.toggleXMacro();
                    return; // 처리 완료
                }
                // --- 변경 끝 ---

                // --- Existing Alt key logic ---
                if (event.key === 'w' || event.key === 'W') {
                    event.preventDefault();
                    // Check if macro is running, if so, maybe prevent 글쓰기 등록? Or stop macro?
                    // Decide on desired behavior. For now, it proceeds.
                    const writeButton = document.querySelector('button.btn_lightpurple.btn_svc.write[type="image"]');
                    if (writeButton) writeButton.click();
                } else if (event.key >= '0' && event.key <= '9') {
                    event.preventDefault();
                    const enabled = await Storage.getAltNumberEnabled();
                    if (enabled) {
                        Gallery.handleFavoriteKey(event.key);
                    }
                } else if (event.key === '`') {
                    event.preventDefault();
                    const ui = document.querySelector('div[style*="position: fixed; top: 50%"]');
                    ui ? ui.remove() : UI.showFavorites();
                }
                // --- End Existing Alt key logic ---
            } else if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
                // Prevent regular Z/X if the corresponding macro *should* be running (optional but good)
                if ((event.key.toUpperCase() === 'Z' && sessionStorage.getItem(MACRO_Z_RUNNING_KEY) === 'true') ||
                    (event.key.toUpperCase() === 'X' && sessionStorage.getItem(MACRO_X_RUNNING_KEY) === 'true')) {
                    console.log(`Macro state is active, ignoring regular ${event.key.toUpperCase()} press.`);
                    event.preventDefault(); // Prevent default Z/X navigation if macro is on
                    return;
                }

                // Check standard shortcut enabled status (no change here)
                const stdShortcutEnabled = await Storage.getShortcutEnabled(`shortcut${event.key.toUpperCase()}Enabled`);
                if (stdShortcutEnabled) {
                    this.handleNavigationKeys(event);
                }
            }
        },

        async loadPageContentAjax(targetLinkUrl, isViewMode) {
            UI.showAlert('로딩 중...');
            let finalUrlToPush = targetLinkUrl;
            let urlToFetch = targetLinkUrl;

            try {
                // Calculate urlToFetch and finalUrlToPush based on isViewMode
                if (isViewMode) {
                    try {
                        const currentUrl = new URL(window.location.href);
                        const targetUrlObj = new URL(targetLinkUrl, window.location.origin);
                        const targetParams = targetUrlObj.searchParams;
                        const targetPage = targetParams.get('page');
                        if (targetPage) {
                            currentUrl.searchParams.set('page', targetPage);
                            ['search_pos', 's_type', 's_keyword', 'exception_mode'].forEach(param => {
                                if (targetParams.has(param)) {
                                    currentUrl.searchParams.set(param, targetParams.get(param));
                                }
                            });
                            urlToFetch = currentUrl.toString();
                            finalUrlToPush = urlToFetch;
                        } else {
                            throw new Error("Target link missing 'page' parameter");
                        }
                    } catch (e) {
                        console.error("Error constructing URL for AJAX fetch in view mode:", e);
                        window.location.href = targetLinkUrl; // Fallback navigation
                        return;
                    }
                }

                // Fetch content
                const response = await fetch(urlToFetch);
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const htmlText = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(htmlText, 'text/html');

                // Find NEW content
                const newTbody = doc.querySelector('table.gall_list tbody');
                let newPagingWrapElement = null;
                const exceptionWrap = doc.querySelector('.bottom_paging_wrapre');
                if (exceptionWrap) {
                    newPagingWrapElement = exceptionWrap;
                } else {
                    const normalWraps = doc.querySelectorAll('.bottom_paging_wrap');
                    if (normalWraps.length > 1) { newPagingWrapElement = normalWraps[1]; }
                    else if (normalWraps.length === 1) { newPagingWrapElement = normalWraps[0]; }
                }

                // Find CURRENT elements
                const currentTbody = document.querySelector('table.gall_list tbody');
                let currentPagingWrapElement = document.querySelector('.bottom_paging_wrapre');
                if (!currentPagingWrapElement) {
                    const currentNormalWraps = document.querySelectorAll('.bottom_paging_wrap');
                    if (currentNormalWraps.length > 1) { currentPagingWrapElement = currentNormalWraps[1]; }
                    else if (currentNormalWraps.length === 1){ currentPagingWrapElement = currentNormalWraps[0]; }
                }

                // Validate and Replace
                if (!newTbody) throw new Error("Could not find 'tbody' in fetched content.");
                if (!currentTbody) throw new Error("Could not find current 'tbody' to replace.");
                currentTbody.innerHTML = newTbody.innerHTML; // Replace tbody

                if (newPagingWrapElement && currentPagingWrapElement) {
                    currentPagingWrapElement.innerHTML = newPagingWrapElement.innerHTML; // Replace pagination
                } else if (!newPagingWrapElement && currentPagingWrapElement) {
                    currentPagingWrapElement.innerHTML = ''; // Clear current pagination
                    console.log("Fetched page has no pagination, clearing current.");
                } else if (newPagingWrapElement && !currentPagingWrapElement) {
                    console.warn("Current page missing pagination wrap, cannot insert new pagination dynamically.");
                }

                // Re-run initializations
                Posts.adjustColgroupWidths(); Posts.addNumberLabels(); Posts.formatDates();

                // Update Browser URL
                history.pushState(null, '', finalUrlToPush);

                // Scroll to top of list
                currentTbody.closest('table.gall_list')?.scrollIntoView({ behavior: 'auto', block: 'start' });

                // Remove loading indicator
                const loadingAlert = Array.from(document.querySelectorAll('div[style*="position: fixed"]')).find(el => el.textContent === '로딩 중...');
                if (loadingAlert) loadingAlert.remove();

                // Update Prefetch Hints After AJAX Load
                addPrefetchHints(); // <<< AJAX 완료 후 프리페칭 힌트 업데이트

            } catch (error) {
                console.error('Failed to load page content via AJAX:', error);
                UI.showAlert('오류 발생: 페이지 로딩 실패');
                // Fallback to full page navigation
                console.log("Falling back to full page navigation.");
                // Fallback should use the original TARGET link URL, not the potentially modified urlToFetch
                window.location.href = targetLinkUrl;
            }
        }, // Add comma if needed

        handleNavigationKeys(event) {
            const active = document.activeElement;
            if (active && ['TEXTAREA', 'INPUT'].includes(active.tagName) || active.isContentEditable) return;

            if (['`', '.'].includes(event.key)) {
                event.preventDefault();
                this.toggleNumberInput(event.key);
                return;
            }

            if (this.numberInput.mode) {
                this.handleNumberInput(event);
                return;
            }

            if (event.key >= '0' && event.key <= '9') {
                const index = event.key === '0' ? 9 : parseInt(event.key, 10) - 1;
                const { validPosts } = Posts.getValidPosts();
                if (index < validPosts.length) validPosts[index].link.click();
                return;
            }

            this.handleShortcuts(event.key.toUpperCase(), event);
        },

        toggleNumberInput(key) {
            if (this.numberInput.mode && this.numberInput.buffer) {
                Posts.navigate(this.numberInput.buffer);
                this.exitNumberInput();
            } else {
                this.numberInput.mode = true;
                this.numberInput.buffer = '';
                this.updateNumberDisplay('Post number: ');
                this.resetNumberTimeout();
            }
        },

        handleNumberInput(event) {
            event.preventDefault();
            if (event.key >= '0' && event.key <= '9') {
                this.numberInput.buffer += event.key;
                this.updateNumberDisplay(`Post number: ${this.numberInput.buffer}`);
                this.resetNumberTimeout();
            } else if (event.key === 'Enter' && this.numberInput.buffer) {
                Posts.navigate(this.numberInput.buffer);
                this.exitNumberInput();
            } else if (event.key === 'Escape') {
                this.exitNumberInput();
            }
        },

        updateNumberDisplay(text) {
            if (!this.numberInput.display) {
                this.numberInput.display = UI.createElement('div', {
                    position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0,0,0,0.7)',
                    color: 'white', padding: '10px 15px', borderRadius: '5px', fontSize: '16px',
                    fontWeight: 'bold', zIndex: '9999'
                });
                document.body.appendChild(this.numberInput.display);
            }
            this.numberInput.display.textContent = text;
        },

        resetNumberTimeout() {
            clearTimeout(this.numberInput.timeout);
            this.numberInput.timeout = setTimeout(() => this.exitNumberInput(), 3000);
        },

        exitNumberInput() {
            this.numberInput.mode = false;
            this.numberInput.buffer = '';
            clearTimeout(this.numberInput.timeout);
            this.numberInput.timeout = null;
            if (this.numberInput.display) {
                this.numberInput.display.remove();
                this.numberInput.display = null;
            }
        },

        async handleShortcuts(key, event) {
            const { galleryType, galleryId, currentPage, isRecommendMode } = Gallery.getPageInfo(); // isRecommendMode 추가
            const isViewMode = window.location.pathname.includes('/board/view/'); // isViewMode 정의

            // --- Simple navigation function (NO scroll saving) ---
            const navigate = url => {
                if (document.readyState === 'complete') {
                    window.location.href = url;
                } else {
                    window.addEventListener('load', () => window.location.href = url, { once: true });
                }
            };

            // Get saved shortcut keys
            const savedKeys = {
                'W': await Storage.getShortcutKey('shortcutWKey') || 'W',
                'C': await Storage.getShortcutKey('shortcutCKey') || 'C',
                'D': await Storage.getShortcutKey('shortcutDKey') || 'D',
                'R': await Storage.getShortcutKey('shortcutRKey') || 'R',
                'Q': await Storage.getShortcutKey('shortcutQKey') || 'Q',
                'E': await Storage.getShortcutKey('shortcutEKey') || 'E',
                'F': await Storage.getShortcutKey('shortcutFKey') || 'F',
                'G': await Storage.getShortcutKey('shortcutGKey') || 'G',
                'A': await Storage.getShortcutKey('shortcutAKey') || 'A',
                'S': await Storage.getShortcutKey('shortcutSKey') || 'S',
                'Z': await Storage.getShortcutKey('shortcutZKey') || 'Z',
                'X': await Storage.getShortcutKey('shortcutXKey') || 'X'
            };

            // Determine base URLs for F and G keys
            let basePath = '';
            if (galleryType !== 'board') { basePath = `/${galleryType}`; }
            const listPath = `/board/lists/`;
            const baseListUrl = `https://gall.dcinside.com${basePath}${listPath}?id=${galleryId}`;
            const recommendListUrl = `${baseListUrl}&exception_mode=recommend`;

            switch (key) {
                case savedKeys['W']: document.querySelector('button#btn_write')?.click(); break;
                case savedKeys['C']: event.preventDefault(); document.querySelector('textarea[id^="memo_"]')?.focus(); break;
                case savedKeys['D']: document.querySelector('button.btn_cmt_refresh')?.click(); break;
                case savedKeys['R']: location.reload(); break; // Full reload
                case savedKeys['Q']: window.scrollTo(0, 0); break;
                case savedKeys['E']: document.querySelector('table.gall_list')?.scrollIntoView({ block: 'start' }); break;
                    // F and G now use the simple navigate without scroll saving
                case savedKeys['F']: navigate(baseListUrl); break;
                case savedKeys['G']: navigate(recommendListUrl); break;

                    // --- REVISED A and S Logic (Mode Switching + View Mode Handling) ---
                case savedKeys['A']: // 이전 페이지
                case savedKeys['S']: { // 다음 페이지
                    let targetLinkElement = null;
                    let targetPagingBox = null;
                    let emExistsInTarget = false;

                    // 1. Determine the correct paging box (동일 로직)
                    const exceptionPagingWrap = document.querySelector('.bottom_paging_wrapre');
                    if (exceptionPagingWrap) {
                        targetPagingBox = exceptionPagingWrap.querySelector('.bottom_paging_box');
                    } else {
                        const normalPagingWraps = document.querySelectorAll('.bottom_paging_wrap');
                        if (normalPagingWraps.length > 1) {
                            targetPagingBox = normalPagingWraps[1]?.querySelector('.bottom_paging_box');
                        } else if (normalPagingWraps.length === 1) {
                            targetPagingBox = normalPagingWraps[0]?.querySelector('.bottom_paging_box');
                        }
                    }

                    // 2. Find the target link element (동일 로직)
                    if (targetPagingBox) {
                        const currentPageElement = targetPagingBox.querySelector('em');
                        emExistsInTarget = !!currentPageElement; // em 존재 여부 확인

                        if (key === savedKeys['A']) {
                            if (currentPageElement) {
                                const prevSibling = currentPageElement.previousElementSibling;
                                if (prevSibling?.tagName === 'A' && prevSibling.hasAttribute('href')) { targetLinkElement = prevSibling; }
                            } else { // em이 없는 경우 (검색 결과 등) prev 버튼 찾기
                                targetLinkElement = targetPagingBox.querySelector('a.search_prev[href]');
                            }
                        } else { // key === savedKeys['S']
                            if (currentPageElement) {
                                const nextSibling = currentPageElement.nextElementSibling;
                                if (nextSibling?.tagName === 'A' && nextSibling.hasAttribute('href')) { targetLinkElement = nextSibling; }
                            } else { // em이 없는 경우 next 버튼 찾기
                                targetLinkElement = targetPagingBox.querySelector('a.search_next[href]');
                            }
                        }
                    } // end if(targetPagingBox)

                    // 3. Get Navigation Mode and Execute
                    if (targetLinkElement) {
                        const targetUrlFromHref = targetLinkElement.href; // 링크의 href 속성값
                        const navMode = await Storage.getPageNavigationMode(); // 설정값 읽기

                        if (navMode === 'ajax') {
                            // AJAX 모드: 기존 로직 실행 (isViewMode 전달)
                            this.loadPageContentAjax(targetUrlFromHref, isViewMode);
                        } else {
                            // --- Full Load 모드 ---
                            // <<< 스크롤 위치 저장 호출 추가 >>>
                            this.saveScrollPosition();
                            if (isViewMode) {
                                // 현재 페이지가 View 모드일 때: 현재 URL 기반으로 page와 search_pos 변경
                                try {
                                    // 1. 대상 링크(targetLinkElement)의 href에서 목표 page와 search_pos 추출
                                    const linkUrl = new URL(targetUrlFromHref);
                                    const targetPage = linkUrl.searchParams.get('page');
                                    const targetSearchPos = linkUrl.searchParams.get('search_pos'); // 대상 링크의 search_pos 값

                                    if (targetPage) {
                                        // 2. 현재 페이지 URL을 가져와서 필요한 파라미터만 교체
                                        const currentUrl = new URL(window.location.href);
                                        currentUrl.searchParams.set('page', targetPage); // page 파라미터 업데이트

                                        // 3. search_pos 파라미터 처리
                                        if (targetSearchPos) {
                                            // 대상 링크에 search_pos가 있으면 그 값으로 설정
                                            currentUrl.searchParams.set('search_pos', targetSearchPos);
                                        } else {
                                            // 대상 링크에 search_pos가 없으면 현재 URL에서도 제거
                                            currentUrl.searchParams.delete('search_pos');
                                        }

                                        // 4. 변경된 URL로 이동 (페이지 새로고침 발생)
                                        window.location.href = currentUrl.toString();
                                    } else {
                                        // 링크 URL에 page 파라미터가 없는 예외적인 경우 (Fallback)
                                        console.warn("Full Load (View Mode): Target link missing 'page' parameter. Navigating directly.", targetUrlFromHref);
                                        window.location.href = targetUrlFromHref;
                                    }
                                } catch (e) {
                                    console.error("Full Load (View Mode): Error processing URL. Navigating directly.", e, targetUrlFromHref);
                                    // URL 처리 중 오류 발생 시 (Fallback)
                                    window.location.href = targetUrlFromHref;
                                }
                            } else {
                                // 현재 페이지가 View 모드가 아닐 때 (List 모드): 그냥 대상 링크 URL로 이동
                                window.location.href = targetUrlFromHref;
                            }
                            // --- Full Load 모드 끝 ---
                        }

                    } else if (key === savedKeys['A']) {
                        // 첫 페이지 알림 (em이 있을 때만)
                        if (emExistsInTarget) {
                            UI.showAlert('첫 페이지입니다.');
                        }
                    }
                    break; // End of A/S block
                }
                    // --- END REVISED A and S Logic ---

                case savedKeys['Z']: await this.navigatePrevPost(galleryType, galleryId, currentPage); break; // Z/X still use full navigation
                case savedKeys['X']: await this.navigateNextPost(galleryType, galleryId, currentPage); break; // Z/X still use full navigation
            }
        },
        async navigatePrevPost() {
            const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
            if (!crtIcon) return; // Not on a post view or can't find current post icon

            // --- Try finding the previous post on the CURRENT page first ---
            let row = crtIcon.closest('tr')?.previousElementSibling;
            while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                row = row.previousElementSibling;
            }

            if (row) {
                // --- Found previous post on the same page ---
                const prevLinkElement = row.querySelector('td.gall_tit a:first-child');
                if (prevLinkElement) {
                    window.location.href = prevLinkElement.href; // Navigate directly
                }
                return; // Done
            }

            // --- If no previous post on the current page, check for previous PAGE link ---
            const prevPageLinkElement = this.findPaginationLink('prev'); // Find '<' or 'prev search' link

            if (prevPageLinkElement && prevPageLinkElement.href) {
                // --- Previous PAGE link exists ---
                const isPrevSearchLink = prevPageLinkElement.classList.contains('search_prev');

                if (isPrevSearchLink) {
                    // --- Handle "Previous Search" link (special logic) ---
                    console.log("[Z Nav] Detected 'Previous Search' link. Starting special logic...");
                    try {
                        const prevSearchBlockFirstPageUrl = new URL(prevPageLinkElement.href);
                        console.log("[Z Nav] Fetching previous search block page 1:", prevSearchBlockFirstPageUrl.toString());
                        const doc1 = await this.fetchPage(prevSearchBlockFirstPageUrl.toString());

                        const allPagingBoxes = doc1.querySelectorAll('.bottom_paging_box');
                        let pagingBox1 = null;
                        console.log(`[Z Nav] Found ${allPagingBoxes.length} paging boxes in fetched page 1.`);
                        if (allPagingBoxes.length > 1) {
                            pagingBox1 = allPagingBoxes[1]; // Assume second is gallery list
                            console.log("[Z Nav] Using the second paging box.");
                        } else if (allPagingBoxes.length === 1) {
                            pagingBox1 = allPagingBoxes[0]; // Use the only one found
                            console.log("[Z Nav] Only one paging box found, using it.");
                        }

                        if (!pagingBox1) {
                            throw new Error("Could not find the relevant pagination box on the first page of the previous search block.");
                        }
                        console.log("[Z Nav] Relevant paging box content:", pagingBox1.innerHTML);

                        let lastPageNum = 1;
                        const nextSearchLink1 = pagingBox1.querySelector('a.search_next');
                        if (nextSearchLink1) {
                            const lastPageLinkElement = nextSearchLink1.previousElementSibling;
                            if (lastPageLinkElement?.tagName === 'A' && lastPageLinkElement.href) {
                                const pageNumStr = new URL(lastPageLinkElement.href).searchParams.get('page');
                                if (pageNumStr) lastPageNum = parseInt(pageNumStr, 10);
                            }
                        } else {
                            const pageLinks = pagingBox1.querySelectorAll('a:not(.search_prev):not(.search_next)');
                            if (pageLinks.length > 0) {
                                const lastLink = pageLinks[pageLinks.length - 1];
                                if(lastLink?.href){
                                    const pageNumStr = new URL(lastLink.href).searchParams.get('page');
                                    if (pageNumStr) lastPageNum = parseInt(pageNumStr, 10);
                                }
                            }
                        }
                        console.log("[Z Nav] Calculated lastPageNum for previous block:", lastPageNum);

                        const prevSearchBlockLastPageUrl = new URL(prevSearchBlockFirstPageUrl);
                        prevSearchBlockLastPageUrl.searchParams.set('page', lastPageNum.toString());
                        console.log("[Z Nav] Fetching previous search block last page:", prevSearchBlockLastPageUrl.toString());
                        const doc2 = await this.fetchPage(prevSearchBlockLastPageUrl.toString());

                        const finalPostLinkHref = this.getLastValidPostLink(doc2);
                        console.log("[Z Nav] Found finalPostLinkHref on last page:", finalPostLinkHref);

                        if (finalPostLinkHref) {
                            const targetPostUrl = new URL(finalPostLinkHref);
                            const targetNo = targetPostUrl.searchParams.get('no');

                            if (targetNo) {
                                const currentUrl = new URL(window.location.href);
                                currentUrl.searchParams.set('no', targetNo);
                                currentUrl.searchParams.set('page', lastPageNum.toString());
                                const targetSearchPos = prevSearchBlockFirstPageUrl.searchParams.get('search_pos');
                                if (targetSearchPos) currentUrl.searchParams.set('search_pos', targetSearchPos);
                                else currentUrl.searchParams.delete('search_pos');

                                console.log("[Z Nav] Final navigation URL:", currentUrl.toString());
                                window.location.href = currentUrl.toString();
                            } else { throw new Error("Could not extract 'no' from final post link."); }
                        } else { throw new Error("Could not find the last valid post on the last page of the previous search block."); }

                    } catch (error) {
                        console.error("[Z Nav] Error processing 'Previous Search' navigation:", error);
                        UI.showAlert('"이전 검색" 블록 이동 중 오류가 발생했습니다.');
                        window.location.href = prevPageLinkElement.href; // Fallback
                    }
                } else {
                    // --- Handle regular previous page link ---
                    try {
                        const doc = await this.fetchPage(prevPageLinkElement.href);
                        const lastValidLinkHref = this.getLastValidPostLink(doc);

                        if (lastValidLinkHref) {
                            const targetLinkUrl = new URL(lastValidLinkHref);
                            const targetNo = targetLinkUrl.searchParams.get('no');

                            if (targetNo) {
                                const currentUrl = new URL(window.location.href);
                                currentUrl.searchParams.set('no', targetNo);
                                const prevPageListUrl = new URL(prevPageLinkElement.href);
                                const targetPage = prevPageListUrl.searchParams.get('page');
                                if (targetPage) currentUrl.searchParams.set('page', targetPage);
                                const targetSearchPos = prevPageListUrl.searchParams.get('search_pos');
                                if (targetSearchPos) currentUrl.searchParams.set('search_pos', targetSearchPos);
                                else currentUrl.searchParams.delete('search_pos');

                                window.location.href = currentUrl.toString();
                            } else { throw new Error("Could not extract 'no' from last valid post link."); }
                        } else { UI.showAlert('이전 페이지에 표시할 게시글이 없습니다.'); }
                    } catch (error) {
                        console.error("Error fetching/processing previous page for Z nav:", error);
                        UI.showAlert('이전 페이지 로딩 중 오류가 발생했습니다.');
                    }
                }
            } else {
                // --- NO previous post on current page AND NO previous page link ---
                // --- This means we are on the first post of page 1. Check for newer posts. ---
                const currentUrl = new URL(window.location.href);
                const currentPostNoStr = currentUrl.searchParams.get('no');

                if (!currentPostNoStr) {
                    UI.showAlert('현재 글 번호를 찾을 수 없습니다. 페이지를 새로고침 해주세요.');
                    return;
                }
                const currentPostNo = parseInt(currentPostNoStr, 10);
                if (isNaN(currentPostNo)) {
                    UI.showAlert('현재 글 번호가 유효하지 않습니다.');
                    return;
                }

                // Construct the URL for the list view of the current page (page 1)
                const listUrl = new URL(window.location.href);
                // Make sure path points to lists, not view
                listUrl.pathname = listUrl.pathname.replace(/(\/board)\/view\/?/, '$1/lists/');
                listUrl.searchParams.set('page', '1'); // Explicitly page 1
                listUrl.searchParams.delete('no');    // Remove post number

                UI.showAlert('최신 글 확인 중...'); // Feedback

                try {
                    const doc = await this.fetchPage(listUrl.toString());
                    // Get all valid posts from the fetched page 1 list
                    const allPostsOnPage1 = this.getValidPostsFromDoc(doc); // Uses existing helper

                    // Check if any post on the fetched list is newer than the currently viewed post
                    const newerPosts = allPostsOnPage1.filter(p => p.num > currentPostNo);

                    if (newerPosts.length > 0) {
                        // Newer posts exist! Navigate to the newest one found on page 1.
                        const newestPost = allPostsOnPage1.sort((a, b) => b.num - a.num)[0];

                        if (newestPost && newestPost.link) {
                            UI.showAlert('새로운 글을 발견하여 이동합니다.');

                            // Construct the VIEW url for the newest post
                            const targetViewUrl = new URL(newestPost.link); // Base link from list item
                            targetViewUrl.searchParams.set('page', '1'); // Set page=1 for context

                            // Preserve relevant parameters (like exception_mode, search) from the current URL
                            const currentSearchParams = new URLSearchParams(window.location.search);
                            ['exception_mode', 'search_pos', 's_type', 's_keyword'].forEach(param => {
                                if (currentSearchParams.has(param)) {
                                    targetViewUrl.searchParams.set(param, currentSearchParams.get(param));
                                }
                            });

                            window.location.href = targetViewUrl.toString();

                        } else {
                            // Should not happen if newerPosts.length > 0, but as a fallback:
                            UI.showAlert('첫 게시글입니다.');
                        }
                    } else {
                        // No newer posts found after checking.
                        UI.showAlert('첫 게시글입니다.');
                    }
                } catch (error) {
                    console.error("Error checking for newer posts:", error);
                    UI.showAlert('최신 글 확인 중 오류가 발생했습니다.');
                    // Optionally fall back to just showing the original alert
                    // UI.showAlert('첫 게시글입니다.');
                }
            }
            // --- End of Page Boundary / Newer Post Check ---
        }, // End navigatePrevPost

        async navigateNextPost() {
            const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
            let nextValidRowLink = null;

            if (crtIcon) {
                let row = crtIcon.closest('tr')?.nextElementSibling;
                while (row) {
                    if (Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                        nextValidRowLink = row.querySelector('td.gall_tit a:first-child');
                        break;
                    }
                    row = row.nextElementSibling;
                }
            }

            if (nextValidRowLink) {
                // --- 현재 페이지 내 다음 글 처리 ---

                window.location.href = nextValidRowLink.href; // 스크롤 복원과 함께 직접 이동
            } else {
                // --- 페이지 경계 처리 (Fetch 후 View URL 재구성) ---
                const nextPageLink = this.findPaginationLink('next'); // 다음 페이지 목록 링크 찾기

                if (nextPageLink && nextPageLink.href) {
                    try {
                        // 1. 다음 페이지 목록 HTML 가져오기
                        const doc = await this.fetchPage(nextPageLink.href);
                        // 2. 다음 페이지의 첫 번째 유효 글 링크 찾기 (절대 URL 반환)
                        const firstValidLinkHref = this.getFirstValidPostLink(doc);

                        if (firstValidLinkHref) {
                            // 3. 첫 번째 글 링크에서 'no' 값 추출
                            const targetLinkUrl = new URL(firstValidLinkHref);
                            const targetNo = targetLinkUrl.searchParams.get('no');

                            if (targetNo) {
                                // 4. 현재 URL을 기준으로 최종 이동 URL 생성
                                const currentUrl = new URL(window.location.href);
                                //    - 'no' 업데이트
                                currentUrl.searchParams.set('no', targetNo);
                                //    - 'page' 업데이트 (다음 페이지 목록 링크에서 가져오기)
                                const nextPageListUrl = new URL(nextPageLink.href);
                                const targetPage = nextPageListUrl.searchParams.get('page');
                                if (targetPage) {
                                    currentUrl.searchParams.set('page', targetPage);
                                }
                                //    - 'search_pos' 업데이트 (다음 페이지 목록 링크에서 가져오기)
                                const targetSearchPos = nextPageListUrl.searchParams.get('search_pos');
                                if (targetSearchPos) {
                                    currentUrl.searchParams.set('search_pos', targetSearchPos);
                                } else {
                                    currentUrl.searchParams.delete('search_pos');
                                }
                                //    - s_type, s_keyword 등은 현재 URL의 값 유지됨

                                // 5. 스크롤 위치 저장 및 최종 URL로 이동
                                window.location.href = currentUrl.toString();

                            } else {
                                console.error("Could not extract 'no' from first valid post link:", firstValidLinkHref);
                                UI.showAlert('다음 글 정보 로딩 중 오류가 발생했습니다.');
                            }
                        } else {
                            UI.showAlert('다음 페이지에 표시할 게시글이 없습니다.');
                        }
                    } catch (error) {
                        console.error("Error fetching/processing next page for X nav:", error);
                        UI.showAlert('다음 페이지 로딩 중 오류가 발생했습니다.');
                    }
                } else {
                    UI.showAlert('마지막 게시글입니다.');
                }
                // --- 페이지 경계 처리 끝 ---
            }
        },
        getNextValidLink() {
            const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
            if (!crtIcon) return null;
            let row = crtIcon.closest('tr')?.nextElementSibling;
            while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                row = row.nextElementSibling;
            }
            return row?.querySelector('td.gall_tit a:first-child');
        },

        async fetchPage(url) {
            const response = await fetch(url);
            const text = await response.text();
            return new DOMParser().parseFromString(text, 'text/html');
        },

        getLastValidPostLink(doc) {
            // 특정 갤러리 목록을 감싸는 컨테이너를 먼저 찾음
            // '.gall_listwrap' 클래스를 가진 첫 번째 요소를 대상으로 가정
            // 만약 구조가 다르다면 이 선택자를 조정해야 할 수 있음
            const galleryListWrap = doc.querySelector('.gall_listwrap'); // <<< 범위 제한 추가
            if (!galleryListWrap) {
                console.error("Could not find gallery list container (.gall_listwrap) in fetched document.");
                return null; // 컨테이너 못 찾으면 null 반환
            }

            // 찾은 컨테이너 내부의 tbody 안의 tr 만을 대상으로 함
            const rows = Array.from(galleryListWrap.querySelectorAll('tbody tr')); // <<< 범위 제한 추가

            for (let i = rows.length - 1; i >= 0; i--) {
                const row = rows[i];
                // isValidPost 검사는 동일
                if (Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                    const link = row.querySelector('td.gall_tit a:first-child');
                    if (link && link.href) {
                        // 절대 URL 반환 로직은 동일
                        return new URL(link.getAttribute('href'), doc.baseURI).href;
                    }
                }
            }
            return null; // 유효한 링크 못 찾음
        },

        getNewerPosts(doc, currentNo) {
            const posts = this.getValidPostsFromDoc(doc);
            return posts.filter(p => p.num > currentNo).sort((a, b) => a.num - b.num);
        },

        getValidPostsFromDoc(doc) {
            return Array.from(doc.querySelectorAll('table.gall_list tbody tr'))
                .filter(row => Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject')))
                .map(row => {
                const num = parseInt(row.querySelector('td.gall_num').textContent.trim().replace(/\[\d+\]\s*/, ''), 10);
                return { num, link: row.querySelector('td.gall_tit a:first-child')?.href };
            });
        }
    };

    // --- Prefetching Logic ---
    function addPrefetchHints() {
        // Check if prefetch is supported
        const isPrefetchSupported = (() => {
            const link = document.createElement('link');
            return link.relList && link.relList.supports && link.relList.supports('prefetch');
        })();

        if (!isPrefetchSupported) return;

        // --- Remove previously added hints by this script ---
        document.querySelectorAll('link[data-dc-prefetch="true"]').forEach(link => link.remove());

        // --- Function to add prefetch link to head ---
        const addHint = (href) => {
            if (!href) return;
            const fullHref = new URL(href, window.location.origin).toString();
            if (document.querySelector(`link[rel="prefetch"][href="${fullHref}"]`)) return;

            try {
                const link = document.createElement('link');
                link.rel = 'prefetch';
                link.href = fullHref;
                link.as = 'document';
                link.setAttribute('data-dc-prefetch', 'true');
                document.head.appendChild(link);
                // console.log('Prefetch hint added:', fullHref);
            } catch (e) {
                console.error("Failed to add prefetch hint:", fullHref, e);
            }
        };

        // --- 1. Prefetch Next/Previous PAGE Links ---
        let targetPagingBox = null;
        const exceptionPagingWrap = document.querySelector('.bottom_paging_wrapre');
        if (exceptionPagingWrap) {
            targetPagingBox = exceptionPagingWrap.querySelector('.bottom_paging_box');
        } else {
            const normalPagingWraps = document.querySelectorAll('.bottom_paging_wrap');
            if (normalPagingWraps.length > 1) { targetPagingBox = normalPagingWraps[1]?.querySelector('.bottom_paging_box'); }
            else if (normalPagingWraps.length === 1) { targetPagingBox = normalPagingWraps[0]?.querySelector('.bottom_paging_box'); }
        }

        if (targetPagingBox) {
            const currentPageElement = targetPagingBox.querySelector('em');
            let prevPageLinkHref = null;
            let nextPageLinkHref = null;

            if (currentPageElement) {
                const prevPageSibling = currentPageElement.previousElementSibling;
                if (prevPageSibling?.tagName === 'A' && prevPageSibling.hasAttribute('href')) { prevPageLinkHref = prevPageSibling.href; }
                const nextPageSibling = currentPageElement.nextElementSibling;
                if (nextPageSibling?.tagName === 'A' && nextPageSibling.hasAttribute('href')) { nextPageLinkHref = nextPageSibling.href; }
            } else { // No <em>, check for search prev/next
                prevPageLinkHref = targetPagingBox.querySelector('a.search_prev[href]')?.href;
                nextPageLinkHref = targetPagingBox.querySelector('a.search_next[href]')?.href;
            }
            addHint(prevPageLinkHref);
            addHint(nextPageLinkHref);
        }

        // --- 2. Prefetch Next/Previous POST Links (Z/X keys) ---
        const currentPostIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
        if (currentPostIcon) {
            const currentRow = currentPostIcon.closest('tr');
            let prevPostLinkHref = null;
            let nextPostLinkHref = null;

            // Find Previous Valid Post Link
            let prevRow = currentRow?.previousElementSibling;
            while (prevRow) {
                if (Posts.isValidPost(prevRow.querySelector('td.gall_num'), prevRow.querySelector('td.gall_tit'), prevRow.querySelector('td.gall_subject'))) {
                    prevPostLinkHref = prevRow.querySelector('td.gall_tit a:first-child')?.href;
                    break; // Found the first valid previous post
                }
                prevRow = prevRow.previousElementSibling;
            }

            // Find Next Valid Post Link
            let nextRow = currentRow?.nextElementSibling;
            while (nextRow) {
                if (Posts.isValidPost(nextRow.querySelector('td.gall_num'), nextRow.querySelector('td.gall_tit'), nextRow.querySelector('td.gall_subject'))) {
                    nextPostLinkHref = nextRow.querySelector('td.gall_tit a:first-child')?.href;
                    break; // Found the first valid next post
                }
                nextRow = nextRow.nextElementSibling;
            }

            // Add hints for post links
            addHint(prevPostLinkHref);
            addHint(nextPostLinkHref);
        }
    }
    // --- End Prefetching Logic ---
    function restoreScrollPosition() {
        const savedScrollY = sessionStorage.getItem('dcinsideShortcut_scrollPos');
        if (savedScrollY !== null) {
            // console.log('Found saved scroll position:', savedScrollY); // 디버깅용 로그
            const scrollY = parseInt(savedScrollY, 10);
            if (!isNaN(scrollY)) {
                // 저장된 위치로 스크롤 이동
                // 페이지 렌더링이 완료된 후 스크롤해야 정확하므로 약간의 지연(setTimeout)을 고려할 수 있음
                // 하지만 우선 즉시 실행해보고 문제가 발생하면 setTimeout 추가
                window.scrollTo(0, scrollY);
                // console.log('Scrolled to:', scrollY); // 디버깅용 로그
            }
            // 사용 후에는 세션 스토리지에서 제거하여 일반적인 새로고침/페이지 이동 시 영향 없도록 함
            sessionStorage.removeItem('dcinsideShortcut_scrollPos');
            // console.log('Removed saved scroll position.'); // 디버깅용 로그
        } else {
            // console.log('No saved scroll position found.'); // 디버깅용 로그
        }
    }
    // Initialization
    function init() {
        // --- 글쓰기 페이지 관련 함수 ---
        // 제목 입력란의 플레이스홀더 라벨("제목을 입력해 주세요.")을 제거합니다.
        function removeTitlePlaceholder() {
            const subjectInput = document.getElementById('subject');
            // 제목 입력 필드와 특정 상위 클래스 존재 여부로 글쓰기 페이지 확인
            if (subjectInput && subjectInput.closest('.input_write_tit')) {
                const label = document.querySelector('label.txt_placeholder[for="subject"]');
                if (label) {
                    label.remove(); // 라벨 요소 제거
                }
            }
        }

        // 제목 입력 필드에서 Tab 키를 누르면 본문 편집 영역으로 포커스를 이동시킵니다.
        function setupTabFocus() {
            const subjectInput = document.getElementById('subject'); // 제목 입력 필드
            const contentEditable = document.querySelector('.note-editable'); // 본문 편집 영역

            // 두 요소가 모두 존재하고, 아직 이벤트 리스너가 추가되지 않았을 때만 실행
            if (subjectInput && contentEditable && !subjectInput.hasAttribute('data-tab-listener-added')) {
                subjectInput.addEventListener('keydown', function(event) {
                    // Tab 키가 눌렸고 Shift 키는 눌리지 않았을 때
                    if (event.key === 'Tab' && !event.shiftKey) {
                        event.preventDefault(); // 기본 Tab 동작 방지
                        contentEditable.focus();  // 본문 편집 영역으로 포커스 이동
                    }
                });
                // 리스너가 추가되었음을 표시 (중복 추가 방지)
                subjectInput.setAttribute('data-tab-listener-added', 'true');
            }
        }
        // --- 글쓰기 페이지 관련 함수 끝 ---

        // 키보드 이벤트 리스너 등록
        document.addEventListener('keydown', e => Events.handleKeydown(e));
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Alt' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
                e.preventDefault();
            }
        });
        // --- Function to run after page load ---
        const onPageLoad = () => {
            Posts.adjustColgroupWidths();
            Posts.addNumberLabels();
            Posts.formatDates();
            removeTitlePlaceholder();
            setupTabFocus();
            addPrefetchHints();
            restoreScrollPosition();
            Events.triggerMacroNavigation(); // <<< ADD THIS CALL
        };
        // 페이지 로드 완료 시점
        if (document.readyState === 'complete') {
            onPageLoad(); // Run immediately
        } else {
            window.addEventListener('load', onPageLoad, { once: true }); // Run on load
        }

        // --- MutationObserver 설정 ---
        // 1. 글 목록(tbody) 변경 감지 옵저버
        const listObserver = new MutationObserver(() => {
            setTimeout(() => {
                Posts.adjustColgroupWidths();// 목록 내용 변경 시 너비 재조정은 불필요할 수 있으나, 혹시 모르니 유지
                Posts.addNumberLabels();
                Posts.formatDates();
            }, 100);
        });
        const listTbody = document.querySelector('table.gall_list tbody');
        if (listTbody) {
            listObserver.observe(listTbody, { childList: true, subtree: false });
        }

        // 2. 전체 문서(body) 변경 감지 옵저버
        const bodyObserver = new MutationObserver(() => {
            // 페이지 전환 등으로 colgroup이 새로 생기거나 변경될 수 있으므로 여기서 호출
            Posts.adjustColgroupWidths(); // <<< 너비 조정 함수 호출 추가

            const currentListTbody = document.querySelector('table.gall_list tbody');
            if (currentListTbody) {
                if (!currentListTbody.querySelector('.number-label')) {
                    Posts.addNumberLabels();
                }
                Posts.formatDates();
            }
            removeTitlePlaceholder();
            setupTabFocus();
        });
        bodyObserver.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();

/*
Copyright (c) 2025 nonohako([email protected])
dcinside shortcut © 2025 by nonohako is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.
To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
*/