kone
소미소프트

소미소프트

RJ 미리보기 서포트 (Tempermonkey 기반)

02/21/2026, 10:44:38
유틸
8123 views · 14 likes
1

개인용으로 만들었는데 생각보다 깔끔하게 나와서 공유해봄

RJ가 포함된 번호를 드래그하고 Shift Z를 누르면 dlsite 설명으로 등록된 설명 썸네일을 가져와서 보여주고
DB에 등록해둔 경우 내가 그 파일을 갖고 있는지 안갖고있는지를 알려주는 스크립트

Tempermonkey 사용법은 검색하면 나오니까 넘어가고

기본적인 조작은 RJ가 포함된 텍스트, URL을 드래그하고 Shift Z를 누르면 작동
DB등록은 아래참조


1


탬퍼몽키 누르고 Update Database를 눌러서

1


뜨는 창에 RJ가 포함되어있는 텍스트를 추가해주면 됨
줄바꿈 단위로 인식하고 RJ+숫자만 붙어있으면 앞에있던 뒤에있던 [ ] 로 감싸져있던 상관안함

텍스트만드는것까지 같이 만들까했는데 폴더로 정리하는 사람, 압축파일로 정리하는 사람 다 다를것같아서 그냥 텍스트 넣어두면 뜨게해놨음


1


장점은 내가 갖고있는지 여부랑, 파일명을 같이 저장해서 db에 두면 뭐라고 저장해놨는지 보기 편하다는 점이랑
AI작품같은 경우 들어가도 VPN안키면 설명 이미지가 안보이는데 얘는 그런거 무시하고 바로 볼 수 있어서 편함

이미지 없는 복구글 뭐였는지 기억이 안나서 만들었는데 생각보다 반응도 빠릿빠릿하고 편해서 공유해봄

---

스크립트 등록 방법 :

Tempermonkey - 새 스크립트 만들기

아래 펼쳐서 싹다 붙여넣기

