kone
소미소프트
소미소프트

본인 보유 게임리스트 코네에서 하이라이트로 뜨게하기 (보유게임 중복확인,복구가능 게임 보기) v1.2

08/05/2026, 20:19:14
유틸
28251 views · 10 likes

코네에 올라오는 게임중, 자신이 이미 가지고 있는 게임인지 아닌지 매번 하나하나씩 Everything이나 파일트리에 비교하시나요?

설마 방주털이 게시글 올라올 때도 하는 건 아니죠? ㄷㄷ 너무 번거롭네.

복구하시는 분들도 하이라이트 되는 식별번호 보면서 복구가능한지 쉽게 알 수 있습니다.

이제 이 스크립트 써서 수고 덜으세요.

AI 살살 달래가며 만들어 왔습니다.

현제 스크립트는 본문 맨 아래 써 있는 데로, 기능 수정과 추가 계획이 있으니 확인 바랍니다.


준비물

  1. 브라우저 확장 프로그램 탬퍼몽키 (사용법 모르시면 알아서 찾아보시고)

  2. 본인이 보유한 게임 식별들이 적힌 텍스트 (파일트리여도 됩니다. ‘사용법_4_파싱 예’ 참고)


사용법

(브라우저 ‘새로고침’을 잘 사용해주시기 바랍니다. 템퍼몽키 스크립트들은 적용이 되려면 해야하는 경우가 많습니다.)

  1. 스크립트를 템퍼모키로 실행시킵니다.

  2. 우측-중단에 돋보기 버블 아이콘을 클릭합니다.

1
  1. 텍스트 박스에 본인이 보유한 게임 식별번호들이 적힌 텍스트 뭉치를 넣습니다.

  2. 'Update Database' 버튼을 누르면 스크립트는 텍스트를 파싱합니다.

코네 웹페이지에 매칭되는 식별번호를 확인해 하이라이트 하며, Database 와 Matches 값을 업데이트해 표시합니다.

파싱 예 (레직스 표현식 경계 \b 사용):

레직스의 \b 는 글자/알파벳/언더바가 아닌 것들을 단어 경계로 인정합니다.

단어 경계가 시작이나 끝에 있다면 식별번호를 골라내도록 파싱이 됩니다.

이 스크립트 경우, 아래 경우 식별번호 파싱이 안됩니다:

  • 숫자로 시작

  • 지원하지 않는 판매처 식별 글자로 시작

  • 판매처 식별 글자 후 숫자가 아닌 글자로 이어지면

  • 지원하지 않는 판매처 식별에 추가적인 언더바

파싱되는 예 시:

  • RJ12345

  • (RJ12345)

  • [rj12345]

  • {Rj12345}

  • rj12345,

  • RJ12345.

  • RJ12345:

  • RJ12345;

  • RJ12345!

  • RJ12345?

  • "RJ12345"

  • 'RJ12345'

1

Database : 데이터베이스에 등록한 식별번호 수 (중복 제외)

Matches : 데이터베이스의 식별번호들과 매칭된 웹페이지에 적힌 식별번호들 수

위 스샷은 예로 들어 식별번호만 텍스트 박스에 보이는데, 파싱된 후의 결과입니다.

브라우저 새로고침을 누르면 식별번호만이 남게 됩니다.

(지원되는 판매처 번호는 아래 ‘식별번호 양식’ 요약 참조)

식별번호 양식

(팬제작이나 등등 식별불가는 텍스트 파싱부터가 다른 식으로 접근해야 하며 복잡해져서 포함치 않습니다)

(여기 테이블에 더 추가할 것이 보이면 말씀해주시기 바랍니다)

판매/배포처

웹사이트

식별번호 패턴

Booth

https://booth.pm

BT...

FanzaGames

https://www.dmm.co.jp

Fanza… | d_... | FZ_... | FZ

Getchu

https://www.getchu.com

GC...

Jast

https://jaststore.com

jast...

072Project

https://072project.com

RA...

DLsite

https://www.dlsite.com

RJ... | VJ...

Steam

https://store.steampowered.com

Steam... | ST...

VNDB

https://vndb.org

VN...

(비쥬얼 노벨 위주인 사이트.

별도 판매식별이 불가하며 이곳에 등록되어 있을 경우)

  1. 발견된 식별들을 게임 데이터베이스로 만들어서 탬퍼몽키 브라우저 확장 프로그램 내부 저장소에 보관.

(브라우저 끄거나 캐시 지워도 데이터 유지 됨)

  1. 'Highlight On' 버튼으로 해두면, 코네 게시글 목록 제목과 게시글 본문에서 발견되는 게임 데이터베이스를 하이라이트 시킴.

