(async function(){
'use strict';
var Z=[10,10];
var GAP=300;
var VERIFY_GAP=700;
var PAGE_GAP=400;
var MAX_PAGE=200;
var NL=String.fromCharCode(10);
function sleep(ms){
return new Promise(function(resolve){
setTimeout(resolve,ms);
});
}
function log(){
var args=['[글정리]'].concat(Array.prototype.slice.call(arguments));
console.log.apply(console,args);
}
function atLeast(value,limit){
return Math.max(value,limit)===value;
}
function isDigits(value){
var s=String(value==null?'':value).trim();
var i;
var c;
if(!s){
return false;
}
for(i=0;i<s.length;i++){
c=s.charCodeAt(i);
if(c<48 || c>57){
return false;
}
}
return true;
}
function compactText(value){
var s=String(value==null?'':value);
var out='';
var spaced=false;
var i;
var c;
for(i=0;i<s.length;i++){
c=s.charCodeAt(i);
if(c===9 || c===10 || c===13 || c===32){
if(out && !spaced){
out+=' ';
spaced=true;
}
}else{
out+=s.charAt(i);
spaced=false;
}
}
return out.trim();
}
function compactNoSpace(value){
var s=String(value==null?'':value);
var out='';
var i;
var c;
for(i=0;i<s.length;i++){
c=s.charCodeAt(i);
if(c!==9 && c!==10 && c!==13 && c!==32){
out+=s.charAt(i);
}
}
return out.trim();
}
function num(value){
var s=String(value==null?'':value).split(',').join('').trim();
var sign=1;
var started=false;
var digits='';
var i;
var c;
for(i=0;i<s.length;i++){
c=s.charCodeAt(i);
if(!started && s.charAt(i)==='-'){
sign=-1;
started=true;
continue;
}
if(c>=48 && c<=57){
digits+=s.charAt(i);
started=true;
}else if(digits){
break;
}
}
if(!digits){
return NaN;
}
return sign*Number(digits);
}
function cleanPathParts(pathname){
var raw=String(pathname||'').split('/');
var out=[];
var i;
for(i=0;i<raw.length;i++){
if(raw[i]){
out.push(raw[i]);
}
}
return out;
}
function idsFromUrl(value,base){
try{
var u=new URL(value,base||location.href);
var parts=cleanPathParts(u.pathname);
var index=parts.indexOf('board');
if(index===-1){
return null;
}
var boardId=parts[index+2]||'';
var postId=parts[index+3]||'';
if(!isDigits(boardId)){
return null;
}
return {
boardId:boardId,
postId:isDigits(postId)?postId:''
};
} catch(error){
return null;
}
}
var currentUrl=new URL(location.href);
if(currentUrl.searchParams.get('my')!=='post'){
alert('내 글 목록 페이지에서 실행하세요.');
return;
}
var BOARD_ID=currentUrl.searchParams.get('come_idx')||'';
if(!isDigits(BOARD_ID)){
var currentIds=idsFromUrl(location.href,location.href);
if(currentIds){
BOARD_ID=currentIds.boardId;
}
}
if(!isDigits(BOARD_ID)){
var linksForBoard=document.querySelectorAll('a[href]');
var boardLinkIndex;
for(boardLinkIndex=0;boardLinkIndex<linksForBoard.length;boardLinkIndex++){
var boardIds=idsFromUrl(
linksForBoard[boardLinkIndex].getAttribute('href'),
location.href
);
if(boardIds && boardIds.postId){
BOARD_ID=boardIds.boardId;
break;
}
}
}
if(!isDigits(BOARD_ID)){
alert('게시판 번호를 확인할 수 없습니다.');
return;
}
var ignored=new Set();
var deleted=0;
var protectedCount=0;
var skipped=0;
window.__INVEN_POST_DELETE_STOP__=false;
var box=document.createElement('div');
box.style=
'position:fixed;right:15px;bottom:15px;z-index:999999;'+
'background:#111;color:#fff;padding:12px 15px;'+
'font:13px/1.55 monospace;border-radius:7px;'+
'max-width:500px;white-space:pre-wrap;';
document.body.appendChild(box);
function status(text){
box.textContent=
'[게시글 정리]'+
NL+
text+
NL+
'삭제 '+deleted+
' / 보호 '+protectedCount+
' / 제외 '+skipped;
}
function checkStop(){
if(window.__INVEN_POST_DELETE_STOP__){
throw new Error(
'사용자가 작업을 중지했습니다.'
);
}
}
async function request(url,options){
checkStop();
var opts=options||{};
opts.credentials='include';
opts.cache='no-store';
var response=await fetch(url,opts);
if(
response.status===403 ||
response.status===429
){
throw new Error(
'서버 제한 감지 HTTP '+
response.status
);
}
if(!response.ok){
throw new Error(
'HTTP '+
response.status
);
}
return response;
}
function findHeaderIndex(headers,name){
var i;
var text;
for(i=0;i<headers.length;i++){
text=compactNoSpace(
headers[i].textContent
);
if(text===name){
return i;
}
}
for(i=0;i<headers.length;i++){
text=compactNoSpace(
headers[i].textContent
);
if(
name==='추천' &&
text.indexOf('비추천')!==-1
){
continue;
}
if(text.indexOf(name)!==-1){
return i;
}
}
return -1;
}
function postIdFromRow(row,baseUrl){
var numCell=
row.querySelector(
'td.num'
);
var raw='';
if(numCell){
var span=
numCell.querySelector(
'span'
);
raw=compactText(
span
? span.textContent
: numCell.textContent
);
if(isDigits(raw)){
return raw;
}
}
var links=
row.querySelectorAll(
'a[href]'
);
var i;
for(i=0;i<links.length;i++){
var ids=idsFromUrl(
links[i].getAttribute('href'),
baseUrl
);
if(
ids &&
ids.boardId===String(BOARD_ID) &&
ids.postId
){
return ids.postId;
}
}
return '';
}
function subjectFromRow(row,id,baseUrl){
var subject=
row.querySelector(
'a.subject-link[href]'
);
if(subject){
return subject;
}
var links=
row.querySelectorAll(
'a[href]'
);
var i;
for(i=0;i<links.length;i++){
var ids=idsFromUrl(
links[i].getAttribute('href'),
baseUrl
);
if(
ids &&
ids.boardId===String(BOARD_ID) &&
ids.postId===String(id)
){
return links[i];
}
}
return null;
}
async function getList(page){
var u=new URL(location.href);
u.searchParams.set(
'my',
'post'
);
u.searchParams.set(
'p',
String(page)
);
var response=
await request(
u.href
);
var html=
await response.text();
var d=
new DOMParser()
.parseFromString(
html,
'text/html'
);
var posts=
new Map();
var tables=
d.querySelectorAll(
'table'
);
var tableIndex;
for(
tableIndex=0;
tableIndex<tables.length;
tableIndex++
){
var table=
tables[tableIndex];
var headers=
table.querySelectorAll(
'thead th'
);
var recIndex=
findHeaderIndex(
headers,
'추천'
);
var downIndex=
findHeaderIndex(
headers,
'비추천'
);
var rows=
table.querySelectorAll(
'tbody tr'
);
var rowIndex;
for(
rowIndex=0;
rowIndex<rows.length;
rowIndex++
){
var row=
rows[rowIndex];
var id=
postIdFromRow(
row,
u.href
);
if(!isDigits(id)){
continue;
}
var subject=
subjectFromRow(
row,
id,
u.href
);
if(!subject){
continue;
}
var cells=
row.querySelectorAll(
'td'
);
var recommend=NaN;
var notrecommend=NaN;
if(
recIndex!==-1 &&
cells[recIndex]
){
recommend=
num(
cells[
recIndex
].textContent
);
}
if(
downIndex!==-1 &&
cells[downIndex]
){
notrecommend=
num(
cells[
downIndex
].textContent
);
}
if(
!Number.isFinite(
recommend
)
){
var recCell=
row.querySelector(
'td.recommend,td.recom,td.rec'
);
if(recCell){
recommend=
num(
recCell.textContent
);
}
}
if(
!Number.isFinite(
notrecommend
)
){
var downCell=
row.querySelector(
'td.notrecommend,td.not-recommend,td.down,td.dislike'
);
if(downCell){
notrecommend=
num(
downCell.textContent
);
}
}
posts.set(
id,
{
id:id,
title:
compactText(
subject.textContent
)||id,
recommend:
recommend,
notrecommend:
notrecommend
}
);
}
}
if(!posts.size){
var allRows=
d.querySelectorAll(
'tr'
);
var allRowIndex;
for(
allRowIndex=0;
allRowIndex<allRows.length;
allRowIndex++
){
var row2=
allRows[
allRowIndex
];
var id2=
postIdFromRow(
row2,
u.href
);
if(!isDigits(id2)){
continue;
}
var subject2=
subjectFromRow(
row2,
id2,
u.href
);
if(!subject2){
continue;
}
var rec2=NaN;
var down2=NaN;
var rc=
row2.querySelector(
'td.recommend,td.recom,td.rec'
);
var dc=
row2.querySelector(
'td.notrecommend,td.not-recommend,td.down,td.dislike'
);
if(rc){
rec2=
num(
rc.textContent
);
}
if(dc){
down2=
num(
dc.textContent
);
}
posts.set(
id2,
{
id:id2,
title:
compactText(
subject2.textContent
)||id2,
recommend:
rec2,
notrecommend:
down2
}
);
}
}
return Array.from(
posts.values()
);
}
async function removePost(id,page){
var body=
new FormData();
body.append(
'come_idx',
BOARD_ID
);
body.append(
'p',
String(page)
);
body.append(
'l',
String(id)
);
body.append(
'my',
'post'
);
await request(
'/board/bbs/include/multi_delete.php',
{
method:'POST',
headers:{
'X-Requested-With':
'XMLHttpRequest'
},
body:body
}
);
return true;
}
try{
var page=1;
while(page<=MAX_PAGE){
checkStop();
status(
page+
'페이지 목록 확인'
);
var posts=
await getList(page);
if(!posts.length){
log(
page+
'페이지 없음 → 종료'
);
break;
}
posts.reverse();
log(
page+
'페이지 | 게시글 '+
posts.length+
'개'
);
var attempted=[];
var i;
for(
i=0;
i<posts.length;
i++
){
checkStop();
var post=
posts[i];
if(
ignored.has(
post.id
)
){
continue;
}
if(
!Number.isFinite(
post.recommend
) ||
!Number.isFinite(
post.notrecommend
) ||
atLeast(
post.recommend,
Z[0]
) ||
atLeast(
post.notrecommend,
Z[1]
)
){
ignored.add(
post.id
);
protectedCount++;
log(
'보호 | '+
post.id+
' | '+
post.title
);
continue;
}
status(
page+
'P '+
(i+1)+
'/'+
posts.length+
NL+
post.title
);
try{
await removePost(
post.id,
page
);
attempted.push(
post
);
} catch(error3){
if(
String(
error3.message
).indexOf('403')!==-1 ||
String(
error3.message
).indexOf('429')!==-1
){
throw error3;
}
ignored.add(
post.id
);
skipped++;
log(
'삭제 실패 | '+
post.title+
' | '+
error3.message
);
}
await sleep(GAP);
}
if(attempted.length){
status(
page+
'페이지 삭제 결과 확인'
);
await sleep(
VERIFY_GAP
);
var after=
await getList(page);
var remaining=
new Set();
var ai;
for(
ai=0;
ai<after.length;
ai++
){
remaining.add(
after[ai].id
);
}
var changed=0;
var ti;
for(
ti=0;
ti<attempted.length;
ti++
){
var target=
attempted[ti];
if(
!remaining.has(
target.id
)
){
deleted++;
changed++;
log(
'삭제 완료 | '+
target.title
);
}else{
ignored.add(
target.id
);
skipped++;
log(
'삭제 거부 | '+
target.title
);
}
}
if(changed!==0){
log(
page+
'페이지 '+
changed+
'개 삭제 → 재확인'
);
continue;
}
}
log(
page+
'페이지 삭제 가능 게시글 없음 → '+
(page+1)+
'페이지'
);
page++;
await sleep(
PAGE_GAP
);
}
status(
'전체 완료'
);
log(
'===== 전체 완료 ====='
);
log(
'삭제 '+deleted
);
log(
'보호 '+protectedCount
);
log(
'삭제 불가/실패 '+
skipped
);
} catch(error4){
status(
'중단'+
NL+
error4.message
);
log(
'===== 자동 중단 ===== | '+
error4.message
);
}
})();