펼치기
// ==UserScript==
// @name         RJ Search Helper
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Search for selected text in a local database and show formatted results
// @author       FLAG
// @match        *://kone.gg/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
    'use strict';

    // CSS for the popup and settings
    const styles = `
        #ark-result-overlay {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.6);
            z-index: 9999;
            display: none;
            justify-content: center;
            align-items: center;
        }
        #ark-search-popup {
            position: relative;
            background: #222;
            color: #fff;
            padding: 20px;
            padding-top: 40px;
            border-radius: 10px;
            box-shadow: 0 6px 25px rgba(0,0,0,0.5);
            font-family: 'Inter', sans-serif;
            font-size: 16px;
            width: 640px;
            max-width: 90vw;
            max-height: 90vh;
            overflow-y: auto;
            border: 1px solid #444;
        }
        #ark-search-popup .result-item { margin-bottom: 8px; line-height: 1.5; border-bottom: 1px solid #333; padding-bottom: 6px; }
        #ark-search-popup .close-btn { position: absolute; top: 5px; right: 12px; cursor: pointer; color: #888; font-size: 24px; z-index: 10001; }
        #ark-search-popup .close-btn:hover { color: #fff; }

        #ark-image-container {
            display: flex;
            overflow-x: auto;
            gap: 12px;
            margin-top: 15px;
            padding-bottom: 8px;
        }
        #ark-image-container img {
            height: 384px;
            border-radius: 6px;
            border: 1px solid #444;
            flex-shrink: 0;
            cursor: pointer;
        }
        #ark-image-container::-webkit-scrollbar { height: 8px; }
        #ark-image-container::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; }

        #ark-settings-overlay {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.8);
            z-index: 10001;
            display: none;
            justify-content: center;
            align-items: center;
        }
        #ark-settings-modal {
            background: #1e1e1e;
            padding: 20px;
            border-radius: 10px;
            width: 80%;
            max-width: 600px;
            color: #eee;
        }
        #ark-db-input {
            width: 100%;
            height: 300px;
            background: #111;
            color: #0f0;
            border: 1px solid #333;
            padding: 10px;
            font-family: monospace;
            margin: 10px 0;
            border-radius: 5px;
        }
        .ark-btn {
            background: #00d1ff;
            color: #000;
            border: none;
            padding: 8px 16px;
            border-radius: 5px;
            cursor: pointer;
            font-weight: bold;
        }
        .ark-btn:hover { background: #00b8e6; }
        #ark-toast {
            position: fixed;
            bottom: 50px; left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 209, 255, 0.9);
            color: #000;
            padding: 10px 20px;
            border-radius: 20px;
            font-weight: bold;
            font-size: 14px;
            z-index: 11000;
            display: none;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            pointer-events: none;
        }
        .ark-sync-section {
            margin-bottom: 20px;
            background: #222;
            padding: 15px;
            border-radius: 8px;
            border: 1px solid #333;
        }
        .ark-sync-section label {
            display: flex;
            align-items: center;
            cursor: pointer;
            font-weight: bold;
            color: #00d1ff;
        }
        #ark-sync-controls {
            margin-top: 12px;
            display: flex;
            align-items: center;
            gap: 12px;
            padding-left: 24px;
        }
        #ark-filename {
            font-size: 13px;
            color: #888;
            font-style: italic;
        }
    `;

    const styleSheet = document.createElement("style");
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);

    // Initial Elements
    const resultOverlay = document.createElement('div');
    resultOverlay.id = 'ark-result-overlay';
    document.body.appendChild(resultOverlay);

    const popup = document.createElement('div');
    popup.id = 'ark-search-popup';
    resultOverlay.appendChild(popup);

    const toast = document.createElement('div');
    toast.id = 'ark-toast';
    document.body.appendChild(toast);

    function showToast(message) {
        toast.innerText = message;
        toast.style.display = 'block';
        setTimeout(() => {
            toast.style.display = 'none';
        }, 2000);
    }

    const overlay = document.createElement('div');
    overlay.id = 'ark-settings-overlay';
    overlay.innerHTML = `
        <div id="ark-settings-modal">
            <h3 style="margin-top: 0; color: #00d1ff;">⚙️ 데이터 베이스 설정</h3>

            <div class="ark-sync-section">
                <label>
                    <input type="checkbox" id="ark-sync-toggle" style="width: 18px; height: 18px; margin-right: 10px;">
                    로컬 파일과 동기화 (Live Sync)
                </label>
                <div id="ark-sync-controls">
                    <button class="ark-btn" id="ark-select-file" style="font-size: 12px; padding: 5px 12px;">파일 선택</button>
                    <span id="ark-filename">선택된 파일 없음</span>
                </div>
            </div>

            <p style="margin-bottom: 5px;">가상 데이터베이스 (로컬 동기화 비활성화 시 사용):</p>
            <textarea id="ark-db-input" placeholder="텍스트를 여기에 붙여넣어주세요..."></textarea>

            <div style="text-align: right; margin-top: 10px;">
                <button class="ark-btn" id="ark-save-db">저장</button>
                <button class="ark-btn" id="ark-close-settings" style="background: #444; color: #fff; margin-left: 10px;">닫기</button>
            </div>
        </div>
    `;
    document.body.appendChild(overlay);

    // IndexedDB for FileHandle persistence
    async function getDBHandle() {
        return new Promise((resolve) => {
            const request = indexedDB.open("ArkDB", 1);
            request.onupgradeneeded = e => e.target.result.createObjectStore("handles");
            request.onsuccess = e => {
                const db = e.target.result;
                const tx = db.transaction("handles", "readonly");
                const store = tx.objectStore("handles");
                const getReq = store.get("local_file");
                getReq.onsuccess = () => resolve(getReq.result);
                getReq.onerror = () => resolve(null);
            };
            request.onerror = () => resolve(null);
        });
    }

    async function setDBHandle(handle) {
        return new Promise((resolve) => {
            const request = indexedDB.open("ArkDB", 1);
            request.onsuccess = e => {
                const db = e.target.result;
                const tx = db.transaction("handles", "readwrite");
                const store = tx.objectStore("handles");
                store.put(handle, "local_file");
                tx.oncomplete = () => resolve(true);
            };
        });
    }

    // Database access
    async function getDatabase() {
        const syncEnabled = GM_getValue('ark_sync_enabled', false);
        if (syncEnabled) {
            const handle = await getDBHandle();
            if (handle) {
                try {
                    // Check for permission (shows prompt if needed)
                    if (await handle.queryPermission() !== 'granted') {
                        if (await handle.requestPermission() !== 'granted') {
                            throw new Error('Permission denied');
                        }
                    }
                    const file = await handle.getFile();
                    const text = await file.text();
                    return text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
                } catch (err) {
                    console.error('Local file read failed:', err);
                    showToast('로컬 파일을 읽을 수 없습니다. 설정을 확인해주세요.');
                }
            }
        }

        const raw = GM_getValue('ark_db', '');
        return raw.split('\n').map(line => line.trim()).filter(line => line.length > 0);
    }

    function saveDatabase(text) {
        GM_setValue('ark_db', text);
        showToast('저장되었습니다.');
        overlay.style.display = 'none';
    }

    // Search logic
    async function search(query) {
        if (!query) return { results: [], rjCode: null };
        const match = query.match(/(RJ|ST)\d+/i);
        const searchToken = match ? match[0].toUpperCase() : query.trim();

        if (!searchToken) return { results: [], rjCode: null };

        const db = await getDatabase();
        const results = db.filter(line => line.toUpperCase().includes(searchToken));
        return { results, rjCode: searchToken.startsWith('RJ') ? searchToken : null };
    }

    function getDLSiteImages(rjCode) {
        if (!rjCode) return [];
        const numStr = rjCode.substring(2);
        const num = parseInt(numStr);
        const folderNum = (Math.floor(num / 1000) + 1) * 1000;
        const folder = "RJ" + folderNum.toString().padStart(numStr.length, '0');
        const baseUrl = `https://img.dlsite.jp/modpub/images2/work/doujin/${folder}/`;

        // 5 images as requested (Main + 4 samples)
        const suffixes = ["_img_main", "_img_smp1", "_img_smp2", "_img_smp3", "_img_smp4"];
        return suffixes.map(s => `${baseUrl}${rjCode.toUpperCase()}${s}.webp`);
    }

    // Result formatting
    function formatResult(line) {
        let prefix = "";
        const trimmedLine = line.trim();
        const hasBrackets = /\[.*\]$/.test(trimmedLine);
        const hasMibun = trimmedLine.includes("미번");

        if (hasBrackets && hasMibun) {
            prefix = "★@ ";
        } else if (hasBrackets) {
            prefix = "★ ";
        } else if (hasMibun) {
            prefix = "@ ";
        }

        return prefix + line;
    }

    // UI Logic
    function showPopup(data) {
        const { results, rjCode } = data;
        popup.innerHTML = `
            <span class="close-btn">×</span>
            <h3 style="margin-top: 0;">Search Results</h3>
            <div id="ark-popup-content"></div>
        `;

        const contentArea = popup.querySelector('#ark-popup-content');

        if (results.length === 0) {
            contentArea.innerHTML = `<div style="color: #ff4d4d; font-weight: bold;">현재 저장소에 없습니다!</div>`;
        } else {
            contentArea.innerHTML = `<div style="color: #00d1ff; font-weight: bold; margin-bottom: 10px;">파일을 발견했습니다!</div>`;
            results.forEach(res => {
                const div = document.createElement('div');
                div.className = 'result-item';
                div.innerText = formatResult(res);
                contentArea.appendChild(div);
            });
        }

        // Add Images if RJ code exists
        if (rjCode) {
            const imgContainer = document.createElement('div');
            imgContainer.id = 'ark-image-container';

            // Add horizontal scroll on mouse wheel
            imgContainer.addEventListener('wheel', (e) => {
                if (e.deltaY !== 0) {
                    e.preventDefault();
                    imgContainer.scrollLeft += e.deltaY;
                }
            });

            const images = getDLSiteImages(rjCode);
            images.forEach(url => {
                const img = document.createElement('img');
                img.src = url;
                img.onerror = () => img.remove();
                img.onclick = () => window.open(url, '_blank');
                imgContainer.appendChild(img);
            });
            contentArea.appendChild(imgContainer);
        }

        resultOverlay.style.display = 'flex';

        popup.querySelector('.close-btn').addEventListener('click', () => {
            resultOverlay.style.display = 'none';
        });
    }

    // Events
    window.addEventListener('keydown', (e) => {
        const selection = window.getSelection().toString().trim();

        if (e.shiftKey && (e.key === 'Z' || e.key === 'z')) {
            if (selection) {
                search(selection).then(data => {
                    showPopup(data);
                });
            }
        }

        if (e.shiftKey && (e.key === 'C' || e.key === 'c')) {
            if (selection) {
                try {
                    const decoded = atob(selection);
                    GM_setClipboard(decoded);
                    showToast('복호화 성공!');
                    console.log('Decoded and copied to clipboard:', decoded);
                } catch (err) {
                    console.error('Base64 decode failed:', err);
                }
            }
        }
    });

    // Close popup on click outside
    window.addEventListener('mousedown', (e) => {
        if (e.target === resultOverlay) {
            resultOverlay.style.display = 'none';
        }
        if (!overlay.contains(e.target) && e.target !== overlay) {
            // This handles the settings overlay click outside if needed,
            // but the logic was a bit mixed. Let's focus on search overlay.
        }
    });

    // Close settings overlay on click outside
    overlay.addEventListener('mousedown', (e) => {
        if (e.target === overlay) {
            overlay.style.display = 'none';
        }
    });

    // Settings Menu
    GM_registerMenuCommand("Update Database", async () => {
        document.getElementById('ark-db-input').value = GM_getValue('ark_db', '');

        const syncToggle = document.getElementById('ark-sync-toggle');
        const syncEnabled = GM_getValue('ark_sync_enabled', false);
        syncToggle.checked = syncEnabled;

        const syncControls = document.getElementById('ark-sync-controls');
        syncControls.style.display = syncEnabled ? 'flex' : 'none';

        const handle = await getDBHandle();
        if (handle) {
            document.getElementById('ark-filename').innerText = handle.name;
        }

        overlay.style.display = 'flex';
    });

    document.getElementById('ark-sync-toggle').addEventListener('change', (e) => {
        const enabled = e.target.checked;
        GM_setValue('ark_sync_enabled', enabled);
        document.getElementById('ark-sync-controls').style.display = enabled ? 'flex' : 'none';
    });

    const selectFileBtn = document.getElementById('ark-select-file');
    if (selectFileBtn) {
        selectFileBtn.addEventListener('click', async () => {
            const picker = window.showOpenFilePicker || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.showOpenFilePicker : null);

            if (!picker) {
                alert('현재 브라우저에서 로컬 파일 동기화 기능을 지원하지 않거나, 보안 연결(HTTPS)이 아닙니다. 크롬/엣지 브라우저를 사용해주세요.');
                return;
            }

            try {
                const [handle] = await picker({
                    types: [{ description: 'Text Files', accept: { 'text/plain': ['.txt'] } }],
                    multiple: false
                });
                await setDBHandle(handle);
                document.getElementById('ark-filename').innerText = handle.name;
                showToast('연결되었습니다: ' + handle.name);
            } catch (err) {
                console.error('File selection failed:', err);
                if (err.name !== 'AbortError') {
                    showToast('파일 선택 중 오류가 발생했습니다.');
                }
            }
        });
    }

    document.getElementById('ark-save-db').addEventListener('click', () => {
        const text = document.getElementById('ark-db-input').value;
        saveDatabase(text);
    });

    document.getElementById('ark-close-settings').addEventListener('click', () => {
        overlay.style.display = 'none';
    });

})();