'Highlight Off' 버튼으로 해두면, 하이라이트 끔.

1


추가 편리성 기능:

  1. 대체로 GUI 스크립트는 우측-하단에 위치하게 만드는걸 필자는 알기에,

이 스크립트 GUI는 우측-중단에서 시작되게 만듬

  1. 돋보기 아이콘 버블 UI로 GUI 확대/축소 가능

1
  1. 웹페이지에 뭐 가리지 않도록 GUI 드래그해서 이동시키는 것도 가능

  2. 브라우저 화면 밖으로 나가지 않게 만듬

  3. GUI 확장/축소 누를 시, 이 두 GUI 모드 버튼이 마우스 아래로 집중되도록 GUI 확장/축소 방식 변경


스크립트

  1. 아래 요약에 적은 스크립트를 템퍼몽키에 넣어서 실행해주세요.

  2. 게임 식별들이 적힌 텍스트를 템퍼몽키 실행해서 열린 GUI의 텍스트 박스에 넣어주세요.

제가 스크립트 여기에 붙여넣기 하려니까 코네 녀석이 스크립트와 충돌을 하군요.

하이라이트 된 부분의 전체 줄이 문제가 있으니, 아래대로 바꿔주세요.

(이거 충돌 계속 되서 여기에 적는 것도 힘드네요. 한번 충돌하니 미친듯이 막 충돌함)

첫 줄: // ==UserScript

위에 거 후에 바로 적을 거: ==


마지막 @grant 의 다음 줄: // ==/UserScript

위에 거 바로 후에 적을 거: ==

(이 외에 발견되는 부분 보이면 말씀주세요)

스크립트 v1.2

// UserScript

// @name 보유중 게임식별번호 매칭

// @namespace http://tampermonkey.net/

// @version 1.2

// @description 보유 게임 식벽들을 포함한 텍스트 넣고, 코네 사이트에서 발견되면 하이라이트 시킴

// @author 공승아

// @match https://kone.gg/*

// @grant GM_getValue

// @grant GM_setValue

// /UserScript

