DCInside Gallery View + Image Viewer

디시인사이드 갤러리뷰 + 이미지 뷰어 + 다운로드 + 모달 통합. 목록 페이지를 카드형 갤러리로 변환하고, 게시글 본문·댓글·미디어를 모달로 미리보기. 이미지 뷰어에서 확대/축소·선택 다운로드·ZIP 다운로드 지원.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         DCInside Gallery View + Image Viewer
// @namespace    http://tampermonkey.net/
// @version      5.1
// @description  디시인사이드 갤러리뷰 + 이미지 뷰어 + 다운로드 + 모달 통합. 목록 페이지를 카드형 갤러리로 변환하고, 게시글 본문·댓글·미디어를 모달로 미리보기. 이미지 뷰어에서 확대/축소·선택 다운로드·ZIP 다운로드 지원.
// @author       You
// @match        https://gall.dcinside.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @license      MIT
// ==/UserScript==

(function () {
'use strict';

/* 모바일 제외 */
if (/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent)) return;

/* ================================================================
   페이지 타입 판별
================================================================ */
var path = location.pathname;
var isArticlePage = /\/(board|mgallery\/board|mini\/board)\/view\//.test(path);
var isListPage = !isArticlePage &&
    /\/(board|mgallery\/board|mini\/board)\/(lists|recommend)\//.test(path);

/* ================================================================
   설정 기본값
================================================================ */
var DEFAULTS = {
    viewer: true,
    viewerOpacity: 0.9,
    viewerType: 1,
    viewerWidth: 70,
    scrollSpeed: 1,
    preloadImage: true,
    preloadCount: 1,
    galleryView: true,
    fetchDelay: 200,
    showThumbnailStrip: true,
    theme: 'light',
    columns: 3,
    thumbMode: 'crop'  // 'crop' | 'full'
};
var f = JSON.parse(JSON.stringify(DEFAULTS));

(function loadSettings() {
    for (var k in f) {
        if (!f.hasOwnProperty(k)) continue;
        var saved = GM_getValue('dci_' + k, undefined);
        if (saved !== undefined) f[k] = saved;
    }
    f.viewerOpacity  = Math.max(0, Math.min(1, parseFloat(f.viewerOpacity)  || 0.9));
    f.viewerType     = parseInt(f.viewerType, 10)  || 1;
    f.viewerWidth    = Math.max(30, Math.min(100, parseInt(f.viewerWidth, 10) || 70));
    f.scrollSpeed    = parseFloat(f.scrollSpeed)   || 1;
    f.preloadCount   = Math.max(1, parseInt(f.preloadCount, 10) || 1);
    f.fetchDelay     = Math.max(100, Math.min(1000, parseInt(f.fetchDelay, 10) || 200));
    f.columns        = Math.max(1, Math.min(6, parseInt(f.columns, 10) || 3));
    if (f.thumbMode !== 'crop' && f.thumbMode !== 'full') f.thumbMode = 'crop';
    if (f.theme !== 'dark') f.theme = 'light';
})();

/* ================================================================
   테마
================================================================ */
var isDark = f.theme === 'dark';
function applyTheme(dark) {
    isDark = dark;
    document.body.classList.toggle('dci-dark', dark);
    f.theme = dark ? 'dark' : 'light';
    GM_setValue('dci_theme', f.theme);
}

/* ================================================================
   유틸리티
================================================================ */
function sanitizeFileName(name) {
    return (name || '')
        .replace(/[\\/:*?"<>|\r\n\t]/g, '')
        .replace(/\s+/g, ' ')
        .trim();
}
function getCleanTitle(raw) {
    if (!raw) return '';
    return String(raw)
        .replace(/\s*-\s*디시인사이드.*/gi, '')
        .replace(/\s*-\s*dcinside\.com.*/gi, '')
        .replace(/\s+/g, ' ')
        .trim();
}
function getBoardName() {
    var board = '';
    var sels = [
        '.gallary_head .title_name', '.gallery_name',
        '.gall_tit_wrap .tit_name', 'h4.tit_name',
        '.sub_tit_txt', '.gall_name'
    ];
    sels.some(function (s) {
        var el = document.querySelector(s);
        if (el && el.textContent.trim()) {
            board = el.textContent.trim().replace(/갤러리$/, '').trim();
            return true;
        }
        return false;
    });
    if (!board) {
        var m = location.search.match(/[?&]id=([^&]+)/);
        if (m) board = decodeURIComponent(m[1]);
    }
    return (board || 'dcinside').trim();
}
function extractTitle(doc) {
    if (!doc) return 'untitled';
    var sels = [
        '.view_content_wrap .title_txt',
        '.gallview_head .title_txt',
        '.title_subject',
        'h4.title_txt'
    ];
    for (var i = 0; i < sels.length; i++) {
        var el = doc.querySelector(sels[i]);
        if (!el) continue;
        var t = el.cloneNode(true);
        t.querySelectorAll('button,small,img,svg,.icon,.badge').forEach(function (x) { x.remove(); });
        var txt = t.textContent.replace(/\s+/g, ' ').trim();
        if (txt) return getCleanTitle(txt);
    }
    var og = doc.querySelector('meta[property="og:title"]');
    if (og) return getCleanTitle(og.getAttribute('content') || '');
    return getCleanTitle(doc.title || 'untitled');
}
function buildFileName(title) {
    return sanitizeFileName(getBoardName() + ' ' + (title || extractTitle(document)));
}
function getExt(ct, url) {
    ct = (ct || '').toLowerCase();
    if (ct.includes('png'))  return '.png';
    if (ct.includes('gif'))  return '.gif';
    if (ct.includes('webp')) return '.webp';
    if (ct.includes('jpeg') || ct.includes('jpg')) return '.jpg';
    if (ct.includes('mp4'))  return '.mp4';
    if (ct.includes('webm')) return '.webm';
    var m = (url || '').split('?')[0].match(/\.([a-zA-Z0-9]+)$/);
    return m ? '.' + m[1].toLowerCase() : '.jpg';
}
function dlBlob(blob, name) {
    var a = document.createElement('a');
    var u = URL.createObjectURL(blob);
    a.href = u; a.download = name;
    document.body.appendChild(a);
    a.click();
    setTimeout(function () { URL.revokeObjectURL(u); a.remove(); }, 600);
}
function rUrl(u) {
    if (!u) return '';
    u = u.trim();
    if (u.startsWith('//'))  return 'https:' + u;
    if (u.startsWith('/'))   return 'https://gall.dcinside.com' + u;
    return u;
}
function esc(t) {
    if (t == null) return '';
    var d = document.createElement('div');
    d.textContent = String(t);
    return d.innerHTML;
}
function sanitizeHtml(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    div.querySelectorAll('script,style,link,meta').forEach(function (e) { e.remove(); });
    div.querySelectorAll('*').forEach(function (el) {
        Array.from(el.attributes).forEach(function (a) {
            var n = a.name, v = (a.value || '').trim().toLowerCase();
            if (n.startsWith('on')) { el.removeAttribute(n); return; }
            if ((n === 'href' || n === 'src') && v.startsWith('javascript:')) {
                el.removeAttribute(n);
            }
        });
        if (el.tagName === 'A' && el.getAttribute('href')) {
            el.setAttribute('rel', 'noopener noreferrer');
        }
    });
    return div.innerHTML;
}

/* 본문 텍스트에 섞인 JS 잔재 제거 */
function cleanBodyText(text) {
    if (!text) return '';
    text = text.replace(/if\s*\(window\.outlink[^;]*;?\s*/gi, '');
    text = text.replace(/window\.\w+\s*\([^)]*\)\s*;?\s*/g, '');
    text = text.replace(/\bvar\s+\w+\s*=\s*[^;]+;\s*/g, '');
    text = text.replace(/document\.\w+[^;]*;\s*/g, '');
    text = text.replace(/function\s*\w*\s*\([^)]*\)\s*\{[^}]*\}/g, '');
    text = text.replace(/\n{3,}/g, '\n\n').replace(/ {2,}/g, ' ').trim();
    return text;
}

/* ================================================================
   GM 네트워크 헬퍼
================================================================ */
function gmFetch(url) {
    return new Promise(function (resolve, reject) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            headers: {
                'Referer': 'https://gall.dcinside.com/',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8',
                'User-Agent': navigator.userAgent
            },
            timeout: 25000,
            onload: function (r) {
                if (r.status >= 200 && r.status < 300) {
                    try {
                        resolve(new DOMParser().parseFromString(r.responseText, 'text/html'));
                    } catch (e) {
                        reject(e);
                    }
                } else {
                    reject(new Error('HTTP ' + r.status));
                }
            },
            onerror:   function (e) { reject(e || new Error('Network error')); },
            ontimeout: function ()  { reject(new Error('Timeout')); }
        });
    });
}
function gmFetchBlob(url) {
    return new Promise(function (ok, fail) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            timeout: 30000,
            headers: { 'Referer': 'https://gall.dcinside.com/' },
            onload: function (r) {
                if (r.status >= 200 && r.status < 300 && r.response) {
                    var ct = '';
                    var m = (r.responseHeaders || '').match(/content-type:\s*([^\r\n;]+)/i);
                    if (m) ct = m[1].trim();
                    ok({ blob: r.response, contentType: ct });
                } else {
                    fail(new Error('HTTP ' + r.status));
                }
            },
            onerror:   function (e) { fail(e || new Error('Network')); },
            ontimeout: function ()  { fail(new Error('Timeout')); }
        });
    });
}

/* ================================================================
   이미지 판별
================================================================ */
function isDccon(img) {
    if (!img) return true;
    var src = (img.getAttribute('src') || img.getAttribute('data-src') || '').toLowerCase();
    if (src.includes('/dccon/') || src.includes('/emoticon/')) return true;
    var cls = (img.className || '').toLowerCase();
    if (cls.includes('dccon') || cls.includes('emoticon')) return true;
    var w = parseInt(img.getAttribute('width'))  || img.naturalWidth  || 0;
    var h = parseInt(img.getAttribute('height')) || img.naturalHeight || 0;
    if (w > 0 && h > 0 && w <= 60 && h <= 60) return true;
    return false;
}
function isRepImg(img) {
    if (!img) return false;
    var p = img.parentElement;
    for (var d = 0; p && d < 6; d++, p = p.parentElement) {
        var cls = (p.className || '').toLowerCase();
        if (cls.includes('rep_img')      || cls.includes('article-rep')  ||
            cls.includes('thumb-wrap')   || cls.includes('gall_rep')     ||
            cls.includes('list_thumb')   || cls.includes('imgbox')       ||
            cls.includes('banner')       || cls.includes('logo')         ||
            cls.includes('writing_image_wrap_v2')) return true;
        if (p.id && (p.id.includes('rep') || p.id.includes('thumb'))) return true;
    }
    return false;
}
function isSkipImg(img) { return isDccon(img) || isRepImg(img); }

/* ================================================================
   목록 행 탐색
================================================================ */
function findListRows() {
    var rows = [];

    /* 1순위: table tbody > tr.ub-content */
    document.querySelectorAll('table tbody').forEach(function (tb) {
        var trs = tb.querySelectorAll('tr.ub-content');
        trs.forEach(function (tr) { rows.push(tr); });
    });
    if (rows.length) return rows;

    /* 2순위: .ub-content (공지 제외) */
    document.querySelectorAll('.ub-content:not(.notice)').forEach(function (el) {
        rows.push(el);
    });
    if (rows.length) return rows;

    /* 3순위: .gall_list tr */
    document.querySelectorAll('.gall_list tr, table.gall_list tr').forEach(function (tr) {
        if (tr.querySelector('.gall_tit') || tr.querySelector('.ub-gall-tit')) rows.push(tr);
    });
    if (rows.length) return rows;

    /* 4순위: li */
    document.querySelectorAll('.gall_list li, .list_content li').forEach(function (li) {
        rows.push(li);
    });
    return rows;
}
function getRowHref(row) {
    var sels = [
        '.gall_tit a:not(.reply_numbox):not(.icon_reply_parent)',
        '.ub-gall-tit a', '.subject a', 'td.title a', 'a.title'
    ];
    for (var i = 0; i < sels.length; i++) {
        var a = row.querySelector(sels[i]);
        if (a && a.getAttribute('href')) {
            var h = rUrl(a.getAttribute('href'));
            if (h.includes('view')) return h;
        }
    }
    if (row.tagName === 'A' && row.getAttribute('href')) return rUrl(row.getAttribute('href'));
    return null;
}
function getRowTitle(row) {
    var sels = [
        '.gall_tit a:not(.reply_numbox):not(.icon_reply_parent)',
        '.ub-gall-tit a', '.subject a', 'td.title a'
    ];
    for (var i = 0; i < sels.length; i++) {
        var a = row.querySelector(sels[i]);
        if (a) return (a.getAttribute('title') || a.textContent || '').trim();
    }
    return '';
}
function getRowNum(row) {
    var sels = ['.gall_num', '.ub-gall-num', 'td.num', '.num'];
    for (var i = 0; i < sels.length; i++) {
        var el = row.querySelector(sels[i]);
        if (el) return el.textContent.trim();
    }
    return '';
}
function getRowReco(row) {
    var sels = ['.gall_recommend', '.ub-gall-reco', 'td.recommend', '.recommend'];
    for (var i = 0; i < sels.length; i++) {
        var el = row.querySelector(sels[i]);
        if (el) return el.textContent.trim();
    }
    return '0';
}
function getRowReply(row) {
    var el = row.querySelector('.reply_num,.replyNum,.gall_reply_num,.reply_numbox');
    return el ? el.textContent.trim() : '';
}
function isNoticeRow(row) {
    if (!row) return true;
    var num = getRowNum(row);
    if (!num) return false;
    if (!/^\d+$/.test(num)) return true;
    if (row.classList.contains('notice') || row.classList.contains('gall_notice')) return true;
    return false;
}

