(async function(){
'use strict';
var Z=[10,3];
var GAP=200;
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 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')!=='opi'){
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;
}
}
}
var userNode=document.querySelector('#header-userinfo .user-name');
var USER_NAME=userNode ? compactText(userNode.textContent) : '';
if(!isDigits(BOARD_ID) || !USER_NAME){
alert('게시판 번호 또는 로그인 닉네임을 확인할 수 없습니다.');
return;
}
var randomBytes=crypto.getRandomValues(new Uint8Array(16));
var CHK='';
var ri;
for(ri=0;ri<randomBytes.length;ri++){
CHK+=randomBytes[ri].toString(16).padStart(2,'0');
}
var ignored=new Set();
var deleted=0;
var protectedCount=0;
var skipped=0;
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;
}
async function request(params){
var body=new URLSearchParams();
var key;
body.append('comeidx',BOARD_ID);
body.append('sortorder','date');
body.append('out','json');
body.append('replynick','');
body.append('replyidx','0');
body.append('chkcode',CHK);
body.append('uploadurl','');
body.append('imageposition','');
body.append('videoloading','lazy');
for(key in params){
if(Object.prototype.hasOwnProperty.call(params,key)){
body.append(key,String(params[key]));
}
}
var response=await fetch(
'/common/board/comment.json.php?dummy='+Date.now(),
{
method:'POST',
credentials:'include',
headers:{
'Accept':'application/json',
'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'
},
body:body
}
);
if(response.status===403 || response.status===429){
throw new Error(
'서버 제한 감지 HTTP '+response.status
);
}
if(!response.ok){
throw new Error(
'HTTP '+response.status
);
}
return response.json();
}
function addComments(target,data){
var groups=data.commentlist||[];
var gi;
var ci;
var list;
var c;
var id;
for(gi=0;gi<groups.length;gi++){
list=groups[gi].list||[];
for(ci=0;ci<list.length;ci++){
c=list[ci];
id='';
if(c && c.__attr__ && c.__attr__.cmtidx){
id=String(c.__attr__.cmtidx);
}
if(id){
target.set(id,c);
}
}
}
}
async function getComments(articleCode){
var all=new Map();
var first=await request({
articlecode:articleCode,
act:'list'
});
var total=Number(first.cmtcount)||0;
var step=Number(first.pagecount)||100;
var pages=Math.ceil(total/step);
var pageIndex;
addComments(all,first);
for(pageIndex=1;pageIndex<pages;pageIndex++){
var next=await request({
articlecode:articleCode,
act:'list',
titles:String(pageIndex*step)
});
addComments(all,next);
}
return Array.from(all.values());
}
async function removeComment(articleCode,id){
var result=await request({
articlecode:articleCode,
act:'del',
cmtcodes:String(id)
});
return Number(result.message)===1;
}
async function getList(page){
var u=new URL(location.href);
u.searchParams.set('my','opi');
u.searchParams.set('p',String(page));
var response=await fetch(
u.href,
{
credentials:'include',
cache:'no-store'
}
);
if(response.status===403 || response.status===429){
throw new Error(
'서버 제한 감지 HTTP '+response.status
);
}
if(!response.ok){
throw new Error(
'목록 HTTP '+response.status
);
}
var html=await response.text();
var d=new DOMParser()
.parseFromString(
html,
'text/html'
);
var posts=new Map();
var links=
d.querySelectorAll(
'a.subject-link[href]'
);
var linkIndex;
for(linkIndex=0;linkIndex<links.length;linkIndex++){
var ids=idsFromUrl(
links[linkIndex].getAttribute('href'),
u.href
);
if(
ids &&
ids.boardId===String(BOARD_ID) &&
ids.postId
){
posts.set(
ids.postId,
{
id:ids.postId,
title:
compactText(
links[linkIndex].textContent
)||ids.postId
}
);
}
}
if(!posts.size){
var allRows=
d.querySelectorAll(
'tr'
);
var rowIndex;
for(rowIndex=0;rowIndex<allRows.length;rowIndex++){
var rowLinks=
allRows[rowIndex]
.querySelectorAll(
'a[href]'
);
var rowLinkIndex;
for(rowLinkIndex=0;rowLinkIndex<rowLinks.length;rowLinkIndex++){
var rowIds=idsFromUrl(
rowLinks[rowLinkIndex].getAttribute('href'),
u.href
);
if(
rowIds &&
rowIds.boardId===String(BOARD_ID) &&
rowIds.postId
){
posts.set(
rowIds.postId,
{
id:rowIds.postId,
title:
compactText(
rowLinks[rowLinkIndex].textContent
)||rowIds.postId
}
);
break;
}
}
}
}
return Array.from(posts.values());
}
function plainText(html){
var temp=
document.createElement(
'div'
);
temp.innerHTML=
String(html||'');
return compactText(
temp.textContent||
temp.innerText||
''
).slice(0,80);
}
async function processPost(post){
status(
'댓글 조회'+
NL+
post.title
);
var comments=
await getComments(
post.id
);
var mine=[];
var ci;
for(ci=0;ci<comments.length;ci++){
if(
compactText(
comments[ci].o_name||''
)===USER_NAME
){
mine.push(
comments[ci]
);
}
}
mine.reverse();
var changed=0;
var mi;
for(mi=0;mi<mine.length;mi++){
var c=mine[mi];
var id='';
if(
c &&
c.__attr__ &&
c.__attr__.cmtidx
){
id=
String(
c.__attr__.cmtidx
);
}
if(
!id ||
ignored.has(id)
){
continue;
}
var recommend=
Number(
c.o_recommend
);
var notrecommend=
Number(
c.o_notrecommend
);
var dice=0;
if(c.__attr__){
dice=
Number(
c.__attr__.dice
)||0;
}
if(
!Number.isFinite(recommend) ||
!Number.isFinite(notrecommend) ||
atLeast(recommend,Z[0]) ||
atLeast(notrecommend,Z[1]) ||
dice!==0
){
ignored.add(id);
protectedCount++;
continue;
}
var text=
plainText(
c.o_comment
);
status(
'삭제 중'+
NL+
post.title+
NL+
text
);
try{
if(
await removeComment(
post.id,
id
)
){
deleted++;
changed++;
log(
'삭제 완료 | '+
post.title+
' | '+
text
);
}else{
ignored.add(id);
skipped++;
log(
'삭제 거부 | '+
post.title
);
}
} catch(error3){
if(
String(
error3.message
).indexOf('403')!==-1 ||
String(
error3.message
).indexOf('429')!==-1
){
throw error3;
}
ignored.add(id);
skipped++;
log(
'삭제 실패 | '+
post.title+
' | '+
error3.message
);
}
await sleep(GAP);
}
return changed;
}
try{
var page=1;
var pageSignatures=new Set();
while(page<=MAX_PAGE){
status(
page+
'페이지 목록 확인'
);
var posts=
await getList(page);
if(!posts.length){
log(
page+
'페이지 없음 → 종료'
);
break;
}
posts.reverse();
var ids=[];
var si;
for(si=0;si<posts.length;si++){
ids.push(
posts[si].id
);
}
var sig=
page+
'|'+
ids.join(',');
log(
page+
'페이지 | 게시글 '+
posts.length+
'개'
);
var changed=0;
var pi;
for(pi=0;pi<posts.length;pi++){
status(
page+
'P '+
(pi+1)+
'/'+
posts.length+
NL+
posts[pi].title
);
try{
changed+=
await processPost(
posts[pi]
);
} catch(error4){
if(
String(
error4.message
).indexOf('403')!==-1 ||
String(
error4.message
).indexOf('429')!==-1
){
throw error4;
}
skipped++;
log(
'게시글 실패 | '+
posts[pi].title+
' | '+
error4.message
);
}
}
if(changed!==0){
log(
page+
'페이지 '+
changed+
'개 삭제 → 재확인'
);
continue;
}
if(
pageSignatures.has(sig)
){
log(
page+
'페이지 변화 없음'
);
}
pageSignatures.add(sig);
log(
page+
'페이지 삭제 가능 댓글 없음 → '+
(page+1)+
'페이지'
);
page++;
}
status('전체 완료');
log(
'===== 전체 완료 ====='
);
log(
'삭제 '+deleted
);
log(
'보호 '+protectedCount
);
log(
'삭제 불가/실패 '+
skipped
);
} catch(error5){
status(
'중단'+
NL+
error5.message
);
log(
'===== 자동 중단 ===== | '+
error5.message
);
}
})();