(function () {

"use strict";

const CONTENT_SELECTORS = [

"div.relative.min-h-60",

'#post-list a[class*="group/article"] div.grid-cols-22 div.col-span-12 div.flex.items-center.gap-1 span'

];

const DB_KEY = "code_database_v4";

const VIEW_KEY = "highlight_visible_v4";

let database = new Set(

GM_getValue(DB_KEY, [])

);

let highlightVisible =

GM_getValue(VIEW_KEY, true);

let highlightElements = [];

let matchCount = 0;

// ---------------------------------------------

// Database parsing

// ---------------------------------------------

function parseDatabase(text) {

const result = new Set();

const regex =

/\b(?:BT|Fanza|d_|FZ_|FZ|GC|RA|RJ|VJ|Steam|ST|VN)\d+\b/gi;

let match;

while ((match = regex.exec(text)) !== null) {

result.add(

match[0].toUpperCase()

);

}

return result;

}

function saveDatabase(newDatabase) {

database = newDatabase;

GM_setValue(

DB_KEY,

[...database]

);

}

// ---------------------------------------------

// Highlight cleanup

// ---------------------------------------------

function clearHighlights() {

for (const span of highlightElements) {

const parent =

span.parentNode;

if (!parent)

continue;

parent.replaceChild(

document.createTextNode(

span.textContent

),

span

);

parent.normalize();

}

highlightElements = [];

matchCount = 0;

}

// ---------------------------------------------

// Highlight style

// ---------------------------------------------

function applyHighlightStyle(span) {

if (highlightVisible) {

span.style.backgroundColor =

"yellow";

span.style.color =

"black";

span.style.fontWeight =

"bold";

}

else {

span.style.backgroundColor =

"";

span.style.color =

"";

span.style.fontWeight =

"";

}

}

function refreshHighlightAppearance() {

for (const span of highlightElements) {

applyHighlightStyle(span);

}

}

// ---------------------------------------------

// Scan webpage

// ---------------------------------------------

function scanPage() {

const areas =

document.querySelectorAll(

CONTENT_SELECTORS.join(",")

);

if (!areas.length)

return;

const textNodes = [];

for (const area of areas) {

const walker =

document.createTreeWalker(

area,

NodeFilter.SHOW_TEXT

);

let node;

while ((node = walker.nextNode())) {

textNodes.push(node);

}

}

for (const textNode of textNodes) {

processTextNode(textNode);

}

}

function processTextNode(textNode) {

const text =

textNode.nodeValue;

const regex =

/\b(?:BT|Fanza|d_|FZ_|FZ|GC|RA|RJ|VJ|Steam|ST|VN)\d+\b/gi;

const matches = [];

let match;

while ((match = regex.exec(text)) !== null) {

const value =

match[0].toUpperCase();

if (database.has(value)) {

matches.push({

start: match.index,

end:

match.index +

match[0].length

});

}

}

if (!matches.length)

return;

const fragment =

document.createDocumentFragment();

let lastIndex = 0;

for (const item of matches) {

fragment.appendChild(

document.createTextNode(

text.substring(

lastIndex,

item.start

)

)

);

const span =

document.createElement(

"span"

);

span.className =

"tm-code-match";

span.textContent =

text.substring(

item.start,

item.end

);

applyHighlightStyle(span);

highlightElements.push(span);

matchCount++;

fragment.appendChild(span);

lastIndex =

item.end;

}

fragment.appendChild(

document.createTextNode(

text.substring(lastIndex)

)

);

textNode.replaceWith(fragment);

}

// ---------------------------------------------

// Public functions used by GUI (Part 2)

// ---------------------------------------------

window.TM_CodeMatcher = {

getDatabaseSize() {

return database.size;

},

getMatchCount() {

return matchCount;

},

updateDatabase(text) {

const newDB =

parseDatabase(text);

saveDatabase(newDB);

clearHighlights();

scanPage();

refreshHighlightAppearance();

},

toggleHighlight() {

highlightVisible =

!highlightVisible;

GM_setValue(

VIEW_KEY,

highlightVisible

);

refreshHighlightAppearance();

return highlightVisible;

},

isHighlightVisible() {

return highlightVisible;

},

loadDatabaseText() {

return [

...database

].join("\n");

}

};

// ---------------------------------------------

// Initial scan

// ---------------------------------------------

window.addEventListener(

"load",

() => {

scanPage();

}

);

// ==================================================

// Isolated Shadow DOM GUI

// ==================================================

(function buildGUI() {

const host = document.createElement("div");

host.style.position = "fixed";

host.style.right = "20px";

host.style.top = "50%";

host.style.transform = "translateY(-50%)";

host.style.zIndex = "2147483647";

document.documentElement.appendChild(host);

const shadow =

host.attachShadow({

mode: "open"

});

shadow.innerHTML = `

<style>

* {

box-sizing:border-box;

font-family:Arial,sans-serif;

}

#bubble {

width:46px;

height:46px;

border-radius:50%;

background:#4285f4;

color:white;

display:flex;

align-items:center;

justify-content:center;

cursor:grab;

user-select:none;

box-shadow:

0 3px 12px rgba(0,0,0,.3);

}

#panel {

display:none;

width:340px;

background:white;

color:#222;

border:1px solid #ccc;

border-radius:10px;

padding:12px;

box-shadow:

0 5px 20px rgba(0,0,0,.25);

}

#header {

display:flex;

justify-content:space-between;

font-weight:bold;

margin-bottom:10px;

cursor:grab;

}

#close {

cursor:pointer;

}

textarea {

width:100%;

height:150px;

resize:none;

padding:8px;

border:1px solid #aaa;

}

.info {

margin:10px 0;

line-height:20px;

}

button {

padding:7px 12px;

border:none;

border-radius:5px;

color:white;

font-weight:bold;

cursor:pointer;

margin-right:5px;

}

#update {

background:#4285f4;

}

#toggle {

background:#34a853;

}

</style>

<div id="bubble">

🔍

</div>

<div id="panel">

<div id="header">

<span>

Code Matcher

</span>

<span id="close">

✕

</span>

</div>

<textarea id="input"

placeholder="Paste BT/Fanza/d_/FZ_/FZ/GC/RA/RJ/VJ/Steam/ST/VN codes here">

</textarea>

<div class="info">

Database:

<span id="db"></span>

<br>

Matches:

<span id="matches"></span>

</div>

<button id="update">

Update Database

</button>

<button id="toggle">

</button>

</div>

`;

const bubble =

shadow.querySelector("#bubble");

const panel =

shadow.querySelector("#panel");

const header =

shadow.querySelector("#header");

const close =

shadow.querySelector("#close");

const input =

shadow.querySelector("#input");

const update =

shadow.querySelector("#update");

const toggle =

shadow.querySelector("#toggle");

const db =

shadow.querySelector("#db");

const matches =

shadow.querySelector("#matches");

input.value =

window.TM_CodeMatcher

.loadDatabaseText();

// ---------------------------------------------

// View update

// ---------------------------------------------

function refreshInfo() {

db.textContent =

window.TM_CodeMatcher

.getDatabaseSize();

matches.textContent =

window.TM_CodeMatcher

.getMatchCount();

toggle.textContent =

window.TM_CodeMatcher

.isHighlightVisible()

? "Highlight ON"

: "Highlight OFF";

toggle.style.background =

window.TM_CodeMatcher

.isHighlightVisible()

? "#34a853"

: "#ea4335";

}

refreshInfo();

// ---------------------------------------------

// Open / close

// ---------------------------------------------

bubble.onclick = (e) => {

if (wasDragging) {

wasDragging = false;

return;

}

const x =

e.clientX;

const y =

e.clientY;

panel.style.display =

"block";

bubble.style.display =

"none";

const panelRect =

panel.getBoundingClientRect();

host.style.left =

(x - panelRect.width + 23) + "px";

host.style.top =

(y - 23) + "px";

host.style.right =

"auto";

host.style.transform =

"none";

keepInsideViewport();

};

close.onclick = (e) => {

e.stopPropagation();

const closeRect =

close.getBoundingClientRect();

const centerX =

closeRect.left +

(closeRect.width / 2);

const centerY =

closeRect.top +

(closeRect.height / 2);

host.style.left =

(centerX - 23) + "px";

host.style.top =

(centerY - 23) + "px";

host.style.right =

"auto";

host.style.transform =

"none";

panel.style.display =

"none";

bubble.style.display =

"flex";

wasDragging = false;

};

// ---------------------------------------------

// Buttons

// ---------------------------------------------

update.onclick = () => {

window.TM_CodeMatcher

.updateDatabase(

input.value

);

refreshInfo();

};

toggle.onclick = () => {

window.TM_CodeMatcher

.toggleHighlight();

refreshInfo();

};

// ---------------------------------------------

// Dragging

// ---------------------------------------------

let wasDragging = false;

function clamp(value, min, max) {

return Math.min(

Math.max(value, min),

max

);

}

function keepInsideViewport() {

const rect =

host.getBoundingClientRect();

let left =

rect.left;

let top =

rect.top;

left =

clamp(

left,

0,

window.innerWidth -

rect.width

);

top =

clamp(

top,

0,

window.innerHeight -

rect.height

);

host.style.left =

left + "px";

host.style.top =

top + "px";

host.style.right =

"auto";

host.style.transform =

"none";

}

function makeDraggable(element) {

let dragging = false;

let offsetX = 0;

let offsetY = 0;

let moved = false;

element.addEventListener(

"pointerdown",

e => {

if (

e.target.closest("#close") ||

e.target.tagName === "BUTTON" ||

e.target.tagName === "TEXTAREA"

) {

return;

}

dragging = true;

moved = false;

const rect =

host.getBoundingClientRect();

offsetX =

e.clientX -

rect.left;

offsetY =

e.clientY -

rect.top;

element.setPointerCapture(

e.pointerId

);

}

);

element.addEventListener(

"pointermove",

e => {

if (!dragging)

return;

wasDragging = true;

moved = true;

host.style.transform =

"none";

let x =

e.clientX -

offsetX;

let y =

e.clientY -

offsetY;

const rect =

host.getBoundingClientRect();

x =

clamp(

x,

0,

window.innerWidth -

rect.width

);

y =

clamp(

y,

0,

window.innerHeight -

rect.height

);

host.style.left =

x + "px";

host.style.top =

y + "px";

host.style.right =

"auto";

}

);

element.addEventListener(

"pointerup",

() => {

dragging = false;

}

);

}

makeDraggable(bubble);

makeDraggable(header);

window.addEventListener(

"resize",

keepInsideViewport

);

})();

})();


