| r2069 vs r2070 | ||
|---|---|---|
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 1 | // ==UserScript== | |
| 2 | // @name NamuFix | |
| 3 | // @namespace http://litehell.info/ | |
| 4 | // @description 나무위키 등 더시드 사용 위키의 편집 인터페이스 등을 개선합니다. | |
| 5 | // @include https://namu.wiki/* | |
| 6 | // @include https://alphawiki.org/* | |
| 7 | // @include https://www.alphawiki.org/* | |
| 8 | // @include https://theseed.io/* | |
| 9 | // @include https://board.namu.wiki/* | |
| 10 | // @version 180630.0 | |
| 11 | // @author LiteHell | |
| 12 | // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js | |
| 13 | // @require https://cdn.rawgit.com/LiteHell/NamuFix/5326c9aada134f65bba171d12f5ca5d042fd4fca/korCountryNames.js | |
| 14 | // @require https://cdn.rawgit.com/LiteHell/NamuFix/0ea78119c377402a10bbdfc33365c5195ce7fccc/FlexiColorPicker.js | |
| 15 | // @require https://cdn.rawgit.com/Caligatio/jsSHA/v2.3.1/src/sha.js | |
| 16 | // @require https://cdn.rawgit.com/zenozeng/color-hash/v1.0.3/dist/color-hash.js | |
| 17 | // @require https://cdn.rawgit.com/ben-liang/pnglib/91a91b7f840fdf19ef34a32df8051f2178957293/pnglib.js | |
| 18 | // @require https://cdn.rawgit.com/stewartlord/identicon.js/7c4b4efdb7e2aba458eba14b24ba14e8e2bcdb2a/identicon.js | |
| 19 | // @require https://cdn.jsdelivr.net/npm/[email protected] | |
| 20 | // @require https://cdn.rawgit.com/LiteHell/TooSimplePopupLib/7f2a8a81f11f980c1dfa6b5b2213cd38b8bbde3c/TooSimplePopupLib.js | |
| 21 | // @require https://cdn.rawgit.com/wkpark/jsdifflib/dc19d085db5ae71cdff990aac8351607fee4fd01/difflib.js | |
| 22 | // @require https://cdn.rawgit.com/wkpark/jsdifflib/dc19d085db5ae71cdff990aac8351607fee4fd01/diffview.js | |
| 23 | // @require https://cdn.rawgit.com/LiteHell/NamuFix/d6bcc377c563c2745af0b8078ce5dc97f2c19910/namuapi.js | |
| 24 | // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.3/moment-with-locales.min.js | |
| 25 | // @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone-with-data.min.js | |
| 26 | // @require https://cdnjs.cloudflare.com/ajax/libs/async/2.6.0/async.min.js | |
| 27 | // @connect cdn.rawgit.com | |
| 28 | // @connect cdnjs.cloudflare.com | |
| 29 | // @connect jsdelivr.net | |
| 30 | // @connect api.github.com | |
| 31 | // @connect ipinfo.io | |
| 32 | // @connect wtfismyip.com | |
| 33 | // @connect www.googleapis.com | |
| 34 | // @connect web.archive.org | |
| 35 | // @connect archive.is | |
| 36 | // @connect www.vpngate.net | |
| 37 | // @connect namufix.wikimasonry.org | |
| 38 | // @connect phpgongbu.ga | |
| 39 | // @connect twitter.com | |
| 40 | // @connect facebook.com | |
| 41 | // @connect www.facebook.com | |
| 42 | // @grant GM_addStyle | |
| 43 | // @grant GM_openInTab | |
| 44 | // @grant GM_xmlhttpRequest | |
| 45 | // @grant GM_getValue | |
| 46 | // @grant GM_setValue | |
| 47 | // @grant GM_deleteValue | |
| 48 | // @grant GM_listValues | |
| 49 | // @grant GM_info | |
| 50 | // @grant GM_getResourceURL | |
| 51 | // @grant GM.openInTab | |
| 52 | // @grant GM.xmlHttpRequest | |
| 53 | // @grant GM.getValue | |
| 54 | // @grant GM.setValue | |
| 55 | // @grant GM.deleteValue | |
| 56 | // @grant GM.listValues | |
| 57 | // @grant GM.info | |
| 58 | // @grant GM.getResourceUrl | |
| 59 | // @run-at document-end | |
| 60 | // @noframes | |
| 61 | // ==/UserScript== | |
| 62 | /* jshint ignore:start */ | |
| 63 | /* | |
| 64 | Copyright (c) 2015 LiteHell | |
| 5 | 65 | |
| 6 | ||
| 66 | Permission is hereby granted, free of charge, to any person | |
| 67 | obtaining a copy of this software and associated documentation | |
| 68 | files (the "Software"), to deal in the Software without | |
| 69 | restriction, including without limitation the rights to use, | |
| 70 | copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 71 | copies of the Software, and to permit persons to whom the | |
| 72 | Software is furnished to do so, subject to the following | |
| 73 | conditions: | |
| 7 | 74 | |
| 8 | ||
| 9 | ||
| 75 | The above copyright notice and this permission notice shall be | |
| 76 | included in all copies or substantial portions of the Software. | |
| 10 | 77 | |
| 11 | ||
| 12 | ||
| 13 | ||
| 14 | ||
| 15 | ||
| 16 | ||
| 17 | ||
| 18 | ||
| 19 | ||
| 20 | ||
| 21 | ||
| 22 | ||
| 23 | ||
| 24 | ||
| 25 | ||
| 26 | ||
| 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
| 80 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 81 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| 82 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 83 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 84 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
| 85 | OTHER DEALINGS IN THE SOFTWARE. | |
| 86 | */ | |
| 27 | 87 | |
| 28 | ||
| 29 | ||
| 30 | ||
| 88 | try { | |
| 89 | if (typeof GM_addStyle !== 'undefined') { | |
| 90 | var GM_addStyle = function (text) { | |
| 91 | var style = document.createElement("style"); | |
| 92 | style.innerHTML = text; | |
| 93 | document.head.appendChild(style); | |
| 94 | } | |
| 95 | } | |
| 31 | 96 | |
| 32 | ||
| 33 | ||
| 34 | ||
| 35 | == | |
| 36 | === 1 | |
| 37 | ||
| 38 | ||
| 39 | ||
| 40 | ||
| 41 | ||
| 42 | ||
| 43 | ||
| 44 | ||
| 45 | ||
| 46 | ||
| 47 | ||
| 48 | ||
| 49 | ||
| 50 | ||
| 51 | ||
| 52 | ||
| 53 | ||
| 54 | ||
| 55 | ||
| 56 | ||
| 57 | ||
| 58 | ||
| 59 | ||
| 60 | ||
| 61 | ||
| 62 | ||
| 63 | ||
| 64 | ||
| 65 | ||
| 66 | ||
| 67 | ||
| 68 | ||
| 69 | ||
| 70 | ||
| 71 | ||
| 72 | ||
| 73 | ||
| 74 | ||
| 75 | ||
| 76 | ||
| 77 | ||
| 78 | ||
| 79 | ||
| 80 | ||
| 81 | ||
| 82 | ||
| 83 | ||
| 84 | ||
| 85 | ||
| 86 | ||
| 97 | function NFStorage() { | |
| 98 | function jsonParsable(str) { | |
| 99 | try { | |
| 100 | JSON.parse(str); | |
| 101 | return true; | |
| 102 | } catch (err) { | |
| 103 | return false; | |
| 104 | } | |
| 105 | } | |
| 106 | var discards = ['save', 'load', 'delete', 'export', 'import']; | |
| 107 | this.save = async function () { | |
| 108 | for (var i in this) { | |
| 109 | if (discards.indexOf(i) != -1) continue; | |
| 110 | await GM.setValue('SET_' + i, JSON.stringify(this[i])); | |
| 111 | } | |
| 112 | }; | |
| 113 | this.load = async function () { | |
| 114 | var sets = await GM.listValues(); | |
| 115 | for (var i = 0; i < sets.length; i++) { | |
| 116 | var now = sets[i]; | |
| 117 | if (now.indexOf('SET_') != 0) continue; | |
| 118 | if (discards.indexOf(now.substring(4)) != -1) continue; | |
| 119 | let sval = await GM.getValue(now); | |
| 120 | this[now.substring(4)] = jsonParsable(sval) ? JSON.parse(sval) : sval; | |
| 121 | } | |
| 122 | }; | |
| 123 | this.delete = async function (key) { | |
| 124 | if (discards.indexOf(key) != -1) return; | |
| 125 | await GM.deleteValue('SET_' + key); | |
| 126 | delete this[key]; | |
| 127 | }; | |
| 128 | this.export = async function (excludes) { | |
| 129 | let sets = await GM.listValues(); | |
| 130 | let result = {}; | |
| 131 | for (let i of sets) { | |
| 132 | if (i.indexOf('SET_') != 0) continue; | |
| 133 | let setname = i.substring(4) | |
| 134 | if (discards.includes(setname) || excludes.includes(setname)) continue; | |
| 135 | result[setname] = await GM.getValue(i); | |
| 136 | } | |
| 137 | return result; | |
| 138 | }; | |
| 139 | this.import = async function (data) { | |
| 140 | for (let i in data) { | |
| 141 | await GM.setValue('SET_' + i, data[i]); | |
| 142 | } | |
| 143 | } | |
| 144 | } | |
| 145 | if (location.host === 'board.namu.wiki') { | |
| 146 | async function runBoardFix() { | |
| 147 | let SET = new NFStorage(); | |
| 148 | await SET.load(); | |
| 149 | // 시간대 자동 변경 | |
| 150 | if (SET.noLocaltimeOnNamuBoard !== false) { | |
| 151 | let times = document.querySelectorAll('.read_header > .meta > .time, .fbMeta .time'); | |
| 152 | let origTimezone = "UTC" // America/Asuncion 아님. | |
| 153 | for (let i = 0; i < times.length; i++) { | |
| 154 | let t = moment.tz(times[i].textContent.trim(), "YYYY.MM.DD HH:mm", origTimezone); | |
| 155 | times[i].textContent = t.tz(moment.tz.guess()).format("YYYY.MM.DD HH:mm z") | |
| 156 | } | |
| 157 | if (times.length !== 0) | |
| 158 | console.log(`[NamuFix] 게시판 시간대 변경 완료. ${origTimezone} > ${moment.tz.guess()}`); | |
| 159 | } | |
| 160 | // 아카이브 버튼 | |
| 161 | if (document.querySelector('.viewDocument .read_footer .btnArea a[onclick]') !== null) { | |
| 162 | let extraMenuLink = document.querySelector('.viewDocument .read_footer .btnArea a[onclick]'); | |
| 163 | let documentId = /document_([0-9]+)/.exec(extraMenuLink.className)[1]; | |
| 164 | let archiveLink = document.createElement("a"); | |
| 165 | archiveLink.href = "#"; | |
| 166 | archiveLink.target = "_blank"; | |
| 167 | archiveLink.textContent = "아카이브"; | |
| 168 | let clickHandler = (evt) => { | |
| 169 | evt.preventDefault(); | |
| 170 | archiveLink.textContent = "(진행중)"; | |
| 171 | archiveLink.removeAttribute("href"); | |
| 172 | archiveLink.removeEventListener("click", clickHandler); | |
| 173 | GM.xmlHttpRequest({ | |
| 174 | method: 'POST', | |
| 175 | url: 'https://phpgongbu.ga/archive/', | |
| 176 | headers: { | |
| 177 | "Content-Type": "application/x-www-form-urlencoded", | |
| 178 | "User-Agent": `Mozilla/5.0 (compatible; NamuFix/${GM.info.script.version}` | |
| 179 | }, | |
| 180 | data: `board_num=${documentId}`, | |
| 181 | onload: (res) => { | |
| 182 | GM.openInTab(res.finalUrl); | |
| 183 | archiveLink.textContent = "아카이브됨"; | |
| 184 | archiveLink.href = res.finalUrl; | |
| 185 | } | |
| 186 | }); | |
| 187 | }; | |
| 188 | archiveLink.addEventListener("click", clickHandler); | |
| 189 | extraMenuLink.parentNode.insertBefore(archiveLink, extraMenuLink.nextSibling); | |
| 190 | } | |
| 191 | // 댓글 상용구 | |
| 192 | if (document.querySelector('.write_comment') && SET.commentMacros) { | |
| 193 | console.log('[NamuFix] 댓글창 감지됨.') | |
| 194 | let writeAuthorDiv = document.querySelector('.write_author'); | |
| 195 | for(let i of SET.commentMacros.split(',')) { | |
| 196 | let macroName = i.split(':')[0]; | |
| 197 | let macroContentParts = i.split(':'); | |
| 198 | macroContentParts.shift(); | |
| 199 | let macroContent = macroContentParts.join(':'); | |
| 200 | let macroBtn = document.createElement("button"); | |
| 201 | console.log(`[NamuFix] 매크로 버튼 추가중 (이름: ${macroName}, 내용: ${macroContent})`) | |
| 202 | macroBtn.setAttribute('type', 'button'); | |
| 203 | macroBtn.innerHTML = '상용구 (' + macroName + ')'; | |
| 204 | macroBtn.addEventListener('click', (evt) => { | |
| 205 | evt.preventDefault(); | |
| 206 | let xeEditor = document.querySelector('.xpress-editor'), | |
| 207 | hiddenContentInput = document.querySelector('.write_comment > input[name="content"]'), | |
| 208 | writeForm = document.querySelector('.write_comment > .write_form'); | |
| 209 | // XE에디터 제거 | |
| 210 | xeEditor.parentNode.removeChild(xeEditor); | |
| 211 | // 기존 content input 제거 | |
| 212 | hiddenContentInput.parentNode.removeChild(hiddenContentInput); | |
| 213 | // content input 추가 | |
| 214 | let newContentInput = document.createElement('input'); | |
| 215 | newContentInput.setAttribute('type', 'hidden'); | |
| 216 | newContentInput.name = "content"; | |
| 217 | newContentInput.value = macroContent; | |
| 218 | writeForm.appendChild(newContentInput); | |
| 219 | // 댓글 작성 | |
| 220 | document.querySelector('.write_author button[type="submit"]').click(); | |
| 221 | }) | |
| 222 | writeAuthorDiv.appendChild(macroBtn,writeAuthorDiv.lastChild); | |
| 223 | } | |
| 224 | } | |
| 225 | } | |
| 226 | runBoardFix(); | |
| 227 | } else | |
| 228 | (async function () { | |
| 229 | console.log(`[NamuFix] 현재 버전 : ${GM.info.script.version}`); | |
| 230 | if (location.hostname == 'no-ssl.namu.wiki') | |
| 231 | location.hostname = 'namu.wiki'; | |
| 87 | 232 | |
| 88 | ||
| 89 | ||
| 90 | ||
| 91 | ||
| 92 | ||
| 93 | ||
| 94 | ||
| 95 | ||
| 96 | ||
| 97 | ||
| 98 | ||
| 99 | ||
| 100 | ||
| 101 | ||
| 102 | ||
| 103 | ||
| 104 | ||
| 105 | ||
| 106 | ||
| 107 | ||
| 108 | ||
| 109 | ||
| 110 | ||
| 111 | ||
| 112 | ||
| 113 | ||
| 114 | ||
| 115 | ||
| 116 | ||
| 117 | ||
| 118 | ||
| 119 | ||
| 120 | ||
| 121 | ||
| 122 | ||
| 123 | ||
| 124 | ||
| 125 | ||
| 126 | ||
| 127 | ||
| 128 | ||
| 129 | ||
| 130 | ||
| 131 | ||
| 132 | ||
| 133 | ||
| 134 | ||
| 135 | ||
| 136 | ||
| 137 | ||
| 138 | ||
| 233 | function insertCSS(url) { | |
| 234 | // 나무위키 CSP 빡세서 getResourceUrl 쓰면 보안 오류남. | |
| 235 | GM.xmlHttpRequest({ | |
| 236 | method: 'GET', | |
| 237 | url: url, | |
| 238 | onload: (res) => { | |
| 239 | let styleTag = document.createElement("style"); | |
| 240 | styleTag.innerHTML = res.responseText; | |
| 241 | document.head.appendChild(styleTag); | |
| 242 | } | |
| 243 | }); | |
| 244 | } | |
| 139 | 245 | |
| 140 | ||
| 141 | ||
| 142 | ||
| 143 | ||
| 144 | ||
| 145 | ||
| 146 | ||
| 147 | ||
| 148 | ||
| 149 | ||
| 150 | ||
| 151 | ||
| 152 | ||
| 153 | ||
| 154 | ||
| 155 | ||
| 156 | ||
| 157 | ||
| 158 | ||
| 159 | ||
| 160 | ||
| 161 | ||
| 162 | ||
| 163 | ||
| 164 | ||
| 165 | ||
| 166 | ||
| 167 | ||
| 168 | ||
| 169 | ||
| 170 | ||
| 171 | ||
| 172 | ||
| 173 | ||
| 174 | ||
| 175 | ||
| 176 | ||
| 177 | ||
| 178 | ||
| 179 | ||
| 180 | ||
| 181 | ||
| 182 | ||
| 183 | ||
| 184 | ||
| 185 | ||
| 186 | ||
| 187 | ||
| 188 | ||
| 189 | ||
| 190 | ||
| 246 | insertCSS("https://cdn.rawgit.com/LiteHell/NamuFix/284db44ac1d89ff0cbd1155c3372db38be3bc140/NamuFix.css"); | |
| 247 | insertCSS("https://cdn.rawgit.com/LiteHell/TooSimplePopupLib/edad912e28eeacdc3fd8b6e6b7ac5cafc46d95b6/TooSimplePopupLib.css"); | |
| 248 | insertCSS("https://cdn.rawgit.com/wkpark/jsdifflib/dc19d085db5ae71cdff990aac8351607fee4fd01/diffview.css"); | |
| 249 | console.log('[NamuFix] CSS 삽입됨.'); | |
| 191 | 250 | |
| 192 | ||
| 193 | ||
| 194 | ||
| 195 | ||
| 196 | ||
| 197 | ||
| 198 | ||
| 199 | ||
| 200 | ||
| 201 | ||
| 202 | ||
| 203 | ||
| 204 | ||
| 205 | ||
| 206 | ||
| 207 | ||
| 208 | ||
| 209 | ||
| 210 | ||
| 211 | ||
| 212 | ||
| 213 | ||
| 214 | ||
| 215 | ||
| 216 | ||
| 217 | ||
| 218 | ||
| 219 | ||
| 220 | ||
| 221 | ||
| 222 | ||
| 223 | ||
| 224 | ||
| 225 | ||
| 226 | ||
| 227 | ||
| 228 | ||
| 229 | ||
| 230 | ||
| 231 | ||
| 232 | ||
| 233 | ||
| 234 | ||
| 235 | ||
| 236 | ||
| 237 | ||
| 238 | ||
| 239 | ||
| 240 | ||
| 241 | ||
| 242 | ||
| 251 | // 업데이트 확인 | |
| 252 | GM.xmlHttpRequest({ | |
| 253 | method: "GET", | |
| 254 | url: "https://api.github.com/repos/LiteHell/NamuFix/releases/latest", | |
| 255 | onload: function (res) { | |
| 256 | var obj = JSON.parse(res.responseText); | |
| 257 | if (typeof obj.message !== 'undefined' && obj.message.indexOf('API rate limit') != -1) { | |
| 258 | console.log('[NamuFix] NamuFix 업데이트 연기! (GitHub API 제한에 따른 오류)'); | |
| 259 | return; // GitHub API 오류 | |
| 260 | } | |
| 261 | var currentVersion = GM.info.script.version; | |
| 262 | if(!obj.tag_name) { | |
| 263 | console.log('[NamuFix] NamuFix 업데이트 연기! (최신 버전을 읽을 수 없음)'); | |
| 264 | return; | |
| 265 | } | |
| 266 | var latestVersion = obj.tag_name; | |
| 267 | } | |
| 268 | }); | |
| 243 | 269 | |
| 244 | ||
| 245 | ||
| 246 | ||
| 247 | ||
| 248 | ||
| 249 | ||
| 250 | ||
| 251 | ||
| 252 | ||
| 253 | ||
| 254 | ||
| 255 | ||
| 256 | ||
| 257 | ||
| 258 | ||
| 259 | ||
| 260 | ||
| 261 | ||
| 262 | ||
| 263 | ||
| 264 | ||
| 265 | ||
| 266 | ||
| 267 | ||
| 268 | ||
| 269 | ||
| 270 | ||
| 271 | ||
| 272 | ||
| 273 | ||
| 274 | ||
| 275 | ||
| 276 | ||
| 277 | ||
| 278 | ||
| 279 | ||
| 280 | ||
| 281 | ||
| 282 | ||
| 283 | ||
| 284 | ||
| 285 | ||
| 286 | ||
| 287 | ||
| 288 | ||
| 289 | ||
| 290 | ||
| 291 | ||
| 292 | ||
| 293 | ||
| 294 | ||
| 270 | function nOu(a) { | |
| 271 | return typeof a === 'undefined' || a == null; | |
| 272 | } | |
| 295 | 273 | |
| 296 | ||
| 297 | ||
| 298 | ||
| 299 | ||
| 300 | ||
| 301 | ||
| 302 | ||
| 303 | ||
| 304 | ||
| 305 | ||
| 306 | ||
| 307 | ||
| 308 | ||
| 309 | ||
| 310 | ||
| 311 | ||
| 312 | ||
| 313 | ||
| 314 | ||
| 315 | ||
| 316 | ||
| 317 | ||
| 318 | ||
| 319 | ||
| 320 | ||
| 321 | ||
| 322 | ||
| 323 | ||
| 324 | ||
| 325 | ||
| 326 | ||
| 327 | ||
| 328 | ||
| 329 | ||
| 330 | ||
| 331 | ||
| 332 | ||
| 333 | ||
| 334 | ||
| 335 | ||
| 336 | ||
| 337 | ||
| 338 | ||
| 339 | ||
| 340 | ||
| 341 | ||
| 342 | ||
| 343 | ||
| 344 | ||
| 345 | ||
| 346 | ||
| 274 | // HTML 이스케이프 함수 | |
| 275 | function encodeHTMLComponent(text) { | |
| 276 | var result = text; | |
| 277 | // http://www.w3schools.com/php/func_string_htmlspecialchars.asp 참고함. | |
| 278 | result = result.replace(/&/gmi, "&"); | |
| 279 | result = result.replace(/</gmi, "<"); | |
| 280 | result = result.replace(/>/gmi, ">"); | |
| 281 | result = result.replace(/'/gmi, "'"); | |
| 282 | result = result.replace(/"/gmi, """); | |
| 283 | return result; | |
| 284 | } | |
| 347 | 285 | |
| 348 | ||
| 349 | ||
| 350 | ||
| 351 | ||
| 352 | ||
| 353 | ||
| 354 | ||
| 355 | ||
| 356 | ||
| 357 | ||
| 358 | ||
| 359 | ||
| 360 | ||
| 361 | ||
| 362 | ||
| 363 | ||
| 364 | ||
| 365 | ||
| 366 | ||
| 367 | ||
| 368 | ||
| 369 | ||
| 370 | ||
| 371 | ||
| 372 | ||
| 373 | ||
| 374 | ||
| 375 | ||
| 376 | ||
| 377 | ||
| 378 | ||
| 379 | ||
| 380 | ||
| 381 | ||
| 382 | ||
| 383 | ||
| 384 | ||
| 385 | ||
| 386 | ||
| 387 | ||
| 388 | ||
| 389 | ||
| 390 | ||
| 391 | ||
| 392 | ||
| 393 | ||
| 394 | ||
| 395 | ||
| 396 | ||
| 397 | ||
| 398 | ||
| 286 | function validateIP(ip) { | |
| 287 | return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(ip) || /^([0-9a-f]){1,4}(:([0-9a-f]){1,4}){7}$/i.test(ip); | |
| 288 | } | |
| 399 | 289 | |
| 400 | ||
| 401 | ||
| 402 | ||
| 403 | ||
| 404 | ||
| 405 | ||
| 406 | ||
| 407 | ||
| 408 | ||
| 409 | ||
| 410 | ||
| 411 | ||
| 412 | ||
| 413 | ||
| 414 | ||
| 415 | ||
| 416 | ||
| 417 | ||
| 418 | ||
| 419 | ||
| 420 | ||
| 421 | ||
| 422 | ||
| 423 | ||
| 424 | ||
| 425 | ||
| 426 | ||
| 427 | ||
| 428 | ||
| 429 | ||
| 430 | ||
| 431 | ||
| 432 | ||
| 433 | ||
| 434 | ||
| 435 | ||
| 436 | ||
| 437 | ||
| 438 | ||
| 439 | ||
| 440 | ||
| 441 | ||
| 442 | ||
| 443 | ||
| 444 | ||
| 445 | ||
| 446 | ||
| 447 | ||
| 448 | ||
| 449 | ||
| 450 | ||
| 290 | function formatDateTime(t) { | |
| 291 | var d = new Date(t); | |
| 292 | return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일 ${(['일', '월', '화', '수', '목', '금', '토'])[d.getDay()]}요일 ${ d.getHours() > 12 ? '오후' : '오전'} ${d.getHours() - (d.getHours() > 12 ? 12 : 0)}시 ${d.getMinutes()}분 ${d.getSeconds()}초`; | |
| 293 | } | |
| 451 | 294 | |
| 452 | ||
| 453 | ||
| 454 | ||
| 455 | ||
| 456 | ||
| 457 | ||
| 458 | ||
| 459 | ||
| 460 | ||
| 461 | ||
| 462 | ||
| 463 | ||
| 464 | ||
| 465 | ||
| 466 | ||
| 467 | ||
| 468 | ||
| 469 | ||
| 470 | ||
| 471 | ||
| 472 | ||
| 473 | ||
| 474 | ||
| 475 | ||
| 476 | ||
| 477 | ||
| 478 | ||
| 479 | ||
| 480 | ||
| 481 | ||
| 482 | ||
| 483 | ||
| 484 | ||
| 485 | ||
| 486 | ||
| 487 | ||
| 488 | ||
| 489 | ||
| 490 | ||
| 491 | ||
| 492 | ||
| 493 | ||
| 494 | ||
| 495 | ||
| 496 | ||
| 497 | ||
| 498 | ||
| 499 | ||
| 500 | ||
| 501 | ||
| 502 | ||
| 295 | function formatTimespan(timespan) { | |
| 296 | var units = [{ | |
| 297 | name: "주", | |
| 298 | unit: 60 * 60 * 24 * 7, | |
| 299 | value: 0 | |
| 300 | }, | |
| 301 | { | |
| 302 | name: "일", | |
| 303 | unit: 60 * 60 * 24, | |
| 304 | value: 0 | |
| 305 | }, | |
| 306 | { | |
| 307 | name: "시간", | |
| 308 | unit: 60 * 60, | |
| 309 | value: 0 | |
| 310 | }, | |
| 311 | { | |
| 312 | name: "분", | |
| 313 | unit: 60, | |
| 314 | value: 0 | |
| 315 | }, | |
| 316 | { | |
| 317 | name: "초", | |
| 318 | unit: 1, | |
| 319 | value: 0 | |
| 320 | } | |
| 321 | ]; | |
| 322 | for (var i = 0; i < units.length; i++) { | |
| 323 | while (timespan >= units[i].unit) { | |
| 324 | timespan -= units[i].unit; | |
| 325 | units[i].value++; | |
| 326 | } | |
| 327 | } | |
| 328 | return units.filter(function (x) { | |
| 329 | return x.value != 0; | |
| 330 | }).map(function (x) { | |
| 331 | return x.value + x.name | |
| 332 | }).join(' '); | |
| 333 | } | |
| 503 | 334 | |
| 504 | ||
| 505 | ||
| 506 | ||
| 507 | ||
| 508 | ||
| 509 | ||
| 510 | ||
| 511 | ||
| 512 | ||
| 513 | ||
| 514 | ||
| 515 | ||
| 516 | ||
| 517 | ||
| 518 | ||
| 519 | ||
| 520 | ||
| 521 | ||
| 522 | ||
| 523 | ||
| 524 | ||
| 525 | ||
| 526 | ||
| 527 | ||
| 528 | ||
| 529 | ||
| 530 | ||
| 531 | ||
| 532 | ||
| 533 | ||
| 534 | ||
| 535 | ||
| 536 | ||
| 537 | ||
| 538 | ||
| 539 | ||
| 540 | ||
| 541 | ||
| 542 | ||
| 543 | ||
| 544 | ||
| 545 | ||
| 546 | ||
| 547 | ||
| 548 | ||
| 549 | ||
| 550 | ||
| 551 | ||
| 552 | ||
| 553 | ||
| 554 | ||
| 335 | var hashDictionary512 = {}; | |
| 336 | var hashDictionary1 = {}; | |
| 337 | var hashDictionary256 = {}; | |
| 338 | var ipDictionary = {}; | |
| 555 | 339 | |
| 556 | === 1001 ~ 1100(완료) === | |
| 557 | || 1001 || [[안구]] || 1002 || [[구주]] || | |
| 558 | || 1003 || [[주화]] || 1004 || [[화염방사]] || | |
| 559 | || 1005 || [[사칭]] || 1006 || [[칭따오]] || | |
| 560 | || 1007 || [[오스트랄로피테쿠스]] || 1008 || [[스파게티]] || | |
| 561 | || 1009 || [[티파니]] || 1010 || [[니스]] || | |
| 562 | || 1011 || [[스키장]] || 1012 || [[장비(삼국지)]] || | |
| 563 | || 1013 || [[비장]] || 1014 || [[장지]] || | |
| 564 | || 1015 || [[지나]] || 1016 || [[나무]] || | |
| 565 | || 1017 || [[무스]] || 1018 || [[스승의 은혜]] || | |
| 566 | || 1019 || [[혜리]] || 1020 || [[리스]] || | |
| 567 | || 1021 || [[스키 장비]] || 1022 || [[비긴 어게인]] || | |
| 568 | || 1023 || [[인사동]] || 1024 || [[동대문]] || | |
| 569 | || 1025 || [[문지기]] || 1026 || [[기안]] || | |
| 570 | || 1027 || [[안티]] || 1028 || [[티비]] || | |
| 571 | || 1029 || [[비룡]] || 1030 || [[용수]] || | |
| 572 | || 1031 || [[수치]] || 1032 || [[치수]] || | |
| 573 | || 1033 || [[수영장]] || 1034 || [[장 르노]] || | |
| 574 | || 1035 || [[노무현]] || 1036 || [[현대 유니콘스]] || | |
| 575 | || 1037 || [[스테이크]] || 1038 || [[크고 아름다운]] || | |
| 576 | || 1039 || [[운석 금메달]] || 1040 || [[달력]] || | |
| 577 | || 1041 || [[역사지도]] || 1042 || [[도나]] || | |
| 578 | || 1043 || [[나프탈렌]] || 1044 || [[렌터카]] || | |
| 579 | || 1045 || [[카를루스 에두아르두 카스트루 다 시우바]] || 1046 || [[바이오하자드 더 스테이지]] || | |
| 580 | || 1047 || [[지가르데]] || 1048 || [[데벤져]] || | |
| 581 | || 1049 || [[져주기 게임]] || 1050 || [[임신 엔딩]] || | |
| 582 | || 1051 || [[딩고]] || 1052 || [[고리]] || | |
| 583 | || 1053 || [[리쌍]] || 1054 || [[쌍방울 레이더스]] || | |
| 584 | || 1055 || [[스튜]] || 1056 || [[튜브]] || | |
| 585 | || 1057 || [[브라질]] || 1058 || [[질소를 샀더니 과자를 덤으로]] || | |
| 586 | || 1059 || [[로보트 태권 V]] || 1060 || [[VDT 증후군]] || | |
| 587 | || 1061 || [[군장]] || 1062 || [[장갑기병 보톰즈 외로운 그림자 다시]] || | |
| 588 | || 1063 || [[시로타니 타다오미]] || 1064 || [[미장원]] || | |
| 589 | || 1065 || [[원두]] || 1066 || [[두부]] || | |
| 590 | || 1067 || [[부산광역시]] || 1068 || [[시리우스]] || | |
| 591 | || 1069 || [[스2갤]] || 1070 || [[갤러그]] || | |
| 592 | || 1071 || [[그 고집, 내게도 주세요]] || 1072 || [[요통]] || | |
| 593 | || 1073 || [[통수]] || 1074 || [[수사기록 Pilot]] || | |
| 594 | || 1075 || [[TACC]] || 1076 || [[C 언어]] || | |
| 595 | || 1077 || [[어디에다 대고 신성한 우리 영해에 불질이야]] || 1078 || [[야리스기 도시전설]] || | |
| 596 | || 1079 || [[설정오류]] || 1080 || [[류정옥]] || | |
| 597 | || 1081 || [[옥정IC]] || 1082 || [[CF]] || | |
| 598 | || 1083 || [[FIFA]] || 1084 || [[AEK-999(소녀전선)]] || | |
| 340 | function SHA512(text) { | |
| 341 | if (typeof hashDictionary512[text] === 'undefined') { | |
| 342 | var shaObj = new jsSHA("SHA-512", "TEXT"); | |
| 343 | shaObj.update(text); | |
| 344 | hashDictionary512[text] = shaObj.getHash("HEX"); | |
| 345 | } | |
| 346 | return hashDictionary512[text]; | |
| 347 | } | |
| 348 | ||
| 349 | function SHA1(text) { | |
| 350 | if (typeof hashDictionary1[text] === 'undefined') { | |
| 351 | var shaObj = new jsSHA("SHA-1", "TEXT"); | |
| 352 | shaObj.update(text); | |
| 353 | hashDictionary1[text] = shaObj.getHash("HEX"); | |
| 354 | } | |
| 355 | return hashDictionary1[text]; | |
| 356 | } | |
| 357 | ||
| 358 | function SHA256(text) { | |
| 359 | if (typeof hashDictionary256[text] === 'undefined') { | |
| 360 | var shaObj = new jsSHA("SHA-256", "TEXT"); | |
| 361 | shaObj.update(text); | |
| 362 | hashDictionary256[text] = shaObj.getHash("HEX"); | |
| 363 | } | |
| 364 | return hashDictionary256[text]; | |
| 365 | } | |
| 366 | ||
| 367 | function getIpInfo(ip, cb) { | |
| 368 | if (ipDictionary[ip]) | |
| 369 | return cb(ipDictionary[ip]); | |
| 370 | GM.xmlHttpRequest({ | |
| 371 | method: "GET", | |
| 372 | url: `http://ipinfo.io/${ip}/json`, | |
| 373 | onload: function (res) { | |
| 374 | var resObj = JSON.parse(res.responseText); | |
| 375 | if (res.status === 200 || res.status === 304) { | |
| 376 | if (/^AS[0-9]+ /.test(resObj.org)) { | |
| 377 | resObj.org = resObj.org.replace(/^AS[0-9]+ /, ''); | |
| 378 | } | |
| 379 | if (SET.ipInfoDefaultOrg != 'ipinfo.io') { | |
| 380 | getIpWhois(ip, function (whoisRes) { | |
| 381 | if (!whoisRes.success || whoisRes.raw) { | |
| 382 | ipDictionary[ip] = resObj; | |
| 383 | cb(resObj); | |
| 384 | return; | |
| 385 | } | |
| 386 | var koreanISP = null, | |
| 387 | koreanUser = null; | |
| 388 | if (whoisRes.result.korean && whoisRes.result.korean.ISP && whoisRes.result.korean.ISP.netinfo && whoisRes.result.korean.ISP.netinfo.orgName) { | |
| 389 | koreanISP = whoisRes.result.korean.ISP.netinfo.orgName; | |
| 390 | } else if (whoisRes.result.korean && whoisRes.result.korean.user && whoisRes.result.korean.user.netinfo && whoisRes.result.korean.user.netinfo.orgName) { | |
| 391 | koreanUser = whoisRes.result.korean.user.netinfo.orgName; | |
| 392 | } | |
| 393 | if (SET.ipInfoDefaultOrg === 'KISAuser' && koreanUser !== null) { | |
| 394 | resObj.org = koreanUser; | |
| 395 | } else if (SET.ipInfoDefaultOrg === 'KISAISP' && koreanISP !== null) { | |
| 396 | resObj.org = koreanISP; | |
| 397 | } else if (SET.ipInfoDefaultOrg === 'KISAuserOrISP' && (koreanUser !== null || koreanISP !== null)) { | |
| 398 | resObj.org = koreanUser !== null ? koreanUser : koreanISP; | |
| 399 | } | |
| 400 | ipDictionary[ip] = resObj; | |
| 401 | cb(resObj); | |
| 402 | return; | |
| 403 | }); | |
| 404 | } else { | |
| 405 | ipDictionary[ip] = resObj; | |
| 406 | cb(resObj); | |
| 407 | } | |
| 408 | } else { | |
| 409 | cb(null); | |
| 410 | } | |
| 411 | } | |
| 412 | }); | |
| 413 | } | |
| 414 | ||
| 415 | var whoisDictionary = {}; | |
| 416 | ||
| 417 | function getIpWhois(ip, cb) { | |
| 418 | if (whoisDictionary[ip]) | |
| 419 | return cb(whoisDictionary[ip]); | |
| 420 | GM.xmlHttpRequest({ | |
| 421 | method: "GET", | |
| 422 | url: `http://namufix.wikimasonry.org/whois/ip/${ip}`, | |
| 423 | onload: function (res) { | |
| 424 | var resObj = JSON.parse(res.responseText); | |
| 425 | whoisDictionary[ip] = resObj; | |
| 426 | cb(resObj); | |
| 427 | } | |
| 428 | }); | |
| 429 | } | |
| 430 | ||
| 431 | function enterTimespanPopup(title, callback) { | |
| 432 | var win = TooSimplePopup(); | |
| 433 | win.title(title); | |
| 434 | win.content(function (winContainer) { | |
| 435 | var units = { | |
| 436 | second: 1, | |
| 437 | minute: 60, | |
| 438 | hour: 60 * 60, | |
| 439 | day: 60 * 60 * 24, | |
| 440 | week: 60 * 60 * 24 * 7, | |
| 441 | month: 60 * 60 * 24 * 7 * 4, // 4 주 | |
| 442 | year: 60 * 60 * 24 * 7 * 48 // 48주 | |
| 443 | } | |
| 444 | winContainer.innerHTML = '<style>.timespan-container input.timespan-input {width: 60px;}</style><div class="timespan-container">' + | |
| 445 | ' <input type="number" data-unit="year" class="timespan-input" value="0">년' + | |
| 446 | ' <input type="number" data-unit="month" class="timespan-input" value="0">개월' + | |
| 447 | ' <input type="number" data-unit="week" class="timespan-input" value="0">주' + | |
| 448 | ' <input type="number" data-unit="day" class="timespan-input" value="0">일' + | |
| 449 | ' <input type="number" data-unit="hour" class="timespan-input" value="0">시간' + | |
| 450 | ' <input type="number" data-unit="minute" class="timespan-input" value="0">분' + | |
| 451 | ' <input type="number" data-unit="second" class="timespan-input" value="0">초' + | |
| 452 | '</div>'; | |
| 453 | win.button('닫기', function () { | |
| 454 | win.close(); | |
| 455 | callback(null); | |
| 456 | }); | |
| 457 | win.button('입력', function () { | |
| 458 | var result = 0; | |
| 459 | var isNumberic = function (v) { | |
| 460 | return !isNaN(parseFloat(v)) && isFinite(v); | |
| 461 | } // https://stackoverflow.com/a/9716488 | |
| 462 | var timespanInputs = winContainer.querySelectorAll('input.timespan-input') | |
| 463 | for (var i = 0; i < timespanInputs.length; i++) { | |
| 464 | var timespanInput = timespanInputs[i]; | |
| 465 | if (isNumberic(timespanInput.value)) result += timespanInput.value * units[timespanInput.dataset.unit]; | |
| 466 | } | |
| 467 | win.close(); | |
| 468 | callback(result); | |
| 469 | }) | |
| 470 | }); | |
| 471 | } | |
| 472 | ||
| 473 | function whoisPopup(ip) { | |
| 474 | if (ip === null || ip === "") | |
| 475 | return alert('ip주소를 입력해주세요'); | |
| 476 | var win = TooSimplePopup(); | |
| 477 | win.title('IP WHOIS 조회'); | |
| 478 | win.button('다른 IP 조회', function () { | |
| 479 | var newip = prompt('IP 주소를 입력하세요.'); | |
| 480 | win.close(); | |
| 481 | whoisPopup(newip); | |
| 482 | }) | |
| 483 | win.button('닫기', win.close); | |
| 484 | win.content(function (container) { | |
| 485 | container.innerHTML = '조회중입니다. 잠시만 기다려주세요...'; | |
| 486 | getIpWhois(ip, function (result) { | |
| 487 | if (result.success && !result.raw) { | |
| 488 | var whoisObj = result.result; | |
| 489 | var currentView = 'table'; | |
| 490 | container.innerHTML = '<p>NamuFix 서버를 통해 KISA WHOIS를 조회했습니다. 결과는 다음과 같습니다.(<a href="#" class="switchViewType">JSON 형식으로 보기</a>)</p><div class="whois-content"><table></table></div>'; | |
| 491 | var resultContainer = container.querySelector('.whois-content'); | |
| 492 | var switchViewLink = container.querySelector('a.switchViewType'); | |
| 493 | ||
| 494 | function useTableView() { | |
| 495 | function writeSubInfoTable(subInfoObj, caption) { | |
| 496 | var table = document.createElement('table'); | |
| 497 | table.className = "whois"; | |
| 498 | table.innerHTML += '<caption>' + encodeHTMLComponent(caption) + '</caption>'; | |
| 499 | table.innerHTML += '<thead><tr><th>이름</th><th>내용</th></tr></thead>'; | |
| 500 | var tableHtml = '<tbody>'; | |
| 501 | var koreanContactNames = { | |
| 502 | techContact: '네트워크 담당자 정보', | |
| 503 | adminContact: 'IP주소 관리자 정보', | |
| 504 | abuseContact: '네트워크 abuse 담당자 정보' | |
| 505 | }; | |
| 506 | var koreanNetInfoNames = { | |
| 507 | // 주소범위(range), 프리픽스(prefix), 네트워크이름(servName), 기관명(orgname), 주소(addr), 우편번호(zipCode), 할당내역 등록일(regDate) 필드 포함 | |
| 508 | addr: '주소', | |
| 509 | netType: '네트워크 구분', | |
| 510 | orgName: '기관명', | |
| 511 | orgID: '기관 ID', | |
| 512 | prefix: 'IP prefix', | |
| 513 | range: 'IP 범위', | |
| 514 | regDate: '등록일', | |
| 515 | servName: '네트워크 이름', | |
| 516 | zipCode: '우편번호' | |
| 517 | }; | |
| 518 | for (var i in subInfoObj) { | |
| 519 | if (i == 'techContact' || i == 'adminContact' || i == 'abuseContact') { | |
| 520 | tableHtml += '<tr><td colspan="2" class="whois-subheader">' + koreanContactNames[i] + '</td></tr>'; | |
| 521 | if (subInfoObj[i].name) | |
| 522 | tableHtml += '<tr><td>이름</td><td>' + encodeHTMLComponent(subInfoObj[i].name) + '</td></tr>'; | |
| 523 | if (subInfoObj[i].phone) | |
| 524 | tableHtml += '<tr><td>전화번호</td><td>' + encodeHTMLComponent(subInfoObj[i].phone) + '</td></tr>'; | |
| 525 | if (subInfoObj[i].email) | |
| 526 | tableHtml += '<tr><td>연락처</td><td>' + encodeHTMLComponent(subInfoObj[i].email) + '</td></tr>'; | |
| 527 | } else if (i == 'netinfo') { | |
| 528 | // KISA 씨발놈들.... API 설명서에 다 적어두지... | |
| 529 | tableHtml += '<tr><td colspan="2" class="whois-subheader">네트워크 정보</td></tr>'; | |
| 530 | for (var na in subInfoObj[i]) { | |
| 531 | tableHtml += '<tr><td>' + (koreanNetInfoNames[na] ? koreanNetInfoNames[na] : na) + '</td><td>' + encodeHTMLComponent(subInfoObj[i][na]) + '</td></tr>'; | |
| 532 | } | |
| 533 | } | |
| 534 | } | |
| 535 | tableHtml += '</tbody>'; | |
| 536 | table.innerHTML += tableHtml; | |
| 537 | return table; | |
| 538 | } | |
| 539 | switchViewLink.innerText = '(진행중입니다)'; | |
| 540 | currentView = 'progress'; | |
| 541 | resultContainer.innerHTML = '<p>(진행중입니다.)</p>' | |
| 542 | ||
| 543 | function writeRest() { | |
| 544 | if (whoisObj.korean && whoisObj.korean.ISP) | |
| 545 | resultContainer.appendChild(writeSubInfoTable(whoisObj.korean.ISP, "IP 주소 보유기관 정보 (국문)")); | |
| 546 | if (whoisObj.korean && whoisObj.korean.user) | |
| 547 | resultContainer.appendChild(writeSubInfoTable(whoisObj.korean.user, "IP 주소 이용기관 정보 (국문)")); | |
| 548 | if (whoisObj.english && whoisObj.english.ISP) | |
| 549 | resultContainer.appendChild(writeSubInfoTable(whoisObj.english.ISP, "IP 주소 보유기관 정보 (영문)")); | |
| 550 | if (whoisObj.english && whoisObj.english.user) | |
| 551 | resultContainer.appendChild(writeSubInfoTable(whoisObj.english.user, "IP 주소 이용기관 정보 (영문)")); | |
| 552 | switchViewLink.innerText = 'JSON 형식으로 보기'; | |
| 553 | currentView = 'table'; | |
| 554 | } | |
| 555 | if (whoisObj.countryCode == 'none') { | |
| 556 | resultContainer.innerHTML = '<style>.whois-subheader {background: #9D75D9; color: white; text-align: center;} table.whois tr, table.whois td{border: 1px #9D75D9 solid; border-collapse: collapse;} table.whois td {padding: 5px;} table.whois caption { caption-side: top; }</style>' + | |
| 557 | '<table class="whois"><caption>쿼리 정보</caption>' + | |
| 558 | '<thead><th>이름</th><th>내용</th></tr></thead>' + | |
| 559 | '<tbody>' + | |
| 560 | '<tr><td>쿼리</td><td>' + whoisObj.query + '</td></tr>' + | |
| 561 | '<tr><td>쿼리 유형</td><td>' + whoisObj.queryType + '</td></tr>' + | |
| 562 | '<tr><td>레지스트리</td><td>' + whoisObj.registry + '</td></tr>' + | |
| 563 | '<tr><td>등록 국가</td><td><span class="icon ion-android-star"></span> 없음 (루프백같이 특수한 경우)</td></tr>' + | |
| 564 | '</tbody></table>'; | |
| 565 | writeRest(); | |
| 566 | } else { | |
| 567 | getFlagIcon(whoisObj.countryCode.toLowerCase(), function (flagIconData) { | |
| 568 | resultContainer.innerHTML = '<style>.whois-subheader {background: #9D75D9; color: white; text-align: center;} table.whois tr, table.whois td{border: 1px #9D75D9 solid; border-collapse: collapse;} table.whois td {padding: 5px;} table.whois caption { caption-side: top; }</style>' + | |
| 569 | '<table class="whois"><caption>쿼리 정보</caption>' + | |
| 570 | '<thead><th>이름</th><th>내용</th></tr></thead>' + | |
| 571 | '<tbody>' + | |
| 572 | '<tr><td>쿼리</td><td>' + whoisObj.query + '</td></tr>' + | |
| 573 | '<tr><td>쿼리 유형</td><td>' + whoisObj.queryType + '</td></tr>' + | |
| 574 | '<tr><td>레지스트리</td><td>' + whoisObj.registry + '</td></tr>' + | |
| 575 | '<tr><td>등록 국가</td><td><img style="height: 1em;" src="' + flagIconData + '">' + korCountryNames[whoisObj.countryCode.toUpperCase()] + '</td></tr>' + | |
| 576 | '</tbody></table>'; | |
| 577 | writeRest(); | |
| 578 | }); | |
| 579 | } | |
| 580 | } | |
| 581 | ||
| 582 | function useJsonView() { | |
| 583 | switchViewLink.innerText = '표 형식으로 보기'; | |
| 584 | currentView = 'json'; | |
| 585 | resultContainer.innerHTML = '<textarea readonly style="width: 50vw; height: 600px; max-height: 90vh;"></textarea>'; | |
| 586 | resultContainer.querySelector('textarea').value = JSON.stringify(whoisObj); | |
| 587 | } | |
| 588 | switchViewLink.addEventListener('click', function (evt) { | |
| 589 | evt.preventDefault(); | |
| 590 | if (currentView == 'json') { | |
| 591 | useTableView(); | |
| 592 | } else if (currentView == 'table') { | |
| 593 | useJsonView(); | |
| 594 | } else if (currentView == 'progress') { | |
| 595 | alert('잠시만 기다려주세요.'); | |
| 596 | } | |
| 597 | }) | |
| 598 | useTableView(); | |
| 599 | } else if (result.success && result.raw) { | |
| 600 | container.innerHTML = '<p>NamuFix 서버에서 다음과 같은 WHOIS 결과를 얻었습니다.</p><textarea readonly style="width: 50vw; height: 600px; max-height: 80vh;"></textarea>'; | |
| 601 | container.querySelector('textarea').value = result.result; | |
| 602 | } else if (result.error.namufix) { | |
| 603 | alert('NamuFix 서버측에서 오류가 발생했습니다.\n\n메세지 : ' + result.error.message + '\n오류 코드 : ' + result.error.code); | |
| 604 | win.close(); | |
| 605 | } else if (result.error.kisa) { | |
| 606 | container.innerHTML = '<p>KISA WHOIS 조회결과 다음과 같은 오류가 반환됐습니다.</p><p><em>' + result.error.message + '</em></p><p>오류 코드는 ' + result.error.code + '입니다.</p>'; | |
| 607 | } | |
| 608 | }); | |
| 609 | }); | |
| 610 | } | |
| 611 | ||
| 612 | // To bypass CSP | |
| 613 | var flagIconDictionary = {}; | |
| 614 | ||
| 615 | function getFlagIcon(countryCode, cb) { | |
| 616 | if (flagIconDictionary[countryCode]) | |
| 617 | return cb(flagIconDictionary[countryCode]); | |
| 618 | GM.xmlHttpRequest({ | |
| 619 | method: 'GET', | |
| 620 | url: `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.9.0/flags/4x3/${countryCode}.svg`, | |
| 621 | onload: function (res) { | |
| 622 | flagIconDictionary[countryCode] = "data:image/svg+xml;base64," + btoa(res.responseText); | |
| 623 | return cb(flagIconDictionary[countryCode]); | |
| 624 | } | |
| 625 | }); | |
| 626 | } | |
| 627 | ||
| 628 | // 문서/역사/편집 페이지 등에서 버튼 추가 함수 | |
| 629 | function addArticleButton(text, onclick) { | |
| 630 | var aTag = document.createElement("a"); | |
| 631 | aTag.className = "btn btn-secondary"; | |
| 632 | aTag.setAttribute("role", "button"); | |
| 633 | aTag.innerHTML = text; | |
| 634 | aTag.href = "#"; | |
| 635 | aTag.addEventListener('click', (evt) => { | |
| 636 | evt.preventDefault(); | |
| 637 | onclick(evt); | |
| 638 | }); | |
| 639 | var buttonGroup = document.querySelector('body.Liberty .liberty-content .content-tools .btn-group , body.senkawa .wiki-article-menu > div.btn-group'); | |
| 640 | buttonGroup.insertBefore(aTag, buttonGroup.firstChild); | |
| 641 | }; | |
| 642 | ||
| 643 | function uniqueID() { | |
| 644 | var dt = Date.now(); | |
| 645 | var url = location.href; | |
| 646 | var randomized = Math.floor(Math.random() * 48158964189489678525869410); | |
| 647 | return SHA512(String(dt).concat(dt, '\n', url, '\n', String(randomized))); | |
| 648 | } | |
| 649 | ||
| 650 | function listenPJAX(callback) { | |
| 651 | // create elements | |
| 652 | var pjaxButton = document.createElement("button"); | |
| 653 | var scriptElement = document.createElement("script"); | |
| 654 | ||
| 655 | // configure button | |
| 656 | pjaxButton.style.display = "none"; | |
| 657 | pjaxButton.id = "nfFuckingPJAX" | |
| 658 | pjaxButton.addEventListener("click", callback); | |
| 659 | ||
| 660 | // configure script | |
| 661 | scriptElement.setAttribute("type", "text/javascript"); | |
| 662 | scriptElement.innerHTML = '$(document).bind("pjax:end", function(){document.querySelector("button#nfFuckingPJAX").click();})'; | |
| 663 | ||
| 664 | // add elements | |
| 665 | document.body.appendChild(pjaxButton); | |
| 666 | document.head.appendChild(scriptElement); | |
| 667 | } | |
| 668 | ||
| 669 | var SET = new NFStorage(); | |
| 670 | ||
| 671 | async function INITSET() { // Storage INIT | |
| 672 | await SET.load(); | |
| 673 | if (nOu(SET.tempsaves)) | |
| 674 | SET.tempsaves = {}; | |
| 675 | if (nOu(SET.recentlyUsedTemplates)) | |
| 676 | SET.recentlyUsedTemplates = []; | |
| 677 | if (nOu(SET.imgurDeletionLinks)) | |
| 678 | SET.imgurDeletionLinks = []; | |
| 679 | if (nOu(SET.discussIdenti)) | |
| 680 | SET.discussIdenti = 'icon'; // icon, headBg, none | |
| 681 | if (nOu(SET.discussIdentiLightness)) | |
| 682 | SET.discussIdentiLightness = 0.7; | |
| 683 | if (nOu(SET.discussIdentiSaturation)) | |
| 684 | SET.discussIdentiSaturation = 0.5; | |
| 685 | if (nOu(SET.favorites)) | |
| 686 | SET.favorites = []; | |
| 687 | if (nOu(SET.customIdenticons)) | |
| 688 | SET.customIdenticons = {}; | |
| 689 | if (nOu(SET.hideDeletedWhenDiscussing)) | |
| 690 | SET.hideDeletedWhenDiscussing = 0; | |
| 691 | else if (typeof SET.hideDeletedWhenDiscussing !== "Number") | |
| 692 | SET.hideDeletedWhenDiscussing = Number(SET.hideDeletedWhenDiscussing); | |
| 693 | if (nOu(SET.discussAnchorPreviewType)) | |
| 694 | SET.discussAnchorPreviewType = 1; // 0 : None, 1 : mouseover, 2 : quote | |
| 695 | else | |
| 696 | SET.discussAnchorPreviewType = Number(SET.discussAnchorPreviewType); | |
| 697 | if (nOu(SET.removeNFQuotesInAnchorPreview)) | |
| 698 | SET.removeNFQuotesInAnchorPreview = false; | |
| 699 | if (nOu(SET.lookupIPonDiscuss)) | |
| 700 | SET.lookupIPonDiscuss = true; | |
| 701 | if (nOu(SET.ignoreNonSenkawaWarning)) | |
| 702 | SET.ignoreNonSenkawaWarning = false; | |
| 703 | if (nOu(SET.loadUnvisibleReses)) | |
| 704 | SET.loadUnvisibleReses = false; | |
| 705 | if (nOu(SET.ipInfoDefaultOrg)) | |
| 706 | SET.ipInfoDefaultOrg = "ipinfo.io"; //ipinfo.io, KISAISP, KISAuser, KISAuserOrISP | |
| 707 | if (nOu(SET.addAdminLinksForLiberty)) | |
| 708 | SET.addAdminLinksForLiberty = false; | |
| 709 | if (nOu(SET.autoTempsaveSpan)) | |
| 710 | SET.autoTempsaveSpan = 1000 * 60 * 5; // 5분 | |
| 711 | if (nOu(SET.addBatchBlockMenu)) | |
| 712 | SET.addBatchBlockMenu = false; | |
| 713 | if (nOu(SET.noLocaltimeOnNamuBoard)) | |
| 714 | SET.noLocaltimeOnNamuBoard = true; | |
| 715 | if (nOu(SET.fileUploadReqLimit)) | |
| 716 | SET.fileUploadReqLimit = 3; | |
| 717 | if (nOu(SET.adminReqLimit)) | |
| 718 | SET.adminReqLimit = 3; | |
| 719 | if (nOu(SET.quickBlockReasonTemplate_discuss)) | |
| 720 | SET.quickBlockReasonTemplate_discuss = '긴급조치 https://${host}/thread/${threadNo} #${messageNo}' // ${host}, ${threadNo}, ${messageNo} | |
| 721 | if (nOu(SET.quickBlockReasonTemplate_history)) | |
| 722 | SET.quickBlockReasonTemplate_history = '긴급조치 [[${docName}]] ${revisionNo}' // ${host}, ${docName}, ${revisionNo} | |
| 723 | if (nOu(SET.quickBlockDefaultDuration)) | |
| 724 | SET.quickBlockDefaultDuration = 0; | |
| 725 | if (nOu(SET.addQuickBlockLink)) | |
| 726 | SET.addQuickBlockLink = false; | |
| 727 | if (nOu(SET.notifyForUnvisibleThreads)) | |
| 728 | SET.notifyForUnvisibleThreads = false; | |
| 729 | if (nOu(SET.checkWhoisNetTypeOnDiscuss)) | |
| 730 | SET.checkWhoisNetTypeOnDiscuss = false; | |
| 731 | if (nOu(SET.checkedServerNotices)) | |
| 732 | SET.checkedServerNotices = []; | |
| 733 | if (nOu(SET.additionalScript)) | |
| 734 | SET.additionalScript = ""; | |
| 735 | if (nOu(SET.umiCookie)) | |
| 736 | SET.umiCookie = ""; | |
| 737 | if (nOu(SET.unprefixedFilename)) | |
| 738 | SET.unprefixedFilename= false; | |
| 739 | if (nOu(SET.addSnsShareButton)) | |
| 740 | SET.addSnsShareButton = false; | |
| 741 | if (nOu(SET.commentMacros)) | |
| 742 | SET.commentMacros = ''; | |
| 743 | if (nOu(SET.ipBlockHistoryCheckDelay)) | |
| 744 | SET.ipBlockHistoryCheckDelay = 500; | |
| 745 | if (nOu(SET.identiconLibrary)) | |
| 746 | SET.identiconLibrary = 'jdenticon'; // jdenticon, identicon, gravatar, gravatar-monster | |
| 747 | if (nOu(SET.emphasizeResesWhenMouseover)) | |
| 748 | SET.emphasizeResesWhenMouseover = false; | |
| 749 | await SET.save(); | |
| 750 | } | |
| 751 | ||
| 752 | var nfMenuDivider = document.createElement("div"); | |
| 753 | if (document.querySelectorAll('.dropdown-divider').length == 0) { | |
| 754 | (function () { | |
| 755 | nfMenuDivider.className = "dropdown-divider"; | |
| 756 | document.querySelector('nav.navbar ul.nav li.nav-item.dropdown.user-menu-parent .dropdown-menu').appendChild(nfMenuDivider); | |
| 757 | document.querySelector('nav.navbar ul.nav li.nav-item.dropdown.user-menu-parent .dropdown-menu').appendChild(document.createElement("div")); | |
| 758 | })(); | |
| 759 | } else { | |
| 760 | (function () { | |
| 761 | nfMenuDivider.className = "dropdown-divider"; | |
| 762 | var secondDivider = document.querySelectorAll('.user-menu-parent .dropdown-divider')[1]; | |
| 763 | secondDivider.parentNode.insertBefore(nfMenuDivider, secondDivider); | |
| 764 | })(); | |
| 765 | } | |
| 766 | ||
| 767 | function addItemToMemberMenu(text, onclick) { | |
| 768 | var menuItem = document.createElement("a"); | |
| 769 | menuItem.className = "dropdown-item"; | |
| 770 | menuItem.href = "#NothingToLink"; | |
| 771 | menuItem.innerHTML = text; | |
| 772 | menuItem.addEventListener('click', onclick); | |
| 773 | nfMenuDivider.parentNode.insertBefore(menuItem, nfMenuDivider.nextSibling); | |
| 774 | } | |
| 775 | ||
| 776 | let vpngateCache = [], | |
| 777 | vpngateCrawlledAt = -1; | |
| 778 | ||
| 779 | function getVPNGateIPList() { | |
| 780 | return new Promise((resolve, reject) => { | |
| 781 | GM.xmlHttpRequest({ | |
| 782 | method: "GET", | |
| 783 | url: "https://namufix.wikimasonry.org/vpngate/list", | |
| 784 | onload: function (res) { | |
| 785 | let resObj = JSON.parse(res.responseText); | |
| 786 | if (resObj.success) | |
| 787 | resolve(resObj.result); | |
| 788 | else | |
| 789 | reject(resObj.message); | |
| 790 | } | |
| 791 | }); | |
| 792 | }); | |
| 793 | } | |
| 794 | ||
| 795 | async function checkVPNGateIP(ip) { | |
| 796 | return new Promise((resolve, reject) => { | |
| 797 | GM.xmlHttpRequest({ | |
| 798 | method: "GET", | |
| 799 | url: "https://namufix.wikimasonry.org/vpngate/check/" + encodeURIComponent(ip), | |
| 800 | onload: function (res) { | |
| 801 | let resObj = JSON.parse(res.responseText); | |
| 802 | if (resObj.success) | |
| 803 | resolve(resObj.result); | |
| 804 | else | |
| 805 | reject(resObj.message); | |
| 806 | } | |
| 807 | }); | |
| 808 | }); | |
| 809 | } | |
| 810 | ||
| 811 | function makeTabs() { | |
| 812 | var div = document.createElement("div"); | |
| 813 | div.className = "nf-tabs"; | |
| 814 | div.innerHTML = "<ul></ul>"; | |
| 815 | var ul = div.querySelector("ul"); | |
| 816 | return { | |
| 817 | tab: function (text) { | |
| 818 | var item = document.createElement("li"); | |
| 819 | item.innerHTML = text; | |
| 820 | item.addEventListener('click', function () { | |
| 821 | var selectedTabs = div.querySelectorAll('li.selected'); | |
| 822 | for (var i = 0; i < selectedTabs.length; i++) { | |
| 823 | selectedTabs[i].className = selectedTabs[i].className.replace(/selected/mg, ''); | |
| 824 | } | |
| 825 | item.className = "selected"; | |
| 826 | }); | |
| 827 | ul.appendChild(item); | |
| 828 | return { | |
| 829 | click: function (callback) { | |
| 830 | item.addEventListener('click', callback); | |
| 831 | return this; | |
| 832 | }, | |
| 833 | selected: function () { | |
| 834 | if (item.className.indexOf('selected') == -1) item.className += ' selected'; | |
| 835 | return this; | |
| 836 | } | |
| 837 | }; | |
| 838 | }, | |
| 839 | get: function () { | |
| 840 | return div; | |
| 841 | } | |
| 842 | }; | |
| 843 | } | |
| 844 | ||
| 845 | // i : {author : {name, isIP}, defaultReason, defaultDuration} | |
| 846 | function quickBlockPopup(i) { | |
| 847 | let win = TooSimplePopup(); | |
| 848 | win.title("빠른 차단"); | |
| 849 | win.content(el => { | |
| 850 | el.innerHTML = `<p>차단 대상 : ${encodeHTMLComponent(i.author.name)} ${i.author.isIP ? "<i>(IP)</i>" : "<i>(계정)</i>"}</p>차단 사유 : <input type="text" id="reason"></input><br>차단 기간 : <input type="number" id="duration"></input><a href="#" id="enterDuration">(간편하게 입력)</a><br><div id="allowLoginDiv">로그인 허용 : <input type="checkbox" id="allowLogin"></input></div><br><i>(참고 : NamuFix 설정에서 차단기간 기본값을 변경할 수 있습니다.)` | |
| 851 | if (!i.author.isIP) { | |
| 852 | el.querySelector('#allowLoginDiv').style.display = 'none'; | |
| 853 | } | |
| 854 | el.querySelector('a#enterDuration').addEventListener('click', (evt) => { | |
| 855 | evt.preventDefault(); | |
| 856 | enterTimespanPopup("차단기간 간편입력", (span) => { | |
| 857 | if (span) | |
| 858 | el.querySelector('#duration').value = span; | |
| 859 | }) | |
| 860 | }) | |
| 861 | el.querySelector('#duration').value = i.defaultDuration; | |
| 862 | el.querySelector('#reason').value = i.defaultReason; | |
| 863 | el.querySelector('#reason').style.width = '500px'; | |
| 864 | el.querySelector('#reason').style.maxWidth = '30vw'; | |
| 865 | win.button('취소', win.close); | |
| 866 | win.button('확인', () => { | |
| 867 | if (i.author.isIP) { | |
| 868 | namuapi.blockIP({ | |
| 869 | ip: i.author.name, | |
| 870 | note: el.querySelector('#reason').value, | |
| 871 | expire: el.querySelector('#duration').value, | |
| 872 | allowLogin: el.querySelector('#allowLogin').checked | |
| 873 | }, (err, data) => { | |
| 874 | if (err) | |
| 875 | alert('오류 발생 : ' + err); | |
| 876 | else | |
| 877 | win.close(); | |
| 878 | }) | |
| 879 | } else { | |
| 880 | namuapi.blockAccount({ | |
| 881 | id: i.author.name, | |
| 882 | note: el.querySelector('#reason').value, | |
| 883 | expire: el.querySelector('#duration').value, | |
| 884 | allowLogin: el.querySelector('#allowLogin').checked | |
| 885 | }, (err, data) => { | |
| 886 | if (err) | |
| 887 | alert('오류 발생 : ' + err); | |
| 888 | else | |
| 889 | win.close(); | |
| 890 | }) | |
| 891 | } | |
| 892 | }); | |
| 893 | }); | |
| 894 | } | |
| 895 | ||
| 896 | function createDesigner(buttonBar) { | |
| 897 | var Designer = {}; | |
| 898 | Designer.button = function (txt) { | |
| 899 | var btn = document.createElement('button'); | |
| 900 | btn.className = 'NamaEditor NEMenuButton'; | |
| 901 | btn.setAttribute('type', 'button'); | |
| 902 | btn.innerHTML = txt; | |
| 903 | ||
| 904 | buttonBar.appendChild(btn); | |
| 905 | var r = { | |
| 906 | click: function (func) { | |
| 907 | btn.addEventListener('click', func); | |
| 908 | return r; | |
| 909 | }, | |
| 910 | hoverMessage: function (msg) { | |
| 911 | btn.setAttribute('title', msg); | |
| 912 | return r; | |
| 913 | }, | |
| 914 | right: function () { | |
| 915 | btn.className += ' NEright'; | |
| 916 | return r; | |
| 917 | }, | |
| 918 | active: function () { | |
| 919 | btn.setAttribute('active', 'yes'); | |
| 920 | return r; | |
| 921 | }, | |
| 922 | deactive: function () { | |
| 923 | btn.removeAttribute('active') | |
| 924 | return r; | |
| 925 | }, | |
| 926 | remove: function () { | |
| 927 | btn.parentNode.removeChild(btn); | |
| 928 | return r; | |
| 929 | }, | |
| 930 | use: function () { | |
| 931 | buttonBar.appendChild(btn); | |
| 932 | return r; | |
| 933 | } | |
| 934 | }; | |
| 935 | return r; | |
| 936 | }; | |
| 937 | Designer.dropdown = function (txt) { | |
| 938 | var dropdownButton = document.createElement("div"); | |
| 939 | var dropdown = document.createElement("div"); | |
| 940 | var dropdownList = document.createElement("ul"); | |
| 941 | dropdownButton.innerHTML = '<div class="NEDropdownButtonLabel NamaEditor">' + txt + '</div>'; | |
| 942 | dropdownButton.className = 'NamaEditor NEMenuButton'; | |
| 943 | dropdown.className = 'NamaEditor NEDropDown'; | |
| 944 | dropdown.appendChild(dropdownList); | |
| 945 | dropdownButton.appendChild(dropdown); | |
| 946 | buttonBar.appendChild(dropdownButton); | |
| 947 | ||
| 948 | var dbHover = false, | |
| 949 | dbBHover = false; | |
| 950 | dropdown.style.display = 'none'; | |
| 951 | dropdownButton.addEventListener('click', function () { | |
| 952 | var dropdowns = buttonBar.querySelectorAll(".NamaEditor.NEMenuButton > .NamaEditor.NEDropDown"); | |
| 953 | for (var i = 0; i < dropdowns.length; i++) { | |
| 954 | if (dropdowns[i] != dropdown) { | |
| 955 | dropdowns[i].style.display = 'none'; | |
| 956 | dropdowns[i].parentNode.removeAttribute("hover"); | |
| 957 | } else if (dropdown.style.display.trim() == 'none') { | |
| 958 | dropdown.style.display = ''; | |
| 959 | dropdownButton.setAttribute("hover", "yes"); | |
| 960 | } else { | |
| 961 | dropdown.style.display = 'none'; | |
| 962 | dropdownButton.removeAttribute("hover"); | |
| 963 | } | |
| 964 | } | |
| 965 | }); | |
| 966 | ||
| 967 | var hr = { | |
| 968 | button: function (iconTxt, txt) { | |
| 969 | var liTag = document.createElement('li'); | |
| 970 | liTag.innerHTML = '<span class="NEHeadIcon">' + iconTxt + '</span><span class="NEDescText">' + txt + '</span>' | |
| 971 | liTag.addEventListener('click', function () { | |
| 972 | dropdown.style.display = ''; | |
| 973 | }) | |
| 974 | dropdownList.appendChild(liTag); | |
| 975 | var r = { | |
| 976 | icon: function (iconTxt) { | |
| 977 | liTag.querySelector('.NEHeadIcon').innerHTML = iconTxt; | |
| 978 | return r; | |
| 979 | }, | |
| 980 | text: function (txt) { | |
| 981 | liTag.querySElector('.NEDescText').innerHTML = txt; | |
| 982 | return r; | |
| 983 | }, | |
| 984 | hoverMessage: function (msg) { | |
| 985 | liTag.setAttribute('title', msg); | |
| 986 | return r; | |
| 987 | }, | |
| 988 | click: function (handler) { | |
| 989 | liTag.addEventListener('click', handler); | |
| 990 | return r; | |
| 991 | }, | |
| 992 | right: function () { | |
| 993 | liTag.className += 'NEright'; | |
| 994 | return r; | |
| 995 | }, | |
| 996 | remove: function () { | |
| 997 | dropdownList.removeChild(liTag); | |
| 998 | return r; | |
| 999 | }, | |
| 1000 | insert: function () { | |
| 1001 | dropdownList.appendChild(liTag); | |
| 1002 | return r; | |
| 1003 | }, | |
| 1004 | backwalk: function () { | |
| 1005 | dropdownList.removeChild(ilTag); | |
| 1006 | dropdownList.appendChild(ilTag); | |
| 1007 | return r; | |
| 1008 | } | |
| 1009 | }; | |
| 1010 | return r; | |
| 1011 | }, | |
| 1012 | right: function () { | |
| 1013 | liTag.className += 'NEright'; | |
| 1014 | return hr; | |
| 1015 | }, | |
| 1016 | hoverMessage: function (txt) { | |
| 1017 | dropdownButton.setAttribute('title', txt); | |
| 1018 | return hr; | |
| 1019 | }, | |
| 1020 | clear: function () { | |
| 1021 | dropdownList.innerHTML = ''; | |
| 1022 | return hr; | |
| 1023 | } | |
| 1024 | }; | |
| 1025 | return hr; | |
| 1026 | }; | |
| 1027 | return Designer; | |
| 1028 | } | |
| 1029 | ||
| 1030 | function createTextProcessor(txtarea) { | |
| 1031 | var r = {}; | |
| 1032 | r.value = function () { | |
| 1033 | if (arguments.length == 0) return txtarea.value; | |
| 1034 | else txtarea.value = arguments[0]; | |
| 1035 | }; | |
| 1036 | r.selectionText = function () { | |
| 1037 | if (arguments.length == 0) return txtarea.value.substring(txtarea.selectionStart, txtarea.selectionEnd); | |
| 1038 | else { | |
| 1039 | var s = txtarea.selectionStart; | |
| 1040 | var t = txtarea.value.substring(0, txtarea.selectionStart); | |
| 1041 | t += arguments[0]; | |
| 1042 | t += txtarea.value.substring(txtarea.selectionEnd); | |
| 1043 | txtarea.value = t; | |
| 1044 | txtarea.focus(); | |
| 1045 | txtarea.selectionStart = s; | |
| 1046 | txtarea.selectionEnd = s + arguments[0].length; | |
| 1047 | } | |
| 1048 | }; | |
| 1049 | r.selectionStart = function () { | |
| 1050 | if (arguments.length == 0) return txtarea.selectionStart; | |
| 1051 | else txtarea.selectionStart = arguments[0]; | |
| 1052 | }; | |
| 1053 | r.selectionTest = function (r) { | |
| 1054 | return this.selectionText().search(r) != -1; | |
| 1055 | }; | |
| 1056 | r.valueTest = function (r) { | |
| 1057 | return this.value().search(r) != -1; | |
| 1058 | }; | |
| 1059 | r.selectionEnd = function () { | |
| 1060 | if (arguments.length == 0) return txtarea.selectionEnd; | |
| 1061 | else txtarea.selectionEnd = arguments[0]; | |
| 1062 | }; | |
| 1063 | r.selectionLength = function () { | |
| 1064 | if (arguments.length == 0) return (txtarea.selectionEnd - txtarea.selectionStart); | |
| 1065 | else txtarea.selectionEnd = txtarea.selectionStart + arguments[0]; | |
| 1066 | }; | |
| 1067 | r.select = function (s, e) { | |
| 1068 | txtarea.focus(); | |
| 1069 | txtarea.selectionStart = s; | |
| 1070 | if (typeof e !== 'undefined') txtarea.selectionEnd = e; | |
| 1071 | } | |
| 1072 | r.WrapSelection = function (l, r) { | |
| 1073 | if (arguments.length == 1) var r = l; | |
| 1074 | var t = this.selectionText(); | |
| 1075 | if (typeof t === 'undefined' || t == null || t == '') t = '내용'; | |
| 1076 | var s = this.selectionStart() | |
| 1077 | t = l + t + r; | |
| 1078 | this.selectionText(t); | |
| 1079 | this.select(s + l.length, s + t.length - r.length) | |
| 1080 | }; | |
| 1081 | r.ToggleWrapSelection = function (l, r) { | |
| 1082 | function isWrapped(t) { | |
| 1083 | return t.indexOf(l) == 0 && t.lastIndexOf(r) == (t.length - r.length); | |
| 1084 | } | |
| 1085 | if (arguments.length == 1) var r = l; | |
| 1086 | var t = this.selectionText(); | |
| 1087 | var t_m = this.value().substring(this.selectionStart() - l.length, this.selectionEnd() + r.length); | |
| 1088 | var wrappedInSelection = isWrapped(t); | |
| 1089 | var wrappedOutOfSelection = isWrapped(t_m); | |
| 1090 | if (wrappedInSelection) { | |
| 1091 | var s = this.selectionStart(); | |
| 1092 | this.selectionText(t.substring(l.length, t.length - r.length)); | |
| 1093 | this.select(s, s + t.length - l.length - r.length); | |
| 1094 | } else if (wrappedOutOfSelection) { | |
| 1095 | var s = this.selectionStart() - l.length; | |
| 1096 | this.selectionStart(s); | |
| 1097 | this.selectionEnd(s + t_m.length); | |
| 1098 | this.selectionText(t_m.substring(l.length, t_m.length - r.length)); | |
| 1099 | this.select(s, s + t_m.length - l.length - r.length); | |
| 1100 | } else { | |
| 1101 | this.WrapSelection(l, r); | |
| 1102 | } | |
| 1103 | }; | |
| 1104 | return r; | |
| 1105 | } | |
| 1106 | ||
| 1107 | function getFile(callback, allowMultiple) { | |
| 1108 | if (typeof allowMultiple === "undefined") var allowMultiple = false; | |
| 1109 | var elm = document.createElement("input"); | |
| 1110 | elm.setAttribute("type", "file"); | |
| 1111 | if (allowMultiple) elm.setAttribute("multiple", "1"); | |
| 1112 | elm.style.visibility = "hidden"; | |
| 1113 | elm.setAttribute("accept", "image/*"); | |
| 1114 | document.body.appendChild(elm); | |
| 1115 | elm.addEventListener('change', function (evt) { | |
| 1116 | callback(evt.target.files, function () { | |
| 1117 | document.body.removeChild(elm); | |
| 1118 | }) | |
| 1119 | }); | |
| 1120 | elm.click(); | |
| 1121 | } | |
| 1122 | ||
| 1123 | await INITSET(); | |
| 1124 | console.log("[NamuFix] 설정 초기화 완료"); | |
| 1125 | if (SET.umiCookie.trim().length !== 0) { | |
| 1126 | document.cookie = 'umi=' + SET.umiCookie + ";path=/;domain=namu.wiki"; | |
| 1127 | console.log("[NamuFix] umi 쿠키 설정 완료"); | |
| 1128 | } | |
| 1129 | ||
| 1130 | async function mainFunc() { | |
| 1131 | // 환경 감지 | |
| 1132 | var ENV = {}; | |
| 1133 | ENV.IsSSL = /^https/.test(location.href); | |
| 1134 | ENV.IsEditing = location.pathname.toLowerCase().indexOf('/edit/') == 0; | |
| 1135 | ENV.Discussing = location.pathname.toLowerCase().indexOf('/thread/') == 0; | |
| 1136 | ENV.IsDocument = location.pathname.toLowerCase().indexOf('/w/') == 0; //&& document.querySelector('p.wiki-edit-date'); | |
| 1137 | ENV.IsSettings = location.pathname.toLowerCase().indexOf('/settings/') == 0; | |
| 1138 | ENV.IsHistory = location.pathname.toLowerCase().indexOf('/history/') == 0; | |
| 1139 | ENV.IsUserContribsPage = /^\/contribution\/(?:author|ip)\/.+\/(?:document|discuss)/.test(location.pathname); | |
| 1140 | ENV.IsUploadPage = location.pathname.toLowerCase().indexOf('/upload/') == 0; | |
| 1141 | ENV.IsDiff = location.pathname.toLowerCase().indexOf('/diff/') == 0; | |
| 1142 | ENV.IsLoggedIn = document.querySelectorAll('body.Liberty img.profile-img, img.user-img').length == 1; | |
| 1143 | ENV.IsSearch = location.pathname.indexOf('/search/') == 0; | |
| 1144 | ENV.IsEditingRequest = /^\/edit_request\/([0-9]+)\/edit/.test(location.pathname); | |
| 1145 | ENV.IsWritingRequest = /^\/new_edit_request\/.+/.test(location.pathname); | |
| 1146 | ENV.IsIPACL = /^\/admin\/ipacl/.test(location.pathname); | |
| 1147 | ENV.IsBoardIPACL = /^\/admin\/boardipacl/.test(location.pathname); | |
| 1148 | ENV.IsSuspendAccount = /^\/admin\/boardsuspendaccount/.test(location.pathname); | |
| 1149 | ENV.IsBoardSuspendAccount = /^\/admin\/suspend_account/.test(location.pathname); | |
| 1150 | ENV.IsBlockHistory = /^\/BlockHistory/.test(location.pathname); | |
| 1151 | ENV.IsRecentChanges = location.pathname.indexOf('/RecentChanges') == 0; | |
| 1152 | ENV.skinName = /(senkawa|Liberty|namuvector)/i.exec(document.body.className)[1].toLowerCase(); | |
| 1153 | if (location.pathname.indexOf('/edit_request') == 0) | |
| 1154 | ENV.EditRequestNo = /^\/edit_request\/([0-9]+)/.exec(location.pathname); | |
| 1155 | if (ENV.IsLoggedIn) { | |
| 1156 | ENV.UserName = document.querySelector('body.Liberty .navbar-login .login-menu .dropdown-menu .dropdown-item:first-child, div.user-info > div.user-info > div:first-child').textContent.trim(); | |
| 1157 | } | |
| 1158 | if (document.querySelector("input[name=section]")) | |
| 1159 | ENV.section = document.querySelector("input[name=section]").value; | |
| 1160 | if (document.querySelector("body.senkawa h1.title > a")) | |
| 1161 | ENV.docTitle = document.querySelector("body.senkawa h1.title > a").textContent; | |
| 1162 | else if (document.querySelector("body.senkawa h1.title")) | |
| 1163 | ENV.docTitle = document.querySelector("body.senkawa h1.title").textContent; | |
| 1164 | else if (ENV.skinName == "liberty" && ENV.IsDiscussing) | |
| 1165 | ENV.docTitle = /^(.+) \(토론\)/.exec(document.querySelector('.liberty-content .liberty-content-header .title h1').textContent.trim())[1] | |
| 1166 | else if (/^\/[a-zA-Z_]+\/(.+)/.test(location.pathname)) | |
| 1167 | ENV.docTitle = decodeURIComponent(/^\/[a-zA-Z_]+\/(.+)/.exec(location.pathname)[1]); | |
| 1168 | else | |
| 1169 | ENV.docTitle = document.querySelector('body.Liberty .liberty-content-header .title h1').textContent; | |
| 1170 | ENV.docTitle = ENV.docTitle.trim(); | |
| 1171 | if (ENV.Discussing) { | |
| 1172 | ENV.topicNo = /^\/thread\/([^#]+)/.exec(location.pathname)[1]; | |
| 1173 | ENV.topicTitle = document.querySelector('body.Liberty .wiki-article h2.wiki-heading:first-child , article > h2').innerHTML.trim(); | |
| 1174 | } | |
| 1175 | if (ENV.IsDiff) { | |
| 1176 | //ENV.docTitle = /diff\/(.+?)\?/.exec(location.href)[1]; | |
| 1177 | ENV.beforeRev = Number(/[\&\?]oldrev=([0-9]+)/.exec(location.href)[1]); | |
| 1178 | ENV.afterRev = Number(/[\&\?]rev=([0-9]+)/.exec(location.href)[1]); | |
| 1179 | } | |
| 1180 | if (ENV.IsSearch) { | |
| 1181 | ENV.SearchQuery = decodeURIComponent(location.pathname.substring(8)); | |
| 1182 | } | |
| 1183 | if (nOu(ENV.section)) | |
| 1184 | ENV.section = -2; | |
| 1185 | GM.xmlHttpRequest({ | |
| 1186 | method: "GET", | |
| 1187 | url: "https://wtfismyip.com/json", | |
| 1188 | onload: function (res) { | |
| 1189 | var ip = JSON.parse(res.responseText).YourFuckingIPAddress; | |
| 1190 | if (!ENV.IsLoggedIn) ENV.UserName = ip; | |
| 1191 | ENV.IPAddress = ip; | |
| 1192 | } | |
| 1193 | }); | |
| 1194 | ||
| 1195 | // 서버 공지사항 확인 | |
| 1196 | // 서버 점검안내 같은거 이걸로 띄울 계획 | |
| 1197 | GM.xmlHttpRequest({ | |
| 1198 | method: 'GET', | |
| 1199 | url: 'https://namufix.wikimasonry.org/notice?' + Date.now(), | |
| 1200 | onload: (res) => { | |
| 1201 | let obj; | |
| 1202 | try { | |
| 1203 | obj = JSON.parse(res.responseText); | |
| 1204 | } catch (err) { | |
| 1205 | console.error("[NamuFix] NamuFix 서버 관련 공지사항을 불러오는 중 오류가 발생했습니다."); | |
| 1206 | console.error(err); | |
| 1207 | } | |
| 1208 | // after < now < before | |
| 1209 | if (!obj.hasNotice) { | |
| 1210 | return; | |
| 1211 | } else if (obj.validAfter && obj.validAfter >= Date.now()) { | |
| 1212 | return; | |
| 1213 | } else if (obj.validBefore && obj.validBefore <= Date.now()) { | |
| 1214 | return; | |
| 1215 | } else if (obj.id && SET.checkedServerNotices.includes(obj.id)) { | |
| 1216 | return; | |
| 1217 | } | |
| 1218 | console.log("[NamuFix] 서버 공지사항 존재함."); | |
| 1219 | if (obj.id) { | |
| 1220 | SET.checkedServerNotices.push(obj.id); | |
| 1221 | SET.save(); | |
| 1222 | } | |
| 1223 | if (obj.content) { | |
| 1224 | let win = TooSimplePopup(); | |
| 1225 | win.title("서버 공지사항"); | |
| 1226 | win.content(e => e.innerHTML = obj.content); | |
| 1227 | win.button('닫기', win.close); | |
| 1228 | } | |
| 1229 | if (obj.hotfix) { | |
| 1230 | eval(obj.hotfix); | |
| 1231 | } | |
| 1232 | if (obj.easterEgg) { | |
| 1233 | eval(obj.easterEgg); | |
| 1234 | } | |
| 1235 | } | |
| 1236 | }); | |
| 1237 | eval(SET.additionalScript); | |
| 1238 | ||
| 1239 | if (ENV.IsEditing || ENV.Discussing || ENV.IsEditingRequest || ENV.IsWritingRequest) { | |
| 1240 | if (document.querySelector("textarea") !== null && !document.querySelector("textarea").hasAttribute("readonly")) { | |
| 1241 | var rootDiv = document.createElement("div"); | |
| 1242 | if (ENV.IsEditing || ENV.IsEditingRequest || ENV.IsWritingRequest) { | |
| 1243 | // 탭 추가 | |
| 1244 | var previewTab = document.createElement("div"); | |
| 1245 | var diffTab = document.createElement("div"); | |
| 1246 | var initalPreviewTabHTML = '<iframe id="nfPreviewFrame" name="nfPreviewFrame" style="width: 100%; height: 600px; display: block; border: 1px solid black;"></iframe>'; | |
| 1247 | document.querySelector('textarea').parentNode.insertBefore(previewTab, document.querySelector('textarea').nextSibling); | |
| 1248 | document.querySelector('textarea').parentNode.insertBefore(diffTab, document.querySelector('textarea').nextSibling); | |
| 1249 | ||
| 1250 | // 나무위키 자체 편집/미리보기 탭 제거 | |
| 1251 | document.querySelector('#editForm .nav.nav-tabs').setAttribute("style", "display:none;"); | |
| 1252 | ||
| 1253 | function hideAndShow(no) { | |
| 1254 | rootDiv.style.display = no == 0 ? '' : 'none'; | |
| 1255 | previewTab.style.display = no == 1 ? '' : 'none'; | |
| 1256 | diffTab.style.display = no == 2 ? '' : "none"; | |
| 1257 | } | |
| 1258 | hideAndShow(0); | |
| 1259 | var tabs = makeTabs(); | |
| 1260 | tabs.tab("편집").selected().click(function () { | |
| 1261 | hideAndShow(0); | |
| 1262 | }); | |
| 1263 | tabs.tab("미리보기").click(function () { | |
| 1264 | previewTab.innerHTML = initalPreviewTabHTML; | |
| 1265 | hideAndShow(1); | |
| 1266 | var form = document.querySelector('form#editForm'); | |
| 1267 | form.setAttribute("method", "POST"); | |
| 1268 | form.setAttribute("target", "nfPreviewFrame"); | |
| 1269 | form.setAttribute("action", "/preview/" + ENV.docTitle); | |
| 1270 | form.submit(); | |
| 1271 | }); | |
| 1272 | tabs.tab("비교").click(function () { | |
| 1273 | hideAndShow(2); | |
| 1274 | diffTab.innerHTML = '<span style="font-size: 15px;">처리중입니다...</span>'; | |
| 1275 | var editUrl = 'https://' + location.host + (ENV.IsWritingRequest ? '/new_edit_request/' : '/edit/').concat(ENV.docTitle, ENV.section != -2 ? '?section='.concat(ENV.section) : ''); | |
| 1276 | if (ENV.IsEditingRequest) | |
| 1277 | editUrl = location.href; // 귀찮음.... | |
| 1278 | namuapi.theseedRequest({ | |
| 1279 | url: editUrl, | |
| 1280 | method: "GET", | |
| 1281 | onload: function (res) { | |
| 1282 | var parser = new DOMParser(); | |
| 1283 | var doc = parser.parseFromString(res.responseText, "text/html"); | |
| 1284 | var latestBaseRev = doc.querySelector('input[name="baserev"]').value; | |
| 1285 | var token = doc.querySelector('input[name="token"]').value; | |
| 1286 | ||
| 1287 | //update edit token | |
| 1288 | document.querySelector('input[name="token"]').value = token; | |
| 1289 | ||
| 1290 | if (doc.querySelectorAll('textarea').length < 1) { | |
| 1291 | diffTab.innerHTML = '<span style="font-size: 15px; color:red;">오류가 발생했습니다.</span>'; | |
| 1292 | return; | |
| 1293 | } | |
| 1294 | var remoteWikitext = doc.querySelector('textarea').value; | |
| 1295 | var wikitext = document.querySelector("textarea.NamaEditor.NETextarea").value; | |
| 1296 | diffTab.innerHTML = '<div style="width: 100%;">' + | |
| 1297 | '<div style="padding: 0; width: 100%; margin: 0px; max-height: 600px; overflow: scroll;" id="diffResult">' + | |
| 1298 | '</div>' + | |
| 1299 | '</div>'; | |
| 1300 | var result = diffTab.querySelector('#diffResult'); | |
| 1301 | var base = difflib.stringAsLines(remoteWikitext); | |
| 1302 | var newtxt = difflib.stringAsLines(wikitext); | |
| 1303 | ||
| 1304 | // create a SequenceMatcher instance that diffs the two sets of lines | |
| 1305 | var sm = new difflib.SequenceMatcher(base, newtxt); | |
| 1306 | var opcodes = sm.get_opcodes(); | |
| 1307 | ||
| 1308 | while (result.firstChild) result.removeChild(result.firstChild); | |
| 1309 | result.appendChild(diffview.buildView({ | |
| 1310 | baseTextLines: base, | |
| 1311 | newTextLines: newtxt, | |
| 1312 | opcodes: opcodes, | |
| 1313 | // set the display titles for each resource | |
| 1314 | baseTextName: "리비전 r" + document.querySelector('input[name="baserev"]').value, | |
| 1315 | newTextName: "편집중", | |
| 1316 | contextSize: 3, | |
| 1317 | viewType: 2 | |
| 1318 | })); | |
| 1319 | } | |
| 1320 | }); | |
| 1321 | }); | |
| 1322 | document.querySelector("#editForm").insertBefore(tabs.get(), document.querySelector("#editForm").firstChild); | |
| 1323 | } | |
| 1324 | ||
| 1325 | // Init (Add Elements) | |
| 1326 | var buttonBar = document.createElement('div'); | |
| 1327 | var txtarea = document.createElement('textarea'); | |
| 1328 | buttonBar.className = 'NamaEditor NEMenu'; | |
| 1329 | txtarea.className = 'NamaEditor NETextarea' | |
| 1330 | txtarea.name = document.querySelector("textarea").name; | |
| 1331 | rootDiv.className += ' NamaEditor NERoot'; | |
| 1332 | rootDiv.appendChild(buttonBar); | |
| 1333 | rootDiv.appendChild(txtarea); | |
| 1334 | ||
| 1335 | // Functions To Design | |
| 1336 | var Designer = createDesigner(buttonBar); | |
| 1337 | ||
| 1338 | // Functions To Process | |
| 1339 | var TextProc = createTextProcessor(txtarea); | |
| 1340 | ||
| 1341 | // Some Basic MarkUp Functions | |
| 1342 | function FontSizeChanger(isIncrease) { | |
| 1343 | var pattern = /^{{{\+([1-5]) (.+?)}}}$/; | |
| 1344 | var t, s = TextProc.selectionStart(); | |
| 1345 | if (TextProc.selectionTest(pattern)) { | |
| 1346 | var t = TextProc.selectionText(); | |
| 1347 | var fontSize = t.replace(pattern, '$1'); | |
| 1348 | var innerText = t.replace(pattern, '$2'); | |
| 1349 | if (isIncrease) fontSize++; | |
| 1350 | else fontSize--; | |
| 1351 | ||
| 1352 | if (5 < fontSize) fontSize = 5; | |
| 1353 | if (fontSize < 1) fontSize = 1; | |
| 1354 | t = '{{{+' + fontSize + ' ' + innerText + '}}}'; | |
| 1355 | } else { | |
| 1356 | t = '{{{+1 ' + TextProc.selectionText() + '}}}'; | |
| 1357 | } | |
| 1358 | TextProc.selectionText(t); | |
| 1359 | TextProc.select(s, s + t.length); | |
| 1360 | } | |
| 1361 | ||
| 1362 | function NewHeadingMacro() { | |
| 1363 | var headingPattern = /^(=+) (.+?) (=+)$/; | |
| 1364 | ||
| 1365 | function repeatString(str, count) { | |
| 1366 | var r = ""; | |
| 1367 | for (var i = 0; i < count; i++) r += str; | |
| 1368 | return r; | |
| 1369 | } | |
| 1370 | if (TextProc.selectionTest(headingPattern)) { | |
| 1371 | var matches = headingPattern.exec(TextProc.selectionText()); | |
| 1372 | var headingLevel, headingLevel_Left = matches[1].length, | |
| 1373 | headingContent = matches[2], | |
| 1374 | headingLevel_Right = matches[3].length; | |
| 1375 | if (headingLevel_Left != headingLevel_Right) { | |
| 1376 | // normalize | |
| 1377 | TextProc.selectionText(repeatString("=", headingLevel_Left) + " " + headingContent + " " + repeatString("=", headingLevel_Left)); | |
| 1378 | return; | |
| 1379 | } else { | |
| 1380 | headingLevel = headingLevel_Left; | |
| 1381 | if (headingLevel < 6) headingLevel++; | |
| 1382 | else if (headingLevel == 6) headingLevel = 1; | |
| 1383 | else if (headingLevel > 6) headingLevel = 6; | |
| 1384 | TextProc.selectionText(repeatString("=", headingLevel) + " " + headingContent + " " + repeatString("=", headingLevel)); | |
| 1385 | return; | |
| 1386 | } | |
| 1387 | } else if (TextProc.selectionText().length == 0) { | |
| 1388 | TextProc.selectionText("\n== 제목 ==\n") | |
| 1389 | TextProc.selectionStart(TextProc.selectionStart() + 1); | |
| 1390 | TextProc.selectionEnd(TextProc.selectionEnd() - 1); | |
| 1391 | } else if (TextProc.selectionText().length != 0) { | |
| 1392 | TextProc.selectionText("\n== " + TextProc.selectionText().replace(/\n/mg, '[br]') + " ==\n") | |
| 1393 | TextProc.selectionStart(TextProc.selectionStart() + 1); | |
| 1394 | TextProc.selectionEnd(TextProc.selectionEnd() - 1); | |
| 1395 | } | |
| 1396 | } | |
| 1397 | ||
| 1398 | function TextColorChange() { | |
| 1399 | var colorMarkUpPattern = /^{{{(#[a-zA-Z0-9]+) (.*)}}}$/; | |
| 1400 | var color = '#000000', | |
| 1401 | text = ''; | |
| 1402 | if (TextProc.selectionTest(colorMarkUpPattern)) { | |
| 1403 | // 색상 마크업이 적용된 텍스트 | |
| 1404 | var matches = colorMarkUpPattern.exec(TextProc.selectionText()); | |
| 1405 | color = matches[1]; | |
| 1406 | text = matches[2]; | |
| 1407 | } else if (TextProc.selectionText().lengh == 0) { | |
| 1408 | // 선택된 텍스트 없음 | |
| 1409 | text = '내용'; | |
| 1410 | } else { | |
| 1411 | // 텍스트 선택됨 | |
| 1412 | text = TextProc.selectionText(); | |
| 1413 | } | |
| 1414 | var w = window.TooSimplePopup(); | |
| 1415 | var c = w.close; | |
| 1416 | w.title('색 지정').content(function (e) { | |
| 1417 | var pickerWrapper = document.createElement('div'); | |
| 1418 | var sliderWrapper = document.createElement('div'); | |
| 1419 | var picker = document.createElement('div'); | |
| 1420 | var slider = document.createElement('div'); | |
| 1421 | var pickerIndicator = document.createElement('div'); | |
| 1422 | var sliderIndicator = document.createElement('div'); | |
| 1423 | var colorPreview = document.createElement('div'); | |
| 1424 | pickerWrapper.appendChild(picker); | |
| 1425 | pickerWrapper.appendChild(pickerIndicator); | |
| 1426 | sliderWrapper.appendChild(slider); | |
| 1427 | sliderWrapper.appendChild(sliderIndicator); | |
| 1428 | ||
| 1429 | picker.className = "NamaEditor FlexiColorPicker Picker"; | |
| 1430 | pickerIndicator.className = "NamaEditor FlexiColorPicker PickerIndicator"; | |
| 1431 | slider.className = "NamaEditor FlexiColorPicker Slider"; | |
| 1432 | sliderIndicator.className = "NamaEditor FlexiColorPicker SliderIndicator"; | |
| 1433 | pickerWrapper.className = "NamaEditor FlexiColorPicker PickerWrapper"; | |
| 1434 | sliderWrapper.className = "NamaEditor FlexiColorPicker SliderWrapper"; | |
| 1435 | colorPreview.className = "NamaEditor FlexiColorPicker ColorPreview"; | |
| 1436 | ||
| 1437 | ColorPicker.fixIndicators( | |
| 1438 | sliderIndicator, | |
| 1439 | pickerIndicator | |
| 1440 | ); | |
| 1441 | ColorPicker(slider, picker, function (hex, hsv, rgb, pickerCo, sliderCo) { | |
| 1442 | ColorPicker.positionIndicators( | |
| 1443 | sliderIndicator, | |
| 1444 | pickerIndicator, | |
| 1445 | sliderCo, pickerCo | |
| 1446 | ); | |
| 1447 | color = hex; | |
| 1448 | var reversedColor = { | |
| 1449 | r: 255 - rgb.r, | |
| 1450 | g: 255 - rgb.g, | |
| 1451 | b: 255 - rgb.b | |
| 1452 | }; | |
| 1453 | colorPreview.style.background = `rgb(${reversedColor.r}, ${reversedColor.g}, ${reversedColor.b})`; | |
| 1454 | colorPreview.style.color = color; | |
| 1455 | colorPreview.innerText = color; | |
| 1456 | }).setHex(color); | |
| 1457 | ||
| 1458 | e.appendChild(pickerWrapper); | |
| 1459 | e.appendChild(sliderWrapper); | |
| 1460 | e.appendChild(colorPreview); | |
| 1461 | }).button('지정', function () { | |
| 1462 | TextProc.selectionText('{{{' + color + ' ' + text + '}}}'); | |
| 1463 | c(); | |
| 1464 | }).button('닫기', c); | |
| 1465 | } | |
| 1466 | ||
| 1467 | // Add Basic MarkUp Buttons | |
| 1468 | var decoDropdown = Designer.dropdown('<span class="ion-wand fa fa-magic"></span>').hoverMessage('텍스트 꾸미기'); | |
| 1469 | decoDropdown.button('<strong>A</strong>', '굵게').click(function () { | |
| 1470 | TextProc.ToggleWrapSelection("'''"); | |
| 1471 | }); | |
| 1472 | decoDropdown.button('<em>A</em>', '기울임꼴').click(function () { | |
| 1473 | TextProc.ToggleWrapSelection("''"); | |
| 1474 | }); | |
| 1475 | decoDropdown.button('<del>A</del>', '취소선').click(function () { | |
| 1476 | TextProc.ToggleWrapSelection("--"); | |
| 1477 | }); | |
| 1478 | decoDropdown.button('<span style="text-decoration: underline;">A</span>', '밑줄').click(function () { | |
| 1479 | TextProc.ToggleWrapSelection("__"); | |
| 1480 | }); | |
| 1481 | decoDropdown.button('<span style="color:red;">A</span>', '글씨색').click(TextColorChange); | |
| 1482 | decoDropdown.button('-', '글씨 작게').click(function () { | |
| 1483 | FontSizeChanger(false); | |
| 1484 | }); | |
| 1485 | decoDropdown.button('+', '글씨 크게').click(function () { | |
| 1486 | FontSizeChanger(true); | |
| 1487 | }); | |
| 1488 | ||
| 1489 | // Insertable Media Functions | |
| 1490 | function namuUpload(present_files, present_finisher) { | |
| 1491 | if (typeof present_files === 'undefined' || present_files.screenX) | |
| 1492 | var present_files = null; | |
| 1493 | if (typeof present_finisher === 'undefined') | |
| 1494 | var present_finisher = function () {}; | |
| 1495 | ||
| 1496 | function getCopyrightInfo(callback) { | |
| 1497 | var win = TooSimplePopup(); | |
| 1498 | var contelem; | |
| 1499 | win.title("저작권 정보"); | |
| 1500 | win.content(function (el) { | |
| 1501 | contelem = el; | |
| 1502 | el.innerHTML = '<label>출처</label><input type="text" class="cpinfo" data-name="출처"></input><br>' + | |
| 1503 | '<label>날짜</label><input type="text" class="cpinfo" data-name="날짜"></input><br>' + | |
| 1504 | '<label>저작자</label><input type="text" class="cpinfo" data-name="저작자"></input><br>' + | |
| 1505 | '<label>저작권</label><input type="text" class="cpinfo" data-name="저작권"></input><br>' + | |
| 1506 | '<label>기타</label><input type="text" class="cpinfo" data-name="기타"></input><br>' + | |
| 1507 | '<label>설명</label><input type="text" class="cpinfo" data-name="설명"></input>' + | |
| 1508 | '<p>라이선스, 분류는 구현하기 귀찮습니다. 라이선스는 저작권란에 알아서 써주시고 분류는 알아서 하세요.</p>' | |
| 1509 | }); | |
| 1510 | win.button("삽입", function () { | |
| 1511 | var result = "== 기본 정보 ==\n"; | |
| 1512 | var cpinfos = contelem.querySelectorAll(".cpinfo"); | |
| 1513 | for (var i = 0; i < cpinfos.length; i++) { | |
| 1514 | var cpinfo = cpinfos[i]; | |
| 1515 | result += "|| " + cpinfo.dataset.name + " || " + cpinfo.value + " ||\n"; | |
| 1516 | } | |
| 1517 | result += `\n\n== 기타 ==\n[[NamuFix]] ${GM.info.script.version} 버전을 이용하여 업로드된 이미지입니다.`; | |
| 1518 | callback(result); | |
| 1519 | }); | |
| 1520 | win.button("닫기", win.close); | |
| 1521 | } | |
| 1522 | getCopyrightInfo(function (docuText) { | |
| 1523 | function getFileCallback(files, finish) { | |
| 1524 | if (files.length == 0) { | |
| 1525 | alert('선택된 파일이 없습니다'); | |
| 1526 | return finish(); | |
| 1527 | } | |
| 1528 | ||
| 1529 | let uploadLimit = SET.fileUploadReqLimit || 3; | |
| 1530 | let win = TooSimplePopup(); | |
| 1531 | win.title('업로드 중...'); | |
| 1532 | win.content(el => el.innerHTML = `현재 ${uploadLimit}개의 이미지들을 동시에 업로드하고 있습니다. 설정에서 변경가능합니다.<br><ul><li>현재 진행중</li><li><ul id="inprog"></ul></li></ul>`); | |
| 1533 | async.mapLimit(files, uploadLimit, (file, callback) => { | |
| 1534 | if (!file) callback(null, null); | |
| 1535 | var fn = "파일:" + SHA256(String(Date.now()) + file.name).substring(0, 6) + "_" + file.name; | |
| 1536 | if (SET.unprefixedFilename) { | |
| 1537 | fn = "파일:" + file.name; | |
| 1538 | } | |
| 1539 | if (/\.[A-Z]+$/.test(fn)) { | |
| 1540 | var fnSplitted = fn.split('.'); | |
| 1541 | fnSplitted[fnSplitted.length - 1] = fnSplitted[fnSplitted.length - 1].toLowerCase(); | |
| 1542 | fn = fnSplitted.join('.'); | |
| 1543 | } else if (/\.jpeg$/i.test(fn)) { | |
| 1544 | return callback(null, { | |
| 1545 | success: false, | |
| 1546 | reason: '확장자가 jpeg인 경우 오류가 발생합니다. jpg로 변경해주세요.', | |
| 1547 | name: file.name | |
| 1548 | }); | |
| 1549 | } | |
| 1550 | var uploadImageParams = { | |
| 1551 | file: file, | |
| 1552 | fn: fn, | |
| 1553 | docuText: docuText, | |
| 1554 | log: "NamuFix " + GM.info.script.version + "버전으로 자동 업로드됨.", | |
| 1555 | identifier: (ENV.IsLoggedIn ? "m" : "i") + ":" + ENV.UserName | |
| 1556 | }; | |
| 1557 | let currentListItem = document.createElement("li"); | |
| 1558 | currentListItem.textContent = `${file.name} -> ${fn}`; | |
| 1559 | win.content(el => el.querySelector('ul#inprog').appendChild(currentListItem)); | |
| 1560 | namuapi.uploadImage(uploadImageParams, function (err, resultName) { | |
| 1561 | if (err === null) { | |
| 1562 | callback(null, { | |
| 1563 | success: true, | |
| 1564 | name: file.name, | |
| 1565 | docName: resultName | |
| 1566 | }); | |
| 1567 | } else if (err === "recaptcha_required") { | |
| 1568 | namuapi.resolveRecaptcha(function (res) { | |
| 1569 | if (res == null) { | |
| 1570 | alert('reCAPTCHA를 입력하지 않아 건너뜁니다.'); | |
| 1571 | callback(null, { | |
| 1572 | success: false, | |
| 1573 | reason: 'reCAPTCHA를 입력하지 않았습니다.', | |
| 1574 | name: file.name | |
| 1575 | }); | |
| 1576 | return; | |
| 1577 | } | |
| 1578 | uploadImageParams.recaptchaKey = res; | |
| 1579 | namuapi.uploadImage(uploadImageParams, (err, resultName) => { | |
| 1580 | if (err === null) { | |
| 1581 | callback(null, { | |
| 1582 | success: true, | |
| 1583 | name: file.name, | |
| 1584 | docName: resultName | |
| 1585 | }); | |
| 1586 | } else if (err === "recaptcha_required") { | |
| 1587 | callback(null, { | |
| 1588 | success: false, | |
| 1589 | reason: 'reCAPTCHA를 해결하였으나 다시 한번 더 요구받았습니다.', | |
| 1590 | name: file.name | |
| 1591 | }); | |
| 1592 | } else if (err === "html_error") { | |
| 1593 | callback(null, { | |
| 1594 | success: false, | |
| 1595 | reason: '예상하지 못한 오류입니다. 자세한 정보를 다운로드하여 NamuFix 이슈트래커에 제보해주세요.', | |
| 1596 | name: file.name, | |
| 1597 | data: resultName | |
| 1598 | }); | |
| 1599 | } else { | |
| 1600 | callback(null, { | |
| 1601 | success: false, | |
| 1602 | reason: '알 수 없는 오류입니다.', | |
| 1603 | name: file.name | |
| 1604 | }); | |
| 1605 | } | |
| 1606 | }); | |
| 1607 | }); | |
| 1608 | } else if (err === "html_error") { | |
| 1609 | callback(null, { | |
| 1610 | success: false, | |
| 1611 | reason: '예상하지 못한 오류입니다. 자세한 정보를 다운로드하여 NamuFix 이슈트래커에 제보해주세요.', | |
| 1612 | name: file.name, | |
| 1613 | data: resultName | |
| 1614 | }); | |
| 1615 | } else { | |
| 1616 | callback(null, { | |
| 1617 | success: false, | |
| 1618 | reason: '알 수 없는 오류입니다.', | |
| 1619 | name: file.name | |
| 1620 | }); | |
| 1621 | } | |
| 1622 | win.content(el => el.querySelector('ul#inprog').removeChild(currentListItem)); | |
| 1623 | }); | |
| 1624 | }, (err, results) => { | |
| 1625 | if (err) { | |
| 1626 | alert('오류가 발생했습니다.'); | |
| 1627 | console.error("[NamuFix] 이미지 업로드 중 오류가 발생했습니다."); | |
| 1628 | console.error(err); | |
| 1629 | return; | |
| 1630 | } | |
| 1631 | finish(); | |
| 1632 | win.close(); | |
| 1633 | results = results.filter(v => v !== null); | |
| 1634 | let hasError = false; | |
| 1635 | let imgLinks = ''; | |
| 1636 | for (i of results) { | |
| 1637 | if (i.success) { | |
| 1638 | imgLinks += '[[' + i.docName + ']]'; | |
| 1639 | } else { | |
| 1640 | hasError = true; | |
| 1641 | } | |
| 1642 | } | |
| 1643 | if (imgLinks.length != 0) | |
| 1644 | TextProc.selectionText(TextProc.selectionText() + imgLinks); | |
| 1645 | if (hasError) { | |
| 1646 | let win = TooSimplePopup(); | |
| 1647 | win.title('이미지 업로드 오류'); | |
| 1648 | win.content(el => { | |
| 1649 | el.innerHTML = '<p>오류들이 발생했습니다.</p><ul></ul>'; | |
| 1650 | for (let i of results.filter(v => !v.success)) { | |
| 1651 | let li = document.createElement("li"); | |
| 1652 | li.textContent = `${i.name} : ${i.reason}`; | |
| 1653 | if (i.data) | |
| 1654 | li.innerHTML += ` <a href="${URL.createObjectURL(new Blob(i.data, 'text/html'))}" download="log.html">(자세한 정보 다운로드)</a>` | |
| 1655 | el.querySelector('ul').appendChild(li); | |
| 1656 | } | |
| 1657 | }) | |
| 1658 | win.button('닫기', win.close); | |
| 1659 | } | |
| 1660 | }) | |
| 1661 | } | |
| 1662 | if (present_files != null) { | |
| 1663 | getFileCallback(present_files, present_finisher); | |
| 1664 | } else { | |
| 1665 | getFile(getFileCallback, true); | |
| 1666 | } | |
| 1667 | }) | |
| 1668 | } | |
| 1669 | ||
| 1670 | function InsertYouTube() { | |
| 1671 | var win = TooSimplePopup(); | |
| 1672 | win.title('YouTube 동영상 삽입'); | |
| 1673 | win.content(function (el) { | |
| 1674 | el.innerHTML = '<p style="background: cyan; box-shadow: 2px 2px 2px gray; color:white; padding: 8px; border-radius: 3px; margin-bottom: 5px;">YouTube 동영상을 검색하거나 동영상 주소를 입력하여 YouTube 동영상을 삽입할 수 있습니다.</p>' + | |
| 1675 | '<p><label for="vidUrl" style="width: 120px; display: inline-block;">YouTube 동영상 주소</label><input type="text" name="vidUrl" id="vidUrl" style="width:620px; max-width: 100vw;"></input><button id="insertUrl">삽입</button></p>' + | |
| 1676 | '<hr>' + | |
| 1677 | '<div>' + | |
| 1678 | '<label for="vidQuery" style="width: 120px; display: inline-block;">검색어</label><input type="text" name="vidQuery" id="vidQuery" style="width:620px; max-width: 100vw;"></input><button id="searchVids">검색</button>' + | |
| 1679 | '<div id="results" style="overflow-y: scroll; overflow-x: hidden; width: 820px; max-width: 100vw; height: 400px; max-height: calc(100vh - 300px);"><span style="color:red">검색 결과가 없습니다.</span></div>' + | |
| 1680 | '</div>'; | |
| 1681 | }) | |
| 1682 | var finish = function (vid) { | |
| 1683 | if (vid == null) { | |
| 1684 | alert('무언가 잘못된것 같습니다.'); | |
| 1685 | return; | |
| 1686 | } | |
| 1687 | TextProc.selectionText(TextProc.selectionText() + '[youtube(' + vid + ')]'); | |
| 1688 | win.close(); | |
| 1689 | } | |
| 1690 | // 주소로 삽입 기능 | |
| 1691 | win.content(function (el) { | |
| 1692 | var ExtractYouTubeID = function (url) { | |
| 1693 | // from Lasnv's answer from http://stackoverflow.com/questions/3452546/javascript-regex-how-to-get-youtube-video-id-from-url | |
| 1694 | var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/; | |
| 1695 | var match = url.match(regExp); | |
| 1696 | if (match && match[7].length == 11) | |
| 1697 | return match[7]; | |
| 1698 | else | |
| 1699 | return null; | |
| 1700 | } | |
| 1701 | var insertByUrlFunc = function () { | |
| 1702 | var url = el.querySelector('#vidUrl').value; | |
| 1703 | var vidId = ExtractYouTubeID(url); | |
| 1704 | finish(vidId); | |
| 1705 | } | |
| 1706 | el.querySelector('#insertUrl').addEventListener('click', insertByUrlFunc); | |
| 1707 | el.querySelector('#vidUrl').addEventListener('keyup', function (evt) { | |
| 1708 | if (evt.which == 13 || evt.keycode == 13) { | |
| 1709 | insertByUrlFunc(); | |
| 1710 | return false; | |
| 1711 | } | |
| 1712 | }) | |
| 1713 | }); | |
| 1714 | // 검색 기능 | |
| 1715 | win.content(function (el) { | |
| 1716 | // https://developers.google.com/youtube/v3/docs/search/list | |
| 1717 | var vidSearchFunc = function () { | |
| 1718 | var q = el.querySelector('#vidQuery').value; | |
| 1719 | var resultDiv = el.querySelector('#results'); | |
| 1720 | resultDiv.innerHTML = '<span style="color:orange;">검색중입니다.</span>' | |
| 1721 | GM.xmlHttpRequest({ | |
| 1722 | method: "GET", | |
| 1723 | url: 'https://namufix.wikimasonry.org/youtube/search?q=' + encodeURIComponent(q), | |
| 1724 | onload: function (res) { | |
| 1725 | var jobj = JSON.parse(res.responseText); | |
| 1726 | resultDiv.innerHTML = '<ul></ul>'; | |
| 1727 | var ul = resultDiv.querySelector('ul'); | |
| 1728 | if (res.status != 200 || !jobj.success) { | |
| 1729 | resultDiv.innerHTML = '<span style="color:red;">검색중 오류가 발생했습니다.</span>'; | |
| 1730 | return; | |
| 1731 | } | |
| 1732 | for (var i = 0; i < jobj.items.length; i++) { | |
| 1733 | var vidNow = jobj.items[i]; | |
| 1734 | var li = document.createElement("li"); | |
| 1735 | li.height = '90px'; | |
| 1736 | li.innerHTML = '<img style="height: 90px;" src="//namufix.wikimasonry.org/youtube/thumb/' + vidNow.id.videoId + '"></img>' + | |
| 1737 | '<div style="position: relative; display: inline-block; margin-left: 5px; overflow: hidden; width: 670px; max-width: 100vw;">' + | |
| 1738 | '<span style="font-weight: bold; font-size: 12pt; margin-bottom: 3px;">' + vidNow.snippet.title + '</span><button name="insertThis" class="moreFlat">삽입</button><button name="preview" class="moreFlat">미리보기</button><br><span style="font-size:10pt;">' + vidNow.snippet.description + '</span>' + | |
| 1739 | '</div>'; | |
| 1740 | li.querySelector('[name="preview"]').parentNode.dataset.videoId = vidNow.id.videoId; | |
| 1741 | li.querySelector('[name="preview"]').addEventListener('click', function (evt) { | |
| 1742 | var previewWin = TooSimplePopup(); | |
| 1743 | previewWin.title('미리보기'); | |
| 1744 | previewWin.content(function (el) { | |
| 1745 | var iframe = document.createElement("iframe"); | |
| 1746 | iframe.setAttribute("frameborder", "0"); | |
| 1747 | iframe.setAttribute("src", "//www.youtube.com/embed/" + evt.target.parentNode.dataset.videoId); | |
| 1748 | iframe.style.height = "360px"; | |
| 1749 | iframe.style.width = "640px"; | |
| 1750 | iframe.style.maxWidth = "100vw"; | |
| 1751 | iframe.style.maxHeight = "calc(100vh - 80px)" | |
| 1752 | el.appendChild(iframe); | |
| 1753 | }); | |
| 1754 | previewWin.button('닫기', previewWin.close); | |
| 1755 | }); | |
| 1756 | li.querySelector('[name="insertThis"]').addEventListener('click', function (evt) { | |
| 1757 | finish(evt.target.parentNode.dataset.videoId); | |
| 1758 | }) | |
| 1759 | ul.appendChild(li); | |
| 1760 | } | |
| 1761 | } | |
| 1762 | }); | |
| 1763 | }; | |
| 1764 | ||
| 1765 | el.querySelector('#searchVids').addEventListener('click', vidSearchFunc); | |
| 1766 | el.querySelector('#vidQuery').addEventListener('keyup', function (evt) { | |
| 1767 | if (evt.which == 13 || evt.keycode == 13) { | |
| 1768 | vidSearchFunc(); | |
| 1769 | return false; | |
| 1770 | } | |
| 1771 | }) | |
| 1772 | el.querySelector('#vidUrl').focus(); | |
| 1773 | }) | |
| 1774 | win.button('닫기', win.close); | |
| 1775 | } | |
| 1776 | ||
| 1777 | function MapMacro() { | |
| 1778 | // title(text), content(callback), foot(callback), button(text,onclick), close | |
| 1779 | var win = TooSimplePopup(); | |
| 1780 | win.title("지도 삽입"); | |
| 1781 | win.content(function (el) { | |
| 1782 | var mapDiv = document.createElement("div"); | |
| 1783 | mapDiv.id = "NFMapDiv"; | |
| 1784 | mapDiv.style.maxHeight = "calc(100vh - 100px)"; | |
| 1785 | mapDiv.style.maxWidth = "100vw"; | |
| 1786 | mapDiv.style.height = '480px'; | |
| 1787 | mapDiv.style.width = '640px'; | |
| 1788 | el.appendChild(mapDiv); | |
| 1789 | var initFuncContext = 'var NFMap;\n' + | |
| 1790 | 'function NFMapInit(){\n' + | |
| 1791 | 'var firstLocation=new google.maps.LatLng(37.46455,126.67435);\n' + | |
| 1792 | 'var mapOptions={\n' + | |
| 1793 | 'zoom: 8,\n' + | |
| 1794 | 'center: firstLocation\n,' + | |
| 1795 | 'streetViewControl: false\n' + | |
| 1796 | '};\n' + | |
| 1797 | 'NFMap=new google.maps.Map(document.querySelector("#NFMapDiv"),mapOptions);\n' + | |
| 1798 | '}'; | |
| 1799 | var onloadScript = document.createElement("script"); | |
| 1800 | onloadScript.innerHTML = initFuncContext; | |
| 1801 | el.appendChild(onloadScript); | |
| 1802 | setTimeout(function () { | |
| 1803 | var mapsAPILib = document.createElement("script"); | |
| 1804 | mapsAPILib.setAttribute("src", "//maps.googleapis.com/maps/api/js?key=AIzaSyAqi9PjUr_F54U0whrbMeavFfvNap3kjvA&callback=NFMapInit"); | |
| 1805 | el.appendChild(mapsAPILib); | |
| 1806 | }, 500); | |
| 1807 | }); | |
| 1808 | win.button("삽입", function () { | |
| 1809 | var lat = unsafeWindow.NFMap.getCenter().lat(); | |
| 1810 | var lng = unsafeWindow.NFMap.getCenter().lng(); | |
| 1811 | var zoom = unsafeWindow.NFMap.getZoom(); | |
| 1812 | TextProc.selectionText(TextProc.selectionText() + ' [Include(틀:지도,장소=' + lat + '%2C' + lng + ',zoom=' + zoom + ')] '); | |
| 1813 | win.close(); | |
| 1814 | }) | |
| 1815 | win.button("닫기", win.close); | |
| 1816 | } | |
| 1817 | ||
| 1818 | function DaumTVPotMarkUp() { | |
| 1819 | var vurl = prompt('참고 : 개발중인 기능이므로 이상하게 작동할 수 있습니다.\n\n1. 삽입하고픈 TV팟 동영상을 봅니다\n2. 공유 버튼을 누릅니다.\n3. 거기서 복사한 URL을 입력하십시오.'); | |
| 1820 | var pattern2 = /http:\/\/tvpot\.daum\.net\/v\/(.+?)/; | |
| 1821 | if (!pattern2.test(vurl)) { | |
| 1822 | alert('지원되지 않는 주소 형식입니다.') | |
| 1823 | } else { | |
| 1824 | TextProc.selectionText(TextProc.selectionText() + '{{{#!html <iframe src="//videofarm.daum.net/controller/video/viewer/Video.html?vid=' + vurl.replace(pattern2, '$1') + '&play_loc=undefined&alert=true" style="max-height: 100%; max-width:100%;" frameborder=\'0\' scrolling=\'0\' width=\'640px\' height=\'360px\'></iframe>}}}'); | |
| 1825 | } | |
| 1826 | }; | |
| 1827 | // Add Insertable Things | |
| 1828 | var insertablesDropDown = Designer.dropdown('<span class="ion-paperclip fa fa-paperclip"></span>').hoverMessage('삽입 가능한 미디어'); | |
| 1829 | insertablesDropDown.button('<span class="ion-social-youtube fa fa-youtube-play" style="color:red;"></span>', 'YouTube 동영상').click(InsertYouTube); | |
| 1830 | insertablesDropDown.button('<span class="ion-map fa fa-map"></span>', '지도').click(MapMacro); | |
| 1831 | insertablesDropDown.button('<span class="ion-ios-play-outline fa fa-play" style="color: Aqua;"></span>', '다음 TV팟 동영상').click(DaumTVPotMarkUp); | |
| 1832 | insertablesDropDown.button('<span class="ion-images fa fa-picture-o" style="color: #008275;"></span>', '이미지 업로드').click(namuUpload); | |
| 1833 | ||
| 1834 | Designer.button('<span class="ion-ios-grid-view fa fa-table"></span>').hoverMessage('간단한 표 만들기').click(function () { | |
| 1835 | var numbers = prompt('행과 열을 행숫x열숫 형태로 입력해주세요. 예시: 2x3, 2*3') | |
| 1836 | if (/([0-9]+).([0-9]+)/.test(numbers)) { | |
| 1837 | var num_matches = /([0-9]+).([0-9]+)/.exec(numbers) | |
| 1838 | numbers = [parseInt(num_matches[1]), parseInt(num_matches[2])]; | |
| 1839 | } else { | |
| 1840 | alert('입력이 올바르지 않습니다.'); | |
| 1841 | return; | |
| 1842 | } | |
| 1843 | var win = TooSimplePopup(); | |
| 1844 | win.title('간단한 표 만들기'); | |
| 1845 | win.content(function (container) { | |
| 1846 | // cell: align( left=(, center=:, right=) ), rowspan (^|0-9 |0-9 v|0-9), colspan bgcolor, width, height | |
| 1847 | // row: rowbgcolor | |
| 1848 | // table: align, bgcolor, bordercolor, width | |
| 1849 | container.innerHTML = '<strong>현재 실험중인 기능입니다. 불안정할 수 있습니다.</strong><br>표를 만듭니다.... 공대 감성을 듬뿍 담아 디자인했습니다.<br>칸 안에는 나무마크 위키텍스트를 입력하면 됩니다.<br>Ctrl + 화살표 단축키로 칸 사이를 이동할 수 있습니다.' + | |
| 1850 | '<style>#target-table td {border: 1px solid #dddddd; padding: 5px 10px} #target-table tr {background-color: #f5f5f5 border-collapse: collapse;}</style>' + | |
| 1851 | '<table id="target-table"></table>' + | |
| 1852 | '<div style="display: none;"><button id="disableShortcut" onclick="window.namu.disableShortcutKey=true;"></button><button id="enableShortcut" onclick="window.namu.disableShortcutKey=false;"></button></div>'; | |
| 1853 | var table = container.querySelector('table'); | |
| 1854 | document.querySelector('#disableShortcut').click(); | |
| 1855 | for (var i = 0; i < numbers[1]; i++) { | |
| 1856 | var row = document.createElement("tr"); | |
| 1857 | for (var j = 0; j < numbers[0]; j++) | |
| 1858 | row.innerHTML += '<td contenteditable="true"></td>'; | |
| 1859 | var cols = row.querySelectorAll('td'); | |
| 1860 | for (var j = 0; j < cols.length; j++) { | |
| 1861 | cols[j].addEventListener('keyup', function (evt) { | |
| 1862 | var doPreventDefault = true; | |
| 1863 | var cellOrder = [].indexOf.call(evt.target.parentNode.querySelectorAll('td'), evt.target); | |
| 1864 | if (evt.key == "ArrowDown" && evt.ctrlKey) { | |
| 1865 | var nearCols = evt.target.parentNode.nextElementSibling.querySelectorAll('td'); | |
| 1866 | nearCols[cellOrder < nearCols.length ? cellOrder : 0].focus(); | |
| 1867 | } else if (evt.key == "ArrowUp" && evt.ctrlKey) { | |
| 1868 | var nearCols = evt.target.parentNode.previousElementSibling.querySelectorAll('td'); | |
| 1869 | nearCols[cellOrder < nearCols.length ? cellOrder : 0].focus(); | |
| 1870 | } else if (evt.key == "ArrowRight" && evt.ctrlKey) { | |
| 1871 | evt.target.nextSibling.focus(); | |
| 1872 | } else if (evt.key == "ArrowLeft" && evt.ctrlKey) { | |
| 1873 | evt.target.previousSibling.focus(); | |
| 1874 | } else { | |
| 1875 | doPreventDefault = false; | |
| 1876 | } | |
| 1877 | if (doPreventDefault) | |
| 1878 | evt.preventDefault(); | |
| 1879 | }); | |
| 1880 | } | |
| 1881 | table.appendChild(row); | |
| 1882 | } | |
| 1883 | win.button('닫기', function () { | |
| 1884 | document.querySelector('#enableShortcut').click(); | |
| 1885 | win.close(); | |
| 1886 | }); | |
| 1887 | win.button('삽입', function () { | |
| 1888 | var rows = table.querySelectorAll('tr'); | |
| 1889 | var result = ''; | |
| 1890 | for (var i = 0; i < rows.length; i++) { | |
| 1891 | result += '||'; | |
| 1892 | var cols = rows[i].querySelectorAll('td'); | |
| 1893 | for (var j = 0; j < cols.length; j++) { | |
| 1894 | result += cols[j].innerHTML + '||'; | |
| 1895 | } | |
| 1896 | result += '\n'; | |
| 1897 | } | |
| 1898 | TextProc.selectionText(TextProc.selectionText() + '\n' + result); | |
| 1899 | document.querySelector('#enableShortcut').click(); | |
| 1900 | win.close(); | |
| 1901 | }); | |
| 1902 | }); | |
| 1903 | }) | |
| 1904 | ||
| 1905 | Designer.button('<span class="ion-checkmark-round fa fa-check"></span>').hoverMessage('맞춤법 검사기').click(function () { | |
| 1906 | var win = TooSimplePopup(); | |
| 1907 | win.title('나사 빠진 맞춤법 검사기'); | |
| 1908 | win.content(function (el) { | |
| 1909 | el.innerHTML = "진행중입니다...." | |
| 1910 | }); | |
| 1911 | var textToCheck = TextProc.selectionText().length == 0 ? TextProc.value() : TextProc.selectionText(); | |
| 1912 | GM.xmlHttpRequest({ | |
| 1913 | url: 'https://namufix.wikimasonry.org/kospell', | |
| 1914 | method: 'POST', | |
| 1915 | data: JSON.stringify({ | |
| 1916 | text: textToCheck, | |
| 1917 | unstripNamumark: true | |
| 1918 | }), | |
| 1919 | headers: { | |
| 1920 | "Content-Type": "application/json" | |
| 1921 | }, | |
| 1922 | onload: function (res) { | |
| 1923 | try { | |
| 1924 | var resObj = JSON.parse(res.responseText); | |
| 1925 | if (!resObj.success) { | |
| 1926 | alert('서버에서 오류가 발생했습니다.\n\n오류 코드 : ' + resObj.error.code + '\n메세지 : ' + resObj.error.message); | |
| 1927 | win.close(); | |
| 1928 | return; | |
| 1929 | } | |
| 1930 | } catch (errr) { | |
| 1931 | alert('서버에서 알 수 없는 오류가 발생했습니다.'); | |
| 1932 | win.close(); | |
| 1933 | return; | |
| 1934 | } | |
| 1935 | win.content(function (winel) { | |
| 1936 | winel.innerHTML = '<p>맞춤법 검사 결과입니다. 아직 개발중인 기능이라 나사 빠진듯이 돌아갑니다.</p><style>table.spellresult tbody td, tr {border: #9D75D9 1px solid; border-collapse: collapse;}table.spellresult tbody td:not(.spellhelp) {padding: 3px 7px;word-break: keep-all; white-space: nowrap;}table.spellresult tbody td.spellhelp {word-break: keep-all;}table.spellresult tbody td.spellerror {color: red;}table.spellresult tbody td.spellsuggest {color: darkgreen;}</style><table class="spellresult"><thead><tr><th>틀린말</th><th>대체어</th><th>도움말</th></tr></thead><tbody></tbody></table>'; | |
| 1937 | for (var i = 0; i < resObj.result.length; i++) { | |
| 1938 | var resultEntry = resObj.result[i]; | |
| 1939 | var row = document.createElement('tr'); | |
| 1940 | row.innerHTML = '<td class="spellerror"></td><td class="spellsuggest"></td><td class="spellhelp"></td>'; | |
| 1941 | row.querySelector('.spellerror').innerHTML = resultEntry.errors.join('<br>'); | |
| 1942 | row.querySelector('.spellsuggest').innerHTML = resultEntry.replacements.join('<br>'); | |
| 1943 | row.querySelector('.spellhelp').innerHTML = encodeHTMLComponent(resultEntry.help); | |
| 1944 | winel.querySelector('tbody').appendChild(row); | |
| 1945 | } | |
| 1946 | }); | |
| 1947 | win.button('닫기', win.close); | |
| 1948 | } | |
| 1949 | }) | |
| 1950 | }); | |
| 1951 | ||
| 1952 | Designer.button('<span class="ion-ios-timer-outline fa fa-clock-o"></span>').hoverMessage('아카이브하고 외부링크 삽입').click(function () { | |
| 1953 | var win = TooSimplePopup(); | |
| 1954 | win.title("아카이브한 후 외부링크 삽입"); | |
| 1955 | var linkTo = "", | |
| 1956 | linkText = "", | |
| 1957 | WayBack = false, | |
| 1958 | WayBack = false, | |
| 1959 | WayBackAsMobile = false, | |
| 1960 | archiveIs = false, | |
| 1961 | archiveLinks = []; | |
| 1962 | var refresh; | |
| 1963 | win.content(function (container) { | |
| 1964 | container.innerHTML = '<h1 style="margin: 0px 0px 5px 0px; font-size: 20px;">링크할 곳(외부링크)</h1>' + | |
| 1965 | '<style>#linkTo, #visibleOutput {position: absolute; left: 120px;}</style>' + | |
| 1966 | '<label>링크할 대상</label> <input type="text" id="linkTo" placeholder="e.g. http://www.naver.com" /><br>' + | |
| 1967 | '<label>표시할 텍스트 (출력)</label> <input type="text" id="visibleOutput" placeholder="e.g. 구글" /><br>' + | |
| 1968 | '<h1 style="margin: 5px 0px 5px 0px; font-size: 20px;">아카이브</h1>' + | |
| 1969 | '<strong>참고</strong> : 동일한 주소의 아카이브를 자주 하다 보면 아까 했던 아카이브가 또 나올 수도 있습니다, 이런 경우엔 잠시 몇분정도 기다렸다가 하시면 됩니다.<br>' + | |
| 1970 | '<strong>참고</strong> : 기존의 아카이브들은 무시됩니다.<br>' + | |
| 1971 | '<strong>참고</strong> : 아카이빙하려는 사이트의 서버 설정 결과에 따라 아카이빙이 거부될 수 있습니다.' + | |
| 1972 | '<strong style="color:red;">주의</strong> : 불안정한 기능입니다. 버그에 주의하세요.<br>' + | |
| 1973 | '<input type="checkbox" id="WayBack" /> <label><a href="https://archive.org/web/" target="_blank">WayBack Machine</a>으로 아카이브</label> (<input type="checkbox" id="WayBackMobi" /> 모바일 버전으로)<br>' + | |
| 1974 | '<input type="checkbox" id="archiveIs" /> <label><a href="https://archive.is/" target="_blank" checked>archive.is</a>에서 아카이브</label>'; | |
| 1975 | refresh = function () { | |
| 1976 | linkTo = container.querySelector('#linkTo').value; | |
| 1977 | linkText = container.querySelector('#visibleOutput').value; | |
| 1978 | WayBack = container.querySelector('#WayBack').checked; | |
| 1979 | WayBackAsMobile = container.querySelector('#WayBackMobi').checked; | |
| 1980 | archiveIs = container.querySelector('#archiveIs').checked; | |
| 1981 | } | |
| 1982 | }); | |
| 1983 | win.button("박제/삽입", function () { | |
| 1984 | var waitwin = TooSimplePopup(); | |
| 1985 | waitwin.title('박제중....'); | |
| 1986 | refresh(); | |
| 1987 | if (linkTo.indexOf('http://') != 0 && linkTo.indexOf('https://') != 0) { | |
| 1988 | alert('http:// 또는 https://로 시작하는 외부링크가 아닙니다!'); | |
| 1989 | } | |
| 1990 | waitwin.content(function (container) { | |
| 1991 | container.innerHTML = '박제중입니다....' | |
| 1992 | }); | |
| 1993 | ||
| 1994 | function finishLinking() { | |
| 1995 | var link = '[[' + linkTo + '|링크]]'; | |
| 1996 | if (archiveLinks.length != 0) { | |
| 1997 | link += '('; | |
| 1998 | for (var i = 0; i < archiveLinks.length; i++) { | |
| 1999 | link += '[[' + archiveLinks[i] + '|아카이브' + (i + 1) + ']]'; | |
| 2000 | if (i != archiveLinks.length - 1) link += ','; | |
| 2001 | } | |
| 2002 | link += ')'; | |
| 2003 | } | |
| 2004 | TextProc.selectionText(link + TextProc.selectionText()); | |
| 2005 | waitwin.close(); | |
| 2006 | win.close(); | |
| 2007 | } | |
| 2008 | ||
| 2009 | function archiveOne() { | |
| 2010 | var archiveType; | |
| 2011 | if (WayBack) { | |
| 2012 | archiveType = 'wb'; | |
| 2013 | WayBack = false; | |
| 2014 | } else if (archiveIs) { | |
| 2015 | archiveType = 'ai'; | |
| 2016 | archiveIs = false; | |
| 2017 | } else { | |
| 2018 | finishLinking(); | |
| 2019 | return; | |
| 2020 | } | |
| 2021 | var r = {}; | |
| 2022 | if (archiveType == 'wb') { | |
| 2023 | // 'http://web.archive.org/save/' | |
| 2024 | // 1 -> Mobile Agent | |
| 2025 | r.method = "GET"; | |
| 2026 | r.url = "http://web.archive.org/save/" + linkTo; | |
| 2027 | if (WayBackAsMobile) { | |
| 2028 | r.headers = {}; | |
| 2029 | r.headers["User-Agent"] = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"; | |
| 2030 | } | |
| 2031 | r.onload = function (res) { | |
| 2032 | if (res.status == 403) { | |
| 2033 | alert('오류가 발생했습니다. 크롤링이 금지된 사이트일 수도 있습니다.'); | |
| 2034 | setTimeout(archiveOne, 50); | |
| 2035 | return; | |
| 2036 | } else if (res.status != 200) { | |
| 2037 | alert('알 수 없는 오류가 발생했습니다.'); | |
| 2038 | setTimeout(archiveOne, 50); | |
| 2039 | return; | |
| 2040 | } | |
| 2041 | var matches = /var redirUrl = \"(.+?)\";/.exec(res.responseText); | |
| 2042 | if (matches == null) { | |
| 2043 | alert('WayBack Machine 아카이브 주소를 얻는 데 실패했습니다.'); | |
| 2044 | setTimeout(archiveOne, 50); | |
| 2045 | return; | |
| 2046 | } | |
| 2047 | var archiveUrl = 'http://web.archive.org' + matches[1]; | |
| 2048 | archiveLinks.push(archiveUrl); | |
| 2049 | setTimeout(archiveOne, 50); | |
| 2050 | }; | |
| 2051 | } else if (archiveType == 'ai') { | |
| 2052 | r.method = "GET"; | |
| 2053 | r.url = "https://archive.is"; | |
| 2054 | r.onload = function (res) { | |
| 2055 | // get submitid | |
| 2056 | var parser = new DOMParser(); | |
| 2057 | var indexDoc = parser.parseFromString(res.responseText, "text/html"); | |
| 2058 | var token = indexDoc.querySelector("input[name=submitid][value]"); | |
| 2059 | if (token) { | |
| 2060 | token = token.value; | |
| 2061 | ||
| 2062 | // archive form | |
| 2063 | // 'http://archive.is/submit/' | |
| 2064 | var r2 = {}; | |
| 2065 | r2.method = "POST"; | |
| 2066 | r2.url = "https://archive.is/submit/"; | |
| 2067 | r2.headers = {}; | |
| 2068 | r2.headers["Content-Type"] = "application/x-www-form-urlencoded"; | |
| 2069 | r2.data = "anyway=1&url=" + encodeURIComponent(linkTo) + "&submitid=" + encodeURIComponent(token); | |
| 2070 | r2.onload = function (res) { | |
| 2071 | var matches = /document\.location\.replace\("(.+?)"\)/.exec(res.responseText); | |
| 2072 | if (matches == null) matches = /<meta property="og:url" content="(.+?)"/.exec(res.responseText); | |
| 2073 | if (matches == null) { | |
| 2074 | alert('archive.is 아카이브 주소를 얻는 데 실패했습니다.'); | |
| 2075 | setTimeout(archiveOne, 50); | |
| 2076 | return; | |
| 2077 | } | |
| 2078 | var archiveUrl = matches[1]; | |
| 2079 | archiveLinks.push(archiveUrl); | |
| 2080 | setTimeout(archiveOne, 50); | |
| 2081 | } | |
| 2082 | GM.xmlHttpRequest(r2); | |
| 2083 | } else { | |
| 2084 | alert('archive.is 아카이브 중 토큰을 얻는 데 실패했습니다.') | |
| 2085 | } | |
| 2086 | } | |
| 2087 | } | |
| 2088 | GM.xmlHttpRequest(r); | |
| 2089 | } | |
| 2090 | archiveOne(); | |
| 2091 | }); | |
| 2092 | win.button("닫기", win.close); | |
| 2093 | }); | |
| 2094 | if (ENV.IsEditing) { | |
| 2095 | // Manager Class | |
| 2096 | var tempsaveManager = new function () { | |
| 2097 | var ht = this; | |
| 2098 | this.getTitles = function () { | |
| 2099 | var r = []; | |
| 2100 | for (var i in SET.tempsaves) { | |
| 2101 | r.push(i); | |
| 2102 | } | |
| 2103 | return r; | |
| 2104 | } | |
| 2105 | this.getByTitle = async function (docTitle) { | |
| 2106 | await SET.load(); | |
| 2107 | if (nOu(SET.tempsaves[docTitle])) { | |
| 2108 | SET.tempsaves[docTitle] = []; | |
| 2109 | await SET.save(); | |
| 2110 | } | |
| 2111 | return SET.tempsaves[docTitle]; // {section, text, timestamp} | |
| 2112 | }; | |
| 2113 | this.getByTitleAndSectionNo = async function (docTitle, sectno) { | |
| 2114 | await SET.load(); | |
| 2115 | var b = ht.getByTitle(docTitle); | |
| 2116 | var a = []; | |
| 2117 | for (var i = 0; i < b.length; i++) { | |
| 2118 | if (b[i].section == sectno) | |
| 2119 | a.push(b[i]); | |
| 2120 | } | |
| 2121 | return a; | |
| 2122 | } | |
| 2123 | this.save = async function (docTitle, sectno, timestamp, text, wikihost) { | |
| 2124 | if (typeof wikihost === 'undefined') | |
| 2125 | var wikihost = location.host; | |
| 2126 | await SET.load(); | |
| 2127 | if (nOu(SET.tempsaves[docTitle])) { | |
| 2128 | SET.tempsaves[docTitle] = []; | |
| 2129 | await SET.save(); | |
| 2130 | } | |
| 2131 | SET.tempsaves[docTitle].push({ | |
| 2132 | section: sectno, | |
| 2133 | timestamp: timestamp, | |
| 2134 | text: text, | |
| 2135 | host: wikihost | |
| 2136 | }); | |
| 2137 | await SET.save(); | |
| 2138 | } | |
| 2139 | this.delete = async function (docTitle, sectno, timestamp) { | |
| 2140 | await SET.load(); | |
| 2141 | if (nOu(SET.tempsaves[docTitle])) return; | |
| 2142 | var newArray = []; | |
| 2143 | for (var i = 0; i < SET.tempsaves[docTitle].length; i++) { | |
| 2144 | var keepThis = true; | |
| 2145 | var now = SET.tempsaves[docTitle][i]; | |
| 2146 | switch (arguments.length) { | |
| 2147 | case 1: | |
| 2148 | keepThis = false; | |
| 2149 | break; | |
| 2150 | case 2: | |
| 2151 | if (now.section == sectno) keepThis = false; | |
| 2152 | break; | |
| 2153 | case 3: | |
| 2154 | if (now.section == sectno && now.timestamp == timestamp) keepThis = false; | |
| 2155 | break; | |
| 2156 | } | |
| 2157 | if (keepThis) { | |
| 2158 | newArray.push(SET.tempsaves[docTitle][i]); | |
| 2159 | } | |
| 2160 | } | |
| 2161 | SET.tempsaves[docTitle] = newArray; | |
| 2162 | await SET.save(); | |
| 2163 | } | |
| 2164 | } | |
| 2165 | // Tempsave Menu | |
| 2166 | var tempsaveDropdown = Designer.dropdown('<span class="ion-ios-pricetags-outline fa fa-save"></span>').hoverMessage('임시저장'); | |
| 2167 | tempsaveDropdown.button('<span class="ion-ios-pricetag-outline fa fa-save"></span>', '임시저장').click(async function () { | |
| 2168 | await tempsaveManager.save(ENV.docTitle, ENV.section, Date.now(), txtarea.value); | |
| 2169 | }); | |
| 2170 | tempsaveDropdown.button('<span class="ion-filing fa fa-folder-open-o"></span>', '임시저장 불러오기').click(async function () { | |
| 2171 | // title(text), content(callback), foot(callback), button(text,onclick), close | |
| 2172 | var win = TooSimplePopup(); | |
| 2173 | win.title('임시저장 불러오기') | |
| 2174 | var tempsaveList = await tempsaveManager.getByTitle(ENV.docTitle); | |
| 2175 | win.content(function (el) { | |
| 2176 | el.innerHTML = '<p>현재 편집중인 문단인 경우 문단 번호가 <strong>굵게</strong> 표시됩니다.<br>문단 번호가 -2인 경우는 문단 번호가 감지되지 않은 경우입니다.</p>'; | |
| 2177 | var divWithscrollbars = document.createElement("div"); | |
| 2178 | divWithscrollbars.style.height = '300px'; | |
| 2179 | divWithscrollbars.style.overflow = 'auto'; | |
| 2180 | var table = document.createElement("table"); | |
| 2181 | var headrow = document.createElement("tr"); | |
| 2182 | headrow.innerHTML = '<th>문단 번호</th><th>저장된 날짜와 시간</th><th>불러오기</th>'; | |
| 2183 | table.appendChild(headrow); | |
| 2184 | for (var i = 0; i < tempsaveList.length; i++) { | |
| 2185 | var now = tempsaveList[i]; | |
| 2186 | var tr = document.createElement("tr"); | |
| 2187 | tr.innerHTML = '<td>' + (now.section == ENV.section ? '<strong>' : '') + now.section + (now.section == ENV.section ? '</strong>' : '') + '</td><td>' + formatDateTime(now.timestamp) + '</td>' | |
| 2188 | var td = document.createElement("td"); | |
| 2189 | var btn = document.createElement("button"); | |
| 2190 | btn.setAttribute("type", "button"); | |
| 2191 | btn.innerHTML = "불러오기"; | |
| 2192 | btn.dataset.json = JSON.stringify(now); | |
| 2193 | btn.addEventListener('click', function (evt) { | |
| 2194 | var now = JSON.parse(evt.target.dataset.json); | |
| 2195 | txtarea.value = now.text; | |
| 2196 | }); | |
| 2197 | td.appendChild(btn); | |
| 2198 | tr.appendChild(td); | |
| 2199 | table.appendChild(tr); | |
| 2200 | } | |
| 2201 | divWithscrollbars.appendChild(table); | |
| 2202 | el.appendChild(divWithscrollbars); | |
| 2203 | }); | |
| 2204 | win.button('닫기', win.close); | |
| 2205 | }); | |
| 2206 | tempsaveDropdown.button('<span class="ion-trash-a fa fa-trash" style="color:red;"></span>', '이 문서의 모든 임시저장 삭제').click(async function () { | |
| 2207 | await tempsaveManager.delete(ENV.docTitle); | |
| 2208 | }); | |
| 2209 | tempsaveDropdown.button('<span class="ion-trash-a fa fa-trash" style="color:orangered;"></span>', '이 문서의 이 문단의 모든 임시저장 삭제').click(async function () { | |
| 2210 | await tempsaveManager.delete(ENV.docTitle, ENV.section); | |
| 2211 | }); | |
| 2212 | tempsaveDropdown.button('<span class="ion-trash-a fa fa-trash" style="color:orange;"></span>', '특정 임시저장만 삭제').click(async function () { | |
| 2213 | // title(text), content(callback), foot(callback), button(text,onclick), close | |
| 2214 | var win = TooSimplePopup(); | |
| 2215 | var tempsaveList = await tempsaveManager.getByTitle(ENV.docTitle); | |
| 2216 | win.title('임시저장 삭제'); | |
| 2217 | win.content(function (el) { | |
| 2218 | el.innerHTML = '<p>현재 편집중인 문단인 경우 문단 번호가 <strong>굵게</strong> 표시됩니다.<br>문단 번호가 -2인 경우는 문단 번호가 감지되지 않은 경우입니다.</p>'; | |
| 2219 | var divWithscrollbars = document.createElement("div"); | |
| 2220 | divWithscrollbars.style.height = '300px'; | |
| 2221 | divWithscrollbars.style.overflow = 'auto'; | |
| 2222 | var table = document.createElement("table"); | |
| 2223 | var headrow = document.createElement("tr"); | |
| 2224 | headrow.innerHTML = '<th>문단 번호</th><th>저장된 날짜와 시간</th><th>삭제 버튼</th>'; | |
| 2225 | table.appendChild(headrow); | |
| 2226 | for (var i = 0; i < tempsaveList.length; i++) { | |
| 2227 | var now = tempsaveList[i]; | |
| 2228 | var tr = document.createElement("tr"); | |
| 2229 | tr.innerHTML = '<td>' + (now.section == ENV.section ? '<strong>' : '') + now.section + (now.section == ENV.section ? '</strong>' : '') + '</td><td>' + formatDateTime(now.timestamp) + '</td>' | |
| 2230 | var td = document.createElement("td"); | |
| 2231 | var btn = document.createElement("button"); | |
| 2232 | btn.setAttribute("type", "button"); | |
| 2233 | btn.innerHTML = "삭제하기"; | |
| 2234 | btn.dataset.json = JSON.stringify(now); | |
| 2235 | btn.addEventListener('click', async function (evt) { | |
| 2236 | var now = JSON.parse(evt.target.dataset.json); | |
| 2237 | await tempsaveManager.delete(ENV.docTitle, now.section, now.timestamp); | |
| 2238 | win.close(); | |
| 2239 | }); | |
| 2240 | td.appendChild(btn); | |
| 2241 | tr.appendChild(td); | |
| 2242 | table.appendChild(tr); | |
| 2243 | } | |
| 2244 | divWithscrollbars.appendChild(table); | |
| 2245 | el.appendChild(divWithscrollbars); | |
| 2246 | }); | |
| 2247 | win.button('닫기', win.close); | |
| 2248 | }); | |
| 2249 | if (SET.autoTempsaveSpan > 0) | |
| 2250 | setInterval(async function () { | |
| 2251 | await tempsaveManager.save(ENV.docTitle, ENV.section, Date.now(), txtarea.value); | |
| 2252 | }, SET.autoTempsaveSpan); | |
| 2253 | } | |
| 2254 | // Template Insert Feature | |
| 2255 | var templatesDropdown = Designer.dropdown('<span class="ion-ios-copy-outline fa fa-file"></span>').hoverMessage('템플릿/틀 삽입과 최근에 사용/삽입한 템플릿/틀 기록'); | |
| 2256 | var refreshTemplatesDropdown = async function () { | |
| 2257 | await SET.load(); | |
| 2258 | templatesDropdown.clear(); | |
| 2259 | var rutl = SET.recentlyUsedTemplates.length; | |
| 2260 | ||
| 2261 | function InsertTemplateClosure(na) { | |
| 2262 | return async function () { | |
| 2263 | namuapi.raw(na, async function (templateContent) { | |
| 2264 | await SET.load(); | |
| 2265 | if (SET.recentlyUsedTemplates.indexOf(na) == -1) SET.recentlyUsedTemplates.push(na); | |
| 2266 | await SET.save(); | |
| 2267 | if (na.indexOf('틀:') == 0) | |
| 2268 | TextProc.selectionText(TextProc.selectionText() + '[include(' + na + ')]'); | |
| 2269 | else | |
| 2270 | txtarea.value = templateContent; | |
| 2271 | setTimeout(refreshTemplatesDropdown, 300); | |
| 2272 | }, function () { | |
| 2273 | alert('존재하지 않는 템플릿/틀입니다.'); | |
| 2274 | return; | |
| 2275 | }); | |
| 2276 | }; | |
| 2277 | } | |
| 2278 | for (var i = 0; i < (rutl < 9 ? rutl : 9); i++) { | |
| 2279 | templatesDropdown.button('<span class="ion-ios-paper-outline fa fa-file"></span>', SET.recentlyUsedTemplates[i]).click(InsertTemplateClosure(SET.recentlyUsedTemplates[i])); | |
| 2280 | } | |
| 2281 | templatesDropdown.button('<span class="ion-close-round fa fa-times"></span>', '기록 삭제').click(async function () { | |
| 2282 | await SET.load(); | |
| 2283 | SET.recentlyUsedTemplates = []; | |
| 2284 | await SET.save(); | |
| 2285 | setTimeout(refreshTemplatesDropdown, 300); | |
| 2286 | }); | |
| 2287 | templatesDropdown.button('<span class="ion-plus-round fa fa-plus"></span>', '템플릿/틀 삽입').click(function () { | |
| 2288 | var templateName = prompt('템플릿/틀 이름을 입력하세요.'); | |
| 2289 | if (!/^(?:템플릿|Template|틀):.+/.test(templateName) && !confirm('올바른 템플릿/틀 이름이 아닌 것 같습니다. 계속할까요?')) return; | |
| 2290 | InsertTemplateClosure(templateName)(); | |
| 2291 | setTimeout(refreshTemplatesDropdown, 300); | |
| 2292 | }); | |
| 2293 | }; | |
| 2294 | setTimeout(refreshTemplatesDropdown, 500); | |
| 2295 | ||
| 2296 | // set Size | |
| 2297 | if (ENV.Discussing) | |
| 2298 | rootDiv.style.height = '170px'; | |
| 2299 | else | |
| 2300 | rootDiv.style.height = '600px'; | |
| 2301 | ||
| 2302 | // Add Keyboard Shortcut | |
| 2303 | function overrideBrowserDefaultShortcutKey(evt) { | |
| 2304 | var overrideShortcutKey = true; | |
| 2305 | if (evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 2306 | switch (evt.keyCode) { // Ctrl | |
| 2307 | case 66: // B | |
| 2308 | case 98: | |
| 2309 | case 73: // I | |
| 2310 | case 105: | |
| 2311 | case 68: // D | |
| 2312 | case 100: | |
| 2313 | case 85: // U | |
| 2314 | case 117: | |
| 2315 | case 72: // H | |
| 2316 | case 104: | |
| 2317 | case 219: | |
| 2318 | case 123: | |
| 2319 | case 91: // [ | |
| 2320 | case 221: | |
| 2321 | case 125: | |
| 2322 | case 93: // ] | |
| 2323 | case 83: // S | |
| 2324 | case 115: | |
| 2325 | break; | |
| 2326 | default: | |
| 2327 | overrideShortcutKey = false; | |
| 2328 | break; | |
| 2329 | } | |
| 2330 | } else if (evt.ctrlKey && evt.altKey && !evt.shiftKey) { | |
| 2331 | switch (evt.keyCode) { // Ctrl + Alt | |
| 2332 | case 73: // I | |
| 2333 | case 105: | |
| 2334 | default: | |
| 2335 | overrideShortcutKey = false; | |
| 2336 | break; | |
| 2337 | } | |
| 2338 | } else { | |
| 2339 | overrideShortcutKey = false; | |
| 2340 | } | |
| 2341 | if (overrideShortcutKey) { | |
| 2342 | evt.preventDefault(); | |
| 2343 | evt.stopPropagation(); | |
| 2344 | } | |
| 2345 | } | |
| 2346 | txtarea.addEventListener('keydown', overrideBrowserDefaultShortcutKey); | |
| 2347 | txtarea.addEventListener('keypress', overrideBrowserDefaultShortcutKey); | |
| 2348 | txtarea.addEventListener('keyup', function (evt) { | |
| 2349 | var overrideShortcutKey = true; | |
| 2350 | if (evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 2351 | switch (evt.keyCode) { // Ctrl | |
| 2352 | case 66: // B | |
| 2353 | case 98: | |
| 2354 | TextProc.ToggleWrapSelection("'''"); | |
| 2355 | break; | |
| 2356 | case 73: // I | |
| 2357 | case 105: | |
| 2358 | TextProc.ToggleWrapSelection("''"); | |
| 2359 | break; | |
| 2360 | case 68: // D | |
| 2361 | case 100: | |
| 2362 | TextProc.ToggleWrapSelection("--"); | |
| 2363 | break; | |
| 2364 | case 85: // U | |
| 2365 | case 117: | |
| 2366 | TextProc.ToggleWrapSelection("__"); | |
| 2367 | break; | |
| 2368 | case 72: | |
| 2369 | case 104: | |
| 2370 | NewHeadingMacro(); | |
| 2371 | break; | |
| 2372 | case 219: | |
| 2373 | case 123: | |
| 2374 | case 91: // [ | |
| 2375 | FontSizeChanger(false); | |
| 2376 | break; | |
| 2377 | case 221: | |
| 2378 | case 125: | |
| 2379 | case 93: // ] | |
| 2380 | FontSizeChanger(true); | |
| 2381 | break; | |
| 2382 | case 83: // S | |
| 2383 | case 115: | |
| 2384 | tempsaveManager.save(ENV.docTitle, ENV.section, Date.now(), txtarea.value); | |
| 2385 | break; | |
| 2386 | default: | |
| 2387 | overrideShortcutKey = false; | |
| 2388 | break; | |
| 2389 | } | |
| 2390 | } else if (evt.ctrlKey && evt.altKey && !evt.shiftKey) { | |
| 2391 | switch (evt.keyCode) { // Ctrl + Alt | |
| 2392 | case 73: // I | |
| 2393 | case 105: | |
| 2394 | namuUpload(); | |
| 2395 | break; | |
| 2396 | default: | |
| 2397 | overrideShortcutKey = false; | |
| 2398 | break; | |
| 2399 | } | |
| 2400 | } else { | |
| 2401 | overrideShortcutKey = false; | |
| 2402 | } | |
| 2403 | if (overrideShortcutKey) { | |
| 2404 | evt.preventDefault(); | |
| 2405 | evt.stopPropagation(); | |
| 2406 | } | |
| 2407 | }); | |
| 2408 | ||
| 2409 | // Support drag-drop file upload | |
| 2410 | // from https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop | |
| 2411 | // from https://stackoverflow.com/questions/7237436/how-to-listen-for-drag-and-drop-plain-text-in-textarea-with-jquery | |
| 2412 | txtarea.addEventListener('dragover', function (evt) { | |
| 2413 | evt.preventDefault(); | |
| 2414 | }); | |
| 2415 | txtarea.addEventListener('dragend', function (evt) { | |
| 2416 | evt.preventDefault(); | |
| 2417 | }); | |
| 2418 | txtarea.addEventListener('dragenter', function (evt) { | |
| 2419 | evt.preventDefault(); | |
| 2420 | }); | |
| 2421 | txtarea.addEventListener('drop', function (evt) { | |
| 2422 | evt.preventDefault(); | |
| 2423 | var dt = evt.dataTransfer; | |
| 2424 | var files = []; | |
| 2425 | if (dt.items) { | |
| 2426 | for (var i = 0; i < dt.items.length; i++) | |
| 2427 | if (dt.items[i].kind == "file") { | |
| 2428 | var f = dt.items[i].getAsFile(); | |
| 2429 | if (f.type.indexOf('image/') == 0) | |
| 2430 | files.push(f); | |
| 2431 | } | |
| 2432 | } else { | |
| 2433 | for (var i = 0; i < dt.files.length; i++) { | |
| 2434 | var f = dt.files[i]; | |
| 2435 | if (f.type.indexOf('image/') == 0) | |
| 2436 | files.push(f); | |
| 2437 | } | |
| 2438 | } | |
| 2439 | if (files.length > 0) { | |
| 2440 | namuUpload(files, function () {}); | |
| 2441 | } | |
| 2442 | }) | |
| 2443 | ||
| 2444 | // Support image upload by pasting | |
| 2445 | txtarea.addEventListener('paste', function (evt) { | |
| 2446 | var items = (evt.clipboardData || evt.originalEvent.clipboardData).items; | |
| 2447 | var files = []; | |
| 2448 | for (index in items) { | |
| 2449 | var item = items[index]; | |
| 2450 | if (item.kind === 'file') { | |
| 2451 | var file = item.getAsFile(); | |
| 2452 | if (file.type.indexOf('image/') == 0) | |
| 2453 | files.push(file); | |
| 2454 | } | |
| 2455 | } | |
| 2456 | if (files.length > 0) { | |
| 2457 | namuUpload(files, function () {}); | |
| 2458 | } | |
| 2459 | }) | |
| 2460 | ||
| 2461 | // Add NamuFix Div | |
| 2462 | var oldTextarea = document.querySelector("textarea"); | |
| 2463 | var wText = oldTextarea.value; | |
| 2464 | oldTextarea.parentNode.insertBefore(rootDiv, oldTextarea); | |
| 2465 | oldTextarea.parentNode.removeChild(oldTextarea); | |
| 2466 | txtarea.value = wText; | |
| 2467 | ||
| 2468 | var srwPattern = /\?redirectTo=([^\&]+)/; | |
| 2469 | if (srwPattern.test(location.search)) { | |
| 2470 | if ((txtarea.value.trim().search(/^#redirect .+/) == 0 || txtarea.value.trim().length == 0) || confirm('빈 문서가 아닌것 같습니다만 그래도 계속?')) { | |
| 2471 | txtarea.value = '#redirect ' + decodeURIComponent(srwPattern.exec(location.search)[1]); | |
| 2472 | if (document.querySelectorAll('iframe[title="CAPTCHA 위젯"]').length == 0 && document.querySelector('.alert.alert-danger') !== null) { | |
| 2473 | if (document.querySelector("input#logInput")) document.querySelector("input#logInput").value = "NamuFix를 이용하여 자동 리다이렉트 처리됨."; | |
| 2474 | document.querySelector('#editBtn').click(); | |
| 2475 | } | |
| 2476 | } | |
| 2477 | } | |
| 2478 | } | |
| 2479 | } else if (ENV.IsDocument) { | |
| 2480 | if (ENV.docTitle.trim().indexOf('기여:') == 0) { | |
| 2481 | var target = ENV.docTitle.trim().substring(3).trim(); | |
| 2482 | if (validateIP(target)) { | |
| 2483 | location.assign('/contribution/ip/' + target + '/document'); | |
| 2484 | } else { | |
| 2485 | location.assign('/contribution/author/' + target + '/document'); | |
| 2486 | } | |
| 2487 | } | |
| 2488 | ||
| 2489 | ||
| 2490 | ||
| 2491 | addArticleButton('삭제', (evt) => { | |
| 2492 | evt.preventDefault(); | |
| 2493 | ||
| 2494 | location.href = 'https://namu.wiki/delete/' + ENV.docTitle; | |
| 2495 | }) | |
| 2496 | addArticleButton('이동', (evt) => { | |
| 2497 | evt.preventDefault(); | |
| 2498 | ||
| 2499 | location.href = 'https://namu.wiki/move/' + ENV.docTitle; | |
| 2500 | }) | |
| 2501 | addArticleButton('롤백', function () { | |
| 2502 | var revNo = prompt('리버전 번호를 입력하세요.').trim(); | |
| 2503 | if (revNo.indexOf('r') == 0) revNo = revNo.substring(1); | |
| 2504 | if (/[^0-9]/.test(revNo)) { | |
| 2505 | alert('올바른 입력이 아닙니다! r1 혹은 1과 같이 입력해주세요.'); | |
| 2506 | return; | |
| 2507 | } | |
| 2508 | location.href = "/revert/" + encodeURIComponent(ENV.docTitle) + "?rev=" + revNo; | |
| 2509 | }) | |
| 2510 | addArticleButton('RAW', (evt) => { | |
| 2511 | evt.preventDefault(); | |
| 2512 | ||
| 2513 | location.href = 'https://namu.wiki/raw/' + ENV.docTitle; | |
| 2514 | }) | |
| 2515 | addArticleButton('Blame', (evt) => { | |
| 2516 | evt.preventDefault(); | |
| 2517 | ||
| 2518 | location.href = 'https://namu.wiki/blame/' + ENV.docTitle; | |
| 2519 | }) | |
| 2520 | ||
| 2521 | // 리다이렉트 버튼 추가 | |
| 2522 | addArticleButton('출발지', function (evt) { | |
| 2523 | evt.preventDefault(); | |
| 2524 | ||
| 2525 | var redirectFrom = prompt('출발지 문서명을 입력하세요.'); | |
| 2526 | if (redirectFrom != null && redirectFrom.trim().length != 0) | |
| 2527 | location.href = 'https://' + location.host + '/edit/' + redirectFrom + '?redirectTo=' + ENV.docTitle; | |
| 2528 | }); | |
| 2529 | ||
| 2530 | ||
| 2531 | if (SET.addSnsShareButton && ENV.IsDocument) { | |
| 2532 | addArticleButton('<span class="icon ion-social-facebook fa fab fa-facebook-square"></span>', (evt) => { | |
| 2533 | evt.preventDefault(); | |
| 2534 | ||
| 2535 | GM.openInTab(`http://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(location.href)}`) | |
| 2536 | }) | |
| 2537 | addArticleButton('<span class="icon ion-social-twitter fa fab fa-twitter"></span>', (evt) => { | |
| 2538 | evt.preventDefault(); | |
| 2539 | ||
| 2540 | GM.openInTab(`http://twitter.com/intent/tweet?url=${encodeURIComponent(location.href)}&text=${ENV.docTitle}`) | |
| 2541 | }) | |
| 2542 | } | |
| 2543 | // 리다이렉트로 왔을 시 그 라디이렉트 문서 편집/삭제 링크 추가 | |
| 2544 | if (document.querySelector('article .alert.alert-info, .Liberty .wiki-article .alert.alert-info') && document.querySelector('article .alert.alert-info, .Liberty .wiki-article .alert.alert-info').innerHTML.indexOf('에서 넘어옴') != -1) { | |
| 2545 | var redirectAlert = document.querySelector('article .alert.alert-info, .Liberty .wiki-article .alert.alert-info'); | |
| 2546 | var origDocuName = decodeURIComponent(/\/w\/(.+?)\?noredirect=1/.exec(redirectAlert.querySelector('a.document').href)[1]); | |
| 2547 | var editUrl = '/edit/' + origDocuName; | |
| 2548 | var deleteUrl = '/delete/' + origDocuName; | |
| 2549 | ||
| 2550 | redirectAlert.innerHTML = '<a href="/w/' + encodeURIComponent(origDocuName) + '?noredirect=1" class="document" title="' + encodeHTMLComponent(origDocuName) + '">' + encodeHTMLComponent(origDocuName) + '</a>' + | |
| 2551 | '에서 여기로 넘어왔습니다. 당신은 ' + encodeHTMLComponent(origDocuName) + ' 문서를 <a href="' + editUrl + '">수정</a>하거나 <a href="' + deleteUrl + '">삭제</a>할 수 있습니다.'; | |
| 2552 | } | |
| 2553 | // 상위 문서로의 링크 | |
| 2554 | if (ENV.docTitle.indexOf('/') != -1) { | |
| 2555 | function spSplit(a, b) { | |
| 2556 | var splitted = a.split(b); | |
| 2557 | var result = []; | |
| 2558 | for (var i = 1; i <= splitted.length; i++) { | |
| 2559 | var now = ''; | |
| 2560 | for (var ii = 0; ii < i; ii++) { | |
| 2561 | var append = splitted[ii]; | |
| 2562 | if (ii != i - 1) append += '/'; | |
| 2563 | now += append; | |
| 2564 | } | |
| 2565 | result.push(now); | |
| 2566 | } | |
| 2567 | return result; | |
| 2568 | } | |
| 2569 | var higherDocsWE = {}; | |
| 2570 | var higherDocs = spSplit(ENV.docTitle, '/'); | |
| 2571 | for (var i = 0; i < higherDocs.length; i++) { | |
| 2572 | higherDocsWE[i] = null; | |
| 2573 | } | |
| 2574 | var codwe = 0; | |
| 2575 | var codwnf = 0; | |
| 2576 | for (var i = 0; i < higherDocs.length; i++) { | |
| 2577 | namuapi.raw(higherDocs[i], function (r, t) { | |
| 2578 | higherDocsWE[t] = true; | |
| 2579 | codwe++; | |
| 2580 | }, function (t) { | |
| 2581 | higherDocsWE[t] = false; | |
| 2582 | codwnf++; | |
| 2583 | }); | |
| 2584 | if (i == higherDocs.length - 1) { | |
| 2585 | var hdinid = setInterval(function () { | |
| 2586 | if (codwe + codwnf != higherDocs.length) return; | |
| 2587 | var docTitleTag = document.querySelector('h1.title, body.Liberty .liberty-content .liberty-content-header h1.title'); | |
| 2588 | var hdsPT = document.createElement("p"); | |
| 2589 | var sstl = 0; | |
| 2590 | for (var i = 0; i < higherDocs.length; i++) { | |
| 2591 | if (!higherDocsWE[higherDocs[i]]) continue; | |
| 2592 | var higherDoc = higherDocs[i]; | |
| 2593 | var a = document.createElement("a"); | |
| 2594 | a.setAttribute("href", '/w/' + encodeURIComponent(higherDoc)); | |
| 2595 | a.setAttribute("title", higherDoc); | |
| 2596 | a.className = "wiki-link-internal"; | |
| 2597 | if (i != 0 && higherDoc.substring(sstl).indexOf('/') == 0) | |
| 2598 | a.innerHTML = higherDoc.substring(sstl + 1); | |
| 2599 | else | |
| 2600 | a.innerHTML = higherDoc.substring(sstl); | |
| 2601 | sstl = higherDoc.length; | |
| 2602 | hdsPT.appendChild(a); | |
| 2603 | if (i != higherDocs.length - 1) hdsPT.appendChild(document.createTextNode(" > ")); | |
| 2604 | } | |
| 2605 | docTitleTag.style.marginBottom = '0px'; | |
| 2606 | hdsPT.style.marginBottom = '25px'; | |
| 2607 | docTitleTag.parentNode.insertBefore(hdsPT, docTitleTag.nextSibling); | |
| 2608 | clearInterval(hdinid); | |
| 2609 | }, 200); | |
| 2610 | } | |
| 2611 | } | |
| 2612 | } | |
| 2613 | ||
| 2614 | // 문단 접기 이탤릭 표시 | |
| 2615 | GM_addStyle('.namufix-folded-heading {color: darkgray; opacity: 0.5;}'); | |
| 2616 | var wikiHeadings = document.querySelectorAll('.wiki-heading') | |
| 2617 | for (var i = 0; i < wikiHeadings.length; i++) wikiHeadings[i].addEventListener('click', function (evt) { | |
| 2618 | if (evt.target.tagName === 'A') return; | |
| 2619 | if (evt.target.nextSibling.style.display === 'none') { | |
| 2620 | evt.target.className += ' namufix-folded-heading'; | |
| 2621 | } else { | |
| 2622 | evt.target.className = evt.target.className.replace('namufix-folded-heading', ''); | |
| 2623 | } | |
| 2624 | }) | |
| 2625 | ||
| 2626 | // 차단 링크 링크화 | |
| 2627 | if (ENV.docTitle.startsWith('사용자:')) { | |
| 2628 | let banbox = document.querySelector('.wiki-content [onmouseover][onmouseout]') | |
| 2629 | if (banbox.querySelector('span:first-child').textContent.includes('이 사용자는 차단된 사용자입니다.')) { | |
| 2630 | let reason = banbox.lastChild.textContent, | |
| 2631 | urlPattern = /((http[s]?|ftp):\/)?\/?([^:\/\s]+)((\/\w+)*\/)([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$/mg, | |
| 2632 | urlMatch = urlPattern.exec(reason), | |
| 2633 | linkIndex = 1, | |
| 2634 | firstLink = null; | |
| 2635 | while(urlMatch) { | |
| 2636 | let link = document.createElement("a"); | |
| 2637 | link.href = urlMatch[0]; | |
| 2638 | link.textContent = `차단 링크 ${linkIndex++}`; | |
| 2639 | banbox.appendChild(link); | |
| 2640 | if(!firstLink) firstLink = link; | |
| 2641 | urlMatch = urlPattern.exec(reason); | |
| 2642 | } | |
| 2643 | if (firstLink) { | |
| 2644 | banbox.insertBefore(document.createElement("br"), firstLink); | |
| 2645 | banbox.insertBefore(document.createElement("br"), firstLink); | |
| 2646 | } | |
| 2647 | } | |
| 2648 | } | |
| 2649 | } | |
| 2650 | ||
| 2651 | ||
| 2652 | ||
| 2653 | if (ENV.Discussing) { | |
| 2654 | // 보여지지 않은 쓰레드도 불러오기 | |
| 2655 | let unvisibleResesAllLoaded = false; | |
| 2656 | if (SET.loadUnvisibleReses) { | |
| 2657 | function doLoadUnvisibleReses() { | |
| 2658 | var scriptTag = document.createElement("script"); | |
| 2659 | scriptTag.innerHTML = 'function nfformattime(){$("time[data-nf-format-this]").each(function(){var format = $(this).attr("data-format");var time = $(this).attr("datetime");$(this).text(formatDate(new Date(time), format));});}'; | |
| 2660 | document.body.appendChild(scriptTag); | |
| 2661 | var allUnlockedReses = document.querySelectorAll('#res-container div.res-loading[data-locked="false"]'); | |
| 2662 | for (var i = 0; i < allUnlockedReses.length; i++) { | |
| 2663 | allUnlockedReses[i].setAttribute('data-locked', 'true'); | |
| 2664 | } | |
| 2665 | var reqId = parseInt(allUnlockedReses[0].dataset.id), | |
| 2666 | lastReqId = parseInt(allUnlockedReses[allUnlockedReses.length - 1].dataset.id); | |
| 2667 | console.log('[NamuFix] 보이지 않은 레스 불러오기를 시작합니다. 범위 : ' + reqId + ' 에서 ' + lastReqId); | |
| 2668 | for (console.log('[NamuFix] loadUnvisibleReses 루프 시작!'); reqId <= lastReqId; reqId += 30) { | |
| 2669 | console.log('[NamuFix] 레스 요청중, 시작 번호는 ' + reqId); | |
| 2670 | namuapi.theseedRequest({ | |
| 2671 | method: "GET", | |
| 2672 | url: "https://" + location.host + "/thread/" + ENV.topicNo + "/" + reqId, | |
| 2673 | onload: function (res) { | |
| 2674 | console.log('[NamuFix] 레스 응답 받음, 시작 번호는 ' + reqId); | |
| 2675 | var parser = new DOMParser(); | |
| 2676 | var doc = parser.parseFromString(res.responseText, "text/html"); | |
| 2677 | var timeTags = doc.querySelectorAll('time'); | |
| 2678 | var resTags = doc.querySelectorAll('.res-wrapper'); | |
| 2679 | for (var i = 0; i < timeTags.length; i++) | |
| 2680 | timeTags[i].dataset.nfFormatThis = "true"; | |
| 2681 | for (var i = 0; i < resTags.length; i++) { | |
| 2682 | var resTag = resTags[i]; | |
| 2683 | var targetTag = document.querySelector('#res-container div.res-loading[data-id="' + resTag.dataset.id + '"]'); | |
| 2684 | if (targetTag == null) continue; | |
| 2685 | targetTag.parentNode.insertBefore(resTag, targetTag.nextSibling); | |
| 2686 | targetTag.parentNode.removeChild(targetTag); | |
| 2687 | } | |
| 2688 | var scriptTagId = 'nf-temp-s' + Date.now() + reqId; | |
| 2689 | var scriptTag = document.createElement('script'); | |
| 2690 | scriptTag.id = scriptTagId; | |
| 2691 | scriptTag.innerHTML = 'nfformattime(); var thisTag = document.querySelector("#' + scriptTagId + '"); thisTag.parentNode.removeChild(thisTag);'; | |
| 2692 | document.body.appendChild(scriptTag); | |
| 2693 | setTimeout(() => { | |
| 2694 | if (document.querySelectorAll('#res-container div.res-loading[data-locked="false"]')) { | |
| 2695 | if (!unvisibleResesAllLoaded) { | |
| 2696 | unvisibleResesAllLoaded = true; | |
| 2697 | console.log("[NamuFix] 모든 보이지 않은 쓰레를 불러옴!"); | |
| 2698 | } | |
| 2699 | } | |
| 2700 | }, 100); | |
| 2701 | } | |
| 2702 | }); | |
| 2703 | } | |
| 2704 | } | |
| 2705 | setTimeout(doLoadUnvisibleReses, 600); | |
| 2706 | } | |
| 2707 | ||
| 2708 | // 아이덴티콘 설정들과 변수들 | |
| 2709 | var isIcon = SET.discussIdenti == 'icon'; | |
| 2710 | var isThreadicLike = SET.discussIdenti == 'headBg'; | |
| 2711 | var isIdenticon = SET.discussIdenti == 'identicon'; | |
| 2712 | var colorDictionary = {}, | |
| 2713 | identiconDictionary = {}; | |
| 2714 | ||
| 2715 | // TO-DO : Rewrite Loop codes | |
| 2716 | // #[0-9]+ 엥커 미리보기 | |
| 2717 | function mouseoverPreview() { | |
| 2718 | var anchors = document.querySelectorAll('.res .r-body .wiki-self-link:not([data-nf-title-processed])'); | |
| 2719 | for (var i = 0; i < anchors.length; i++) { | |
| 2720 | var anchor = anchors[i]; | |
| 2721 | if (!/#[0-9]+$/.test(anchor.href)) { | |
| 2722 | continue; | |
| 2723 | } | |
| 2724 | var anchorDirection = document.querySelector('.r-head .num a[id=\'' + /#([0-9]+)$/.exec(anchor.href)[1] + '\']'); | |
| 2725 | ||
| 2726 | anchor.dataset.target = (anchorDirection) ? anchorDirection.id : ""; | |
| 2727 | anchor.addEventListener('mouseenter', function (evt) { | |
| 2728 | var anchorDirection = document.getElementById(evt.target.dataset.target); | |
| 2729 | var obj = {}; | |
| 2730 | if (anchorDirection == null) { | |
| 2731 | obj = { | |
| 2732 | talker: "?????????", | |
| 2733 | message: "존재하지 않는 메세지입니다.", | |
| 2734 | isFirstAuthor: false, | |
| 2735 | notExists: true | |
| 2736 | } | |
| 2737 | } else if (anchorDirection.parentNode.parentNode.parentNode.parentNode.className.indexOf('res-loading') != -1) { | |
| 2738 | obj = { | |
| 2739 | talker: "NOT LOADED YET", | |
| 2740 | message: "아직 불러오지 않은 메세지입니다.", | |
| 2741 | isFirstAuthor: false, | |
| 2742 | notExists: true | |
| 2743 | } | |
| 2744 | } else { | |
| 2745 | var anchorTarget = anchorDirection.parentNode.parentNode.parentNode; | |
| 2746 | obj = { | |
| 2747 | talker: anchorTarget.querySelector('.r-head > a').textContent, | |
| 2748 | message: anchorTarget.querySelector('.r-body').innerHTML, | |
| 2749 | isFirstAuthor: anchorTarget.querySelector('.r-head.first-author') !== null, | |
| 2750 | notExists: false | |
| 2751 | }; | |
| 2752 | } | |
| 2753 | var headBackground = obj.notExists ? "red" : obj.isFirstAuthor ? "#a5df9f" : "#b3b3b3"; | |
| 2754 | var elem = document.createElement("div"); | |
| 2755 | elem.className = 'nfTopicMessage'; | |
| 2756 | elem.innerHTML = `<div style="font-size: 17px; font-family: sans-serif; background: ${headBackground}; padding: 7px 10px 7px 15px; font-weight: bold;">${obj.talker}</div><div style="padding: 15px; font-size: 11px; font-weight: normal;">${obj.message}</div>`; | |
| 2757 | elem.style.position = 'absolute'; | |
| 2758 | elem.style.color = 'black'; | |
| 2759 | elem.style.borderRadius = '4px'; | |
| 2760 | elem.style.background = obj.notExists ? 'darkred' : '#d9d9d9'; | |
| 2761 | elem.style.zIndex = 3; | |
| 2762 | evt.target.appendChild(elem); | |
| 2763 | evt.target.title = ''; | |
| 2764 | }); | |
| 2765 | anchor.addEventListener('mouseleave', function (evt) { | |
| 2766 | //var obj = JSON.parse(evt.target.dataset.targetMessage); | |
| 2767 | if (evt.target.querySelector('.nfTopicMessage')) { | |
| 2768 | var elemToRemove = evt.target.querySelector('.nfTopicMessage'); | |
| 2769 | elemToRemove.parentNode.removeChild(elemToRemove); | |
| 2770 | } | |
| 2771 | }); | |
| 2772 | ||
| 2773 | anchor.dataset.nfTitleProcessed = true; | |
| 2774 | } | |
| 2775 | } | |
| 2776 | ||
| 2777 | function previewAsQuote(message) { | |
| 2778 | var anchors = message.bodyElement.querySelectorAll('.wiki-self-link:not([data-nf-title-processed])'); | |
| 2779 | for (var i = 0; i < anchors.length; i++) { | |
| 2780 | var anchor = anchors[i]; | |
| 2781 | if (!/#[0-9]+$/.test(anchor.href)) { | |
| 2782 | continue; | |
| 2783 | } | |
| 2784 | var numbericId = /#([0-9]+)$/.exec(anchor.href)[1]; | |
| 2785 | var anchorDirection = document.querySelector('.r-head .num a[id=\'' + numbericId + '\']'); | |
| 2786 | if (anchorDirection == null) continue; | |
| 2787 | var anchorTarget = deserializeResDom(anchorDirection.parentNode.parentNode.parentNode); | |
| 2788 | var talker = anchorTarget.author.name, | |
| 2789 | targetMessage = anchorTarget.bodyElement.innerHTML, | |
| 2790 | talkedAt = anchorTarget.element.querySelector('.r-head > span.pull-right').textContent; | |
| 2791 | var blockquoteId = uniqueID(); | |
| 2792 | var blockquoteElement = document.createElement("blockquote"); | |
| 2793 | blockquoteElement.className = "wiki-quote nf-anchor-preview"; | |
| 2794 | blockquoteElement.innerHTML = targetMessage; | |
| 2795 | blockquoteElement.id = blockquoteId; | |
| 2796 | blockquoteElement.innerHTML += `<div style="text-align: right; font-style: italic;">--#${numbericId}, ${talker}, ${talkedAt}</div>`; | |
| 2797 | message.bodyElement.insertBefore(blockquoteElement, message.bodyElement.firstChild); | |
| 2798 | ||
| 2799 | anchor.dataset.quoteId = blockquoteId; | |
| 2800 | anchor.addEventListener('mouseenter', function (evt) { | |
| 2801 | var quote = document.getElementById(evt.target.dataset.quoteId); | |
| 2802 | quote.style.borderColor = '#CCC #CCC #CCC red !important'; | |
| 2803 | quote.style.boxShadow = '2px 2px 3px orange'; | |
| 2804 | }); | |
| 2805 | anchor.addEventListener('mouseleave', function (evt) { | |
| 2806 | var quote = document.getElementById(evt.target.dataset.quoteId); | |
| 2807 | quote.style.borderColor = ''; | |
| 2808 | quote.style.boxShadow = ''; | |
| 2809 | }) | |
| 2810 | } | |
| 2811 | } | |
| 2812 | var previewFunction; | |
| 2813 | switch (SET.discussAnchorPreviewType) { | |
| 2814 | case 1: | |
| 2815 | previewFunction = mouseoverPreview; | |
| 2816 | break; | |
| 2817 | case 2: | |
| 2818 | GM_addStyle('' + | |
| 2819 | 'blockquote.nf-anchor-preview{' + | |
| 2820 | 'border-color: #CCC #CCC #CCC #FF9900 !important;' + | |
| 2821 | 'background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABTklEQVRoge2ZvYrCQBSF71ulEVLIFIIgC5LOaQRhwcbCKSLbCYtFunmDBTuLEKwEKx9ASOsjWNlYni2WwK5ZV83PTMa9B053IedjJpCcS8RisZ5baaIxG/YhfA+eZ8i+QH84g05SlAofSd9c6Cv2ZVQMIuy0rIfP3OqEj0GsQ2E99KVFuL4fQjYgcN7yPoDjctyAsL97vDzehtjrwHrQaw70ngEYgAEYgAFsA4gR9OaAEwDghMNGYyRqmqkeoIfF7oxLnXcL9CqfqQVAIc49FgBiqMpn+ASe8h2wawawbQawbQaw7f8B4HwrQeR4L0REFKt2AwL/dFvFj9WL6sZ3iVELVazgnQcNaKeDebmKPU00poOu8f1AdzAtvx+oQ+HLX7V9watiWttIwncZINNrboniGAAR0eot+HYaDgJk+vqddBiAiOhj8u42AItlQZ8Z9UiwBSnJVAAAAABJRU5ErkJggg==") !important;' + | |
| 2822 | 'margin: 0.5em 0px !important;' + | |
| 2823 | '}' | |
| 2824 | ); | |
| 2825 | previewFunction = previewAsQuote; | |
| 2826 | break; | |
| 2827 | default: | |
| 2828 | previewFunction = function () {}; | |
| 2829 | } | |
| 2830 | ||
| 2831 | // 인용형식 앵커 미리보기안의 앵커 미리보기 삭제 옵션 설정시 CSS 추가 | |
| 2832 | if (SET.removeNFQuotesInAnchorPreview) { | |
| 2833 | GM_addStyle("blockquote.nf-anchor-preview blockquote.nf-anchor-preview {display: none;}"); | |
| 2834 | } | |
| 2835 | ||
| 2836 | // 아이덴티콘 | |
| 2837 | function identiconLoop(message) { | |
| 2838 | if (/^\/discuss\/(.+?)/.test(location.pathname)) return; | |
| 2839 | var colorHash = isThreadicLike ? new ColorHash({ | |
| 2840 | lightness: Number(SET.discussIdentiLightness), | |
| 2841 | saturation: Number(SET.discussIdentiSaturation) | |
| 2842 | }) : new ColorHash(); | |
| 2843 | if (isIdenticon && document.querySelector('#nf-identicon-css') == null) { | |
| 2844 | var cssContent = 'div.nf-identicon { border: 1px solid #808080; margin: 10px; width: 64px; border: 1px black solid; background: white;} .res.hasNFIdenticon {margin-left: 88px; position: relative; top: -76px;}'; | |
| 2845 | var styleTag = document.createElement("style"); | |
| 2846 | styleTag.innerHTML = cssContent; | |
| 2847 | styleTag.id = "nf-identicon-css"; | |
| 2848 | document.head.appendChild(styleTag); | |
| 2849 | } | |
| 2850 | if (isIcon && message.author.isFirst) return; | |
| 2851 | let n = message.author.name; | |
| 2852 | if (!message.author.isIP) { | |
| 2853 | // 로그인 | |
| 2854 | n = '!ID!' + n; | |
| 2855 | } else { | |
| 2856 | // IP | |
| 2857 | n = '!IP!' + n; | |
| 2858 | } | |
| 2859 | n = SHA1(n); | |
| 2860 | ||
| 2861 | var nColor; | |
| 2862 | if (typeof colorDictionary[n] === 'undefined') { | |
| 2863 | nColor = colorHash.hex(n); | |
| 2864 | colorDictionary[n] = nColor; | |
| 2865 | } else { | |
| 2866 | nColor = colorDictionary[n]; | |
| 2867 | } | |
| 2868 | ||
| 2869 | if (isThreadicLike) { | |
| 2870 | message.element.querySelector('.r-head').style.background = nColor; | |
| 2871 | message.element.querySelector('.r-head').style.color = 'white'; | |
| 2872 | message.element.querySelector('.r-head > a').style.color = 'white'; | |
| 2873 | message.element.querySelector('.r-head .num a').style.color = 'white'; | |
| 2874 | } else if (isIcon) { | |
| 2875 | var a = message.element.querySelector('.r-head > a'); | |
| 2876 | var span = document.createElement("span"); | |
| 2877 | span.style.background = nColor; | |
| 2878 | span.style.color = nColor; | |
| 2879 | span.style.marginLeft = '1em'; | |
| 2880 | span.innerHTML = '__'; | |
| 2881 | a.parentNode.insertBefore(span, a.nextSibling); | |
| 2882 | } else if (isIdenticon) { | |
| 2883 | message.element.className += " hasNFIdenticon"; | |
| 2884 | let identicon = document.createElement("div"); | |
| 2885 | identicon.className = "nf-identicon"; | |
| 2886 | identicon.style.position = 'relative'; | |
| 2887 | identicon.innerHTML = '<div class="identicon-pointer" style="width: 0px;height: 0px;position: absolute;border-top: 20px solid transparent;border-right: 15px solid #B0D3AD;left: 64px;border-bottom: 20px solid transparent;"></div><a><img style="width: 64px; height: 64px;"></img></a>'; | |
| 2888 | identicon.querySelector("img").dataset.hash = n; | |
| 2889 | identicon.querySelector("a").dataset.hash = n; | |
| 2890 | identicon.querySelector("a").href = "#NothingToLink"; | |
| 2891 | identicon.querySelector("a").addEventListener('click', async function (evt) { | |
| 2892 | evt.preventDefault(); | |
| 2893 | ||
| 2894 | await SET.load(); | |
| 2895 | var h = evt.target.dataset.hash; | |
| 2896 | if (typeof SET.customIdenticons[h] !== 'undefined') { | |
| 2897 | // custom identicon exists | |
| 2898 | if (confirm('이미 이미지가 설정되어 있습니다. 제거할까요?')) { | |
| 2899 | delete SET.customIdenticons[h]; | |
| 2900 | await SET.save(); | |
| 2901 | alert('설정됐습니다. 새로고침시 적용됩니다'); | |
| 2902 | } | |
| 2903 | } else { | |
| 2904 | if (!confirm('이 아이디 또는 닉네임에 기존 아이덴티콘 대신 다른 이미지를 설정할 수 있습니다.\n설정할까요?')) return; | |
| 2905 | // doesn't exists | |
| 2906 | getFile(function (files, finish) { | |
| 2907 | if (files.length < 0) { | |
| 2908 | alert('선택된 파일이 없습니다.') | |
| 2909 | if (isLastItem) finish(); | |
| 2910 | return; | |
| 2911 | } | |
| 2912 | if (files.length > 1) { | |
| 2913 | alert('한 개의 파일만 선택해주세요.'); | |
| 2914 | finish(); | |
| 2915 | return; | |
| 2916 | } | |
| 2917 | var file = files[0]; | |
| 2918 | if (file) { | |
| 2919 | var reader = new FileReader(); | |
| 2920 | reader.onload = async function (evt) { | |
| 2921 | SET.customIdenticons[h] = reader.result; | |
| 2922 | await SET.save(); | |
| 2923 | alert('설정됐습니다. 새로고침시 적용됩니다'); | |
| 2924 | finish(); | |
| 2925 | }; | |
| 2926 | reader.readAsDataURL(file); | |
| 2927 | } | |
| 2928 | }); | |
| 2929 | } | |
| 2930 | }); | |
| 2931 | if (typeof identiconDictionary[n] === 'undefined' && typeof SET.customIdenticons[n] !== 'undefined') | |
| 2932 | identiconDictionary[n] = SET.customIdenticons[n]; | |
| 2933 | if (typeof identiconDictionary[n] === 'undefined') { | |
| 2934 | switch(SET.identiconLibrary) { | |
| 2935 | case 'gravatar': | |
| 2936 | identiconDictionary[n] = "https://secure.gravatar.com/avatar/" + n.substring(0, 32) + "?s=64&d=identicon" | |
| 2937 | break; | |
| 2938 | case 'gravatar-monster': | |
| 2939 | identiconDictionary[n] = "https://secure.gravatar.com/avatar/" + n.substring(0, 32) + "?s=64&d=monsterid" | |
| 2940 | break; | |
| 2941 | case 'identicon': | |
| 2942 | identiconDictionary[n] = "data:image/svg+xml;base64," + new Identicon(n, {size: 64, format: 'svg'}).toString(); | |
| 2943 | break; | |
| 2944 | default: | |
| 2945 | case 'jdenticon': | |
| 2946 | identiconDictionary[n] = "data:image/svg+xml;base64," + btoa(jdenticon.toSvg(n, 64)); | |
| 2947 | break; | |
| 2948 | } | |
| 2949 | } | |
| 2950 | let identiconImage = identiconDictionary[n]; | |
| 2951 | identicon.querySelector('img').src = identiconImage; | |
| 2952 | message.element.parentNode.insertBefore(identicon, message.element); | |
| 2953 | message.element.querySelector('.r-head').style.borderLeft = 'none'; | |
| 2954 | ||
| 2955 | identicon.querySelector('.identicon-pointer').style.borderRight = '15px solid ' + getComputedStyle(message.element.querySelector('.r-head')).backgroundColor; | |
| 2956 | if (message.element.parentNode.dataset.id != 1) { | |
| 2957 | message.element.parentNode.style.marginTop = '-76px'; | |
| 2958 | identicon.style.marginTop = '-66px'; | |
| 2959 | } | |
| 2960 | } | |
| 2961 | } | |
| 2962 | ||
| 2963 | function checkIP(message) { | |
| 2964 | if (!message.author.isIP) | |
| 2965 | return; | |
| 2966 | var span = document.createElement("span"); | |
| 2967 | span.style.color = "red"; | |
| 2968 | message.nfHeadspan.appendChild(span); | |
| 2969 | span.innerHTML = "[(IP 확인중: IP정보 조회중)]"; | |
| 2970 | // get ip info | |
| 2971 | getIpInfo(message.author.name, function (resObj) { | |
| 2972 | if (resObj !== null) { | |
| 2973 | var country = resObj.country; | |
| 2974 | var countryName = korCountryNames[country.toUpperCase()] || country; | |
| 2975 | var isp = resObj.org; | |
| 2976 | getFlagIcon(country.toLowerCase(), async function (data) { | |
| 2977 | let whoisResult = SET.checkWhoisNetTypeOnDiscuss ? await (new Promise((resolve, reject) => { | |
| 2978 | getIpWhois(message.author.name, whoisRes => resolve(whoisRes)); | |
| 2979 | })) : null; | |
| 2980 | let whoisNetType = false; | |
| 2981 | if (whoisResult && whoisResult.success && !whoisResult.raw) { | |
| 2982 | if (whoisResult.result.korean.ISP && whoisResult.result.korean.ISP.netinfo && whoisResult.result.korean.ISP.netinfo.netType) | |
| 2983 | whoisNetType = whoisResult.result.korean.ISP.netinfo.netType | |
| 2984 | else if (whoisResult.result.korean.user && whoisResult.result.korean.user.netinfo && whoisResult.result.korean.user.netinfo.netType) | |
| 2985 | whoisNetType = whoisResult.result.korean.user.netinfo.netType | |
| 2986 | } | |
| 2987 | span.innerHTML = `[<img src="${data}" style="height: 0.9rem;" title="${countryName}"></img> ${isp}${await checkVPNGateIP(message.author.name) ? " (VPNGATE)" : ""}]${whoisNetType ? "<span style=\"color: darkred;\">[" + whoisNetType + "]</span>" : ""}<a href="#" class="get-whois">[WHOIS]</a>`; | |
| 2988 | span.querySelector('a.get-whois').addEventListener('click', function (evt) { | |
| 2989 | evt.preventDefault(); | |
| 2990 | whoisPopup(message.author.name); | |
| 2991 | }); | |
| 2992 | }); | |
| 2993 | } else { | |
| 2994 | span.innerHTML = "[IP조회실패]<a href=\"#\" class=\"get-whois\">[WHOIS]</a>" | |
| 2995 | span.querySelector('a.get-whois').addEventListener('click', function (evt) { | |
| 2996 | evt.preventDefault(); | |
| 2997 | whoisPopup(message.author.name); | |
| 2998 | }) | |
| 2999 | } | |
| 3000 | }); | |
| 3001 | } | |
| 3002 | ||
| 3003 | function quickBlockLoop(i) { | |
| 3004 | let blockAnchor = document.createElement("a"); | |
| 3005 | blockAnchor.href = "#"; | |
| 3006 | blockAnchor.textContent = "[차단]"; | |
| 3007 | blockAnchor.addEventListener('click', (evt) => { | |
| 3008 | evt.preventDefault(); | |
| 3009 | quickBlockPopup({ | |
| 3010 | author: i.author, | |
| 3011 | defaultDuration: SET.quickBlockDefaultDuration, | |
| 3012 | defaultReason: SET.quickBlockReasonTemplate_discuss.replace(/\$\{host\}/g, location.host) | |
| 3013 | .replace(/\$\{threadNo\}/g, ENV.topicNo) | |
| 3014 | .replace(/\$\{messageNo\}/g, i.no) | |
| 3015 | }); | |
| 3016 | }); | |
| 3017 | i.nfHeadspan.appendChild(blockAnchor); | |
| 3018 | } | |
| 3019 | ||
| 3020 | let emphasizeResStyle = document.createElement("style"); | |
| 3021 | let emphasizedHash = null; | |
| 3022 | document.head.appendChild(emphasizeResStyle); | |
| 3023 | ||
| 3024 | function emphasizeWhenMouseoverLoop(i) { | |
| 3025 | let usernameHash = SHA1((i.author.isIP ? '!IP!' : '!ID!') + i.author.name); | |
| 3026 | i.element.dataset.usernameHash = usernameHash; | |
| 3027 | let linkAnchor = document.createElement("a"); | |
| 3028 | linkAnchor.href = "#"; | |
| 3029 | linkAnchor.className = "nf-emp-thread-link" | |
| 3030 | linkAnchor.innerHTML = '[강조]' // ionicon에 압정 아이콘이 안 보인다. 이런데 쓰면 딱인데... | |
| 3031 | linkAnchor.title = "이 사용자의 쓰레드를 강조합니다." | |
| 3032 | linkAnchor.addEventListener("click", (evt) => { | |
| 3033 | evt.preventDefault(); | |
| 3034 | if (emphasizedHash !== usernameHash) { | |
| 3035 | emphasizedHash = usernameHash; | |
| 3036 | emphasizeResStyle.innerHTML = `.res[data-username-hash="${usernameHash}"] .r-head {background: #8a8a8a !important} .res[data-username-hash="${usernameHash}"] .r-head.first-author {background: #8DAD8A !important} .res[data-username-hash="${usernameHash}"] .nf-emp-thread-link {color: red;} .res:not([data-username-hash="${usernameHash}"]) {filter: blur(1.5px);}` | |
| 3037 | } else { | |
| 3038 | emphasizedHash = null; | |
| 3039 | emphasizeResStyle.innerHTML = ""; | |
| 3040 | } | |
| 3041 | console.log(emphasizedHash); | |
| 3042 | }); | |
| 3043 | i.nfRightHeadSpan.appendChild(linkAnchor); | |
| 3044 | i.element.addEventListener('mouseover', () => { | |
| 3045 | if(emphasizedHash === null) emphasizeResStyle.innerHTML = `.res[data-username-hash="${usernameHash}"] .r-head {background: #8a8a8a !important} .res[data-username-hash="${usernameHash}"] .r-head.first-author {background: #8DAD8A !important}` | |
| 3046 | }) | |
| 3047 | i.element.addEventListener('mouseout', () => { | |
| 3048 | if(emphasizedHash === null) emphasizeResStyle.innerHTML = '' | |
| 3049 | }); | |
| 3050 | } | |
| 3051 | ||
| 3052 | function deserializeResDom(resElement) { | |
| 3053 | let userLink = resElement.querySelector('.r-head > a'), | |
| 3054 | anchor = resElement.querySelector('.r-head .num > a'); | |
| 3055 | return { | |
| 3056 | element: resElement, | |
| 3057 | no: anchor.id, | |
| 3058 | author: { | |
| 3059 | name: resElement.querySelector('.r-head > a').textContent.trim(), | |
| 3060 | isFirst: resElement.querySelector('.r-head').className.includes('first-author'), | |
| 3061 | isIP: resElement.querySelector('.r-head > a').href.includes('/contribution/ip') | |
| 3062 | }, | |
| 3063 | bodyElement: resElement.querySelector('.r-body'), | |
| 3064 | get nfHeadspan() { | |
| 3065 | let headspan = resElement.querySelector('.nf-headinfo') | |
| 3066 | if (!headspan) { | |
| 3067 | headspan = document.createElement("span"); | |
| 3068 | headspan.className = "nf-headinfo"; | |
| 3069 | userLink.parentNode.insertBefore(headspan, userLink.nextSibling); | |
| 3070 | headspan.style.marginLeft = "1em"; | |
| 3071 | } | |
| 3072 | delete this.nfHeadspan; | |
| 3073 | return this.nfHeadspan = headspan; | |
| 3074 | }, | |
| 3075 | get nfRightHeadSpan() { | |
| 3076 | let rightHeadSpan = resElement.querySelector('.nf-headinfo-right') | |
| 3077 | if (!rightHeadSpan) { | |
| 3078 | rightHeadSpan = document.createElement("span"); | |
| 3079 | rightHeadSpan.className = "nf-headinfo-right" | |
| 3080 | let time = resElement.querySelector('.r-head > .pull-right > time'); | |
| 3081 | time.parentNode.insertBefore(rightHeadSpan, time.nextSibling); | |
| 3082 | rightHeadSpan.style.marginLeft = "0.25rem"; | |
| 3083 | } | |
| 3084 | delete this.nfRightHeadSpan; | |
| 3085 | return this.nfRightHeadSpan = rightHeadSpan; | |
| 3086 | } | |
| 3087 | } | |
| 3088 | } | |
| 3089 | ||
| 3090 | function discussLoop() { | |
| 3091 | let handles = { | |
| 3092 | "preview": previewFunction, | |
| 3093 | "identicon": identiconLoop | |
| 3094 | }; | |
| 3095 | if (SET.addQuickBlockLink) handles.quickBlock = quickBlockLoop | |
| 3096 | if (SET.lookupIPonDiscuss) handles.checkIp = checkIP; | |
| 3097 | if (SET.emphasizeResesWhenMouseover) handles.emphasizeReses = emphasizeWhenMouseoverLoop; | |
| 3098 | let messages = document.querySelectorAll('.res-wrapper:not(.res-loading) > .res:not([nf-looped])'); | |
| 3099 | let tmpcnt = 0; //fordebug | |
| 3100 | for (let i of messages) | |
| 3101 | for (let j in handles) { | |
| 3102 | let resObj = deserializeResDom(i); | |
| 3103 | if (i.dataset["nfLoop_" + j] !== "yes") { | |
| 3104 | i.dataset["nfLoop_" + j] = "yes"; | |
| 3105 | setTimeout(() => { | |
| 3106 | handles[j](resObj); | |
| 3107 | }, 0); | |
| 3108 | } | |
| 3109 | } | |
| 3110 | if (SET.notifyForUnvisibleThreads) { | |
| 3111 | Notification.requestPermission().then(() => { | |
| 3112 | if (document.visibilityState === 'hidden') { | |
| 3113 | if (SET.loadUnvisibleReses && !unvisibleResesAllLoaded) | |
| 3114 | return; | |
| 3115 | let n = new Notification(`토론 알림 : ${ENV.topicTitle}`, { | |
| 3116 | body: '토론을 확인해주세요. 알람을 원하지 않으시면 NamuFix 설정에서 비활성화할 수 있습니다.', | |
| 3117 | icon: `/favicon.ico` | |
| 3118 | }) | |
| 3119 | } | |
| 3120 | }); | |
| 3121 | } | |
| 3122 | } | |
| 3123 | discussLoop(); | |
| 3124 | var observer = new MutationObserver(discussLoop); | |
| 3125 | observer.observe(document.querySelector("#res-container"), { | |
| 3126 | childList: true | |
| 3127 | }); | |
| 3128 | ||
| 3129 | } else if (ENV.IsUserContribsPage) { | |
| 3130 | function insertBeforeTable(element) { | |
| 3131 | var bread = document.querySelector("article > ol.breadcrumb.link-nav, body.Liberty .wiki-article ol.breadcrumb.link-nav"); | |
| 3132 | bread.parentNode.insertBefore(element, bread); | |
| 3133 | } | |
| 3134 | ||
| 3135 | function makeHeatTable(times) { | |
| 3136 | try { | |
| 3137 | // 가공 | |
| 3138 | var maps = {}; // { day: {0: int, 1: int}, .... } | |
| 3139 | var maxValue = 0; | |
| 3140 | for (var i = 0; i < 7; i++) { | |
| 3141 | maps[i] = {}; | |
| 3142 | for (var ii = 0; ii < 24; ii++) { | |
| 3143 | maps[i][ii] = 0; | |
| 3144 | } | |
| 3145 | } | |
| 3146 | for (var i = 0; i < times.length; i++) { | |
| 3147 | var ti = times[i]; | |
| 3148 | var v = ++maps[ti.getDay()][ti.getHours()]; | |
| 3149 | if (maxValue < v) maxValue = v; | |
| 3150 | } | |
| 3151 | ||
| 3152 | // 표 생성 | |
| 3153 | var table = document.createElement("table"); | |
| 3154 | var headTr = document.createElement("tr"); | |
| 3155 | headTr.innerHTML = '<th>요일</th>'; | |
| 3156 | table.appendChild(headTr); | |
| 3157 | var dayNames = ['일', '월', '화', '수', '목', '금', '토']; | |
| 3158 | for (var i = 0; i < 7; i++) { | |
| 3159 | var tr = document.createElement("tr"); | |
| 3160 | tr.innerHTML += `<th>${dayNames[i]}</th>`; | |
| 3161 | for (var ii = 0; ii < 24; ii++) { | |
| 3162 | var td = document.createElement("td"); | |
| 3163 | td.innerHTML = ' ' | |
| 3164 | td.style.background = `rgba(61,0,61,${maps[i][ii] / maxValue})`; | |
| 3165 | if (i == 0) { | |
| 3166 | function twoDigits(a) { | |
| 3167 | var p = String(a); | |
| 3168 | return p.length == 1 ? '0' + p : p; | |
| 3169 | } | |
| 3170 | headTr.innerHTML += `<th>${twoDigits(ii)}:00 ~ ${twoDigits(ii + 1)}:00</th>`; | |
| 3171 | } | |
| 3172 | tr.appendChild(td); | |
| 3173 | } | |
| 3174 | table.appendChild(tr); | |
| 3175 | } | |
| 3176 | return table; | |
| 3177 | } catch (err) { | |
| 3178 | alert(err.message); | |
| 3179 | } | |
| 3180 | } | |
| 3181 | ||
| 3182 | var p = document.createElement("p"); | |
| 3183 | p.innerHTML += '<style>.contInfo { border-collapse: collapse; border: 1px solid black; padding: 2px;} #contInfo td {padding: 3px;} #contInfo td:nth-child(2) {border-left: 1px solid black;}</style>'; | |
| 3184 | var ipPattern = /\/ip\/([a-zA-Z0-9:\.]+)\/(?:document|discuss)(?:#.+|)$/; | |
| 3185 | if (ipPattern.test(location.href)) { | |
| 3186 | // ip | |
| 3187 | // check ip | |
| 3188 | var ip = ipPattern.exec(location.href)[1]; | |
| 3189 | var ipInfo = document.createElement("p"); | |
| 3190 | ipInfo.innerHTML = '<div style="border: 1px black solid; padding: 2px;">IP 관련 정보를 조회중입니다. 잠시만 기다려주세요.</div>' | |
| 3191 | insertBeforeTable(ipInfo); | |
| 3192 | getIpInfo(ip, function (resObj) { | |
| 3193 | var country = resObj.country; | |
| 3194 | var countryName = korCountryNames[country.toUpperCase()] || country.toUpperCase(); | |
| 3195 | var isp = resObj.org; | |
| 3196 | getFlagIcon(country.toLowerCase(), async function (countryIcon) { | |
| 3197 | ipInfo.innerHTML = `<table class="contInfo"> | |
| 3198 | <tbody> | |
| 3199 | <tr><td>국가</td><td><img src=\"${countryIcon}\" style=\"height: 0.9rem;\"></img> ${countryName}</td></tr> | |
| 3200 | <tr><td>통신사</td><td>${isp}</td></tr> | |
| 3201 | <tr><td>VPNGATE</td><td>${await checkVPNGateIP(ip) ? "<span style=\"color: black;\">Y</span>" : "N"}</td></tr> | |
| 3202 | <tr><td>KISA WHOIS</td><td><a href="#" class="get-whois">조회하기</a></td></tr> | |
| 3203 | <tr class="nf_ipblockchecking"><td colspan="2">IP 차단기록을 검색하고 있습니다... 잠시만 기다려주세요.</td></tr> | |
| 3204 | <tr class="nf-quickblock-row"><td>빠른차단</td><td><a class="nf-quickblock">빠른차단</a></td></tr> | |
| 3205 | </tbody> | |
| 3206 | <tfoot> | |
| 3207 | <tr><td colspan="2" style="border-top: 1px solid black;">기술적인 한계로, VPNGATE 여부는 "현재 VPNGATE VPN인가?"의 여부이지, "작성 당시에 VPNGATE VPN인가?"의 여부가 아닙니다.<br> | |
| 3208 | 또한 외국 IP의 차단기록의 경우 /32 마스크만 검색하며, 사측의 비공개 차단은 검색이 불가능합니다.</td></tr> | |
| 3209 | </foot> | |
| 3210 | </table>`; | |
| 3211 | ipInfo.querySelector('a.get-whois').addEventListener('click', function (evt) { | |
| 3212 | evt.preventDefault(); | |
| 3213 | whoisPopup(ip); | |
| 3214 | }) | |
| 3215 | ipInfo.querySelector('a.nf-quickblock').addEventListener('click', (evt) => { | |
| 3216 | evt.preventDefault(); | |
| 3217 | quickBlockPopup({ | |
| 3218 | author: { | |
| 3219 | name: ip, | |
| 3220 | isIP: true | |
| 3221 | }, | |
| 3222 | defaultDuration: SET.quickBlockDefaultDuration, | |
| 3223 | defaultReason: "긴급조치" | |
| 3224 | }) | |
| 3225 | }); | |
| 3226 | if(!SET.addQuickBlockLink) | |
| 3227 | ipInfo.querySelector('.nf-quickblock-row').style.display = 'none'; | |
| 3228 | function displayIsBlocked(mask, tbody) { | |
| 3229 | let row = document.createElement("tr"); | |
| 3230 | row.innerHTML = "<td>IP차단기록 (" + mask + ")</td><td class=\"nf_isipblocked\">조회중입니다...</td>"; | |
| 3231 | tbody.appendChild(row); | |
| 3232 | namuapi.searchBlockHistory({ | |
| 3233 | query: ip + mask, | |
| 3234 | isAuthor: false | |
| 3235 | }, function (result) { | |
| 3236 | var filtered = result.filter(function (v) { | |
| 3237 | return v.blocked === ip + mask; | |
| 3238 | }); | |
| 3239 | if (filtered.length == 0) { | |
| 3240 | return row.querySelector('.nf_isipblocked').innerHTML = "차단기록 없음."; | |
| 3241 | } | |
| 3242 | var actType = filtered[0].type; | |
| 3243 | if (actType == "blockIP") { | |
| 3244 | row.querySelector('.nf_isipblocked').innerHTML = filtered[0].blocker + '에 의해 ' + (filtered[0].duration || '') + ' <span style="color:red">차단됨.</span> (일시 : ' + formatDateTime(filtered[0].at) + ', 이유 : ' + filtered[0].reason + ')'; | |
| 3245 | } else if (actType == "unblockIP") { | |
| 3246 | row.querySelector('.nf_isipblocked').innerHTML = filtered[0].blocker + '에 의해 ' + (filtered[0].duration || '') + ' <span style="color:green">차단이 해제됨.</span> (일시 : ' + formatDateTime(filtered[0].at) + ')'; | |
| 3247 | } else { | |
| 3248 | row.querySelector('.nf_isipblocked').innerHTML = "??? 기록 검색중 오류가 발생함."; | |
| 3249 | } | |
| 3250 | }); | |
| 3251 | } | |
| 3252 | getIpWhois(ip, (whoisRes) => { | |
| 3253 | ipInfo.querySelector('.nf_ipblockchecking').parentNode.removeChild(ipInfo.querySelector('.nf_ipblockchecking')); | |
| 3254 | if(!whoisRes.success || whoisRes.raw) { | |
| 3255 | displayIsBlocked('/32', ipInfo.querySelector('tbody')) | |
| 3256 | } else { | |
| 3257 | let prefixes = []; | |
| 3258 | if(whoisRes.result.korean.ISP && whoisRes.result.korean.ISP.netinfo && whoisRes.result.korean.ISP.netinfo.prefix) | |
| 3259 | prefixes = prefixes.concat(whoisRes.result.korean.ISP.netinfo.prefix.split('+')); | |
| 3260 | if(whoisRes.result.korean.user && whoisRes.result.korean.user.netinfo && whoisRes.result.korean.user.netinfo.prefix) | |
| 3261 | prefixes = prefixes.concat(whoisRes.result.korean.user.netinfo.prefix.split('+')); | |
| 3262 | prefixes = prefixes.filter((v,i,s) => s.indexOf(v) === i); // https://stackoverflow.com/a/14438954 | |
| 3263 | if(!prefixes.includes('/32')) prefixes.push('/32'); | |
| 3264 | let delay = 0; | |
| 3265 | for(let prefix of prefixes) { | |
| 3266 | setTimeout(() => {displayIsBlocked(prefix, ipInfo.querySelector('tbody'));}, (delay++) * SET.ipBlockHistoryCheckDelay + 1); | |
| 3267 | } | |
| 3268 | } | |
| 3269 | }); | |
| 3270 | }); | |
| 3271 | }); | |
| 3272 | } else { | |
| 3273 | // parse username | |
| 3274 | var userIdPattern = /^\/contribution\/author\/(.+?)\/(?:document|discuss)/; | |
| 3275 | var userId = userIdPattern.exec(location.pathname)[1]; | |
| 3276 | ||
| 3277 | // block user link | |
| 3278 | if(SET.addQuickBlockLink) { | |
| 3279 | let quickBlockLink = document.createElement("div"); | |
| 3280 | quickBlockLink.innerHTML = '<div style="border: 1px black solid; padding: 2px;">빠른차단 : <a href="#">[차단]</a></div>' | |
| 3281 | quickBlockLink.querySelector('a').addEventListener('click', (evt)=>{ | |
| 3282 | evt.preventDefault(); | |
| 3283 | quickBlockPopup({ | |
| 3284 | author: { | |
| 3285 | name: userId, | |
| 3286 | isIP: false | |
| 3287 | }, | |
| 3288 | defaultDuration: SET.quickBlockDefaultDuration, | |
| 3289 | defaultReason: "긴급조치" | |
| 3290 | }) | |
| 3291 | }) | |
| 3292 | insertBeforeTable(quickBlockLink); | |
| 3293 | } | |
| 3294 | // user blockhistory | |
| 3295 | var userInfo = document.createElement("p"); | |
| 3296 | userInfo.innerHTML = '<div style="border: 1px black solid; padding: 2px;" class="nf_blockhistory">차단 기록 조회중...</div>' | |
| 3297 | insertBeforeTable(userInfo); | |
| 3298 | namuapi.searchBlockHistory({ | |
| 3299 | query: userId, | |
| 3300 | isAuthor: false | |
| 3301 | }, function (result) { | |
| 3302 | var filtered = result.filter(function (v) { | |
| 3303 | return v.blocked == userId | |
| 3304 | }); | |
| 3305 | if (filtered.length == 0) { | |
| 3306 | return userInfo.querySelector('.nf_blockhistory').innerHTML = "차단기록 없음."; | |
| 3307 | } | |
| 3308 | userInfo.querySelector('.nf_blockhistory').innerHTML = '<style>.nf_blockhistory p {margin: 1px; padding: 0px;}</style>'; | |
| 3309 | for(var i = 0; i < filtered.length; i++) { | |
| 3310 | var filteredItem = filtered[i]; | |
| 3311 | var actType = filteredItem.type; | |
| 3312 | if (actType == "blockUser") { | |
| 3313 | userInfo.querySelector(i == 0 ? '.nf_blockhistory' : '.nf_blockhistory_rest').innerHTML += '<p>' + filteredItem.blocker + '에 의해 ' + (filteredItem.duration || '') + ' <span style="color:red">차단됨.</span> (일시 : ' + formatDateTime(filteredItem.at) + ', 이유 : ' + filteredItem.reason + ')' + '</p>'; | |
| 3314 | } else if (actType == "unblockUser") { | |
| 3315 | userInfo.querySelector(i == 0 ? '.nf_blockhistory' : '.nf_blockhistory_rest').innerHTML += '<p>' + filteredItem.blocker + '에 의해 ' + (filteredItem.duration || '') + ' <span style="color:green">차단이 해제됨.</span> (일시 : ' + formatDateTime(filteredItem.at) + ')' + '</p>'; | |
| 3316 | } | |
| 3317 | if (i == 0) { | |
| 3318 | userInfo.querySelector('.nf_blockhistory').innerHTML += '<a href="#" id="nf_more_blockhistory">[차단기록 더 보기]</a><div class="nf_blockhistory_rest" style="display: none;"></div>' | |
| 3319 | userInfo.querySelector('#nf_more_blockhistory').addEventListener('click', (evt) => { | |
| 3320 | evt.preventDefault(); | |
| 3321 | document.querySelector('.nf_blockhistory_rest').style.display = ''; | |
| 3322 | evt.target.style.display = 'none'; | |
| 3323 | }) | |
| 3324 | } else if (i == filtered.length - 1) { | |
| 3325 | userInfo.querySelector('.nf_blockhistory_rest').innerHTML += '<a href="#" id="nf_less_blockhistory">[숨기기]</a>'; | |
| 3326 | userInfo.querySelector('#nf_less_blockhistory').addEventListener('click', (evt) => { | |
| 3327 | evt.preventDefault(); | |
| 3328 | document.querySelector('.nf_blockhistory_rest').style.display = 'none'; | |
| 3329 | userInfo.querySelector('#nf_more_blockhistory').style.display = ''; | |
| 3330 | }) | |
| 3331 | } | |
| 3332 | } | |
| 3333 | }); | |
| 3334 | } | |
| 3335 | ||
| 3336 | if (/\/document(?:#.+|)$/.test(location.href)) { | |
| 3337 | var rows = document.querySelectorAll('table tbody tr'); | |
| 3338 | var contCount = 0, | |
| 3339 | contTotalBytes = 0, | |
| 3340 | contDocuments = 0, | |
| 3341 | deletedDocuments = [], | |
| 3342 | createdDocuments = [], | |
| 3343 | documentNameBefore = "", | |
| 3344 | contributedAt = []; | |
| 3345 | var documents = []; | |
| 3346 | for (var i = 0; i < rows.length; i++) { | |
| 3347 | var row = rows[i]; | |
| 3348 | if (row.querySelector('a')) { | |
| 3349 | var documentName = row.querySelector('a').getAttribute('href'); | |
| 3350 | documentNameBefore = documentName; | |
| 3351 | var contributedBytes = row.querySelector('span.f_r > span').innerHTML; | |
| 3352 | var negativeContribution = /^\-[0-9]+/.test(contributedBytes); | |
| 3353 | if (/^\+[0-9]+/.test(contributedBytes)) contributedBytes = contributedBytes.substring(contributedBytes.indexOf('+')); | |
| 3354 | contributedBytes = Number(contributedBytes); | |
| 3355 | if (documents.indexOf(documentName) == -1) documents.push(documentName); | |
| 3356 | contCount++; | |
| 3357 | if (negativeContribution) | |
| 3358 | contTotalBytes -= contributedBytes; | |
| 3359 | else | |
| 3360 | contTotalBytes += contributedBytes; | |
| 3361 | ||
| 3362 | if (row.querySelector('time')) { | |
| 3363 | contributedAt.push(new Date(row.querySelector('time').getAttribute("datetime"))); | |
| 3364 | } | |
| 3365 | } else if (row.querySelector('i')) { | |
| 3366 | var italicText = row.querySelector('i').innerHTML; | |
| 3367 | if (italicText == '(새 문서)' && createdDocuments.indexOf(documentNameBefore) == -1) createdDocuments.push(documentNameBefore); | |
| 3368 | else if (italicText == '(삭제)' && deletedDocuments.indexOf(documentNameBefore) == -1) deletedDocuments.push(documentNameBefore); | |
| 3369 | } | |
| 3370 | } | |
| 3371 | p.innerHTML += `<table class="contInfo"> | |
| 3372 | <tfoot> | |
| 3373 | <tr><td colspan="2" style="border-top: 1px solid black;">최근 30일간의 데이터만 반영되었으므로, 최근 30일 간의 기여 정보입니다.</td></tr> | |
| 3374 | </foot> | |
| 3375 | <tbody> | |
| 3376 | <tr><td>총 기여 횟수</td><td>${contCount}회</td></tr> | |
| 3377 | <tr><td>기여한 바이트 총합</td><td>${contTotalBytes}자</td></tr> | |
| 3378 | <tr><td>총 기여한 문서 (ACL 변경, 문서 이동 포함) 수</td><td>${documents.length}개</td></tr> | |
| 3379 | <tr><td>삭제한 문서 수</td><td>${deletedDocuments.length}개</td></tr> | |
| 3380 | <tr><td>새로 만든 문서 수</td><td>${createdDocuments.length}개</td></tr> | |
| 3381 | <tr><td>한 문서당 평균 기여 바이트</td><td>${(contTotalBytes / documents.length)}자</td></tr> | |
| 3382 | <tr><td>시간대별 기여/활동 횟수 분포(문서 기여)</td><td><a href="#NothingToLink" id="punch">여기를 눌러 확인</a></td></tr> | |
| 3383 | </tbody> | |
| 3384 | </table>`; | |
| 3385 | p.querySelector('a#punch').addEventListener('click', function (evt) { | |
| 3386 | evt.preventDefault(); | |
| 3387 | ||
| 3388 | var win = TooSimplePopup(); | |
| 3389 | win.title('시간대별 기여/활동 횟수 분포(문서 기여)'); | |
| 3390 | win.content(function (element) { | |
| 3391 | element.appendChild(makeHeatTable(contributedAt)); | |
| 3392 | }); | |
| 3393 | win.button('확인', win.close); | |
| 3394 | }) | |
| 3395 | } else if (/\/discuss(?:#.+|)$/.test(location.href)) { | |
| 3396 | function standardDeviation(numbers) { | |
| 3397 | var total = 0; | |
| 3398 | for (var i = 0; i < numbers.length; i++) { | |
| 3399 | total += numbers[i]; | |
| 3400 | } | |
| 3401 | var avg = total / numbers.length; | |
| 3402 | var temp1 = 0; | |
| 3403 | for (var i = 0; i < numbers.length; i++) { | |
| 3404 | temp1 += Math.pow(numbers[i] - avg, 2); | |
| 3405 | } | |
| 3406 | temp1 /= numbers.length; | |
| 3407 | return Math.sqrt(temp1); | |
| 3408 | } | |
| 3409 | var rows = document.querySelectorAll('table tbody tr'); | |
| 3410 | var docuAndTalks = {}; | |
| 3411 | var talkedAt = []; | |
| 3412 | for (var i = 0; i < rows.length; i++) { | |
| 3413 | var row = rows[i]; | |
| 3414 | if (row.querySelectorAll('a').length == 0) continue; | |
| 3415 | var docuNow = rows[i].querySelector('a').getAttribute('href').replace(/#[0-9]+$/, ''); | |
| 3416 | docuNow = /^\/thread\/(.+)(?:#[0-9]+|)/.exec(docuNow)[1]; | |
| 3417 | if (docuAndTalks[docuNow]) { | |
| 3418 | docuAndTalks[docuNow]++; | |
| 3419 | } else { | |
| 3420 | docuAndTalks[docuNow] = 1; | |
| 3421 | } | |
| 3422 | if (row.querySelector('time')) { | |
| 3423 | talkedAt.push(new Date(row.querySelector('time').getAttribute("datetime"))); | |
| 3424 | } | |
| 3425 | } | |
| 3426 | var totalTalks = 0, | |
| 3427 | avgTalks = 0, | |
| 3428 | discussCount = 0, | |
| 3429 | Talks = []; | |
| 3430 | for (var i in docuAndTalks) { | |
| 3431 | totalTalks += docuAndTalks[i]; | |
| 3432 | Talks.push(docuAndTalks[i]); | |
| 3433 | } | |
| 3434 | discussCount = Object.keys(docuAndTalks).length; | |
| 3435 | avgTalks = totalTalks / discussCount; | |
| 3436 | p.innerHTML += `<table class="contInfo"> | |
| 3437 | <tfoot> | |
| 3438 | <tr><td colspan="2" style="border-top: 1px solid black;">최근 30일 간의 토론 정보만 반영되었으므로, 최근 30일 간의 토론 정보입니다.</td></tr> | |
| 3439 | </tfoot> | |
| 3440 | <tbody> | |
| 3441 | <tr><td>총 발언 수</td><td>${totalTalks}</td></tr> | |
| 599 | ||
| 600 | ||
| 601 | ||
| 602 | ||
| 603 | ||
| 604 | ||
| 605 | ||
| 606 | ||
| 3442 | <tr><td>참여한 토론 수</td><td>${discussCount}</td></tr> | |
| 3443 | <tr><td>한 토론당 평균 발언 수</td><td>${avgTalks}</td></tr> | |
| 3444 | <tr><td>한 토론당 발언 수 표준편차</td><td>${standardDeviation(Talks)}</td></tr> | |
| 3445 | <tr><td>시간대별 기여/활동 횟수 분포(토론)</td><td><a href="#NothingToLink" id="punch">여기를 눌러 확인</a></td></tr> | |
| 3446 | </tbody> | |
| 3447 | </table>`; | |
| 3448 | p.querySelector('a#punch').addEventListener('click', function (evt) { | |
| 3449 | evt.preventDefault(); | |
| 607 | 3450 | |
| 608 | ||
| 609 | ||
| 610 | ||
| 611 | ||
| 612 | ||
| 613 | ||
| 614 | ||
| 615 | ||
| 616 | ||
| 617 | ||
| 618 | ||
| 619 | ||
| 620 | ||
| 621 | ||
| 622 | ||
| 623 | ||
| 624 | ||
| 625 | ||
| 626 | ||
| 627 | ||
| 628 | ||
| 629 | ||
| 630 | ||
| 631 | ||
| 632 | ||
| 633 | ||
| 634 | ||
| 635 | ||
| 636 | ||
| 637 | ||
| 638 | ||
| 639 | ||
| 640 | ||
| 641 | ||
| 642 | ||
| 643 | ||
| 644 | ||
| 645 | ||
| 646 | ||
| 647 | ||
| 648 | ||
| 649 | ||
| 650 | ||
| 651 | ||
| 652 | ||
| 653 | ||
| 654 | ||
| 655 | ||
| 656 | ||
| 657 | ||
| 658 | ||
| 3451 | var win = TooSimplePopup(); | |
| 3452 | win.title('시간대별 기여/활동 횟수 분포(토론)'); | |
| 3453 | win.content(function (container) { | |
| 3454 | container.appendChild(makeHeatTable(talkedAt)); | |
| 3455 | }); | |
| 3456 | win.button('확인', win.close); | |
| 3457 | }) | |
| 3458 | } else { | |
| 3459 | delete p; | |
| 3460 | } | |
| 3461 | if (typeof p !== 'undefined') insertBeforeTable(p); | |
| 3462 | } else if (ENV.IsDiff) { | |
| 3463 | setTimeout(function () { | |
| 3464 | var diffLinksHtml = `<nav> | |
| 3465 | <ul class="pagination"> | |
| 3466 | <li class="page-item"><a href="/diff/${ENV.docTitle}?oldrev=${ENV.beforeRev - 1}&rev=${ENV.beforeRev}"><-- r${ENV.beforeRev - 1} vs r${ENV.beforeRev}</a></li> | |
| 3467 | <li class="page-item"><a href="/w/${ENV.docTitle}?rev=${ENV.beforeRev}">r${ENV.beforeRev} 보기</a></li> | |
| 3468 | <li class="page-item"><a href="/history/${ENV.docTitle}?from=${ENV.afterRev}">역사로</a></li> | |
| 3469 | <li class="page-item"><a href="/w/${ENV.docTitle}?rev=${ENV.afterRev}">r${ENV.afterRev} 보기</a></li> | |
| 3470 | <li class="page-item"><a href="/diff/${ENV.docTitle}?oldrev=${ENV.afterRev}&rev=${ENV.afterRev + 1}">r${ENV.afterRev} vs r${ENV.afterRev + 1} --></a></li> | |
| 3471 | </ul> | |
| 3472 | </nav>`; | |
| 3473 | if (ENV.skinName == "liberty") { | |
| 3474 | var divTag = document.createElement("div"); | |
| 3475 | var articleTag = document.querySelector('.wiki-article'); | |
| 3476 | if (articleTag.querySelector('.diff') == null) return; | |
| 3477 | divTag.innerHTML = diffLinksHtml; | |
| 3478 | articleTag.insertBefore(divTag, articleTag.firstChild.nextSibling); | |
| 3479 | } | |
| 3480 | var divTag = document.createElement("div"); | |
| 3481 | var articleTag = document.querySelector('article'); | |
| 3482 | if (articleTag == null) return; | |
| 3483 | divTag.innerHTML = diffLinksHtml; | |
| 3484 | articleTag.insertBefore(divTag, articleTag.querySelector('article h1').nextSibling); | |
| 3485 | }, 500); | |
| 3486 | } else if (ENV.IsSearch) { | |
| 3487 | if (ENV.SearchQuery.indexOf('기여:') == 0) { | |
| 3488 | var target = ENV.SearchQuery.substring(3).trim(); | |
| 3489 | if (validateIP(target)) { | |
| 3490 | location.pathname = '/contribution/ip/' + target + '/document'; | |
| 3491 | } else { | |
| 3492 | location.pathname = '/contribution/author/' + target + '/document'; | |
| 3493 | } | |
| 3494 | } | |
| 3495 | } else if (ENV.IsIPACL || ENV.IsSuspendAccount || ENV.IsBoardIPACL || ENV.IsBoardSuspendAccount) { | |
| 3496 | var expireSelect = document.querySelector('select[name=expire]'); | |
| 659 | 3497 | |
| 660 | ||
| 661 | ||
| 662 | ||
| 663 | ||
| 664 | ||
| 665 | ||
| 666 | ||
| 667 | ||
| 668 | ||
| 669 | ||
| 670 | ||
| 671 | ||
| 672 | ||
| 673 | ||
| 674 | ||
| 675 | ||
| 676 | ||
| 677 | ||
| 678 | ||
| 679 | ||
| 680 | ||
| 681 | ||
| 682 | ||
| 683 | ||
| 684 | ||
| 685 | ||
| 686 | ||
| 687 | ||
| 688 | ||
| 689 | ||
| 690 | ||
| 691 | ||
| 692 | ||
| 693 | ||
| 694 | ||
| 695 | ||
| 696 | ||
| 697 | ||
| 698 | ||
| 699 | ||
| 700 | ||
| 701 | ||
| 702 | ||
| 703 | ||
| 704 | ||
| 705 | ||
| 706 | ||
| 707 | ||
| 708 | ||
| 709 | ||
| 710 | ||
| 3498 | function enterEasily() { | |
| 3499 | enterTimespanPopup('차단기간 쉽게 입력하기', function (result) { | |
| 3500 | if (result !== null) | |
| 3501 | document.querySelector('input[name="expire"]').value = result; | |
| 3502 | }); | |
| 3503 | } | |
| 711 | 3504 | |
| 712 | === | |
| 713 | ||
| 714 | ||
| 715 | ||
| 716 | || | |
| 717 | || | |
| 718 | ||
| 719 | ||
| 720 | ||
| 721 | ||
| 722 | ||
| 723 | ||
| 724 | ||
| 725 | ||
| 726 | ||
| 727 | ||
| 728 | ||
| 729 | ||
| 730 | ||
| 731 | ||
| 732 | ||
| 733 | ||
| 734 | ||
| 735 | ||
| 736 | ||
| 737 | ||
| 738 | ||
| 739 | ||
| 740 | ||
| 741 | ||
| 742 | ||
| 743 | ||
| 744 | ||
| 745 | ||
| 746 | ||
| 747 | ||
| 748 | ||
| 749 | ||
| 750 | ||
| 751 | ||
| 752 | ||
| 753 | ||
| 754 | ||
| 755 | ||
| 756 | ||
| 757 | ||
| 758 | ||
| 759 | ||
| 760 | ||
| 761 | ||
| 762 | ||
| 3505 | function replaceExpireSelect() { | |
| 3506 | var newExpireInput = document.createElement('input'); | |
| 3507 | newExpireInput.setAttribute("type", "number"); | |
| 3508 | newExpireInput.setAttribute("class", "form-control"); | |
| 3509 | newExpireInput.setAttribute("name", "expire"); | |
| 3510 | var explain = document.createElement("p"); | |
| 3511 | explain.innerText = "차단기간은 초 단위로 입력해야 하며, 영구차단시에는 0을, 사용자 차단에서 차단 해제시에는 -1을 입력하시면 됩니다."; | |
| 3512 | var enterEasilyLink = document.createElement("a"); | |
| 3513 | enterEasilyLink.innerText = "차단기간 간편하게 입력하기"; | |
| 3514 | enterEasilyLink.href = "#"; | |
| 3515 | enterEasilyLink.addEventListener('click', function (evt) { | |
| 3516 | evt.preventDefault(); | |
| 3517 | enterEasily(); | |
| 3518 | }) | |
| 3519 | expireSelect.parentNode.insertBefore(newExpireInput, expireSelect); | |
| 3520 | expireSelect.parentNode.insertBefore(explain, expireSelect); | |
| 3521 | expireSelect.parentNode.insertBefore(document.createElement("br"), expireSelect); | |
| 3522 | expireSelect.parentNode.insertBefore(enterEasilyLink, expireSelect); | |
| 3523 | expireSelect.parentNode.removeChild(expireSelect); | |
| 3524 | } | |
| 3525 | if (expireSelect != null) { | |
| 3526 | var replaceExpireLink = document.createElement("a"); | |
| 3527 | replaceExpireLink.href = "#"; | |
| 3528 | replaceExpireLink.innerText = "초 단위로 차단 기간 입력하기"; | |
| 3529 | replaceExpireLink.addEventListener('click', function (evt) { | |
| 3530 | evt.preventDefault(); | |
| 3531 | replaceExpireSelect(); | |
| 3532 | replaceExpireLink.parentNode.removeChild(replaceExpireLink); | |
| 3533 | }); | |
| 3534 | expireSelect.parentNode.insertBefore(replaceExpireLink, expireSelect.nextSibling); | |
| 3535 | } | |
| 3536 | } else if (ENV.IsHistory) { | |
| 3537 | addArticleButton('토론', function () { | |
| 3538 | location.href = "/discuss/" + encodeURIComponent(ENV.docTitle); | |
| 3539 | }) | |
| 3540 | addArticleButton('리버전 점프', function () { | |
| 3541 | var revNo = prompt('보고 싶은 리버전 번호를 입력하세요.').trim(); | |
| 3542 | if (revNo.indexOf('r') == 0) revNo = revNo.substring(1); | |
| 3543 | if (/[^0-9]/.test(revNo)) { | |
| 3544 | alert('올바른 입력이 아닙니다! r1 혹은 1과 같이 입력해주세요.'); | |
| 3545 | return; | |
| 3546 | } | |
| 3547 | location.href = "/w/" + encodeURIComponent(ENV.docTitle) + "?rev=" + revNo; | |
| 3548 | }) | |
| 3549 | var historyRows = document.querySelectorAll('article .wiki-list li, body.Liberty .wiki-article .wiki-list li'); | |
| 3550 | for (let historyRow of [].slice.call(historyRows)) { | |
| 3551 | // 긴급차단 | |
| 3552 | if (SET.addQuickBlockLink) { | |
| 3553 | let revisionNo = historyRow.querySelector('strong').textContent.trim(); | |
| 3554 | let temp = historyRow.querySelector('span').innerHTML.trim(); | |
| 3555 | historyRow.querySelector('span').innerHTML = temp.substring(0, temp.length - 1) + ' | <a href="#" class="nf-history-quickblock">차단</a>)'; | |
| 3556 | historyRow.querySelector('a.nf-history-quickblock').addEventListener('click', (evt) => { | |
| 3557 | evt.preventDefault(); | |
| 3558 | let user = historyRow.querySelectorAll('a'); | |
| 3559 | user = user[user.length - 1].textContent.trim(); | |
| 3560 | quickBlockPopup({ | |
| 3561 | author: { | |
| 3562 | name: user, | |
| 3563 | isIP: validateIP(user) | |
| 3564 | }, | |
| 3565 | defaultDuration: SET.quickBlockDefaultDuration, | |
| 3566 | defaultReason: SET.quickBlockReasonTemplate_history.replace(/\$\{host\}/g, location.host) | |
| 3567 | .replace(/\$\{revisionNo\}/g, revisionNo) | |
| 3568 | .replace(/\$\{docName\}/g, ENV.docTitle) | |
| 3569 | }); | |
| 3570 | }); | |
| 3571 | } | |
| 3572 | // ACL 가독성 | |
| 3573 | var italicTag = historyRow.querySelector('i'); | |
| 3574 | if (italicTag) { | |
| 3575 | var pattern = /\(([a-zA-Z,]+?)으로 ACL 변경\)/; | |
| 3576 | // 열람 수정 삭제 (한국) 토론 이동 | |
| 3577 | var valuePatternWithBlockKorea = /(admin|member|everyone),(admin|member|everyone),(admin|member|everyone),(true|false),(admin|member|everyone),(admin|member|everyone)/; | |
| 3578 | var valuePattern = /(admin|member|everyone),(admin|member|everyone),(admin|member|everyone),(admin|member|everyone),(admin|member|everyone)/; | |
| 3579 | var icons = ['ion-eye fa fa-eye', 'ion-edit fa fa-edit', 'ion-trash-a fa fa-trash', 'ion-android-textsms fa fa-comments', 'ion-arrow-right-c fa fa-arrow-right', 'ion-flag fa fa-flag']; | |
| 3580 | var koreanAclConds = { | |
| 3581 | 'member': '회원', | |
| 3582 | 'admin': '관리자', | |
| 3583 | 'everyone': '모두' | |
| 3584 | }; | |
| 3585 | if (pattern.test(italicTag.innerHTML)) { | |
| 3586 | var aclText = pattern.exec(italicTag.innerHTML)[1]; | |
| 3587 | var newAclText = ''; | |
| 3588 | var acl = null, | |
| 3589 | isKoreaBlocked = null; | |
| 3590 | var matched = false; | |
| 3591 | if (aclText == 'delete') { | |
| 3592 | newAclText = '(ACL 초기화)'; | |
| 3593 | matched = true; | |
| 3594 | } else if (valuePatternWithBlockKorea.test(aclText)) { | |
| 3595 | acl = valuePatternWithBlockKorea.exec(aclText); | |
| 3596 | isKoreaBlocked = acl[4] == 'true'; | |
| 3597 | acl = [acl[0], acl[1], acl[2], acl[3], acl[5], acl[6]]; | |
| 3598 | matched = true; | |
| 3599 | } else if (valuePattern.test(aclText)) { | |
| 3600 | acl = valuePattern.exec(aclText); | |
| 3601 | matched = true; | |
| 3602 | } | |
| 3603 | if (!matched) | |
| 3604 | continue; | |
| 3605 | if (newAclText == '') { | |
| 3606 | newAclText = '('; | |
| 3607 | for (var i = 1; i <= 5; i++) { | |
| 3608 | var color = acl[i] == 'admin' ? 'red' : acl[i] == 'member' ? 'orange' : null; | |
| 3609 | newAclText += '<span ' + (color ? 'style="color:' + color + '" ' : '') + '><span class="icon ' + icons[i - 1] + '"></span>' + koreanAclConds[acl[i]] + '</span>,'; | |
| 3610 | } | |
| 3611 | if (isKoreaBlocked) { | |
| 3612 | newAclText += '<span style="color: blue;"><span class="icon ' + icons[5] + '"></span>한국 IP 차단</span>,'; | |
| 3613 | } | |
| 3614 | newAclText = newAclText.substring(0, newAclText.length - 1); | |
| 3615 | newAclText += '으로 ACL 변경)'; | |
| 3616 | } | |
| 3617 | italicTag.innerHTML = italicTag.innerHTML.replace(pattern.exec(italicTag.innerHTML)[0], newAclText); | |
| 3618 | } | |
| 3619 | } | |
| 3620 | } | |
| 3621 | } else if (ENV.IsRecentChanges) { | |
| 3622 | let changeRows = document.querySelectorAll('article table.table tbody tr'); | |
| 3623 | for (let row of changeRows) { | |
| 3624 | if (!row.querySelector('a')) continue; // 편집 코멘트 필요없음. | |
| 3625 | let author = { | |
| 3626 | name: row.querySelector('td:nth-child(2) a').textContent.trim() | |
| 3627 | } | |
| 3628 | author.isIP = validateIP(author.name); | |
| 3629 | if(SET.addQuickBlockLink) { | |
| 3630 | let quickBlockAnchor = document.createElement("a"); | |
| 3631 | quickBlockAnchor.textContent = " [차단] " | |
| 3632 | quickBlockAnchor.href = "#"; | |
| 3633 | quickBlockAnchor.addEventListener('click', (evt) => { | |
| 3634 | evt.preventDefault(); | |
| 3635 | quickBlockPopup({ | |
| 3636 | author: author, | |
| 3637 | defaultReason: '긴급차단 - 반달리즘', | |
| 3638 | defaultDuration: SET.quickBlockDefaultDuration | |
| 3639 | }); | |
| 3640 | }) | |
| 3641 | row.querySelector('td:first-child').insertBefore(quickBlockAnchor, row.querySelector('td:first-child span')); | |
| 3642 | } | |
| 3643 | } | |
| 3644 | } | |
| 763 | 3645 | |
| 764 | ||
| 765 | ||
| 766 | ||
| 767 | ||
| 768 | ||
| 769 | ||
| 770 | ||
| 771 | ||
| 772 | ||
| 773 | ||
| 774 | ||
| 775 | ||
| 776 | ||
| 777 | ||
| 778 | ||
| 779 | ||
| 780 | ||
| 781 | ||
| 782 | ||
| 783 | ||
| 784 | ||
| 785 | ||
| 786 | ||
| 787 | ||
| 788 | ||
| 789 | ||
| 790 | ||
| 791 | ||
| 792 | ||
| 793 | ||
| 794 | ||
| 795 | ||
| 796 | ||
| 797 | ||
| 798 | ||
| 799 | ||
| 800 | ||
| 801 | ||
| 802 | ||
| 803 | ||
| 804 | ||
| 805 | ||
| 806 | ||
| 807 | ||
| 808 | ||
| 809 | ||
| 810 | ||
| 811 | ||
| 812 | ||
| 813 | ||
| 814 | ||
| 3646 | if (ENV.skinName == "liberty") { | |
| 3647 | if (SET.addAdminLinksForLiberty) { | |
| 3648 | // 관리자 링크 추가 | |
| 3649 | document.querySelector('nav.navbar ul.nav li.nav-item.dropdown .dropdown-menu').innerHTML += '<div class="dropdown-divider"></div>' + | |
| 3650 | '<a class="dropdown-item" href="/admin/suspend_account">계정 차단</a>' + | |
| 3651 | '<a class="dropdown-item" href="/admin/ipacl">IP 차단</a>' + | |
| 3652 | '<a class="dropdown-item" href="/admin/grant">권한 부여</a>'; | |
| 3653 | if (ENV.IsDocument) { | |
| 3654 | addArticleButton("ACL", function () { | |
| 3655 | location.href = "/acl/" + ENV.docTitle; | |
| 3656 | }) | |
| 3657 | } | |
| 3658 | } | |
| 3659 | if (ENV.IsDocument && ENV.docTitle.indexOf('사용자:') == 0) { | |
| 3660 | addArticleButton("기여내역", function () { | |
| 3661 | var userName = ENV.docTitle.substring(4); | |
| 3662 | location.href = "/contribution/" + (validateIP(userName) ? "ip/" : "author/") + userName + "/document"; | |
| 3663 | }) | |
| 3664 | } | |
| 3665 | } | |
| 3666 | } | |
| 3667 | document.querySelector('nav.navbar ul.nav li.nav-item.dropdown .dropdown-menu').innerHTML += '<div class="dropdown-divider"></div>' + | |
| 3668 | '<a class="dropdown-item" href="https://phpgongbu.ga/archive"><span class="icon ion-wrench"></span> 게시판 아카이브 도구</a>' + | |
| 3669 | '<a class="dropdown-item" href="https://phpgongbu.ga/tocont.php"><span class="icon ion-wrench"></span> 기여내역 점프기</a>' + | |
| 3670 | '<a class="dropdown-item" href="https://phpgongbu.ga/ip.php"><span class="icon ion-wrench"></span> 내 IP 주소 확인</a>'; | |
| 815 | 3671 | |
| 816 | === 1501 ~ 1600(완료) === | |
| 817 | || 1501 || [[드조위키]] || 1502 || [[키르기즈]] || | |
| 818 | || 1503 || [[즈곡크E]] || 1504 || [[Encyclopedia Dramatica]] || | |
| 819 | || 1505 || [[archive.is]] || 1506 || [[S라인]] || | |
| 820 | || 1507 || [[인천광역시]] || 1508 || [[시발]] || | |
| 821 | || 1509 || [[발냄새]] || 1510 || [[새끼]] || | |
| 822 | || 1511 || [[끼워팔기]] || 1512 || [[기벡]] || | |
| 823 | || 1513 || [[벡터(수학)]] || 1514 || [[터너]] || | |
| 824 | || 1515 || [[너님]] || 1516 || [[임자]] || | |
| 825 | || 1517 || [[자취]] || 1518 || [[취소]] || | |
| 826 | || 1519 || [[소인수분해]] || 1520 || [[해충갤]] || | |
| 827 | || 1521 || [[갤러리]] || 1522 || [[리리스 타이탄본]] || | |
| 828 | || 1523 || [[본명]] || 1524 || [[명찰]] || | |
| 829 | || 1525 || [[찰갑]] || 1526 || [[갑각류]] || | |
| 830 | || 1527 || [[유관]] || 1528 || [[관계]] || | |
| 831 | || 1529 || [[계곡]] || 1530 || [[곡성군/교통]] || | |
| 832 | || 1531 || [[통일]] || 1532 || [[일베]] || | |
| 833 | || 1533 || [[베를린]] || 1534 || [[린다]] || | |
| 834 | || 1535 || [[다트]] || 1536 || [[트위스티드 시스터]] || | |
| 835 | || 1537 || [[터널 3D]] || 1538 || [[DD]] || | |
| 836 | || 1539 || [[DTD]] || 1540 || [[DDT]] || | |
| 837 | || 1541 || [[TD]] || 1542 || [[Devenjer]] || | |
| 838 | || 1543 || [[robots.txt]] || 1544 || [[T store]] || | |
| 839 | || 1545 || [[EXID]] || 1546 || [[Deorro]] || | |
| 840 | || 1547 || [[O tvN]] || 1548 || [[N.Flying]] || | |
| 841 | || 1549 || [[GOT7]] || 1550 || [[710번 지방도]] || | |
| 842 | || 1551 || [[도르니안 헤러시]] || 1552 || [[시애틀 매리너스]] || | |
| 843 | || 1553 || [[스리랑카]] || 1554 || [[카이젤 드 엠페러너 벨제바브 4세]] || | |
| 844 | || 1555 || [[세계가 지켜보고 있습니다 ~을(를) 자제해 주십시오]] || 1556 || [[오스트리아]] || | |
| 845 | || 1557 || [[아기]] || 1558 || [[기니]] || | |
| 846 | || 1559 || [[니카라과]] || 1560 || [[과수원]] || | |
| 847 | || 1561 || [[원반생물 로베르가]] || 1562 || [[가면라이더 에그제이드(가면라이더)/신체 부위]] || | |
| 848 | || 1563 || [[위암]] || 1564 || [[암사역]] || | |
| 849 | || 1565 || [[역설]] || 1566 || [[설날]] || | |
| 850 | || 1567 || [[날선승]] || 1568 || [[승리]] || | |
| 851 | || 1569 || [[이가]] || 1570 || [[가이거 계수기]] || | |
| 852 | || 1571 || [[기대승]] || 1572 || [[승호]] || | |
| 853 | || 1573 || [[호서대학교]] || 1574 || [[교사]] || | |
| 854 | || 1575 || [[사진]] || 1576 || [[진시황]] || | |
| 855 | || 1577 || [[황산]] || 1578 || [[산할아버지]] || | |
| 856 | || 1579 || [[지동설]] || 1580 || [[설총]] || | |
| 857 | || 1581 || [[총구]] || 1582 || [[구리(원소)]] || | |
| 858 | || 1583 || [[리그 오브 레전드]] || 1584 || [[드라군 놀이]] || | |
| 859 | || 1585 || [[이산수학 ]] || 1586 || [[학사경고]] || | |
| 860 | || 1587 || [[고슴도치의 딜레마]] || 1588 || [[마술사]] || | |
| 861 | || 1589 || [[사무관 Rainbow 권한 남용 사건]] || 1590 || [[건포도]] || | |
| 862 | || 1591 || [[도비]] || 1592 || [[비기]] || | |
| 863 | || 1593 || [[기술]] || 1594 || [[술 게임]] || | |
| 864 | || 1595 || [[임업]] || 1596 || [[업보]] || | |
| 865 | || 1597 || [[보름달]] || 1598 || [[달거리 ]] || | |
| 866 | || 1599 || [[리소자임]] || 1600 || [[임수동]] || | |
| 867 | 3672 | |
| 868 | ||
| 869 | ||
| 870 | ||
| 871 | ||
| 872 | ||
| 873 | ||
| 874 | ||
| 875 | ||
| 876 | ||
| 877 | ||
| 878 | ||
| 879 | ||
| 880 | ||
| 881 | ||
| 882 | ||
| 883 | ||
| 884 | ||
| 885 | ||
| 886 | ||
| 887 | ||
| 888 | ||
| 889 | ||
| 890 | ||
| 891 | ||
| 892 | ||
| 893 | ||
| 894 | ||
| 895 | ||
| 896 | ||
| 897 | ||
| 898 | ||
| 899 | ||
| 900 | ||
| 901 | ||
| 902 | ||
| 903 | ||
| 904 | ||
| 905 | ||
| 906 | ||
| 907 | ||
| 908 | ||
| 909 | ||
| 910 | ||
| 911 | ||
| 912 | ||
| 913 | ||
| 914 | ||
| 915 | ||
| 916 | ||
| 917 | ||
| 918 | ||
| 3673 | // 아이덴티콘 버그 수정 | |
| 3674 | setInterval(function () { | |
| 3675 | if (!/^\/discuss\/(.+?)/.test(location.pathname)) { | |
| 3676 | return; | |
| 3677 | } | |
| 3678 | var identicons = document.querySelectorAll('.nf-identicon'); | |
| 3679 | for (var i = 0; i < identicons.length; i++) { | |
| 3680 | var ide = identicons[i]; | |
| 3681 | var pa = ide.parentNode; | |
| 919 | 3682 | |
| 920 | ||
| 921 | ||
| 922 | ||
| 923 | ||
| 924 | ||
| 925 | ||
| 926 | ||
| 927 | ||
| 928 | ||
| 929 | ||
| 930 | ||
| 931 | ||
| 932 | ||
| 933 | ||
| 934 | ||
| 935 | ||
| 936 | ||
| 937 | ||
| 938 | ||
| 939 | ||
| 940 | ||
| 941 | ||
| 942 | ||
| 943 | ||
| 944 | ||
| 945 | ||
| 946 | ||
| 947 | ||
| 948 | ||
| 949 | ||
| 950 | ||
| 951 | ||
| 952 | ||
| 953 | ||
| 954 | ||
| 955 | ||
| 956 | ||
| 957 | ||
| 958 | ||
| 959 | ||
| 960 | ||
| 961 | ||
| 962 | ||
| 963 | ||
| 964 | ||
| 965 | ||
| 966 | ||
| 967 | ||
| 968 | ||
| 969 | ||
| 970 | ||
| 971 | ||
| 972 | ||
| 3683 | pa.removeChild(ide); | |
| 3684 | pa.style.marginTop = ''; | |
| 3685 | } | |
| 3686 | if (document.querySelector('#nf-identicon-css') != null) { | |
| 3687 | var cssTag = document.querySelector('#nf-identicon-css'); | |
| 3688 | cssTag.parentNode.removeChild(cssTag); | |
| 3689 | } | |
| 3690 | }, 50); | |
| 973 | 3691 | |
| 974 | ||
| 3692 | addItemToMemberMenu('내 사용자 토론', function (evt) { | |
| 3693 | evt.preventDefault(); | |
| 975 | 3694 | |
| 976 | ||
| 977 | ||
| 978 | ||
| 979 | ||
| 980 | ||
| 981 | ||
| 982 | ||
| 983 | ||
| 984 | ||
| 985 | ||
| 986 | ||
| 987 | ||
| 988 | ||
| 989 | ||
| 990 | ||
| 991 | ||
| 992 | ||
| 993 | ||
| 994 | ||
| 995 | ||
| 996 | ||
| 997 | ||
| 998 | ||
| 999 | ||
| 1000 | ||
| 1001 | ||
| 1002 | ||
| 1003 | ||
| 1004 | ||
| 1005 | ||
| 1006 | ||
| 1007 | ||
| 1008 | ||
| 1009 | ||
| 1010 | ||
| 1011 | ||
| 1012 | ||
| 1013 | ||
| 1014 | ||
| 1015 | ||
| 1016 | ||
| 1017 | ||
| 1018 | ||
| 1019 | ||
| 1020 | ||
| 1021 | ||
| 1022 | ||
| 1023 | ||
| 1024 | ||
| 1025 | ||
| 1026 | ||
| 1027 | ||
| 1028 | ||
| 3695 | location.href = 'https://namu.wiki/discuss/%EC%82%AC%EC%9A%A9%EC%9E%90:Accessable' | |
| 3696 | }); | |
| 1029 | 3697 | |
| 1030 | ||
| 3698 | addItemToMemberMenu('<div class="dropdown-divider"></div>'); | |
| 1031 | 3699 | |
| 1032 | ||
| 1033 | ||
| 1034 | ||
| 1035 | ||
| 1036 | ||
| 1037 | ||
| 1038 | ||
| 1039 | ||
| 1040 | ||
| 1041 | ||
| 1042 | ||
| 1043 | ||
| 1044 | ||
| 1045 | ||
| 1046 | ||
| 1047 | ||
| 1048 | ||
| 1049 | ||
| 1050 | ||
| 1051 | ||
| 1052 | ||
| 1053 | ||
| 1054 | ||
| 1055 | ||
| 1056 | ||
| 1057 | ||
| 1058 | ||
| 1059 | ||
| 1060 | ||
| 1061 | ||
| 1062 | ||
| 1063 | ||
| 1064 | ||
| 1065 | ||
| 1066 | ||
| 1067 | ||
| 1068 | ||
| 1069 | ||
| 1070 | ||
| 1071 | ||
| 1072 | ||
| 1073 | ||
| 1074 | ||
| 1075 | ||
| 1076 | ||
| 1077 | ||
| 1078 | ||
| 1079 | ||
| 1080 | ||
| 1081 | ||
| 1082 | ||
| 1083 | ||
| 1084 | ||
| 3700 | // 설정 메뉴 추가 | |
| 3701 | addItemToMemberMenu("NamuFix 설정", async function (evt) { | |
| 3702 | evt.preventDefault(); | |
| 1085 | 3703 | |
| 1086 | ||
| 3704 | var win = TooSimplePopup(); | |
| 3705 | var elems = {}; | |
| 3706 | win.title('NamuFix 설정'); | |
| 3707 | await SET.load(); | |
| 3708 | win.content(async function (el) { | |
| 3709 | el.className += " NFSettingsContainer"; | |
| 3710 | el.innerHTML = `<style>.NFSettingsContainer h1 {font-size: 17pt; margin-left: 8px;} .NFSettingsContainer h2 {font-size: 13pt; margin-left: 8px;} .NFSettingsContainer .settings-paragraph {margin-left: 8px;}</style> | |
| 3711 | <h1>NamuFix 전역</h1> | |
| 3712 | <div class="settings-paragraph"> | |
| 3713 | <h2>IP정보 조회시 기관명</h2> | |
| 3714 | <div class="settings-paragraph"> | |
| 3715 | <p>IP정보 조회시에 무슨 기관명을 이용할지 결정합니다. 아래 설정에서 KISA WHOIS 결과에서의 기관명이 선택됐는데 KISA WHOIS 결과가 조회되지 않을 시 자동으로 ipinfo.io에서 조회됩니다. 이 설정은 KISA WHOIS 조회를 제외한 모든 IP정보 조회를 동반한 기능(예: 토론시 익명 기여자 IP주소 조회, 익명 기여자 기여목록에서의 IP 정보 등)에 영향을 끼칩니다.</p> | |
| 3716 | <input type="radio" name="ipInfoDefaultOrg" data-setname="ipInfoDefaultOrg" data-setvalue="ipinfo.io">ipinfo.io에서 조회 (기본값)<br> | |
| 3717 | <input type="radio" name="ipInfoDefaultOrg" data-setname="ipInfoDefaultOrg" data-setvalue="KISAuser">KISA WHOIS 결과에서 IP 이용 기관명<br> | |
| 3718 | <input type="radio" name="ipInfoDefaultOrg" data-setname="ipInfoDefaultOrg" data-setvalue="KISAISP">KISA WHOIS 결과에서 IP 보유 기관명<br> | |
| 3719 | <input type="radio" name="ipInfoDefaultOrg" data-setname="ipInfoDefaultOrg" data-setvalue="KISAuserOrISP">KISA WHOIS 결과에서 IP 보유 기관명 혹은 IP 이용 기관명<br> | |
| 3720 | </div> | |
| 3721 | <h2>동시요청제한</h2> | |
| 3722 | <div class="settings-paragraph"> | |
| 3723 | 관리작업시의 동시요청제한 : | |
| 3724 | <input type="number" data-setname="adminReqLimit"></input><br> | |
| 3725 | 이미지 업로드시의 동시 요청 제한 : | |
| 3726 | <input type="number" data-setname="fileUploadReqLimit"></input><br> | |
| 3727 | IP이용자의 기여목록에서 차단기록 검색 딜레이 : | |
| 3728 | <input type="number" data-setname="ipBlockHistoryCheckDelay"></input>ms (1000ms=1초) | |
| 3729 | <br><strong>경고 : 너무 높게 설정하면 reCAPTCHA가 뜹니다.</strong> | |
| 3730 | </div> | |
| 3731 | <h2>umi 쿠키</h2> | |
| 3732 | <div class="settings-paragraph"> | |
| 3733 | <p>NamuFix 실행시 umi 쿠키값을 다음과 같이 변경합니다. 공백으로 설정시 변경하지 않습니다.</p> | |
| 3734 | <input type="text" data-setname="umiCookie"></input> | |
| 3735 | </div> | |
| 3736 | </div> | |
| 3737 | <h1>토론 편의성</h1> | |
| 3738 | <div class="settings-paragraph"> | |
| 3739 | <h2>토론 아이덴티콘</h2> | |
| 3740 | <div class="settings-paragraph"> | |
| 3741 | <input type="radio" name="discussIdenti" data-setname="discussIdenti" data-setvalue="icon">디시라이트 갤러콘 방식<br> | |
| 3742 | <input type="radio" name="discussIdenti" data-setname="discussIdenti" data-setvalue="headBg">스레딕 헬퍼 방식<br> | |
| 3743 | <input type="radio" name="discussIdenti" data-setname="discussIdenti" data-setvalue="identicon">아이덴티콘<br> | |
| 3744 | <input type="radio" name="discussIdenti" data-setname="discussIdenti" data-setvalue="none">사용 안함 | |
| 3745 | </div> | |
| 3746 | <h2>아이덴티콘 라이브러리</h2> | |
| 3747 | <div class="settings-paragraph"> | |
| 3748 | <p>참고 : NamuFix에서 특정 사용자의 이메일주소를 조회할 수 없기에 Gravatar, Gravatar(monsterid)는 해당 사용자의 그라바타와 다르게 나옴.</p> | |
| 3749 | <input type="radio" name="identiconLibrary" data-setname="identiconLibrary" data-setvalue="identicon">stewartlord/identicon.js (GitHub스타일의 아이덴티콘)<br> | |
| 3750 | <input type="radio" name="identiconLibrary" data-setname="identiconLibrary" data-setvalue="jdenticon">jdenticon (원을 포함하는 여러 도형과 다양한 색으로 이루어진 아이덴티콘)<br> | |
| 3751 | <input type="radio" name="identiconLibrary" data-setname="identiconLibrary" data-setvalue="gravatar">Gravatar (Gravatar에서 생성되는 기하학적 패턴 기반의 아이덴티콘)<br> | |
| 3752 | <input type="radio" name="identiconLibrary" data-setname="identiconLibrary" data-setvalue="gravatar-monster">Gravatar(monsterid) (Gravatar에서 생성되는 <del style="color: gray;">존나 못생긴</del> 몬스터) | |
| 3753 | </div> | |
| 3754 | <h2>토론에서 익명 기여자 IP주소 조회</h2> | |
| 3755 | <div class="settings-paragraph"> | |
| 3756 | <p>VPNGate 여부, 통신사, 국가이미지를 IP 주소 옆에 표시합니다. 요청 수가 많을 시 실패할 수 도 있습니다.</p> | |
| 3757 | <input type="checkbox" name="lookupIPonDiscuss" data-setname="lookupIPonDiscuss" data-as-boolean>토론시 익명 기여자 IP 주소 조회</input><br> | |
| 3758 | <input type="checkbox" name="checkWhoisNetTypeOnDiscuss" data-setname="checkWhoisNetTypeOnDiscuss" data-as-boolean>네트워크 유형도 함께 조회 (단 한국 IP만 가능)</input> | |
| 3759 | </div> | |
| 3760 | <h2>토론에서 보여지지 않은 쓰레도 불러오기</h2> | |
| 3761 | <div class="settings-paragraph"> | |
| 3762 | <p>보여지지 않은 쓰레드도 불러오도록 나무위키 토론 스크립트를 수정합니다.</p> | |
| 3763 | <input type="checkbox" name="loadUnvisibleReses" data-setname="loadUnvisibleReses" data-as-boolean>보여지지 않은 토론 쓰레도 불러오기</input> | |
| 3764 | </div> | |
| 3765 | <h2>토론 아이덴티콘 명도</h2> | |
| 3766 | <div class="settings-paragraph"> | |
| 3767 | <p>스레딕 헬퍼 방식을 사용하는 경우에만 적용됩니다.</p> | |
| 3768 | <label for="discussIdentiLightness">명도</label><input name="discussIdentiLightness" data-setname="discussIdentiLightness" type="range" max="1" min="0" step="0.01"><br> | |
| 3769 | <label for="discussIdentiSaturation">순도</label><input name="discussIdentiSaturation" data-setname="discussIdentiSaturation" type="range" max="1" min="0" step="0.01"> | |
| 3770 | </div> | |
| 3771 | <h2>토론시 앵커 미리보기</h2> | |
| 3772 | <div class="settings-paragraph"> | |
| 3773 | <input type="radio" name="discussAnchorPreviewType" data-setname="discussAnchorPreviewType" data-setvalue="0">사용하지 않음<br> | |
| 3774 | <input type="radio" name="discussAnchorPreviewType" data-setname="discussAnchorPreviewType" data-setvalue="1">마우스를 올리면 미리보기 표시<br> | |
| 3775 | <input type="radio" name="discussAnchorPreviewType" data-setname="discussAnchorPreviewType" data-setvalue="2">토론 메세지 위에 인용형식으로 표시<br> | |
| 3776 | <input type="checkbox" name="removeNFQuotesInAnchorPreview" data-setname="removeNFQuotesInAnchorPreview" data-as-boolean>토론 메세지 위에 인용형식으로 표시할때, 인용문 안에 인용 형식으로 표시된 미리보기 제거 | |
| 3777 | </div> | |
| 3778 | <h2>보지 않는 토론 알림</h2> | |
| 3779 | <div class="settings-paragraph"> | |
| 3780 | <p>보지 않는 토론(예 : 탭 여러개를 여는 경우)에 새로운 발언 혹은 블라인드가 생길시 브라우저 API를 이용해 알림을 띄웁니다.<br> | |
| 3781 | 참고 : 토론에서 보여지지 않은 쓰레도 불러오기 기능이 활성화된 경우 보이지 않은 쓰레들을 불려오는 동안은 알림이 뜨지 않습니다.<br> | |
| 3782 | 경고 : 현재 실험중인 기능입니다.</p> | |
| 3783 | <input type="checkbox" data-setname="notifyForUnvisibleThreads" data-as-boolean>보지 않는 토론 알림</input> | |
| 3784 | </div> | |
| 3785 | <h2>마우스를 올리면 해당 사용자의 다른 쓰레 강조</h2> | |
| 3786 | <div class="settings-paragraph"> | |
| 3787 | <p>토론에서 쓰레에 마우스를 올리면 그 사용자의 다른 쓰레를 강조합니다.</p> | |
| 3788 | <input type="checkbox" data-setname="emphasizeResesWhenMouseover" data-as-boolean>마우스를 올리면 해당 사용자의 다른 쓰레 강조</input> | |
| 3789 | </div> | |
| 3790 | </div> | |
| 3791 | <h1>관리 편의성</h1> | |
| 3792 | <div class="settings-paragraph"> | |
| 3793 | <h2>편의기능</h2> | |
| 3794 | <div class="settings-paragraph"> | |
| 3795 | <input type="checkbox" name="addAdminLinksForLiberty" data-setname="addAdminLinksForLiberty" data-as-boolean>Liberty 스킨에 관리자 링크 추가하기</input><br> | |
| 3796 | <input type="checkbox" name="addBatchBlockMenu" data-setname="addBatchBlockMenu" data-as-boolean>일괄 차단 메뉴 추가</input><br> | |
| 3797 | <input type="checkbox" name="addQuickBlockLink" data-setname="addQuickBlockLink" data-as-boolean>빠른 차단 링크 추가</input><br> | |
| 3798 | 토론중 빠른차단 기능에서의 차단사유 템플릿 : <input type="text" data-setname="quickBlockReasonTemplate_discuss" style="width: 500px; max-width: 75vw;"></input><br> | |
| 3799 | 역사페이지 빠른차단 기능에서의 차단사유 템플릿 : <input type="text" data-setname="quickBlockReasonTemplate_history" style="width: 500px; max-width: 75vw;"></input><br> | |
| 3800 | <strong>참고:</strong> 문서명(\${docName})은 URL 인코딩이 되지 않고 리버전 번호(\${revisionNo})는 r로 시작하기 때문에 주소형태의 차단사유 템플릿을 쓰는 것을 권장하지 않습니다.)<br> | |
| 3801 | 빠른차단 기능에서의 차단기간 기본값(초) : <input type="text" data-setname="quickBlockDefaultDuration"></input> | |
| 3802 | </div> | |
| 3803 | </div> | |
| 3804 | <h1>편집 편의성</h1> | |
| 3805 | <div class="settings-paragraph"> | |
| 3806 | <h2>자동저장 시간 간격</h2> | |
| 3807 | <div class="settings-paragraph"> | |
| 3808 | <p>편집중 자동저장 간격을 설정합니다. 0 이하의 값으로 설정할 시 자동으로 이루어지지 않으며 이 경우 단축키나 메뉴를 이용해 수동으로 저장해야 합니다.</p> | |
| 3809 | <input type="number" name="autoTempsaveSpan" data-setname="autoTempsaveSpan"></input>ms (1000ms = 1s) | |
| 3810 | </div> | |
| 3811 | <h2>이미지 업로드시 파일이름 유지</h2> | |
| 3812 | <div class="settings-paragraph"> | |
| 3813 | <p>파일이름에 난수를 덧붙이지 않고 그대로 유지합니다. 파일이름이 중복될시 오류가 발생할 수 있습니다.</p> | |
| 3814 | <input type="checkbox" data-setname="unprefixedFilename" data-as-boolean>파일이름 그대로 유지</input> | |
| 3815 | </div> | |
| 3816 | </div> | |
| 3817 | <h1>게시판</h1> | |
| 3818 | <div class="settings-paragraph"> | |
| 3819 | <h2>게시판 시간대 변경</h2> | |
| 3820 | <div class="settings-paragraph"> | |
| 3821 | <input type="checkbox" name="noLocaltimeOnNamuBoard" data-setname="noLocaltimeOnNamuBoard" data-as-boolean>게시판 시간대를 사용자의 시간대로 자동 변경합니다.</input> | |
| 3822 | </div> | |
| 3823 | <h2>댓글 사용구</h2> | |
| 3824 | <div class="settings-paragraph"> | |
| 3825 | <p>상용구와 상용구는 ,로 구분하며 상용구 이름과 상용구 내용은 :로 구분합니다. (예시: <em>처리중:처리중입니다,기각:기각합니다</em>)</p> | |
| 3826 | <input type="text" data-setname="commentMacros"></input> | |
| 3827 | </div> | |
| 3828 | </div> | |
| 3829 | <h1>기타</h1> | |
| 3830 | <div class="settings-paragraph"> | |
| 3831 | <h2>SNS 공유 버튼</h2> | |
| 3832 | <div class="settings-paragraph"> | |
| 3833 | <input type="checkbox" name="addSnsShareButton" data-setname="addSnsShareButton" data-as-boolean>문서에 트위터/페이스북 공유 버튼을 추가합니다.</input> | |
| 3834 | </div> | |
| 3835 | </div>` | |
| 3836 | var optionTags = document.querySelectorAll('[data-setname]'); | |
| 3837 | await SET.load(); | |
| 3838 | for (var i = 0; i < optionTags.length; i++) { | |
| 3839 | var tag = optionTags[i]; | |
| 3840 | var t = tag.getAttribute('type'); | |
| 3841 | var sn = tag.dataset.setname; | |
| 3842 | if (t == "radio" && tag.dataset.setvalue == SET[sn]) { | |
| 3843 | tag.checked = true; | |
| 3844 | } else if ((t == "checkbox" && tag.dataset.setvalueOnChecked == SET[sn]) || (t == "checkbox" && tag.hasAttribute("data-as-boolean") && SET[sn])) { | |
| 3845 | tag.checked = true; | |
| 3846 | } else if (["text", "password", "number", "range"].indexOf(t) != -1) { | |
| 3847 | tag.value = ["number", "range"].indexOf(t) != -1 ? Number(SET[sn]) : SET[sn]; | |
| 3848 | } | |
| 3849 | } | |
| 3850 | }); | |
| 3851 | win.button('저장하지 않고 닫기', win.close); | |
| 3852 | win.button('저장하고 닫기', async function () { | |
| 3853 | var optionTags = document.querySelectorAll('[data-setname]'); | |
| 3854 | await SET.load(); | |
| 3855 | for (var i = 0; i < optionTags.length; i++) { | |
| 3856 | var tag = optionTags[i]; | |
| 3857 | var t = tag.getAttribute('type'); | |
| 3858 | var sn = tag.dataset.setname; | |
| 3859 | if (t == "radio" && tag.checked) { | |
| 3860 | SET[sn] = tag.dataset.setvalue; | |
| 3861 | } else if (t == "checkbox") { | |
| 3862 | SET[sn] = tag.hasAttribute("data-as-boolean") ? tag.checked : tag.checked ? t.dataset.setvalueOnChecked : t.dataset.setvalueOnUnchecked; | |
| 3863 | } else if (["text", "password", "number", "range"].indexOf(t) != -1) { | |
| 3864 | SET[sn] = tag.value; | |
| 3865 | } | |
| 3866 | } | |
| 3867 | await SET.save(); | |
| 3868 | if (confirm("새로고침해야 설정이 적용됩니다. 새로고침할까요?")) | |
| 3869 | location.reload(); | |
| 3870 | win.close(); | |
| 3871 | }); | |
| 3872 | win.button('설정 백업', async function () { | |
| 3873 | let excludeTempsaves = confirm('임시조치를 제외할까요?'); | |
| 3874 | let excludes = []; | |
| 3875 | if (excludeTempsaves) excludes.push('tempsaves'); | |
| 3876 | let backupText = [JSON.stringify(await SET.export(excludes))]; | |
| 3877 | let win = TooSimplePopup(); | |
| 3878 | win.title('백업중...'); | |
| 3879 | win.content(el => el.innerHTML = "백업중..."); | |
| 3880 | let blob = new Blob(backupText, { | |
| 3881 | type: "application/json" | |
| 3882 | }); | |
| 3883 | let url = URL.createObjectURL(blob); | |
| 3884 | win.content(el => el.innerHTML = `<a href="${url}" download="namufix_backup.json">이 링크</a>을 다른 이름으로 저장해주세요.`) | |
| 3885 | win.button('닫기', win.close); | |
| 3886 | }); | |
| 3887 | win.button('설정 복원', async function () { | |
| 3888 | getFile(async(files) => { | |
| 3889 | if (files.length === 0) | |
| 3890 | return alert('아무것도 선택하지 않았습니다!'); | |
| 3891 | try { | |
| 3892 | let fileReader = new FileReader(); | |
| 3893 | fileReader.onload = async(evt) => { | |
| 3894 | try { | |
| 3895 | let data = JSON.parse(evt.target.result); | |
| 3896 | await SET.import(data); | |
| 3897 | alert('완료했습니다. 확인 버튼을 누르면 새로고침됩니다.'); | |
| 3898 | location.reload(); | |
| 3899 | } catch (err) { | |
| 3900 | return alert('파일을 읽는 중 오류가 발생했습니다: ' + err.message); | |
| 3901 | console.error(err); | |
| 3902 | } | |
| 3903 | } | |
| 3904 | fileReader.readAsText(files[0]); | |
| 3905 | } catch (err) { | |
| 3906 | return alert('파일을 읽는 중 오류가 발생했습니다: ' + err.message); | |
| 3907 | console.error(err); | |
| 3908 | } | |
| 3909 | }, false) | |
| 3910 | }); | |
| 3911 | win.button('설정 초기화', async function () { | |
| 3912 | if (confirm('되돌릴 수 없습니다. 계속 진행하시겠습니까?')) { | |
| 3913 | await SET.load(); | |
| 3914 | for (let i in SET) { | |
| 3915 | if (i == "save" || i == "load" || i == "delete") | |
| 3916 | continue; | |
| 3917 | await SET.delete(i); | |
| 3918 | } | |
| 3919 | if (confirm('초기화에 성공했습니다. 확인 버튼을 누르면 새로고침합니다.')) { | |
| 3920 | location.reload(); | |
| 3921 | } | |
| 3922 | win.close(); | |
| 3923 | } | |
| 3924 | }); | |
| 3925 | }); | |
| 3926 | addItemToMemberMenu('NamuFix 이슈트래커', function (evt) { | |
| 3927 | evt.preventDefault(); | |
| 1087 | 3928 | |
| 1088 | ||
| 1089 | ||
| 1090 | ||
| 1091 | ||
| 1092 | ||
| 1093 | ||
| 1094 | ||
| 1095 | ||
| 1096 | ||
| 1097 | ||
| 1098 | ||
| 1099 | ||
| 1100 | ||
| 1101 | ||
| 1102 | ||
| 1103 | ||
| 1104 | ||
| 1105 | ||
| 1106 | ||
| 1107 | ||
| 1108 | ||
| 1109 | ||
| 1110 | ||
| 1111 | ||
| 1112 | ||
| 1113 | ||
| 1114 | ||
| 1115 | ||
| 1116 | ||
| 1117 | ||
| 1118 | ||
| 1119 | ||
| 1120 | ||
| 1121 | ||
| 1122 | ||
| 1123 | ||
| 1124 | ||
| 1125 | ||
| 1126 | ||
| 1127 | ||
| 1128 | ||
| 1129 | ||
| 1130 | ||
| 1131 | ||
| 1132 | ||
| 1133 | ||
| 1134 | ||
| 1135 | ||
| 1136 | ||
| 1137 | ||
| 1138 | ||
| 1139 | ||
| 1140 | ||
| 3929 | GM.openInTab("https://github.com/LiteHell/NamuFix/issues"); | |
| 3930 | }); | |
| 3931 | addItemToMemberMenu('KISA WHOIS', function (evt) { | |
| 3932 | evt.preventDefault(); | |
| 1141 | 3933 | |
| 1142 | ||
| 3934 | whoisPopup(prompt('조회할 IP주소를 입력하세요.')); | |
| 3935 | }) | |
| 1143 | 3936 | |
| 1144 | == | |
| 1145 | ||
| 1146 | ||
| 1147 | ||
| 1148 | ||
| 1149 | ||
| 1150 | ||
| 1151 | ||
| 1152 | ||
| 1153 | ||
| 1154 | ||
| 1155 | ||
| 1156 | ||
| 1157 | ||
| 1158 | ||
| 1159 | ||
| 1160 | ||
| 1161 | ||
| 1162 | ||
| 1163 | ||
| 1164 | ||
| 1165 | ||
| 1166 | ||
| 1167 | ||
| 1168 | ||
| 1169 | ||
| 1170 | ||
| 1171 | ||
| 1172 | ||
| 1173 | ||
| 1174 | ||
| 1175 | ||
| 1176 | ||
| 1177 | ||
| 1178 | ||
| 1179 | ||
| 1180 | ||
| 1181 | ||
| 1182 | ||
| 1183 | ||
| 1184 | ||
| 1185 | ||
| 1186 | ||
| 1187 | ||
| 1188 | ||
| 1189 | ||
| 1190 | ||
| 1191 | ||
| 1192 | ||
| 1193 | ||
| 1194 | ||
| 1195 | ||
| 1196 | ||
| 3937 | if (SET.addBatchBlockMenu) { | |
| 3938 | addItemToMemberMenu('계정/IP 일괄 차단', function (evt) { | |
| 3939 | evt.preventDefault(); | |
| 3940 | var win = TooSimplePopup(); | |
| 3941 | win.title('계정/IP 일괄 차단'); | |
| 3942 | win.content(function (con) { | |
| 3943 | var expire = 0; | |
| 3944 | con.innerHTML = '<p>아래에 차단할 IP주소/계정들을 입력해주세요.' + | |
| 3945 | '차단 사유 : <input type="text" id="note"></input><br>' + | |
| 3946 | '차단 기간 : <span id="expire_display">영구</span> <a href="#" id="setExpire">(차단기간 설정)</a>(참고 : 0초 = 영구차단)<br>' + | |
| 3947 | '로그인 허용 : <input type="checkbox" id="allowLogin"></input><br>' + | |
| 3948 | '※ IP 차단 해제시에는 차단사유/영구차단 여부/로그인 허용 여부를 설정할 필요 없으며 IP는 IPv4만 인식합니다.</p>' + | |
| 3949 | '차단할 계정/IP (개행으로 구분) : <br>' + | |
| 3950 | '<textarea style="width: 800px; max-width: 80vw; height: 500px; max-height: 80vh;"></textarea>'; | |
| 3951 | con.querySelector('a#setExpire').addEventListener('click', function (evt) { | |
| 3952 | evt.preventDefault(); | |
| 3953 | enterTimespanPopup('차단기간 설정', function (span) { | |
| 3954 | if (span === null) { | |
| 3955 | return alert('입력이 없습니다.'); | |
| 3956 | } else { | |
| 3957 | expire = span; | |
| 3958 | con.querySelector('#expire_display').textContent = span == 0 ? '영구' : expire + '초 '; | |
| 3959 | } | |
| 3960 | }); | |
| 3961 | }) | |
| 3962 | win.button('취소', win.close); | |
| 3963 | win.button('VPNGATE IP 불러오기', async function () { | |
| 3964 | let win = TooSimplePopup(); | |
| 3965 | win.title('불러오는 중'); | |
| 3966 | win.content(e => e.innerHTML = "불러오는 중입니다. 잠시만 기다려주십시오."); | |
| 3967 | let vpngateIPs = await getVPNGateIPList(); | |
| 3968 | let textarea = con.querySelector('textarea'); | |
| 3969 | textarea.value += '\n' + vpngateIPs.map(v => v + "/32").join("\n"); | |
| 3970 | win.close(); | |
| 3971 | }) | |
| 3972 | win.button('차단기록 검색', async function () { | |
| 3973 | let searchWin = TooSimplePopup(); | |
| 3974 | let queryInfo; | |
| 3975 | searchWin.title('차단기록 검색'); | |
| 3976 | searchWin.content(searchWinCon => { | |
| 3977 | searchWinCon.innerHTML = ` | |
| 3978 | <style>.search-prev[disabled], .search-next[disabled] {background: darkgray; text-decoration: line-through;}</style> | |
| 3979 | <div class="table-responsive-sm"> | |
| 3980 | <table class="table table-striped table-bordered table-hover table-sm"> | |
| 3981 | <thead> | |
| 3982 | <tr> | |
| 3983 | <td>선택</td> | |
| 3984 | <td>유형</td> | |
| 3985 | <td>실행자</td> | |
| 3986 | <td>피실행자</td> | |
| 3987 | <td>사유</td> | |
| 3988 | <td>기간</td> | |
| 3989 | <td>차단일시</td> | |
| 3990 | </tr> | |
| 3991 | </thead> | |
| 3992 | <tbody> | |
| 3993 | </tbody> | |
| 3994 | </table> | |
| 3995 | </div> | |
| 3996 | <div class="search-pagination"> | |
| 3997 | </div> | |
| 3998 | `; | |
| 1197 | 3999 | |
| 1198 | ||
| 4000 | function processQuery() { | |
| 4001 | let waitingWin = TooSimplePopup(); | |
| 4002 | waitingWin.title("진행중입니다"); | |
| 4003 | waitingWin.content(waitingWinCon => waitingWinCon.innerHTML = "진행중입니다. 잠시만 기다려주십시오."); | |
| 4004 | namuapi.searchBlockHistory(queryInfo, (result) => { | |
| 4005 | queryInfo.from = result.nextResultPageFrom || null; | |
| 4006 | queryInfo.until = result.prevResultPageUntil || null; | |
| 4007 | let tbody = searchWinCon.querySelector('tbody'); | |
| 4008 | tbody.innerHTML = ""; | |
| 4009 | for (let i of result) { | |
| 4010 | // checkbox, type, blocker, blocked, reason, duration, at | |
| 4011 | tbody.innerHTML += `<tr data-blocked="${encodeHTMLComponent(JSON.stringify(i.blocked))}"><td><input type="checkbox" checked></td><td>${i.type}</td><td>${encodeHTMLComponent(i.blocker)}</td><td>${encodeHTMLComponent(i.blocked)}</td><td>${encodeHTMLComponent(i.reason)}</td><td>${i.duration}</td><td>${formatDateTime(i.at)}</td></tr>`; | |
| 4012 | } | |
| 4013 | waitingWin.close(); | |
| 4014 | }); | |
| 4015 | } | |
| 4016 | searchWin.button('검색', () => { | |
| 4017 | let queryWin = TooSimplePopup(); | |
| 4018 | queryWin.title('쿼리 입력'); | |
| 4019 | queryWin.content(queryWinCon => { | |
| 4020 | queryWinCon.innerHTML = ` | |
| 4021 | <div> | |
| 4022 | 쿼리 : | |
| 4023 | <input type="text" class="search-query" style="width: 500px; max-width: 80vw;"></input> | |
| 4024 | </div>`; | |
| 4025 | queryWin.button('실행자 검색', () => { | |
| 4026 | queryInfo = { | |
| 4027 | query: queryWinCon.querySelector('.search-query').value, | |
| 4028 | isAuthor: true | |
| 4029 | }; | |
| 4030 | processQuery(); | |
| 4031 | }); | |
| 4032 | queryWin.button('내용 검색', () => { | |
| 4033 | queryInfo = { | |
| 4034 | query: queryWinCon.querySelector('.search-query').value, | |
| 4035 | isAuthor: false | |
| 4036 | }; | |
| 4037 | processQuery(); | |
| 4038 | }); | |
| 4039 | queryWin.button('닫기', queryWin.close); | |
| 4040 | }); | |
| 4041 | }); | |
| 4042 | searchWin.button('선택된 항목 추가', () => { | |
| 4043 | let isFirst = true; | |
| 4044 | let textarea = con.querySelector('textarea'); | |
| 4045 | let waitingWin = TooSimplePopup(); | |
| 4046 | waitingWin.title('진행중입니다.'); | |
| 4047 | waitingWin.content(c => c.innerHTML = "진행중입니다."); | |
| 4048 | for (let i of searchWinCon.querySelectorAll('tbody tr')) { | |
| 4049 | if (isFirst) { | |
| 4050 | textarea.value += '\n'; | |
| 4051 | isFirst = false; | |
| 4052 | } | |
| 4053 | if (i.querySelector('input[type="checkbox"]').checked) | |
| 4054 | textarea.value += JSON.parse(i.dataset.blocked) + "\n"; | |
| 4055 | } | |
| 4056 | waitingWin.close(); | |
| 4057 | }); | |
| 4058 | searchWin.button('이전 결과', () => { | |
| 4059 | if (queryInfo.until) { | |
| 4060 | delete queryInfo.from; | |
| 4061 | processQuery(); | |
| 4062 | } else { | |
| 4063 | alert('첫 페이지입니다.'); | |
| 4064 | } | |
| 4065 | }); | |
| 4066 | searchWin.button('다음 결과', () => { | |
| 4067 | if (queryInfo.from) { | |
| 4068 | delete queryInfo.until; | |
| 4069 | processQuery(); | |
| 4070 | } else { | |
| 4071 | alert('마지막 페이지입니다.'); | |
| 4072 | } | |
| 4073 | }); | |
| 4074 | searchWin.button('닫기', searchWin.close); | |
| 4075 | }); | |
| 4076 | }) | |
| 1199 | 4077 | |
| 1200 | ||
| 1201 | ||
| 1202 | ||
| 1203 | ||
| 1204 | ||
| 1205 | ||
| 1206 | ||
| 1207 | ||
| 1208 | ||
| 1209 | ||
| 1210 | ||
| 1211 | ||
| 1212 | ||
| 1213 | ||
| 1214 | ||
| 1215 | ||
| 1216 | ||
| 1217 | ||
| 1218 | ||
| 1219 | ||
| 1220 | ||
| 1221 | ||
| 1222 | ||
| 1223 | ||
| 1224 | ||
| 1225 | ||
| 1226 | ||
| 1227 | ||
| 1228 | ||
| 1229 | ||
| 1230 | ||
| 1231 | ||
| 1232 | ||
| 1233 | ||
| 1234 | ||
| 1235 | ||
| 1236 | ||
| 1237 | ||
| 1238 | ||
| 1239 | ||
| 1240 | ||
| 1241 | ||
| 1242 | ||
| 1243 | ||
| 1244 | ||
| 1245 | ||
| 1246 | ||
| 1247 | ||
| 1248 | ||
| 1249 | ||
| 1250 | ||
| 1251 | ||
| 1252 | ||
| 4078 | function parseTextarea() { | |
| 4079 | let commonData = { | |
| 4080 | note: con.querySelector('input#note').value, | |
| 4081 | expire: expire, | |
| 4082 | allowLogin: con.querySelector('input#allowLogin').checked | |
| 4083 | } | |
| 4084 | return con.querySelector('textarea').value | |
| 4085 | .split('\n') | |
| 4086 | .map(v => v.trim()) | |
| 4087 | .filter(v => v != "") | |
| 4088 | .map((v) => { | |
| 4089 | let ipWithCIDR = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/; | |
| 4090 | let data = JSON.parse(JSON.stringify(commonData)); | |
| 4091 | if (ipWithCIDR.test(v)) | |
| 4092 | data.ip = v; | |
| 4093 | else | |
| 4094 | data.id = v; | |
| 4095 | if (data.ip && !data.ip.includes("/")) data.ip += "/32"; | |
| 4096 | return data; | |
| 4097 | }); | |
| 4098 | } | |
| 1253 | 4099 | |
| 1254 | ||
| 4100 | function commonLoop(_datas, progressCallback) { // returns error object | |
| 4101 | return new Promise((resolve, reject) => { | |
| 4102 | let result = { | |
| 4103 | errors: [], | |
| 4104 | success: [] | |
| 4105 | }; | |
| 4106 | let datas = JSON.parse(JSON.stringify(_datas)); | |
| 4107 | async.eachLimit(datas, SET.adminReqLimit, (data, callback) => { | |
| 4108 | namuapi[data.handlerName](data.parameter, (err, target) => { | |
| 4109 | if (err) { | |
| 4110 | result.errors.push({ | |
| 4111 | target: data.parameter, | |
| 4112 | error: err | |
| 4113 | }); | |
| 4114 | } else { | |
| 4115 | result.success.push(data.parameter); | |
| 4116 | } | |
| 4117 | if (progressCallback) | |
| 4118 | progressCallback(data); | |
| 4119 | callback(); | |
| 4120 | }); | |
| 4121 | }, (err) => { | |
| 4122 | return resolve(result); | |
| 4123 | }); | |
| 4124 | }); | |
| 4125 | } | |
| 4126 | win.button('차단', async function () { | |
| 4127 | var waitingWin = TooSimplePopup(); | |
| 4128 | var errors = []; | |
| 4129 | waitingWin.title('처리중'); | |
| 4130 | waitingWin.content(function (wwcon) { | |
| 4131 | wwcon.innerHTML = "처리중입니다." | |
| 4132 | }); | |
| 4133 | let datas = parseTextarea().map(v => ({ | |
| 4134 | parameter: v, | |
| 4135 | handlerName: v.ip ? "blockIP" : "blockAccount" | |
| 4136 | })); | |
| 4137 | let result = await commonLoop(datas, d => waitingWin.content(wwcon => wwcon.innerHTML = `처리 완료: ${d.parameter.ip || d.parameter.id}`)); | |
| 4138 | if (result.errors.length > 0) { | |
| 4139 | waitingWin.content(wwcon => { | |
| 4140 | wwcon.innerHTML = "오류가 있습니다.<br><br>" + result.errors.map(v => `${encodeHTMLComponent(v.target.ip || v.target.id)} : ${v.error}`).join("<br>"); | |
| 4141 | }); | |
| 4142 | waitingWin.button('닫기', waitingWin.close); | |
| 4143 | } else { | |
| 4144 | waitingWin.close(); | |
| 4145 | } | |
| 4146 | }); | |
| 4147 | win.button('차단 해제', async function () { | |
| 4148 | var waitingWin = TooSimplePopup(); | |
| 4149 | var errors = []; | |
| 4150 | waitingWin.title('처리중'); | |
| 4151 | waitingWin.content(function (wwcon) { | |
| 4152 | wwcon.innerHTML = "처리중입니다." | |
| 4153 | }); | |
| 4154 | let datas = parseTextarea().map(v => { | |
| 4155 | if (v.ip) { | |
| 4156 | return { | |
| 4157 | parameter: v.ip, | |
| 4158 | handlerName: 'unblockIP' | |
| 4159 | }; | |
| 4160 | } else { | |
| 4161 | let tmp = { | |
| 4162 | parameter: v, | |
| 4163 | handlerName: 'blockAccount' | |
| 4164 | }; | |
| 4165 | tmp.parameter.expire = -1; | |
| 4166 | return tmp; | |
| 4167 | } | |
| 4168 | }); | |
| 4169 | let result = await commonLoop(datas, d => waitingWin.content(wwcon => wwcon.innerHTML = `처리 완료: ${d.parameter.id ? d.parameter.id : d.parameter}`)); | |
| 4170 | if (result.errors.length > 0) { | |
| 4171 | waitingWin.content(wwcon => { | |
| 4172 | wwcon.innerHTML = "오류가 있습니다.<br><br>" + result.errors.map(v => `${encodeHTMLComponent(v.target.id || v.target)} : ${v.error}`).join("<br>"); | |
| 4173 | }); | |
| 4174 | waitingWin.button('닫기', waitingWin.close); | |
| 4175 | } else { | |
| 4176 | waitingWin.close(); | |
| 4177 | } | |
| 4178 | }); | |
| 4179 | }); | |
| 4180 | }) | |
| 4181 | } | |
| 1255 | 4182 | |
| 1256 | ||
| 1257 | ||
| 1258 | ||
| 1259 | ||
| 1260 | ||
| 1261 | ||
| 1262 | ||
| 1263 | ||
| 1264 | ||
| 1265 | ||
| 1266 | ||
| 1267 | ||
| 1268 | ||
| 1269 | ||
| 1270 | ||
| 1271 | ||
| 1272 | ||
| 1273 | ||
| 1274 | ||
| 1275 | ||
| 1276 | ||
| 1277 | ||
| 1278 | ||
| 1279 | ||
| 1280 | ||
| 1281 | ||
| 1282 | ||
| 1283 | ||
| 1284 | ||
| 1285 | ||
| 1286 | ||
| 1287 | ||
| 1288 | ||
| 1289 | ||
| 1290 | ||
| 1291 | ||
| 1292 | ||
| 1293 | ||
| 1294 | ||
| 1295 | ||
| 1296 | ||
| 1297 | ||
| 1298 | ||
| 1299 | ||
| 1300 | ||
| 1301 | ||
| 1302 | ||
| 1303 | ||
| 1304 | ||
| 1305 | ||
| 1306 | ||
| 1307 | ||
| 1308 | ||
| 4183 | listenPJAX(mainFunc); | |
| 4184 | await mainFunc(); | |
| 1309 | 4185 | |
| 1310 | ||
| 4186 | if (document.querySelector('body').getAttribute('class').indexOf('senkawa') == -1 && document.querySelector('body').getAttribute('class').indexOf('Liberty') == -1) { | |
| 4187 | await SET.load(); | |
| 4188 | if (!SET.ignoreNonSenkawaWarning) { | |
| 4189 | var win = TooSimplePopup(); | |
| 4190 | win.title('스킨 관련 안내'); | |
| 4191 | win.content(function (element) { | |
| 4192 | element.innerHTML = '<p><strong>안내:</strong> NamuFix는 senkawa/liberty 스킨이 아닌 경우 비정상적으로 작동할 수 있습니다.<br>가능하면 senkawa/liberty 스킨을 사용해주십시오.<br><em>(이 메세지는 한번만 보여집니다.)</em></p>' | |
| 4193 | }); | |
| 4194 | win.button('확인', win.close); | |
| 4195 | SET.ignoreNonSenkawaWarning = true; | |
| 4196 | await SET.save(); | |
| 4197 | } | |
| 4198 | } | |
| 4199 | if (GM.info.scriptHandler === "Greasemonkey" && GM.info.version.startsWith("4.") && !SET.ignoreGM4Warning) { | |
| 4200 | var win = TooSimplePopup(); | |
| 4201 | win.title('Greasemonkey 4와의 호환성 안내'); | |
| 4202 | win.content((container) => container.innerHTML = "<p>Greasemonkey 4+ 버전에서는 NamuFix가 비정상적으로 작동할 가능성이 <strong>매우</strong> 높습니다. 버그를 작동하면 즉시 이슈트래커에 신고해주세요.</p>"); | |
| 4203 | win.button('닫기', win.close); | |
| 4204 | SET.ignoreGM4Warning = true; | |
| 4205 | await SET.save(); | |
| 4206 | } | |
| 4207 | })(); | |
| 4208 | } catch (err) { | |
| 4209 | console.error("[NamuFix] 오류 발생!"); | |
| 4210 | console.error(err); | |
| 4211 | } | |
| 1311 | 4212 | |
| 1312 | === 2401 ~ 2500 === | |
| 1313 | ##단어를 추가하기 전 "반드시" 찾기 기능을 사용해서 이미 나온 단어가 아닌지 확인해주세요!! | |
| 1314 | ##한 번에 한 단어만 추가해 주시기 바랍니다. 만약 10분 뒤에 아무도 추가하지 않았다면 다시 새 단어를 추가하셔도 됩니다. | |
| 1315 | || 2401 || [[ ]] || 2402 || [[ ]] || | |
| 1316 | || 2403 || [[ ]] || 2404 || [[ ]] || | |
| 1317 | || 2405 || [[ ]] || 2406 || [[ ]] || | |
| 1318 | || 2407 || [[ ]] || 2408 || [[ ]] || | |
| 1319 | || 2409 || [[ ]] || 2410 || [[ ]] || | |
| 1320 | || 2411 || [[ ]] || 2412 || [[ ]] || | |
| 1321 | || 2413 || [[ ]] || 2414 || [[ ]] || | |
| 1322 | || 2415 || [[ ]] || 2416 || [[ ]] || | |
| 1323 | || 2417 || [[ ]] || 2418 || [[ ]] || | |
| 1324 | || 2419 || [[ ]] || 2420 || [[ ]] || | |
| 1325 | || 2421 || [[ ]] || 2422 || [[ ]] || | |
| 1326 | || 2423 || [[ ]] || 2424 || [[ ]] || | |
| 1327 | || 2425 || [[ ]] || 2426 || [[ ]] || | |
| 1328 | || 2427 || [[ ]] || 2428 || [[ ]] || | |
| 1329 | || 2429 || [[ ]] || 2430 || [[ ]] || | |
| 1330 | || 2431 || [[ ]] || 2432 || [[ ]] || | |
| 1331 | || 2433 || [[ ]] || 2434 || [[ ]] || | |
| 1332 | || 2435 || [[ ]] || 2436 || [[ ]] || | |
| 1333 | || 2437 || [[ ]] || 2438 || [[ ]] || | |
| 1334 | || 2439 || [[ ]] || 2440 || [[ ]] || | |
| 1335 | || 2441 || [[ ]] || 2442 || [[ ]] || | |
| 1336 | || 2443 || [[ ]] || 2444 || [[ ]] || | |
| 1337 | || 2445 || [[ ]] || 2446 || [[ ]] || | |
| 1338 | || 2447 || [[ ]] || 2448 || [[ ]] || | |
| 1339 | || 2449 || [[ ]] || 2450 || [[ ]] || | |
| 1340 | || 2451 || [[ ]] || 2452 || [[ ]] || | |
| 1341 | || 2453 || [[ ]] || 2454 || [[ ]] || | |
| 1342 | || 2455 || [[ ]] || 2456 || [[ ]] || | |
| 1343 | || 2457 || [[ ]] || 2458 || [[ ]] || | |
| 1344 | || 2459 || [[ ]] || 2460 || [[ ]] || | |
| 1345 | || 2461 || [[ ]] || 2462 || [[ ]] || | |
| 1346 | || 2463 || [[ ]] || 2464 || [[ ]] || | |
| 1347 | || 2465 || [[ ]] || 2466 || [[ ]] || | |
| 1348 | || 2467 || [[ ]] || 2468 || [[ ]] || | |
| 1349 | || 2469 || [[ ]] || 2470 || [[ ]] || | |
| 1350 | || 2471 || [[ ]] || 2472 || [[ ]] || | |
| 1351 | || 2473 || [[ ]] || 2474 || [[ ]] || | |
| 1352 | || 2475 || [[ ]] || 2476 || [[ ]] || | |
| 1353 | || 2477 || [[ ]] || 2478 || [[ ]] || | |
| 1354 | || 2479 || [[ ]] || 2480 || [[ ]] || | |
| 1355 | || 2481 || [[ ]] || 2482 || [[ ]] || | |
| 1356 | || 2483 || [[ ]] || 2484 || [[ ]] || | |
| 1357 | || 2485 || [[ ]] || 2486 || [[ ]] || | |
| 1358 | || 2487 || [[ ]] || 2488 || [[ ]] || | |
| 1359 | || 2489 || [[ ]] || 2490 || [[ ]] || | |
| 1360 | || 2491 || [[ ]] || 2492 || [[ ]] || | |
| 1361 | || 2493 || [[ ]] || 2494 || [[ ]] || | |
| 1362 | || 2495 || [[ ]] || 2496 || [[ ]] || | |
| 1363 | || 2497 || [[ ]] || 2498 || [[ ]] || | |
| 1364 | || 2499 || [[ ]] || 2500 || [[ ]] || | |
| 1365 | ||
| 1366 | ##2500까지 다 적으신 분은 문단 제목을 완료로 바꿔주세요. | |
| 1367 | ||
| 1368 | [각주] | |
| 1369 | [include(틀:문서 가져옴, title=알파위키:보존문서/끝말잇기, version=35)] |