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