업뎃 기록
  • v1.0 26.8.4

  • v1.1 26.8.4

    • 추가: 게시글 목록의, 게시글 제목도 매치&하이라이트

  • v1.2 26.8.5

    • 버그 수정: 돋보기 GUI 드래고 하고 놓을 시 GUI가 확장하던 걸 고침

    • 편리성 추가: GUI 확장/축소 누를때 이 두 GUI 모드 버튼이 마우스에 집중되도록 GUI 확장/축소하게 변경

현제 버그/문제들/ 후 추가할 것들
  • 웹페이지에서 드래그 선택 시, 이 스크립트 GUI도 선택이 되는 문제

  • 페이지 새로 로딩된 정보를 지속확인해 하이라이트 반영 활성화 MutationObserver (필요성 높음)

  • 하이라이트 토글 단축키 추가, 그걸 GUI 반영 (돋보기 아이콘도 변형)

  • GUI 탭 추가: Exclude : 보유한 게임 리스트(노랑색)와 달리, 본인이 이미 확인한 게임 리스트(빨간) 추가.
    식별번호 드래그 선택 후 단추키 누르고, ‘확인겜 식별추가함’ 알림 띄우기

  • GUI 탭 추가: Forced : 게임 데이터베이스에 강제적으로 추가시킬 (파싱 안 함) 특정 이름