---

버그나 수정필요한 부분있으면 말좀

---

업데이트
v0.2

탭 이동 (정보 -> 유틸)

로컬 파일 동기화 기능 추가 (Update Database 누르고 설정 가능)
화면 밖으로 팝업이 생기는 이슈 수정
Shift + C 단축키에 base64 변환 후 클립보드 복사 기능 추가

14
8 comments
02/21
와우
02/21
02/22
처음에 넣고 안되길래 왜그런가 했는데 기다리니까 된다 굿굿 혹시 db파일 주기적으로 자동으로 땡겨가는 방법은 없을까? 스케쥴로 로컬에 있는 폴더명 긁어가서 텍스트 파일로 떨구고 그거 읽어가면 수동으로 업데이트 안해도 돼서 편할거 같은데
02/25
기능 추가 완료 아마 새로고침을 해야 db가 적용될건데 안적어둬서 그런듯
02/22 Edited 02/22
스크립트 공유 감사합니다. 탭: 정보 -> 유틸 스크립트 두 번 중복으로 복붙 하셨습니다. QoF 업뎃 추천: 1. 창이 브라우저 화면 밖으로 나가는 것 방지. 2. 창 띄웠을 때 집중하게 설정. 창 안에서 텍스트 클릭-드래그 하면 창 밖으로 나가는 걸 방지. 3. (옵션) 키 이벤트에 정.확.한. 키 세트 입력 확인. shift+f 절대. shift+f+그외 동시 누르고 있을 때 실행 발생 방지 (다른 명령 키와 겹침 방지 조치). 기능 업뎃 추천: 1. 게임 태그 표시 (스샷만큼이나 중요함. vpn 써도 못 보는 정보일 시, 그렇다고 표시).
02/25
QoF 1번 재현되서 수정 완료 2번, 3번은 어떤거 말하는건지 잘 모르겠고 뭐 때문에 필요한 기능인지 모르겠어서 일단 보류 태그는 넣고싶은데 이미지는 크롤링 안하고 그대로 가져올 수 있는데 태그는 크롤링 한번 거쳐야되서 테스트 후 추가 예정 넣을려면 바로넣는데 ip밴같은거 당하면 귀찮을 수 있어서 좀 보고있네요
미번) 오랄엣치 ♡ 플레이어 - 배덕 시스터 편 - (おーらるえっち♡ぷれいやー -背徳シスター編-)
미번
darkunicorn
05/10 13213 34
공식번역 / 청아 / 이종간) 검은 유원지 + 검은 유원지II: 묵시록
번역
darkunicorn
05/10 51635 129
DOA 캐릭터 스타킹짤
야짤
무이이야
05/10 18755 18
[기계번역] 어서오세요! 어린이 만들기 온천 마을(30일 6/9일까지)
번역
rhnbbb
05/10 68184 305
미츠리 무한의 성욕
영상
bigfan
05/10 40596 227
번역파일 Only / 촉수 / RJ01376315) 음구조복 토에하 ~퇴마 무녀 3D 터치 시뮬레이션~ v1.0.2 (淫穴調伏トエハ ~退魔巫女3Dおさわりシミュレーション~)
번역
darkunicorn
05/10 39418 111
[직번] [Kagto] 반찬당번 마치다짱 단편모음
동인
acefs
05/10 28642 181
시시아 / 벨 / 엘렌 / 니콜 - 리버스 카우걸 영상 - (ZZZ)
영상
브레머튼
05/10 25737 104
미번 / 촉수 / RJ01376315) 음구조복 토에하 ~퇴마 무녀 3D 터치 시뮬레이션~ v1.1.2 (淫穴調伏トエハ ~退魔巫女3Dおさわりシミュレーション~)
미번
darkunicorn
05/10 17133 58
[QHD 24f] 오토메도리 1,2
영상
Shinjiro
05/10 19586 159
미번 / 청아 / RJ01613313) 1년생 아이돌 수행중!? v1.02 (1年生アイドル 修行中!?)
미번
darkunicorn
05/10 13626 40
[요청없는 기습복구]변환장희 샤인미라지/RJ226108/変幻装姫シャインミラージュ 敗北へのカウントダウン
복구
렐루
05/10 37246 81
청아 / RJ01376119) みるくいちご 카리나와 노노아 ~성장 기록~
영상
darkunicorn
05/10 41954 172
구 멸망 방주 정리 및 중복 제거 작업 완료
작업현황
ㅇㅇ
05/10 10995 22
amatake akewo 자위를 배울 틈도 없다 (ft 애덤 스미스)
동인
bigfan
05/10 28118 91
비월선행록 종합 수집 정보
정보
공승아
05/10 22508 35
[실시간 번역기 3.2.10] 어서오세요! 어린이 만들기 온천 마을 지원 중
작업현황
mrpls
05/10 27892 54
[미번] 성욕 처리도 간호사에게 맡겨줘, 선배, 가르쳐 주세요! ~야겜을 빌려준 상대는 호기심 왕성한 인싸 후배였습니다~
미번
미스터장
05/10 12566 26
로빈 (스타레일) 4K / 1080p
영상
브레머튼
05/10 14666 78
[미번] 미번 일겜 17개 + 미번 양겜 2개
미번
미스터장
05/10 32169 94
장리 (명조)
영상
브레머튼
05/10 17979 105
플루라이트 (엔드필드)
영상
브레머튼
05/10 27501 110
고죠고죠샌세 서비스씬만
영상
Ghost
05/10 31360 176
[Asakura Kukuri / Small Marron] 패배 ~내 아야 누나가 함락될 때까지
동인
morgan911
05/10 36114 90
[Jzargo] 특명 수사관 이치가야 린 ~항문 치욕 조교편~
동인
morgan911
05/10 21389 122
[버전업 알림] 음최도시 휴프노즘 1.07 번역 적용완
번역
saika
05/10 39417 145
메이 쨩은 빗치 갸루
동인
hatlatal
05/10 29747 87
오늘의 포로
동인
hatlatal
05/10 21350 80
ai, 원신) 닐루와 즐거운 시간을 보낸 뒤 잔뜩 섹스 했다
야짤
kjh9304
05/10 11770 84
토모에 ○미의♥보지만개 장난감 리뷰
동인
hatlatal
05/10 13441 29