/* ================================================================
   본문 처리
================================================================ */
function processBodyHtml(ac) {
    if (!ac) return '';
    var c = ac.cloneNode(true);

    /* 스크립트·불필요 태그 제거 */
    c.querySelectorAll('script,style,noscript').forEach(function (e) { e.remove(); });
    c.querySelectorAll(
        '.article-rep-image,.rep_img,.gall_rep_img,.writing_image_wrap_v2'
    ).forEach(function (e) { e.remove(); });

    /* 텍스트 노드 JS 잔재 제거 */
    var walker = document.createTreeWalker(c, NodeFilter.SHOW_TEXT, null, false);
    var textNodes = [];
    var n;
    while ((n = walker.nextNode())) textNodes.push(n);
    textNodes.forEach(function (tn) {
        var txt = tn.textContent || '';
        if (/if\s*\(window\.|window\.\w+\s*\(|function\s*\w*\s*\(/.test(txt)) {
            tn.textContent = cleanBodyText(txt);
        }
    });

    /* 미디어 URL 절대화 */
    c.querySelectorAll('img').forEach(function (img) {
        var src = img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('src') || '';
        if (src) img.setAttribute('src', rUrl(src));
        img.removeAttribute('data-src');
        img.removeAttribute('data-original');
        img.removeAttribute('loading');
        img.style.removeProperty('width');
        img.style.removeProperty('height');
    });
    c.querySelectorAll('video').forEach(function (v) {
        v.setAttribute('controls', '');
        v.removeAttribute('autoplay');
        var s = v.getAttribute('data-src') || v.getAttribute('src');
        if (s) v.setAttribute('src', rUrl(s));
        v.removeAttribute('data-src');
    });
    c.querySelectorAll('iframe').forEach(function (ff) {
        var s = ff.getAttribute('data-src') || ff.getAttribute('src');
        if (s) ff.setAttribute('src', rUrl(s));
        ff.removeAttribute('data-src');
        ff.setAttribute('allowfullscreen', 'true');
        ff.setAttribute('loading', 'lazy');
        ff.removeAttribute('width');
        ff.removeAttribute('height');
    });
    c.querySelectorAll('a[href]').forEach(function (a) {
        var h = a.getAttribute('href');
        if (h && !h.startsWith('#') && !h.startsWith('javascript:')) {
            a.setAttribute('href', rUrl(h));
        }
        a.setAttribute('target', '_blank');
    });
    return sanitizeHtml(c.innerHTML);
}

function countBodyMedia(doc) {
    var r = { imgs: 0, vids: 0, ifrV: 0, ifrSrc: [] };
    if (!doc) return r;
    var VIDEO_PAT = [/youtube/i, /youtu\.be/i, /twitch/i, /vimeo/i, /streamable/i];
    var content = doc.querySelector('.writing_view_box') ||
                  doc.querySelector('.write_div') ||
                  doc.querySelector('.gallview_content');
    if (!content) return r;
    var cc = content.cloneNode(true);
    cc.querySelectorAll('.article-rep-image,.rep_img,.gall_rep_img,.writing_image_wrap_v2')
      .forEach(function (e) { e.remove(); });
    cc.querySelectorAll('img').forEach(function (img) { if (!isSkipImg(img)) r.imgs++; });
    cc.querySelectorAll('video').forEach(function () { r.vids++; });
    cc.querySelectorAll('iframe').forEach(function (ff) {
        var s = (ff.getAttribute('src') || ff.getAttribute('data-src') || '').toLowerCase();
        if (VIDEO_PAT.some(function (p) { return p.test(s); })) {
            r.ifrV++;
            var name = 'Video';
            if (/youtube|youtu\.be/.test(s)) name = 'YouTube';
            else if (/twitch/.test(s))       name = 'Twitch';
            else if (/vimeo/.test(s))         name = 'Vimeo';
            if (!r.ifrSrc.includes(name)) r.ifrSrc.push(name);
        }
    });
    return r;
}

function findBodyThumb(doc) {
    if (!doc) return null;
    var content = doc.querySelector('.writing_view_box') ||
                  doc.querySelector('.write_div') ||
                  doc.querySelector('.gallview_content');
    if (!content) return null;
    var cc = content.cloneNode(true);
    cc.querySelectorAll('.article-rep-image,.rep_img,.gall_rep_img,.writing_image_wrap_v2')
      .forEach(function (e) { e.remove(); });
    var imgs = cc.querySelectorAll('img');
    for (var i = 0; i < imgs.length; i++) {
        var img = imgs[i];
        if (isSkipImg(img)) continue;
        var src = rUrl(img.getAttribute('data-src') || img.getAttribute('src') || '');
        if (!src || src.includes('dccon') || src.includes('emoticon')) continue;
        var w = parseInt(img.getAttribute('width'))  || 0;
        var h = parseInt(img.getAttribute('height')) || 0;
        return { src: src, ratio: (w > 0 && h > 0) ? h / w : 0.75 };
    }
    return null;
}

/* ================================================================
   댓글 파싱
================================================================ */
function extractComments(doc) {
    if (!doc) return [];
    var out = [], items = [];

    var containerSels = [
        '.cmt_list_box', '#comment_box', '.reply_wrap',
        '.comment_box', '#comment', '.cmt_list',
        '.comment_list', '.commentArea'
    ];
    var container = null;
    for (var ci = 0; ci < containerSels.length; ci++) {
        container = doc.querySelector(containerSels[ci]);
        if (container) break;
    }
    if (!container) return [];

    var itemSels = [
        'li.ub-content', 'li.cmt_item', 'li.comment-item',
        '.ub-content', '.cmt_item', '.comment_item',
        'li[data-no]', 'li'
    ];
    for (var si = 0; si < itemSels.length; si++) {
        var found = container.querySelectorAll(itemSels[si]);
        if (found.length) { items = Array.from(found); break; }
    }
    if (!items.length) return [];

    items.forEach(function (item) {
        /* 삭제된 댓글 */
        if (item.classList.contains('blocked_user') ||
            item.querySelector('.del_comment,.comment_dccon_del,.cmt_del,.deleted')) {
            out.push({ username: '', isReply: false, text: '[삭제됨]', time: '', isDeleted: true });
            return;
        }

        /* 닉네임 */
        var nickSels = [
            '.nick_comm em', '.nick_comm', 'em.nick',
            '.cmt_writer em', '.writer_nikname', '.nick',
            '.gallog_nick', '.user_nick', '.comment_writer em',
            '.reply_nick em', '.reply_nick'
        ];
        var username = '익명';
        for (var ni = 0; ni < nickSels.length; ni++) {
            var ne = item.querySelector(nickSels[ni]);
            if (ne && ne.textContent.trim()) { username = ne.textContent.trim(); break; }
        }

        /* 대댓글 여부 */
        var isReply = item.classList.contains('reply') ||
                      item.classList.contains('cmt_reply') ||
                      item.classList.contains('re_cmt') ||
                      !!item.querySelector('.ico_reply,.icon_reply,.reply_icon');

        /* 작성 시간 */
        var timeSels = [
            '.date_time', '.cmt_date', '.time_write',
            'time', '.date', '.write_time', '.comment_date'
        ];
        var time = '';
        for (var ti = 0; ti < timeSels.length; ti++) {
            var te = item.querySelector(timeSels[ti]);
            if (te && te.textContent.trim()) {
                time = (te.getAttribute('datetime') || te.textContent).trim();
                break;
            }
        }

        /* 댓글 내용 */
        var msgSels = [
            '.usertxt', '.cmt_cont', '.comment_memo',
            '.reply_cont', '.txt', '.cmt_txt',
            '.comment_text', 'p.usertxt', '.message', '.cmt_memo'
        ];
        var text = '', html = '';
        for (var mi = 0; mi < msgSels.length; mi++) {
            var me = item.querySelector(msgSels[mi]);
            if (!me) continue;
            var mc = me.cloneNode(true);
            mc.querySelectorAll(
                'img.dccon,.dccon,.emoticon,' +
                'img[src*="/dccon/"],img[src*="/emoticon/"],script'
            ).forEach(function (e) { e.remove(); });
            var rawText = mc.textContent.replace(/\s+/g, ' ').trim();
            rawText = cleanBodyText(rawText);
            if (rawText) { text = rawText; html = sanitizeHtml(me.innerHTML); break; }
        }

        /* 디시콘만 있는 댓글 */
        if (!text && !html) {
            var dcconImgs = item.querySelectorAll(
                'img[src*="/dccon/"],img[src*="/emoticon/"],.dccon_wrap'
            );
            if (dcconImgs.length) {
                text = '[디시콘]';
                html = '<span style="color:#888">[디시콘]</span>';
            }
        }

        if (text || html) {
            out.push({ username: username, isReply: isReply, text: text, html: html, time: time, isDeleted: false });
        }
    });
    return out;
}

/* ================================================================
   이미지 뷰어
================================================================ */
var viewerAPI = { openExternal: null };
var viewerOpen = false;

function viewerModule() {
    var images = [], sourceEls = [], preloaded = [];
    var overlay, viewContainer, imgBox, scrollBarWrap, scrollBarTrack;
    var infoLabel, zoomLabel;
    var mode = f.viewerType === 1 ? 'single' : 'scroll';
    var scrollSpd = f.scrollSpeed || 1;
    var zoomLevel = 1;
    var currentMedia = null;
    var dragging = false, dragStartX = 0, dragStartY = 0, panOriginX = 0, panOriginY = 0, panX = 0, panY = 0;
    var selected = new Set();
    var dlBtn, dlBadge, downloading = false;
    var selPanel = null, panelOpen = false;
    var currentIdx = 0;
    var extMode = false, extFileName = null, origImages = [], origSourceEls = [];
    var thumbStrip = null, thumbDragging = false, thumbDragStartIdx = -1;

    function isVideo(u) {
        if (!u) return false;
        var l = u.toLowerCase().split('?')[0];
        return l.endsWith('.mp4') || l.endsWith('.webm');
    }
    function createMedia(idx) {
        if (idx < 0 || idx >= images.length) return document.createElement('div');
        var u = images[idx], el;
        if (isVideo(u)) {
            el = document.createElement('video');
            el.src = u; el.autoplay = true; el.loop = true;
            el.muted = true; el.playsInline = true;
        } else {
            el = document.createElement('img');
            el.src = u;
        }
        return el;
    }
    function applyPan() {
        if (currentMedia)
            currentMedia.style.transform =
                'translate(' + panX + 'px,' + panY + 'px) scale(' + zoomLevel + ')';
    }
    function setZoom(lv) {
        lv = Math.max(0.5, Math.min(5, lv));
        if (lv === zoomLevel) return;
        zoomLevel = lv;
        if (zoomLevel <= 1) { panX = 0; panY = 0; }
        applyPan();
        if (zoomLabel && mode === 'single') {
            zoomLabel.textContent = Math.round(100 * zoomLevel) + '%';
            zoomLabel.style.opacity = '1';
            clearTimeout(zoomLabel._t);
            zoomLabel._t = setTimeout(function () { zoomLabel.style.opacity = '0'; }, 800);
        }
        if (imgBox) imgBox.style.cursor = zoomLevel > 1 ? 'grab' : 'default';
    }
    function toggleSelect(idx) {
        if (idx < 0 || idx >= images.length) return false;
        if (selected.has(idx)) { selected.delete(idx); return false; }
        selected.add(idx); return true;
    }
    function updateUI() {
        if (dlBadge) {
            if (selected.size > 0 && !downloading) {
                dlBadge.textContent = selected.size;
                dlBadge.style.display = 'flex';
            } else if (!downloading) {
                dlBadge.style.display = 'none';
            }
        }
        if (thumbStrip) updateThumbStrip();
    }
    function showFeedback(sel) {
        if (!overlay) return;
        var el = document.createElement('div');
        el.textContent = sel ? '✓' : '✗';
        el.style.cssText =
            'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);' +
            'color:#fff;font-size:48px;font-weight:bold;z-index:20;pointer-events:none;' +
            'background:' + (sel ? 'rgba(0,120,255,.75)' : 'rgba(255,50,50,.75)') + ';' +
            'width:80px;height:80px;border-radius:50%;display:flex;' +
            'align-items:center;justify-content:center;transition:opacity .2s;';
        overlay.appendChild(el);
        setTimeout(function () { el.style.opacity = '0'; }, 200);
        setTimeout(function () { if (el.parentNode) el.remove(); }, 450);
    }
    function getFileName() {
        return extMode && extFileName ? extFileName : buildFileName();
    }
    function doDownload(forcedIdx) {
        if (downloading) return;
        var indices = Array.isArray(forcedIdx)
            ? forcedIdx
            : Array.from(selected).sort(function (a, b) { return a - b; });
        if (!indices.length) indices = [currentIdx];
        indices = indices.filter(function (i) { return i >= 0 && i < images.length; });
        if (!indices.length) return;

        var name = getFileName(), total = indices.length, done = 0;
        downloading = true;

        function setDlState(icon, opacity) {
            if (dlBtn) {
                var ic = dlBtn.querySelector('.dl-icon');
                if (ic) ic.textContent = icon;
                dlBtn.style.opacity = String(opacity);
            }
        }
        setDlState('⏳', 0.7);
        if (dlBadge) {
            dlBadge.textContent = '0/' + total;
            dlBadge.style.display = 'flex';
            dlBadge.style.background = '#ffa500';
        }
        function finish() {
            downloading = false;
            setDlState('⬇', 1);
            if (dlBadge) dlBadge.style.background = '#4CAF50';
            setTimeout(function () {
                if (dlBadge) dlBadge.style.background = '#ff4444';
                updateUI();
            }, 1500);
        }

        if (total === 1) {
            var url = images[indices[0]];
            gmFetchBlob(url)
                .then(function (r) { dlBlob(r.blob, name + getExt(r.contentType, url)); })
                .catch(function () {
                    var a = document.createElement('a');
                    a.href = url; a.download = name + getExt('', url);
                    a.target = '_blank';
                    document.body.appendChild(a); a.click();
                    setTimeout(function () { a.remove(); }, 300);
                })
                .then(finish);
        } else {
            var zip = new JSZip();
            var pad = String(total).length;
            Promise.all(indices.map(function (idx, ii) {
                return gmFetchBlob(images[idx]).then(function (r) {
                    done++;
                    if (dlBadge) dlBadge.textContent = done + '/' + total;
                    var n = String(ii + 1);
                    while (n.length < pad) n = '0' + n;
                    zip.file(n + getExt(r.contentType, images[idx]), r.blob);
                }).catch(function () { done++; });
            })).then(function () {
                return zip.generateAsync({ type: 'blob' });
            }).then(function (b) {
                dlBlob(b, name + '.zip');
            }).catch(console.error).then(finish);
        }
    }
    function doDownloadAll() {
        var all = [];
        for (var j = 0; j < images.length; j++) all.push(j);
        doDownload(all);
    }

    /* ── 선택 패널 ── */
    function hidePanel() { if (selPanel) selPanel.style.display = 'none'; panelOpen = false; }
    function showPanel() {
        if (!selPanel) {
            selPanel = document.createElement('div');
            selPanel.style.cssText =
                'position:absolute;top:50%;right:20px;transform:translateY(-50%);' +
                'width:240px;max-height:70vh;background:rgba(0,0,0,.9);' +
                'border:1px solid #555;border-radius:10px;z-index:30;' +
                'display:flex;flex-direction:column;overflow:hidden;';

            var hdr = document.createElement('div');
            hdr.style.cssText =
                'padding:8px 10px;border-bottom:1px solid #555;' +
                'display:flex;justify-content:space-between;align-items:center;flex-shrink:0;';
            var ts = document.createElement('span');
            ts.id = 'selPanelTitle';
            ts.textContent = '선택 목록 (0)';
            ts.style.cssText = 'color:#fff;font-size:13px;font-weight:bold;';
            hdr.appendChild(ts);

            function pb(t, c, fn) {
                var b = document.createElement('button');
                b.textContent = t; b.style.cssText = c;
                b.addEventListener('click', function (e) { e.stopPropagation(); fn(); });
                return b;
            }
            var bg = document.createElement('div');
            bg.style.cssText = 'display:flex;gap:4px;';
            bg.appendChild(pb('전체', 'background:rgba(0,120,255,.6);color:#fff;border:none;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;', function () {
                for (var j = 0; j < images.length; j++) selected.add(j);
                updateUI();
            }));
            bg.appendChild(pb('초기화', 'background:rgba(255,50,50,.6);color:#fff;border:none;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:11px;', function () {
                selected.clear(); updateUI();
            }));
            bg.appendChild(pb('⬇', 'background:rgba(0,180,0,.6);color:#fff;border:none;border-radius:4px;padding:2px 6px;cursor:pointer;font-size:13px;', doDownload));
            bg.appendChild(pb('✕', 'background:none;color:#fff;border:none;cursor:pointer;font-size:15px;', hidePanel));
            hdr.appendChild(bg);
            selPanel.appendChild(hdr);

            var grid = document.createElement('div');
            grid.id = 'selPanelGrid';
            grid.style.cssText =
                'flex:1;overflow-y:auto;padding:8px;' +
                'display:flex;flex-wrap:wrap;gap:6px;align-content:flex-start;';
            selPanel.appendChild(grid);

            selPanel.addEventListener('wheel', function (e) { e.stopPropagation(); }, { passive: true });
            selPanel.addEventListener('click', function (e) { e.stopPropagation(); });
            overlay.appendChild(selPanel);
        }
        selPanel.style.display = 'flex';
        panelOpen = true;
        updatePanelGrid();
    }
    function updatePanelGrid() {
        if (!selPanel) return;
        var grid = selPanel.querySelector('#selPanelGrid');
        if (!grid) return;
        var ts = selPanel.querySelector('#selPanelTitle');
        if (ts) ts.textContent = '선택 목록 (' + selected.size + ')';
        grid.innerHTML = '';
        if (!selected.size) {
            var em = document.createElement('div');
            em.style.cssText = 'color:#aaa;font-size:12px;text-align:center;width:100%;padding:16px 0;';
            em.textContent = '선택 없음';
            grid.appendChild(em);
            return;
        }
        Array.from(selected).sort(function (a, b) { return a - b; }).forEach(function (idx) {
            if (idx < 0 || idx >= images.length) return;
            var it = document.createElement('div');
            it.style.cssText =
                'position:relative;width:70px;height:70px;border-radius:5px;' +
                'overflow:hidden;cursor:pointer;border:2px solid rgba(0,120,255,.8);flex-shrink:0;';
            var med = isVideo(images[idx]) ? document.createElement('video') : document.createElement('img');
            med.src = images[idx];
            med.style.cssText = 'width:100%;height:100%;object-fit:cover;pointer-events:none;';
            it.appendChild(med);
            var nm = document.createElement('div');
            nm.style.cssText =
                'position:absolute;bottom:2px;left:2px;color:#fff;font-size:9px;' +
                'background:rgba(0,0,0,.6);padding:1px 3px;border-radius:3px;';
            nm.textContent = idx + 1;
            it.appendChild(nm);
            var rb = document.createElement('div');
            rb.style.cssText =
                'position:absolute;top:2px;right:2px;width:16px;height:16px;' +
                'background:rgba(255,0,0,.7);color:#fff;border-radius:50%;' +
                'display:flex;align-items:center;justify-content:center;font-size:10px;cursor:pointer;';
            rb.textContent = '✕';
            rb.addEventListener('click', function (e) {
                e.stopPropagation(); selected.delete(idx); updateUI(); updatePanelGrid();
            });
            it.appendChild(rb);
            (function (capturedIdx) {
                it.addEventListener('click', function (e) {
                    e.stopPropagation(); currentIdx = capturedIdx; showImage();
                });
            })(idx);
            grid.appendChild(it);
        });
    }

    /* ── 스크롤바 / 인포 ── */
    function updateScrollBar() {
        var th = scrollBarTrack ? scrollBarTrack.querySelector('#scrollThumb') : null;
        if (!th) return;
        var hp, tp;
        if (mode === 'single') {
            hp = Math.max(5, 100 / Math.max(1, images.length));
            tp = images.length > 1 ? (currentIdx / (images.length - 1)) * (100 - hp) : 0;
        } else if (imgBox) {
            var sh = imgBox.scrollHeight || 1;
            hp = 100 * (imgBox.clientHeight / sh);
            tp = sh > imgBox.clientHeight ? (imgBox.scrollTop / (sh - imgBox.clientHeight)) * (100 - hp) : 0;
        } else return;
        th.style.height = hp + '%';
        th.style.top = (isNaN(tp) ? 0 : tp) + '%';
    }
    function updateInfo() {
        if (!infoLabel) return;
        if (mode === 'single') {
            infoLabel.textContent = (currentIdx + 1) + ' / ' + images.length;
        } else if (imgBox) {
            var mx = (imgBox.scrollHeight || 1) - imgBox.clientHeight;
            infoLabel.textContent = (mx > 0 ? Math.round(100 * imgBox.scrollTop / mx) : 100) + '%';
        }
    }

    /* ── 썸네일 스트립 ── */
    function createThumbStrip() {
        if (!f.showThumbnailStrip) return;
        thumbStrip = document.createElement('div');
        thumbStrip.style.cssText =
            'position:absolute;bottom:0;left:50%;transform:translateX(-50%);' +
            'height:0;max-width:80%;overflow-x:auto;overflow-y:hidden;' +
            'display:flex;gap:4px;padding:0;background:rgba(0,0,0,.85);' +
            'border-radius:8px 8px 0 0;transition:height .2s,padding .2s;' +
            'z-index:15;scrollbar-width:thin;align-items:center;';
        var visible = false;

        function checkMouse(e) {
            if (!imgBox || !overlay || overlay.style.display === 'none') return;
            var wr = imgBox.getBoundingClientRect();
            var inW = e.clientX >= wr.left && e.clientX <= wr.right &&
                      e.clientY >= wr.top  && e.clientY <= wr.bottom;
            var near = inW && (wr.bottom - e.clientY) < 80;
            var sr = thumbStrip ? thumbStrip.getBoundingClientRect() : null;
            var onS = sr && sr.height > 5 &&
                      e.clientX >= sr.left && e.clientX <= sr.right &&
                      e.clientY >= sr.top  && e.clientY <= sr.bottom;
            if (near || onS) {
                if (!visible) {
                    visible = true;
                    thumbStrip.style.height = '70px';
                    thumbStrip.style.padding = '6px 10px';
                }
            } else {
                if (visible && !thumbDragging) {
                    visible = false;
                    thumbStrip.style.height = '0';
                    thumbStrip.style.padding = '0 10px';
                }
            }
        }
        document.addEventListener('mousemove', checkMouse, { passive: true });
        thumbStrip.addEventListener('wheel', function (e) {
            e.stopPropagation(); e.preventDefault();
            thumbStrip.scrollLeft += e.deltaY;
        }, { passive: false });
        thumbStrip.addEventListener('mousedown', function (e) {
            if (e.button !== 0) return;
            var th = e.target.closest('[data-ti]');
            if (!th) return;
            e.preventDefault();
            thumbDragging = true;
            thumbDragStartIdx = parseInt(th.dataset.ti, 10);
        });
        document.addEventListener('mousemove', function (e) {
            if (!thumbDragging) return;
            var el = document.elementFromPoint(e.clientX, e.clientY);
            if (!el) return;
            var th = el.closest ? el.closest('[data-ti]') : null;
            if (!th) return;
            var idx = parseInt(th.dataset.ti, 10);
            if (isNaN(idx)) return;
            for (var j = Math.min(thumbDragStartIdx, idx); j <= Math.max(thumbDragStartIdx, idx); j++) {
                selected.add(j);
            }
            updateUI();
        });
        document.addEventListener('mouseup', function () { thumbDragging = false; });
        overlay.appendChild(thumbStrip);
    }
    function updateThumbStrip() {
        if (!thumbStrip) return;
        thumbStrip.innerHTML = '';
        images.forEach(function (url, idx) {
            var th = document.createElement('div');
            th.dataset.ti = idx;
            var isSel = selected.has(idx), isCur = idx === currentIdx;
            th.style.cssText =
                'flex-shrink:0;width:54px;height:54px;border-radius:4px;' +
                'overflow:hidden;cursor:pointer;border:2px solid ' +
                (isCur ? '#4a90d9' : isSel ? 'rgba(0,120,255,.8)' : 'transparent') + ';' +
                'opacity:' + (isCur || isSel ? '1' : '0.65') + ';' +
                'position:relative;transition:border .15s;';
            var med = isVideo(url) ? document.createElement('video') : document.createElement('img');
            med.src = url;
            med.style.cssText = 'width:100%;height:100%;object-fit:cover;pointer-events:none;';
            th.appendChild(med);
            if (isSel) {
                var ck = document.createElement('div');
                ck.style.cssText =
                    'position:absolute;top:2px;right:2px;width:14px;height:14px;' +
                    'background:rgba(0,120,255,.9);border-radius:50%;' +
                    'display:flex;align-items:center;justify-content:center;' +
                    'color:#fff;font-size:9px;pointer-events:none;';
                ck.textContent = '✓';
                th.appendChild(ck);
            }
            (function (capturedIdx) {
                th.addEventListener('click', function (e) {
                    e.stopPropagation(); currentIdx = capturedIdx; showImage();
                });
            })(idx);
            thumbStrip.appendChild(th);
        });
        var active = thumbStrip.children[currentIdx];
        if (active) active.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' });
    }

    /* ── 뷰어 DOM 생성 ── */
    function createViewer() {
        overlay = document.createElement('div');
        overlay.id = 'imageViewer';
        overlay.style.cssText =
            'position:fixed;top:0;left:0;width:100vw;height:100vh;' +
            'background:rgba(0,0,0,' + f.viewerOpacity + ');' +
            'display:flex;align-items:center;justify-content:center;' +
            'z-index:2147483647;overflow:hidden;';

        viewContainer = document.createElement('div');
        viewContainer.id = 'viewContainer';
        imgBox = document.createElement('div');
        imgBox.id = 'imgBox';
        viewContainer.appendChild(imgBox);
        overlay.appendChild(viewContainer);
        document.body.appendChild(overlay);

        /* 닫기 버튼 */
        var closeBtn = document.createElement('button');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText =
            'position:absolute;top:20px;right:20px;background:rgba(0,0,0,.5);' +
            'border:none;color:#fff;font-size:28px;width:48px;height:48px;' +
            'border-radius:50%;cursor:pointer;z-index:20;';
        closeBtn.addEventListener('click', closeViewer);
        closeBtn.addEventListener('mouseenter', function () { closeBtn.style.background = 'rgba(255,0,0,.7)'; });
        closeBtn.addEventListener('mouseleave', function () { closeBtn.style.background = 'rgba(0,0,0,.5)'; });
        overlay.appendChild(closeBtn);

        /* 이전/다음 버튼 */
        var prevBtn = document.createElement('button');
        prevBtn.textContent = '‹';
        prevBtn.style.cssText =
            'position:absolute;left:10px;top:50%;transform:translateY(-50%);' +
            'background:rgba(0,0,0,.4);color:#fff;border:none;font-size:46px;' +
            'width:48px;height:80px;border-radius:8px;cursor:pointer;z-index:10;' +
            'opacity:0;transition:opacity .2s;';
        prevBtn.addEventListener('click', function (e) {
            e.stopPropagation();
            if (currentIdx > 0) { currentIdx--; showImage(); }
        });
        var nextBtn = document.createElement('button');
        nextBtn.textContent = '›';
        nextBtn.style.cssText =
            'position:absolute;right:10px;top:50%;transform:translateY(-50%);' +
            'background:rgba(0,0,0,.4);color:#fff;border:none;font-size:46px;' +
            'width:48px;height:80px;border-radius:8px;cursor:pointer;z-index:10;' +
            'opacity:0;transition:opacity .2s;';
        nextBtn.addEventListener('click', function (e) {
            e.stopPropagation();
            if (currentIdx < images.length - 1) { currentIdx++; showImage(); }
        });
        overlay.appendChild(prevBtn);
        overlay.appendChild(nextBtn);

        /* 호버 영역 (화살표 표시) */
        ['left', 'right'].forEach(function (side) {
            var hov = document.createElement('div');
            hov.style.cssText = 'position:absolute;' + side + ':0;top:0;width:70px;height:100%;z-index:9;';
            var btn = side === 'left' ? prevBtn : nextBtn;
            hov.addEventListener('mouseenter', function () { btn.style.opacity = '0.7'; });
            hov.addEventListener('mouseleave', function () { btn.style.opacity = '0'; });
            overlay.appendChild(hov);
        });

        /* 하단 버튼 바 */
        var bottomBar = document.createElement('div');
        bottomBar.style.cssText =
            'position:absolute;bottom:20px;right:20px;display:flex;gap:8px;z-index:10;align-items:center;';

        function mkBtn(txt, fn, id) {
            var b = document.createElement('button');
            var sp = document.createElement('span');
            sp.textContent = txt;
            b.appendChild(sp);
            b.style.cssText =
                'background:rgba(0,0,0,.65);color:#fff;border:1px solid rgba(255,255,255,.3);' +
                'border-radius:50%;width:40px;height:40px;cursor:pointer;' +
                'display:flex;align-items:center;justify-content:center;font-size:15px;';
            if (id) b.id = id;
            b.addEventListener('click', function (e) { e.stopPropagation(); fn(); });
            return b;
        }

        var selBtn = mkBtn('☐', function () {
            var s = toggleSelect(currentIdx);
            var sp = selBtn.querySelector('span');
            if (sp) sp.textContent = s ? '☑' : '☐';
            updateUI(); showFeedback(s);
        }, 'vSelBtn');
        bottomBar.appendChild(selBtn);
        bottomBar.appendChild(mkBtn('−',   function () { setZoom(zoomLevel - 0.25); }));
        bottomBar.appendChild(mkBtn('1:1', function () { setZoom(1); }));
        bottomBar.appendChild(mkBtn('+',   function () { setZoom(zoomLevel + 0.25); }));
        bottomBar.appendChild(mkBtn('📋',  function () { panelOpen ? hidePanel() : showPanel(); }));
        overlay.appendChild(bottomBar);

        /* 다운로드 버튼 */
        dlBtn = document.createElement('button');
        dlBtn.innerHTML = '<span class="dl-icon">⬇</span>';
        dlBtn.style.cssText =
            'position:absolute;bottom:20px;right:68px;background:rgba(0,0,0,.65);' +
            'color:#fff;border:1px solid rgba(255,255,255,.3);border-radius:50%;' +
            'width:40px;height:40px;cursor:pointer;display:flex;align-items:center;' +
            'justify-content:center;font-size:15px;z-index:11;';
        dlBadge = document.createElement('span');
        dlBadge.style.cssText =
            'position:absolute;top:-5px;right:-5px;background:#ff4444;color:#fff;' +
            'border-radius:50%;min-width:18px;height:18px;font-size:10px;font-weight:bold;' +
            'display:none;align-items:center;justify-content:center;padding:0 3px;pointer-events:none;';
        dlBtn.appendChild(dlBadge);
        dlBtn.addEventListener('click', function (e) { e.stopPropagation(); doDownload(); });
        overlay.appendChild(dlBtn);

        /* 전체 다운로드 버튼 */
        var dlAllBtn = document.createElement('button');
        dlAllBtn.style.cssText =
            'position:absolute;bottom:20px;right:115px;background:rgba(0,0,0,.65);' +
            'color:#fff;border:1px solid rgba(255,255,255,.3);border-radius:16px;' +
            'padding:8px 12px;cursor:pointer;font-size:12px;font-weight:bold;z-index:11;white-space:nowrap;';
        dlAllBtn.textContent = '📦 전체';
        dlAllBtn.addEventListener('click', function (e) { e.stopPropagation(); doDownloadAll(); });
        overlay.appendChild(dlAllBtn);

        /* 인포 */
        infoLabel = document.createElement('div');
        infoLabel.style.cssText =
            'position:absolute;bottom:26px;left:50%;transform:translateX(-50%);' +
            'color:#fff;background:rgba(0,0,0,.6);padding:5px 12px;border-radius:5px;' +
            'font-size:13px;user-select:none;z-index:10;pointer-events:none;';
        overlay.appendChild(infoLabel);

        /* 줌 라벨 */
        zoomLabel = document.createElement('div');
        zoomLabel.style.cssText =
            'position:absolute;top:20px;left:50%;transform:translateX(-50%);' +
            'color:#fff;background:rgba(0,0,0,.7);padding:6px 14px;border-radius:16px;' +
            'font-size:14px;font-weight:bold;user-select:none;z-index:10;' +
            'opacity:0;transition:opacity .2s;';
        zoomLabel.textContent = '100%';
        overlay.appendChild(zoomLabel);

        /* 스크롤바 */
        scrollBarWrap = document.createElement('div');
        scrollBarWrap.style.cssText =
            'position:absolute;right:8px;width:8px;height:80%;display:flex;align-items:center;z-index:5;';
        scrollBarTrack = document.createElement('div');
        scrollBarTrack.style.cssText =
            'position:relative;width:100%;height:100%;' +
            'background:rgba(255,255,255,.15);border-radius:4px;cursor:pointer;';
        var sThumb = document.createElement('div');
        sThumb.id = 'scrollThumb';
        sThumb.style.cssText =
            'width:100%;background:rgba(255,255,255,.75);border-radius:4px;position:absolute;top:0;';
        scrollBarTrack.appendChild(sThumb);
        scrollBarWrap.appendChild(scrollBarTrack);
        overlay.appendChild(scrollBarWrap);

        /* 스크롤바 클릭 탐색 */
        scrollBarTrack.addEventListener('mousedown', function (e) {
            var rect = scrollBarTrack.getBoundingClientRect();
            function upd(cy) {
                var r = Math.max(0, Math.min(cy - rect.top, rect.height)) / rect.height;
                if (mode === 'single') {
                    currentIdx = Math.max(0, Math.min(images.length - 1, Math.floor(r * images.length)));
                    showImage();
                } else if (imgBox) {
                    imgBox.scrollTop = r * ((imgBox.scrollHeight || 1) - imgBox.clientHeight);
                }
                updateScrollBar();
            }
            upd(e.clientY);
            function mv(e2) { upd(e2.clientY); }
            function up() { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); }
            document.addEventListener('mousemove', mv);
            document.addEventListener('mouseup', up);
        });

        /* 드래그 패닝 (싱글 모드 + 줌 > 1) */
        imgBox.addEventListener('mousedown', function (e) {
            if (mode !== 'single' || zoomLevel <= 1) return;
            e.preventDefault();
            dragging = true;
            dragStartX = e.clientX; dragStartY = e.clientY;
            panOriginX = panX; panOriginY = panY;
            imgBox.style.cursor = 'grabbing';
        });
        document.addEventListener('mousemove', function (e) {
            if (!dragging) return;
            panX = panOriginX + (e.clientX - dragStartX);
            panY = panOriginY + (e.clientY - dragStartY);
            applyPan();
        });
        document.addEventListener('mouseup', function () {
            if (!dragging) return;
            if (imgBox) imgBox.style.cursor = zoomLevel > 1 ? 'grab' : 'default';
            setTimeout(function () { dragging = false; }, 10);
        });

        /* 휠 */
        viewContainer.addEventListener('wheel', function (e) {
            e.preventDefault();
            if (e.ctrlKey && mode === 'single') {
                setZoom(zoomLevel + (e.deltaY < 0 ? 0.25 : -0.25));
                return;
            }
            if (mode === 'single') {
                if (e.deltaY > 0 && currentIdx < images.length - 1) { currentIdx++; showImage(); }
                else if (e.deltaY < 0 && currentIdx > 0) { currentIdx--; showImage(); }
            } else if (imgBox) {
                imgBox.scrollTop += e.deltaY * scrollSpd;
                updateScrollBar(); updateInfo();
            }
        }, { passive: false });

        /* 배경 클릭 → 닫기 */
        viewContainer.addEventListener('click', function (e) {
            if (e.target === viewContainer) closeViewer();
        });
        imgBox.addEventListener('click', function (e) {
            if (dragging) return;
            if (e.target === imgBox) { closeViewer(); return; }
            e.stopPropagation();
            if (mode === 'single' && zoomLevel <= 1) {
                if (currentIdx < images.length - 1) { currentIdx++; showImage(); }
                else closeViewer();
            }
        });

        /* 키보드 */
        document.addEventListener('keydown', function (e) {
            if (!overlay || overlay.style.display === 'none') return;
            var tag = document.activeElement ? document.activeElement.tagName.toUpperCase() : '';
            if (tag === 'INPUT' || tag === 'TEXTAREA' ||
                (document.activeElement && document.activeElement.isContentEditable)) return;

            if (e.key === 'Escape') { closeViewer(); return; }
            if (e.key === 's' || e.key === 'S') {
                var sv = toggleSelect(currentIdx);
                var sp = overlay.querySelector('#vSelBtn span');
                if (sp) sp.textContent = sv ? '☑' : '☐';
                updateUI(); showFeedback(sv); return;
            }
            if (e.key === 'q' || e.key === 'Q') { doDownload(); return; }
            if (e.key === '+' || e.key === '=') { setZoom(zoomLevel + 0.25); return; }
            if (e.key === '-') { setZoom(zoomLevel - 0.25); return; }
            if (e.key === '0') { setZoom(1); return; }

            var isNext = e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D';
            var isPrev = e.key === 'ArrowLeft'  || e.key === 'a' || e.key === 'A';
            if (mode === 'single') {
                if (isNext && currentIdx < images.length - 1) { e.preventDefault(); currentIdx++; showImage(); }
                else if (isPrev && currentIdx > 0) { e.preventDefault(); currentIdx--; showImage(); }
            } else if (imgBox) {
                if (isNext) imgBox.scrollTop += 80 * scrollSpd;
                else if (isPrev) imgBox.scrollTop -= 80 * scrollSpd;
                requestAnimationFrame(function () { updateScrollBar(); updateInfo(); });
            }
        }, true);

        createThumbStrip();
    }

    /* ── 이미지 표시 ── */
    function showImage() {
        if (!images.length) return;
        currentIdx = Math.max(0, Math.min(images.length - 1, currentIdx));
        viewerOpen = true; zoomLevel = 1; panX = 0; panY = 0;

        var selSpan = overlay ? overlay.querySelector('#vSelBtn span') : null;
        if (selSpan) selSpan.textContent = selected.has(currentIdx) ? '☑' : '☐';

        if (mode === 'single') {
            viewContainer.style.cssText =
                'display:flex;align-items:center;justify-content:center;width:100%;height:100%;';
            imgBox.style.cssText =
                'display:flex;align-items:center;justify-content:center;max-width:95%;max-height:95%;';
            imgBox.innerHTML = '';
            var wr = document.createElement('div');
            wr.style.cssText = 'position:relative;display:flex;align-items:center;justify-content:center;';
            var med = createMedia(currentIdx);
            med.style.cssText = 'max-width:90vw;max-height:88vh;object-fit:contain;user-select:none;';
            currentMedia = med;
            wr.appendChild(med); imgBox.appendChild(wr);
        } else {
            viewContainer.style.cssText =
                'display:flex;align-items:flex-start;justify-content:center;width:100%;height:100%;';
            imgBox.style.cssText =
                'display:flex;flex-direction:column;align-items:center;' +
                'width:' + f.viewerWidth + '%;height:100%;overflow-y:auto;scrollbar-width:none;';
            imgBox.innerHTML = '';
            images.forEach(function (url, idx) {
                var wr2 = document.createElement('div');
                wr2.style.cssText = 'position:relative;width:100%;display:flex;justify-content:center;margin-bottom:4px;';
                var med2 = createMedia(idx);
                med2.style.cssText = 'max-width:100%;object-fit:contain;user-select:none;';
                wr2.appendChild(med2);
                /* 선택 체크박스 */
                var cbx = document.createElement('div');
                cbx.style.cssText =
                    'position:absolute;top:8px;left:8px;width:26px;height:26px;' +
                    'background:' + (selected.has(idx) ? 'rgba(0,120,255,.8)' : 'rgba(0,0,0,.5)') + ';' +
                    'border:2px solid #fff;border-radius:50%;cursor:pointer;' +
                    'display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:bold;';
                cbx.innerHTML = selected.has(idx) ? '✓' : '';
                (function (capturedIdx, capturedCbx) {
                    capturedCbx.addEventListener('click', function (e) {
                        e.stopPropagation();
                        var sv = toggleSelect(capturedIdx);
                        capturedCbx.innerHTML = sv ? '✓' : '';
                        capturedCbx.style.background = sv ? 'rgba(0,120,255,.8)' : 'rgba(0,0,0,.5)';
                        updateUI(); showFeedback(sv);
                    });
                })(idx, cbx);
                wr2.appendChild(cbx);
                imgBox.appendChild(wr2);
            });
        }
        updateInfo(); updateScrollBar(); updateUI();

        /* 프리로드 */
        if (f.preloadImage) {
            for (var j = 1; j <= f.preloadCount; j++) {
                var n = currentIdx + j;
                if (n < images.length && !preloaded.includes(n)) {
                    preloaded.push(n);
                    createMedia(n);
                }
            }
        }
    }

    function openViewer() {
        if (!images.length) return;
        if (!overlay) createViewer();
        overlay.style.display = 'flex';
        showImage();
    }
    function closeViewer() {
        if (overlay) overlay.style.display = 'none';
        viewerOpen = false;
        if (panelOpen) hidePanel();
        /* 외부 모드였으면 원래 이미지 목록으로 복원 */
        if (extMode) {
            extMode = false; extFileName = null;
            images = origImages.slice();
            sourceEls = origSourceEls.slice();
            preloaded = []; selected.clear(); currentIdx = 0;
        }
    }

    /* 외부에서 뷰어 열기 (갤러리 모달에서 호출) */
    viewerAPI.openExternal = function (urls, srcEls, startIdx, fileName) {
        if (!urls || !urls.length) return;
        origImages = images.slice(); origSourceEls = sourceEls.slice();
        images = urls.slice();
        sourceEls = srcEls ? srcEls.slice() : [];
        preloaded = [];
        currentIdx = Math.max(0, Math.min(startIdx || 0, urls.length - 1));
        selected.clear(); extMode = true;
        extFileName = sanitizeFileName(fileName || buildFileName());
        openViewer();
    };

    /* 게시글 페이지에서 이미지 클릭으로 뷰어 열기 */
    if (isArticlePage) {
        var contentEl = document.querySelector('.writing_view_box') ||
                        document.querySelector('.write_div') ||
                        document.querySelector('.gallview_content .write_div');
        if (contentEl) {
            contentEl.querySelectorAll('img,video').forEach(function (el) {
                if (el.tagName === 'IMG') {
                    if (isSkipImg(el)) return;
                    var src = el.getAttribute('data-src') || el.src || '';
                    if (!src) return;
                    if (src.startsWith('//')) src = 'https:' + src;
                    images.push(src); sourceEls.push(el);
                } else {
                    var vsrc = el.getAttribute('src') || '';
                    if (!vsrc) return;
                    if (vsrc.startsWith('//')) vsrc = 'https:' + vsrc;
                    images.push(vsrc); sourceEls.push(el);
                }
            });
        }
        sourceEls.forEach(function (el, idx) {
            el.style.cursor = 'pointer';
            (function (capturedIdx) {
                el.addEventListener('click', function (e) {
                    e.preventDefault();
                    currentIdx = capturedIdx;
                    openViewer();
                });
            })(idx);
        });
        origImages = images.slice();
        origSourceEls = sourceEls.slice();
    }
}

/* ================================================================
   갤러리 뷰
================================================================ */
var gPostData  = new Map();
var gModalVrows = [];
var gModalIdx  = -1;
var gGalleryWrap = null;
var gModal = null, gModalOv = null;
var gTooltip = null, gTTtimer = null;
var gFetchingSet = new Set();
var gProgress = { done: 0, total: 0 };
var MODAL_IMG_W = Math.max(30, Math.min(100, parseInt(GM_getValue('dciMIW', 80), 10) || 80));
var gColumns   = f.columns;
var gThumbMode = f.thumbMode;

/* ── CSS 주입 ── */
function injectCSS() {
    if (document.getElementById('dci-gallery-css')) return;
    var s = document.createElement('style');
    s.id = 'dci-gallery-css';
    s.textContent = `
/* ─────────────────────── 컨트롤 바 ─────────────────────── */
#dciGalleryCtrl {
    display:flex; flex-wrap:wrap; gap:8px; padding:10px 12px;
    background:#f8f9fa; border:1px solid #dee2e6; border-radius:8px;
    align-items:center; margin-bottom:12px; box-sizing:border-box;
}
#dciGalleryCtrl button {
    padding:6px 12px; border:none; border-radius:5px;
    cursor:pointer; font-size:12px; font-weight:bold;
    color:#fff; white-space:nowrap;
}
#dciGalleryCtrl select {
    padding:4px 6px; border:1px solid #ccc; border-radius:4px;
    font-size:12px; cursor:pointer; background:#fff; color:#333;
}

/* ─────────────────────── 갤러리 그리드 ─────────────────────── */
#dciGalleryWrap {
    display:grid;
    grid-template-columns:repeat(var(--dci-cols, 3), 1fr);
    gap:10px; padding:4px 0;
}

/* ─────────────────────── 카드 ─────────────────────── */
.dci-card {
    display:flex; flex-direction:column;
    border:1px solid #dee2e6; border-radius:8px;
    overflow:hidden; background:#fff;
    transition:border-color .2s, box-shadow .2s;
    position:relative; box-shadow:0 1px 3px rgba(0,0,0,.06);
    cursor:pointer;
}
.dci-card:hover {
    border-color:#4a90d9;
    box-shadow:0 4px 14px rgba(74,144,217,.2);
}

/* 썸네일 영역 공통 */
.dci-card .thumb-area {
    overflow:hidden; position:relative;
    background:#f0f0f0;
    display:flex; align-items:center; justify-content:center;
    width:100%;
}
/* crop 모드: 고정 높이 */
.dci-card .thumb-area.crop-mode {
    height:180px;
}
.dci-card .thumb-area.crop-mode img.gallery-preview-image {
    width:100%; height:100%; object-fit:cover; display:block;
}
/* full 모드: 원본 비율 */
.dci-card .thumb-area.full-mode {
    height:auto; min-height:60px;
}
.dci-card .thumb-area.full-mode img.gallery-preview-image {
    width:100%; height:auto; object-fit:contain; display:block;
}

.dci-card .card-info  { padding:6px 8px; }
.dci-card .card-title {
    font-size:12px; color:#333; line-height:1.4;
    display:-webkit-box; -webkit-line-clamp:2;
    -webkit-box-orient:vertical; overflow:hidden;
    word-break:break-all; margin-bottom:3px;
}
.dci-card .card-meta { font-size:11px; color:#888; display:flex; gap:6px; }

/* 배지 */
.dci-badge-top {
    position:absolute; top:5px; right:5px;
    background:#4a90d9; color:#fff;
    padding:2px 6px; border-radius:10px;
    font-size:10px; font-weight:bold; z-index:2; white-space:nowrap;
}
.dci-badge-bottom {
    position:absolute; bottom:5px; right:5px;
    background:rgba(0,0,0,.6); color:#fff;
    padding:2px 6px; border-radius:10px;
    font-size:10px; font-weight:bold; z-index:2; white-space:nowrap;
    display:none;
}

/* ─────────────────────── 모달 ─────────────────────── */
#galleryModal .mc {
    background:#f9f9f9; padding:16px; border-radius:6px;
    margin-bottom:14px; max-height:52vh; overflow-y:auto;
    line-height:1.8; word-break:break-word; color:#333; font-size:14px;
}
#galleryModal .mc img {
    max-width:var(--miw, 80%); height:auto !important;
    display:block; margin:10px auto; border-radius:4px; cursor:pointer;
}
#galleryModal .mc video {
    max-width:100%; max-height:480px; display:block;
    margin:10px auto; border-radius:4px; background:#000;
}
#galleryModal .mc iframe {
    max-width:100%; width:100%; min-height:340px; display:block;
    margin:10px auto; border-radius:4px; border:1px solid #ddd;
}
#galleryModal .mc a { color:#2980b9; }
#galleryModal .mcs { border-top:2px solid #eee; margin-top:14px; padding-top:14px; }
#galleryModal .mci {
    background:#f5f5f5; border-radius:6px;
    padding:9px 11px; margin-bottom:5px;
}
#galleryModal .mci.reply {
    margin-left:24px; background:#ececec;
    border-left:3px solid #ccc;
}
#galleryModal .mci.del { padding:6px 11px; background:#f0f0f0; }
#galleryModal .mcb {
    color:#444; font-size:13px; line-height:1.55; word-break:break-word;
}
#galleryModal .mcb img {
    max-height:70px !important; display:inline !important;
    vertical-align:middle; margin:2px; width:auto !important;
}
#galleryModal .mcb a { color:#2980b9; }
#galleryModal .mnav {
    display:flex; justify-content:space-between; align-items:center;
    margin-bottom:12px; padding-bottom:10px; border-bottom:1px solid #eee;
}
#galleryModal .mnav-btn {
    background:#eee; color:#333; border:none;
    padding:6px 14px; border-radius:6px; cursor:pointer; font-size:13px;
}
#galleryModal .mnav-btn:hover:not(:disabled) { background:#ddd; }
#galleryModal .mnav-btn:disabled { opacity:.35; cursor:default; }

/* ─────────────────────── 다크 테마 ─────────────────────── */
body.dci-dark #dciGalleryCtrl {
    background:#1e1e1e; border-color:#444; color:#ddd;
}
body.dci-dark #dciGalleryCtrl span  { color:#aaa !important; }
body.dci-dark #dciGalleryCtrl select {
    background:#2a2a2a; color:#ddd; border-color:#555;
}
body.dci-dark .dci-card {
    background:#2a2a2a; border-color:#444; color:#ddd;
}
body.dci-dark .dci-card:hover {
    border-color:#4a90d9; box-shadow:0 4px 14px rgba(74,144,217,.3);
}
body.dci-dark .dci-card .thumb-area { background:#1a1a1a; }
body.dci-dark .dci-card .card-title { color:#ddd; }
body.dci-dark .dci-card .card-meta  { color:#888; }
body.dci-dark #galleryModal {
    background:#1e1e1e !important; border-color:#555 !important; color:#ddd !important;
}
body.dci-dark #galleryModal h3  { color:#eee !important; }
body.dci-dark #galleryModal .mc { background:#252525; color:#ddd; }
body.dci-dark #galleryModal .mcs { border-color:#444; }
body.dci-dark #galleryModal .mci { background:#2a2a2a; }
body.dci-dark #galleryModal .mci.reply { background:#252525; border-left-color:#555; }
body.dci-dark #galleryModal .mci.del   { background:#222; }
body.dci-dark #galleryModal .mcb       { color:#ccc; }
body.dci-dark #galleryModal .mnav      { border-color:#444; }
body.dci-dark #galleryModal .mnav-btn  { background:#333; color:#ddd; }
body.dci-dark #galleryModal .mnav-btn:hover:not(:disabled) { background:#444; }
body.dci-dark #galleryModal a          { color:#6cb4ee !important; }
body.dci-dark #galleryModal .img-size-ctrl { background:#333 !important; }
body.dci-dark #galleryModal .img-size-ctrl label { color:#aaa !important; }
body.dci-dark .dci-tooltip {
    background:#1e1e1e !important; border-color:#555 !important; color:#ddd !important;
}
body.dci-dark .dci-badge-bottom { background:rgba(255,255,255,.15); }
`;
    document.head.appendChild(s);
}

/* ── 열 수 / 썸네일 모드 적용 ── */
function applyColumns(n) {
    gColumns = n; f.columns = n; GM_setValue('dci_columns', n);
    if (gGalleryWrap) gGalleryWrap.style.setProperty('--dci-cols', n);
}
function applyThumbMode(mode) {
    gThumbMode = mode; f.thumbMode = mode; GM_setValue('dci_thumbMode', mode);
    if (!gGalleryWrap) return;
    gGalleryWrap.querySelectorAll('.thumb-area').forEach(function (ta) {
        ta.classList.remove('crop-mode', 'full-mode');
        ta.classList.add(mode + '-mode');
        var img = ta.querySelector('img.gallery-preview-image');
        if (!img) return;
        if (mode === 'crop') {
            img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'cover';
        } else {
            img.style.width = '100%'; img.style.height = 'auto'; img.style.objectFit = 'contain';
        }
    });
}

/* ── 툴팁 ── */
function createTooltip() {
    gTooltip = document.createElement('div');
    gTooltip.className = 'dci-tooltip';
    gTooltip.style.cssText =
        'position:fixed;z-index:99999;background:#fff;border:1px solid #ddd;' +
        'border-radius:8px;padding:12px;max-width:320px;max-height:320px;' +
        'overflow-y:auto;color:#333;font-size:12px;line-height:1.5;' +
        'box-shadow:0 4px 16px rgba(0,0,0,.15);pointer-events:none;' +
        'display:none;word-break:break-word;';
    document.body.appendChild(gTooltip);
}
function showTT(data, pos) {
    if (!gTooltip) return;
    var h = '';
    if (data.bodyText) {
        var cleaned = cleanBodyText(data.bodyText);
        var p = cleaned.replace(/\n+/g, ' ').substring(0, 250);
        h += '<div style="margin-bottom:6px">' + esc(p) + (cleaned.length > 250 ? '…' : '') + '</div>';
    } else {
        h += '<div style="color:#aaa;margin-bottom:6px">본문 없음</div>';
    }
    if (data.media && data.media.ifrV > 0) {
        h += '<div style="color:#e74c3c;font-size:11px">▶️ 영상 ' + data.media.ifrV + '</div>';
    }
    if (data.comments && data.comments.length) {
        h += '<div style="border-top:1px solid #eee;padding-top:6px;margin-top:4px">' +
             '<div style="color:#f39c12;font-weight:bold;margin-bottom:4px;font-size:11px">' +
             '💬 댓글 ' + data.comments.length + '</div>';
        data.comments.filter(function (c) { return !c.isDeleted; }).slice(0, 3).forEach(function (c) {
            var txt = cleanBodyText(c.text).substring(0, 60);
            h += '<div style="font-size:11px;margin-bottom:2px;color:#555">' +
                 '<span style="color:#2980b9">' + (c.isReply ? '↳ ' : '') + esc(c.username) + '</span> ' +
                 esc(txt + (c.text.length > 60 ? '…' : '')) + '</div>';
        });
        if (data.comments.length > 3) {
            h += '<div style="color:#aaa;font-size:10px">…외 ' + (data.comments.length - 3) + '개</div>';
        }
        h += '</div>';
    }
    gTooltip.innerHTML = h;
    gTooltip.style.display = 'block';
    var O = 12, x = pos.x + O, y = pos.y + O;
    var r = gTooltip.getBoundingClientRect();
    if (x + r.width  > innerWidth)  x = pos.x - r.width  - O;
    if (y + r.height > innerHeight) y = pos.y - r.height - O;
    gTooltip.style.left = Math.max(4, x) + 'px';
    gTooltip.style.top  = Math.max(4, y) + 'px';
}
function hideTT() {
    clearTimeout(gTTtimer); gTTtimer = null;
    if (gTooltip) gTooltip.style.display = 'none';
}

/* ── 모달 ── */
function createModal() {
    if (gModalOv) return;
    gModalOv = document.createElement('div');
    gModalOv.style.cssText =
        'position:fixed;top:0;left:0;width:100%;height:100%;' +
        'background:rgba(0,0,0,.55);z-index:9998;display:none;';
    gModalOv.addEventListener('click', closeModal);

    gModal = document.createElement('div');
    gModal.id = 'galleryModal';
    gModal.style.cssText =
        'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' +
        'background:#fff;border:1px solid #ddd;border-radius:12px;' +
        'padding:22px;max-width:780px;width:92%;max-height:86vh;overflow-y:auto;' +
        'color:#333;font-size:14px;line-height:1.6;' +
        'box-shadow:0 8px 36px rgba(0,0,0,.18);z-index:9999;display:none;';
    gModal.addEventListener('click', function (e) { e.stopPropagation(); });

    document.body.appendChild(gModalOv);
    document.body.appendChild(gModal);

    document.addEventListener('keydown', function (e) {
        if (!gModal || gModal.style.display !== 'block') return;
        if (e.key === 'Escape')     { closeModal(); return; }
        if (e.key === 'ArrowLeft')  { e.preventDefault(); navModal(-1); }
        if (e.key === 'ArrowRight') { e.preventDefault(); navModal(1); }
    });
}
function navModal(dir) {
    var ni = gModalIdx + dir;
    if (ni < 0 || ni >= gModalVrows.length) return;
    var vrow = gModalVrows[ni];
    if (!vrow || !gPostData.has(vrow)) return;
    cleanModalMedia();
    openModal(vrow);
}
function cleanModalMedia() {
    if (!gModal) return;
    gModal.querySelectorAll('video').forEach(function (v) {
        try { v.pause(); v.src = ''; } catch (e) { /* ignore */ }
    });
    gModal.querySelectorAll('iframe').forEach(function (ff) {
        try { ff.src = ''; } catch (e) { /* ignore */ }
    });
}
function openModal(vrow) {
    if (!vrow) return;
    var d = gPostData.get(vrow);
    if (!d) return;
    gModalIdx = gModalVrows.indexOf(vrow);
    var curW = MODAL_IMG_W;
    var hh = '';

    /* 네비게이션 */
    hh += '<div class="mnav">' +
          '<button class="mnav-btn" id="mPrev"' + (gModalIdx <= 0 ? ' disabled' : '') + '>◀ 이전</button>' +
          '<span style="font-size:13px">' + (gModalIdx + 1) + ' / ' + gModalVrows.length + '</span>' +
          '<button class="mnav-btn" id="mNext"' + (gModalIdx >= gModalVrows.length - 1 ? ' disabled' : '') + '>다음 ▶</button>' +
          '</div>';

    /* 제목 + 닫기 */
    hh += '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;gap:10px">' +
          '<h3 style="margin:0;font-size:16px;flex:1">' + esc(d.title || '제목 없음') + '</h3>' +
          '<span id="mClose" style="font-size:22px;cursor:pointer;flex-shrink:0;line-height:1;color:#888">✕</span>' +
          '</div>';

    /* 원본 링크 */
    if (vrow._href) {
        hh += '<div style="margin-bottom:10px">' +
              '<a href="' + esc(vrow._href) + '" target="_blank" style="color:#2980b9;font-size:13px">📄 원본 글 열기 →</a>' +
              '</div>';
    }

    /* 이미지 컨트롤 */
    if (d.media && d.media.imgs > 0) {
        hh += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center">' +
              '<div class="img-size-ctrl" style="flex:1;display:flex;align-items:center;gap:8px;padding:8px 10px;background:#f0f0f0;border-radius:6px;">' +
              '<span style="font-size:12px;white-space:nowrap">🖼️ ' + d.media.imgs + '장</span>' +
              '<input type="range" id="mImgSlider" min="30" max="100" value="' + curW + '" style="flex:1;accent-color:#4a90d9;cursor:pointer;">' +
              '<span id="mImgLabel" style="font-size:12px;white-space:nowrap">' + curW + '%</span>' +
              '</div>' +
              '<button id="mDlAll" style="background:#4a90d9;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:bold;white-space:nowrap">⬇ 전체 다운로드</button>' +
              '</div>';
    }

    /* 본문 */
    if (d.bodyHtml) {
        hh += '<div class="mc" style="--miw:' + curW + '%">' + d.bodyHtml + '</div>';
    } else if (d.bodyText) {
        hh += '<div class="mc" style="white-space:pre-wrap">' + esc(cleanBodyText(d.bodyText)) + '</div>';
    } else {
        hh += '<div style="color:#aaa;margin-bottom:14px;font-size:13px">본문 없음</div>';
    }

    /* 댓글 */
    if (d.comments && d.comments.length) {
        hh += '<div class="mcs">' +
              '<div style="font-weight:bold;font-size:15px;margin-bottom:10px;display:flex;align-items:center;gap:8px">' +
              '💬 댓글 (' + d.comments.length + ')' +
              '<button id="mCmtToggle" style="background:#eee;color:#555;border:none;padding:2px 10px;border-radius:10px;font-size:12px;cursor:pointer">접기</button>' +
              '</div><div id="mCmtList">';
        d.comments.forEach(function (c) {
            if (c.isDeleted) {
                hh += '<div class="mci del"><span style="color:#aaa;font-style:italic">[삭제됨]</span></div>';
                return;
            }
            hh += '<div class="mci' + (c.isReply ? ' reply' : '') + '">' +
                  '<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;flex-wrap:wrap">' +
                  (c.isReply ? '<span style="color:#bbb">↳</span>' : '') +
                  '<span style="color:#2980b9;font-weight:bold;font-size:12px">' + esc(c.username) + '</span>';
            if (c.time) {
                hh += '<span style="font-size:11px;margin-left:auto;color:#aaa">' + esc(c.time) + '</span>';
            }
            hh += '</div><div class="mcb">' + (c.html || esc(cleanBodyText(c.text))) + '</div></div>';
        });
        hh += '</div></div>';
    }

    if (!gModal) return;
    gModal.innerHTML = hh;
    gModal.style.display = 'block';
    if (gModalOv) gModalOv.style.display = 'block';
    gModal.scrollTop = 0;

    /* 이미지 슬라이더 */
    var slider = gModal.querySelector('#mImgSlider');
    var label  = gModal.querySelector('#mImgLabel');
    var mc     = gModal.querySelector('.mc');
    if (slider && mc) {
        slider.addEventListener('input', function (e) {
            var v = e.target.value;
            mc.style.setProperty('--miw', v + '%');
            if (label) label.textContent = v + '%';
            MODAL_IMG_W = parseInt(v, 10);
            GM_setValue('dciMIW', v);
        });
    }

    /* 이미지 클릭 → 뷰어 */
    var imgs = gModal.querySelectorAll('.mc img');
    var vUrls = [], vEls = [];
    imgs.forEach(function (img) {
        if (!isDccon(img) && !isRepImg(img) && img.src) {
            vUrls.push(img.src); vEls.push(img);
            img.style.cursor = 'pointer';
        }
    });
    vEls.forEach(function (img, idx) {
        (function (capturedIdx) {
            img.addEventListener('click', function (e) {
                e.preventDefault(); e.stopPropagation();
                if (viewerAPI.openExternal) {
                    viewerAPI.openExternal(vUrls, vEls, capturedIdx, buildFileName(d.title));
                }
            });
        })(idx);
    });

    /* 전체 다운로드 */
    var dlAll = gModal.querySelector('#mDlAll');
    if (dlAll && vUrls.length) {
        dlAll.addEventListener('click', function (e) {
            e.stopPropagation();
            var name = buildFileName(d.title || 'download');
            var total = vUrls.length, done = 0;
            dlAll.disabled = true;
            dlAll.textContent = '⏳ 0/' + total;
            dlAll.style.background = '#888';

            function finish() {
                setTimeout(function () {
                    dlAll.textContent = '⬇ 전체 다운로드';
                    dlAll.style.background = '#4a90d9';
                    dlAll.disabled = false;
                }, 2000);
            }
            if (total === 1) {
                gmFetchBlob(vUrls[0])
                    .then(function (r) {
                        dlBlob(r.blob, name + getExt(r.contentType, vUrls[0]));
                        dlAll.textContent = '✅ 완료'; dlAll.style.background = '#4CAF50';
                        finish();
                    })
                    .catch(function () {
                        dlAll.textContent = '❌ 실패'; dlAll.style.background = '#e74c3c'; finish();
                    });
            } else {
                var zip = new JSZip();
                var pad = String(total).length;
                Promise.all(vUrls.map(function (url, ii) {
                    return gmFetchBlob(url).then(function (r) {
                        done++;
                        dlAll.textContent = '⏳ ' + done + '/' + total;
                        var n = String(ii + 1);
                        while (n.length < pad) n = '0' + n;
                        zip.file(n + getExt(r.contentType, url), r.blob);
                    }).catch(function () { done++; });
                })).then(function () {
                    dlAll.textContent = '📦 압축 중…';
                    return zip.generateAsync({ type: 'blob' });
                }).then(function (b) {
                    dlBlob(b, name + '.zip');
                    dlAll.textContent = '✅ 완료'; dlAll.style.background = '#4CAF50'; finish();
                }).catch(function () {
                    dlAll.textContent = '❌ 실패'; dlAll.style.background = '#e74c3c'; finish();
                });
            }
        });
    }

    /* 버튼 이벤트 */
    var mClose = gModal.querySelector('#mClose');
    if (mClose) mClose.addEventListener('click', closeModal);
    var mPrev = gModal.querySelector('#mPrev');
    if (mPrev) mPrev.addEventListener('click', function () { navModal(-1); });
    var mNext = gModal.querySelector('#mNext');
    if (mNext) mNext.addEventListener('click', function () { navModal(1); });
    var tb = gModal.querySelector('#mCmtToggle');
    var cc = gModal.querySelector('#mCmtList');
    if (tb && cc) {
        tb.addEventListener('click', function () {
            var visible = cc.style.display !== 'none';
            cc.style.display = visible ? 'none' : '';
            tb.textContent   = visible ? '펼치기' : '접기';
        });
    }
}
function closeModal() {
    cleanModalMedia();
    if (gModal)   { gModal.style.display = 'none'; gModal.innerHTML = ''; }
    if (gModalOv) gModalOv.style.display = 'none';
    gModalIdx = -1;
}

/* ── 컨트롤 바 ── */
function createControls(anchor, count) {
    var old = document.getElementById('dciGalleryCtrl');
    if (old) old.remove();

    var bc = document.createElement('div');
    bc.id = 'dciGalleryCtrl';

    var ss = document.createElement('span');
    ss.id = 'gStatus';
    ss.style.cssText = 'font-size:12px;margin-left:auto;';
    ss.textContent = count > 0 ? ('로딩 중… (0/' + count + ')') : '게시글 없음';

    function btn(t, bg, fn) {
        var b = document.createElement('button');
        b.textContent = t; b.style.background = bg;
        b.addEventListener('click', fn);
        return b;
    }

    /* 모두 열기 */
    bc.appendChild(btn('🔗 모두열기', '#4a90d9', function () {
        var links = [];
        gModalVrows.forEach(function (v) { if (v._href) links.push(v._href); });
        if (!links.length) { alert('열 게시글 없음'); return; }
        if (!confirm(links.length + '개를 새 탭으로 열겠습니까?')) return;
        links.forEach(function (l, i) {
            setTimeout(function () { GM_openInTab(l, { active: false, insert: true }); }, i * 120);
        });
    }));

    /* 선택 열기 */
    bc.appendChild(btn('✅ 선택 열기', '#f0ad4e', function () {
        var links = [];
        document.querySelectorAll('.dci-card-cb:checked').forEach(function (cb) {
            var card = cb.closest('.dci-card');
            if (card && card.dataset.href) links.push(card.dataset.href);
        });
        if (!links.length) { alert('선택된 게시글 없음'); return; }
        if (!confirm(links.length + '개를 새 탭으로 열겠습니까?')) return;
        links.forEach(function (l, i) {
            setTimeout(function () { GM_openInTab(l, { active: false, insert: true }); }, i * 120);
        });
    }));

    /* 전체선택 토글 */
    var allSel = false;
    var b3 = btn('☑️ 전체선택', '#777', function () {
        allSel = !allSel;
        document.querySelectorAll('.dci-card-cb').forEach(function (cb) { cb.checked = allSel; });
        b3.textContent = allSel ? '☑️ 전체해제' : '☑️ 전체선택';
    });
    bc.appendChild(b3);

    /* 썸네일 모드 토글 */
    var thumbModeBtn = btn(
        gThumbMode === 'crop' ? '🔲 풀사이즈' : '⬜ 크롭', '#6c757d',
        function () {
            var newMode = gThumbMode === 'crop' ? 'full' : 'crop';
            applyThumbMode(newMode);
            thumbModeBtn.textContent = newMode === 'crop' ? '🔲 풀사이즈' : '⬜ 크롭';
        }
    );
    bc.appendChild(thumbModeBtn);

    /* 열 수 선택 */
    var colWrap = document.createElement('div');
    colWrap.style.cssText = 'display:flex;align-items:center;gap:5px;';
    var colLabel = document.createElement('span');
    colLabel.style.cssText = 'font-size:12px;white-space:nowrap;';
    colLabel.textContent = '열:';
    var colSel = document.createElement('select');
    [1, 2, 3, 4, 5, 6].forEach(function (n) {
        var o = document.createElement('option');
        o.value = n; o.textContent = n + '열';
        if (n === gColumns) o.selected = true;
        colSel.appendChild(o);
    });
    colSel.addEventListener('change', function () { applyColumns(parseInt(colSel.value, 10)); });
    colWrap.appendChild(colLabel); colWrap.appendChild(colSel);
    bc.appendChild(colWrap);

    /* 테마 토글 */
    var themeBtn = btn(isDark ? '☀️ 라이트' : '🌙 다크', isDark ? '#555' : '#333', function () {
        var nowDark = !document.body.classList.contains('dci-dark');
        applyTheme(nowDark);
        themeBtn.textContent  = nowDark ? '☀️ 라이트' : '🌙 다크';
        themeBtn.style.background = nowDark ? '#555' : '#333';
    });
    bc.appendChild(themeBtn);
    bc.appendChild(ss);

    if (anchor && anchor.parentNode) {
        anchor.parentNode.insertBefore(bc, anchor);
    } else {
        var main = document.querySelector('.gall_listwrap,.gall_list,.board_list,#container');
        if (main) main.prepend(bc);
        else document.body.prepend(bc);
    }
    return bc;
}

function updStatus() {
    var s = document.getElementById('gStatus');
    if (!s) return;
    if (gProgress.done < gProgress.total) {
        s.textContent = '로딩 ' + gProgress.done + '/' + gProgress.total + '…';
    } else {
        s.textContent = '게시글: ' + gModalVrows.length + '개 (공지 제외)';
    }
}

/* ── 카드 생성 ── */
function makeCard(vrow) {
    var href = getRowHref(vrow); if (!href) return null;
    var title = getRowTitle(vrow) || '제목 없음';
    var num   = getRowNum(vrow);
    var reco  = getRowReco(vrow);
    var reply = getRowReply(vrow);

    var card = document.createElement('div');
    card.className = 'dci-card';
    card.dataset.href = href;
    vrow._href = href;
    vrow._card = card;

    /* 썸네일 영역 */
    var ta = document.createElement('div');
    ta.className = 'thumb-area ' + gThumbMode + '-mode';

    var loading = document.createElement('div');
    loading.style.cssText =
        'display:flex;flex-direction:column;align-items:center;justify-content:center;' +
        'width:100%;min-height:120px;color:#ccc;font-size:20px;gap:3px;';
    loading.innerHTML = '⏳<span style="font-size:10px">로딩중</span>';
    ta.appendChild(loading);

    /* 배지 – 상단 (미디어 타입) */
    var badgeTop = document.createElement('span');
    badgeTop.className = 'dci-badge-top';
    badgeTop.textContent = '…';
    ta.appendChild(badgeTop);

    /* 배지 – 하단 (이미지 개수) */
    var badgeBot = document.createElement('span');
    badgeBot.className = 'dci-badge-bottom';
    ta.appendChild(badgeBot);

    /* 체크박스 (좌상단) */
    var cb = document.createElement('input');
    cb.type = 'checkbox'; cb.className = 'dci-card-cb';
    cb.style.cssText =
        'position:absolute;top:5px;left:5px;width:15px;height:15px;' +
        'cursor:pointer;z-index:3;accent-color:#4a90d9;';
    cb.addEventListener('click',     function (e) { e.stopPropagation(); });
    cb.addEventListener('mousedown', function (e) { e.stopPropagation(); });
    ta.appendChild(cb);

    /* 미리보기 버튼 (좌하단) */
    function makePrevBtn() {
        var pb = document.createElement('span');
        pb.textContent = '👁';
        pb.style.cssText =
            'position:absolute;bottom:5px;left:5px;background:rgba(0,0,0,.5);' +
            'color:#fff;width:22px;height:22px;border-radius:50%;cursor:pointer;' +
            'font-size:11px;z-index:3;display:flex;align-items:center;justify-content:center;';
        pb.addEventListener('mouseenter', function () { pb.style.background = 'rgba(74,144,217,.9)'; });
        pb.addEventListener('mouseleave', function () { pb.style.background = 'rgba(0,0,0,.5)'; });
        pb.addEventListener('click', function (e) {
            e.preventDefault(); e.stopPropagation(); hideTT(); openModal(vrow);
        });
        return pb;
    }
    ta.appendChild(makePrevBtn());
    card.appendChild(ta);

    /* 정보 영역 */
    var info = document.createElement('div'); info.className = 'card-info';
    var tDiv = document.createElement('div'); tDiv.className = 'card-title';
    tDiv.textContent = title + (reply ? ' [' + reply + ']' : '');
    info.appendChild(tDiv);
    var meta = document.createElement('div'); meta.className = 'card-meta';
    meta.innerHTML = '<span>👍' + esc(reco) + '</span><span>#' + esc(num) + '</span>';
    info.appendChild(meta);
    card.appendChild(info);

    /* 카드 클릭 → 새 탭 (좌클릭만) */
    card.addEventListener('click', function (e) {
        if (e.button !== 0) return;
        if (e.target === cb || e.target.closest('.dci-badge-top,.dci-badge-bottom')) return;
        /* 미리보기 버튼 클릭은 해당 버튼의 리스너에서 처리 */
        if (e.target.textContent === '👁') return;
        window.open(href, '_blank');
    });
    /* 가운데 클릭 → 새 탭 */
    card.addEventListener('mousedown', function (e) {
        if (e.button !== 1) return;
        e.preventDefault();
        GM_openInTab(href, { active: false, insert: true });
    });

    /* 툴팁 */
    var lastMouse = { x: 0, y: 0 };
    card.addEventListener('mouseenter', function (e) {
        lastMouse = { x: e.clientX, y: e.clientY };
        clearTimeout(gTTtimer);
        gTTtimer = setTimeout(function () {
            var d = gPostData.get(vrow);
            if (d) showTT(d, lastMouse);
        }, 350);
    });
    card.addEventListener('mousemove', function (e) {
        lastMouse = { x: e.clientX, y: e.clientY };
        if (gTooltip && gTooltip.style.display === 'block') {
            gTooltip.style.left = Math.max(4, lastMouse.x + 12) + 'px';
            gTooltip.style.top  = Math.max(4, lastMouse.y + 12) + 'px';
        }
    });
    card.addEventListener('mouseleave', hideTT);

    /* fetch 후 UI 업데이트 함수 (외부 노출) */
    vrow._updateCard = function (data, thumb) {
        /* 배지 – 상단 */
        var typeParts = [];
        if (data.media.vids  > 0) typeParts.push('🎬' + data.media.vids);
        if (data.media.ifrV  > 0) typeParts.push('▶️' + data.media.ifrV);
        if (data.comments.length > 0) typeParts.push('💬' + data.comments.length);
        if (typeParts.length) {
            badgeTop.textContent = typeParts.join(' ');
            badgeTop.style.background =
                data.media.ifrV > 0 ? '#c0392b' :
                data.media.vids > 0 ? '#9b59b6' : '#4a90d9';
        } else {
            var tm = data.media.imgs + data.media.vids + data.media.ifrV;
            badgeTop.textContent = tm === 0 ? '없음' : '';
            badgeTop.style.background = '#aaa';
        }

        /* 배지 – 하단 (이미지 수) */
        if (data.media.imgs > 0) {
            badgeBot.textContent = '📷 ' + data.media.imgs;
            badgeBot.style.display = 'block';
            badgeBot.style.background =
                data.media.imgs >= 10 ? 'rgba(231,76,60,.8)' :
                data.media.imgs >= 5  ? 'rgba(243,156,18,.8)' : 'rgba(0,0,0,.6)';
        } else {
            badgeBot.style.display = 'none';
        }

        /* 썸네일 이미지 교체 */
        /* 오버레이 요소 보존 */
        var overlays = Array.from(
            ta.querySelectorAll('.dci-badge-top,.dci-badge-bottom,.dci-card-cb')
        );
        ta.innerHTML = '';

        if (thumb) {
            var tImg = document.createElement('img');
            tImg.className = 'gallery-preview-image';
            tImg.src = thumb.src;
            if (gThumbMode === 'crop') {
                tImg.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
            } else {
                tImg.style.cssText = 'width:100%;height:auto;object-fit:contain;display:block;';
            }
            tImg.onerror = function () { this.style.display = 'none'; };
            ta.appendChild(tImg);
        } else {
            var noImg = document.createElement('div');
            noImg.style.cssText =
                'display:flex;flex-direction:column;align-items:center;justify-content:center;' +
                'width:100%;min-height:100px;color:#bbb;font-size:12px;gap:3px;';
            noImg.innerHTML = '🖼️<span>이미지 없음</span>';
            ta.appendChild(noImg);
        }
        /* 오버레이 복원 */
        overlays.forEach(function (el) { ta.appendChild(el); });
        /* 미리보기 버튼 재추가 */
        ta.appendChild(makePrevBtn());
    };

    vrow._setError = function () {
        badgeTop.textContent = '❌'; badgeTop.style.background = '#ccc';
        var overlays = Array.from(
            ta.querySelectorAll('.dci-badge-top,.dci-badge-bottom,.dci-card-cb')
        );
        ta.innerHTML =
            '<div style="display:flex;align-items:center;justify-content:center;' +
            'width:100%;min-height:100px;color:#e74c3c;font-size:11px;' +
            'flex-direction:column;gap:3px;">⚠️<span>로딩 실패</span></div>';
        overlays.forEach(function (el) { ta.appendChild(el); });
        ta.appendChild(makePrevBtn());
    };

    return card;
}

/* ── fetch ── */
async function gFetch(vrow, delay) {
    if (!vrow || vrow.dataset.fetched) return;
    var href = vrow._href; if (!href) return;
    if (gFetchingSet.has(href)) return;
    gFetchingSet.add(href);
    vrow.dataset.fetched = '1';

    await new Promise(function (r) { setTimeout(r, delay); });

    try {
        var doc   = await gmFetch(href);
        var ac    = doc.querySelector('.writing_view_box') ||
                    doc.querySelector('.write_div') ||
                    doc.querySelector('.gallview_content .write_div');

        var rawBodyText = ac
            ? ac.textContent.replace(/\t/g, ' ').replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim()
            : '';
        var bodyText = cleanBodyText(rawBodyText);
        var bodyHtml = processBodyHtml(ac);
        var title    = extractTitle(doc);
        var media    = countBodyMedia(doc);
        var comments = extractComments(doc);
        var thumb    = findBodyThumb(doc);

        gPostData.set(vrow, { bodyText, bodyHtml, title, media, comments });
        if (vrow._updateCard) vrow._updateCard({ media, comments }, thumb);
    } catch (e) {
        console.error('[DCI Gallery] 로딩 실패:', href, e.message || e);
        if (vrow._setError) vrow._setError();
    }

    gProgress.done++;
    updStatus();
}

/* ── 갤러리 시작 ── */
async function startGallery() {
    if (document.getElementById('dciGalleryCtrl')) return;
    injectCSS(); createTooltip(); createModal();
    if (isDark) document.body.classList.add('dci-dark');

    var rows  = findListRows();
    var vrows = rows.filter(function (v) { return !isNoticeRow(v); });
    gModalVrows = vrows;
    gProgress.total = vrows.length;
    gProgress.done  = 0;

    /* 기준 앵커 찾기 */
    var anchor = null;
    if (rows.length) {
        var tableEl = rows[0].closest('table');
        anchor = tableEl || rows[0].closest('ul,ol,.gall_listwrap,.gall_list');
    }
    if (!anchor) {
        anchor = document.querySelector('.gall_listwrap,.gall_list,table.gall_list,#container .board_list');
    }

    createControls(anchor, vrows.length);
    if (!vrows.length) { updStatus(); return; }

    /* 갤러리 그리드 삽입 */
    gGalleryWrap = document.createElement('div');
    gGalleryWrap.id = 'dciGalleryWrap';
    gGalleryWrap.style.setProperty('--dci-cols', gColumns);

    var tableElMain = rows[0] ? rows[0].closest('table') : null;
    if (tableElMain) {
        tableElMain.style.display = 'none';
        tableElMain.parentNode.insertBefore(gGalleryWrap, tableElMain.nextSibling);
    } else if (anchor) {
        anchor.style.display = 'none';
        anchor.parentNode.insertBefore(gGalleryWrap, anchor.nextSibling);
    } else {
        document.body.appendChild(gGalleryWrap);
    }

    vrows.forEach(function (v) {
        var card = makeCard(v);
        if (card) gGalleryWrap.appendChild(card);
    });

    /* 배치 fetch */
    var batchSize = 5;
    for (var i = 0; i < vrows.length; i += batchSize) {
        var batch = vrows.slice(i, i + batchSize);
        await Promise.all(batch.map(function (v, bi) {
            return gFetch(v, bi * f.fetchDelay);
        }));
        if (i + batchSize < vrows.length) {
            await new Promise(function (r) { setTimeout(r, 300); });
        }
    }
    updStatus();
}

/* ================================================================
   설정 메뉴 (Tampermonkey 메뉴)
================================================================ */
function settingsModule() {
    GM_registerMenuCommand('⚙️ DCInside 갤러리뷰 설정', function () {
        var old = document.getElementById('dciSettOv');
        if (old) { old.remove(); return; }

        var ov = document.createElement('div');
        ov.id = 'dciSettOv';
        ov.style.cssText =
            'position:fixed;top:0;left:0;width:100%;height:100%;' +
            'background:rgba(0,0,0,.5);z-index:2147483646;' +
            'display:flex;align-items:center;justify-content:center;';
        ov.addEventListener('click', function (e) { if (e.target === ov) ov.remove(); });

        var w = document.createElement('div');
        w.style.cssText =
            'background:' + (isDark ? '#1e1e1e' : '#fff') + ';' +
            'color:' + (isDark ? '#ddd' : '#333') + ';' +
            'border-radius:12px;padding:22px;width:400px;max-height:80vh;' +
            'overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.2);';
        w.innerHTML =
            '<div style="font-size:17px;font-weight:bold;margin-bottom:14px;text-align:center">' +
            '⚙️ DCInside 갤러리뷰 설정</div>';

        var localF = JSON.parse(JSON.stringify(f));

        var items = [
            { k: 'galleryView',     l: '갤러리 뷰 사용',        t: 'bool' },
            { k: 'viewer',          l: '이미지 뷰어 사용',       t: 'bool' },
            { k: 'viewerOpacity',   l: '뷰어 배경 투명도',       t: 'num',  min: 0,   max: 1,    step: 0.1 },
            { k: 'viewerType',      l: '뷰어 타입',              t: 'sel',  opts: [['1','싱글'], ['2','스크롤']] },
            { k: 'viewerWidth',     l: '스크롤 뷰어 너비(%)',    t: 'num',  min: 30,  max: 100 },
            { k: 'scrollSpeed',     l: '스크롤 속도',            t: 'num',  min: 0.1, max: 5, step: 0.1 },
            { k: 'preloadImage',    l: '이미지 프리로드',        t: 'bool' },
            { k: 'preloadCount',    l: '프리로드 장 수',         t: 'num',  min: 1,   max: 5 },
            { k: 'fetchDelay',      l: 'fetch 딜레이 (ms)',      t: 'num',  min: 100, max: 1000 },
            { k: 'columns',         l: '갤러리 열 수',           t: 'num',  min: 1,   max: 6 },
            { k: 'thumbMode',       l: '썸네일 표시',            t: 'sel',  opts: [['crop','크롭 (고정 높이)'], ['full','풀사이즈 (원본 비율)']] },
            { k: 'showThumbnailStrip', l: '썸네일 스트립 표시', t: 'bool' },
            { k: 'theme',           l: '테마',                   t: 'sel',  opts: [['light','라이트'], ['dark','다크']] },
        ];

        var sep = isDark ? '#444' : '#eee';
        items.forEach(function (item) {
            var row = document.createElement('div');
            row.style.cssText =
                'display:flex;align-items:center;justify-content:space-between;' +
                'padding:8px 4px;border-bottom:1px solid ' + sep + ';';
            var lbl = document.createElement('span');
            lbl.textContent = item.l; lbl.style.fontSize = '13px';
            row.appendChild(lbl);

            if (item.t === 'bool') {
                var cbx = document.createElement('input');
                cbx.type = 'checkbox'; cbx.checked = !!localF[item.k];
                cbx.style.cssText = 'width:17px;height:17px;cursor:pointer;accent-color:#4a90d9;';
                cbx.addEventListener('change', function () { localF[item.k] = cbx.checked; });
                row.appendChild(cbx);
            } else if (item.t === 'num') {
                var inp = document.createElement('input');
                inp.type = 'number'; inp.value = localF[item.k];
                if (item.min  !== undefined) inp.min  = item.min;
                if (item.max  !== undefined) inp.max  = item.max;
                if (item.step !== undefined) inp.step = item.step;
                inp.style.cssText =
                    'width:75px;padding:4px;border:1px solid #ccc;' +
                    'border-radius:4px;font-size:13px;' +
                    'background:' + (isDark ? '#2a2a2a' : '#fff') + ';' +
                    'color:' + (isDark ? '#ddd' : '#333') + ';';
                inp.addEventListener('input', function () { localF[item.k] = parseFloat(inp.value); });
                row.appendChild(inp);
            } else if (item.t === 'sel') {
                var sel = document.createElement('select');
                sel.style.cssText =
                    'padding:4px;border:1px solid #ccc;border-radius:4px;font-size:13px;cursor:pointer;' +
                    'background:' + (isDark ? '#2a2a2a' : '#fff') + ';' +
                    'color:' + (isDark ? '#ddd' : '#333') + ';';
                item.opts.forEach(function (o) {
                    var op = document.createElement('option');
                    op.value = o[0]; op.textContent = o[1];
                    if (String(localF[item.k]) === o[0]) op.selected = true;
                    sel.appendChild(op);
                });
                sel.addEventListener('change', function () { localF[item.k] = sel.value; });
                row.appendChild(sel);
            }
            w.appendChild(row);
        });

        /* 버튼 */
        var br = document.createElement('div');
        br.style.cssText = 'display:flex;gap:8px;margin-top:14px;justify-content:center;';
        function mkB(t, bg, fn) {
            var b = document.createElement('button');
            b.textContent = t;
            b.style.cssText =
                'padding:8px 18px;border:none;border-radius:6px;cursor:pointer;' +
                'font-size:13px;font-weight:bold;color:#fff;background:' + bg + ';';
            b.addEventListener('click', fn);
            return b;
        }
        br.appendChild(mkB('✅ 저장', '#4a90d9', function () {
            for (var k in localF) {
                if (!localF.hasOwnProperty(k)) continue;
                f[k] = localF[k];
                GM_setValue('dci_' + k, localF[k]);
            }
            ov.remove();
            location.reload();
        }));
        br.appendChild(mkB('🔄 초기화', '#f39c12', function () {
            if (!confirm('모든 설정을 기본값으로 초기화하겠습니까?')) return;
            for (var k in DEFAULTS) {
                if (!DEFAULTS.hasOwnProperty(k)) continue;
                f[k] = DEFAULTS[k];
                GM_setValue('dci_' + k, DEFAULTS[k]);
            }
            ov.remove();
            location.reload();
        }));
        br.appendChild(mkB('❌ 취소', '#888', function () { ov.remove(); }));
        w.appendChild(br);
        ov.appendChild(w);
        document.body.appendChild(ov);
    });
}

/* ================================================================
   진입점
================================================================ */
function init() {
    settingsModule();
    if (f.viewer)      viewerModule();
    if (isListPage && f.galleryView) setTimeout(startGallery, 500);
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}

})();