10
9 comments
08/05
08/05
08/05
제거에 저런 코드명만 추출하는 기능이 있으면 이걸 사용하기 좀 더 편하려나요?
"제거" 라는 건 본인 게임 파일들 말하는 건가요? 준비물에도 써 있듯이, 파일트리도 된다고 썼는데, 검색해 보시면 어렵지 않게 찾아서 만드실 수 있습니다. 맨 아래 '현제 버그/문제들/ 후 추가할 것들' 외로 뭐 불편한 게 있나요?
크레아 스트러글로 하루에 3번침...
일반
ㅇㅇ
08/25 7971 0
오늘나온 신작 평점 좆박은 이유를 알겠네
야짤
bunit
08/25 28377 19
시이발 스푸키밀크라이프 이건 업데이트 할 생각이 없나
일반
goldenlay
08/25 5217 3
[복구요청] 민간정의 회사2 + dlc
요청
fjeif2
08/25 5768 0
[복구요청] 음해의 트랜스트랩
요청
데스포르카
08/25 6291 0
트랜스 피메일 판타지 레거시 질문
질문
reeeeeeee
08/25 5368 0
44G944KL44Gb44KK44KT 작가 과거 영상 음성 추가 5
영상
abbaca
08/25 114630 88
앨리스 시리즈
동인
Byeongu 981204
08/25 113957 67
근데 솔루이 전작해봐야함?
질문
루파조아
08/25 6055 0
파파카츠 무쌍 타이틀로고 한글화 끝.
작업현황
Neko_Aqua
08/25 20818 20
[구매보급,ai번역] 교칙 개변~키모오타로 바꾼 학원~
번역
GGmen
08/25 281418 209
[자막만]죽을 만큼 자존심이 강한 츠요츠요인척하는 폭유 쿨 엘프 베인쨩. 외로움을 잘 타고 너무 야해서, 여러 가지를 알게 되고 바로 타락한다. AI번역
소리
하츠네미쿠
08/25 57650 54
[구매보급]《팔리지않은 처녀》 에~ 잇♪ 지금부터 언니와 섹스하는 거야~ 엄청난 욕정 역 레○프로, 손을 씻어도 냄새가 날 정도로, 변태 언니에게 진한 정액을 가득
소리
이름뭐하지
08/25 46714 51
【순애/노모】낯을 가리는 미망인 설녀씨와 저주의 반지
동인
HoneyWorks
08/25 144029 207
이나리 환생주점
질문
pigpower
08/25 6149 0
란스10 보물상자 리세마라 어캐하는거임?
질문
3333
08/25 4808 0
sao2질문
질문
afe1231
08/25 6341 0
이마이즈미 8편 10월에 발매 예정
일반
axe2442
08/25 5438 0
【순애/노모】임모럴 루틴
동인
HoneyWorks
08/25 141211 193
femboy futa house 혹시 안드 에뮬로 해본사람 잇을까?
질문
whimpercatt
08/25 6685 -1
[복구요청]RJ01557728전직 여검사 라일라는 퀘스트 중!
요청
kinta109
08/25 9113 3
칸나 발바닥
야짤
인생망함
08/25 19105 2
[복구요청] -나기츠루기 -유혹의 학원 7대 불가사의-
요청
phese
08/25 8159 0
[AI 번역] 약혼녀의 여동생은 얼굴 SSR, 성격 최악 지옥의 에로 댄스녀 2
동인
earjantbye
08/25 149881 220
왜 미번탭에 개꼴리는건 체험판이 많지?라고 생각했는데
일반
azezazazezaz
08/25 7077 1
thirst도 다 쳐먹었겠다
일반
jin-roh
08/25 6043 2
[복구요청] 타락의음탕뱀파이어
요청
타마모노마에
08/25 6471 1
스포)코인 푸씨 재밌는 겜인데
후기 및 공략
grayman
08/25 14202 4
Shaggy susu 영상 모음(원신)
영상
붉은생존자
08/25 83730 56
다시는 안온다더니 다시왔네
일반
보태배는 사랑
08/25 7855 -18