PageSourceSearch

https://wmu.or.kr/js/robots.1785394112.js

js wmu.or.kr collected 2026-09-25 11:26:05 UTC 76,768 bytes, 1,611 lines download raw bytes

1let robots_css = ` 
2  .robots-button {all: unset; display: inline-block; background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; text-align: center; text-decoration: none; line-height: normal; -webkit-appearance: none; -moz-appearance: none;}
3  .robots-svg:not(.robots-form__itemCancel):not(.robots-form__itemConfirm) {width: 15px;height: 15px;}
4  .robots-divider {width: 100%;height: 1px;background-color: #e6e8ea;}
5
6  .robots-background {box-sizing: border-box;display: none;position: fixed;top: 50%; left: 50%;transform: translate(-50%, -50%);width: 100%;height: 100%;opacity: 0.9;background-color: #eeeff3;z-index: 9998;}
7  .robots-modal > :nth-child(1 of .robots-divider) {position: sticky;top: 130px;z-index: 10000;}
8  .robots-modal > :nth-child(2 of .robots-divider) {position: sticky;top: 181px;z-index: 10000;}
9  .robots-modal > :nth-child(3 of .robots-divider) {position: sticky;top: 182px;height: 30px;background: #fff;z-index: 10000;}
10  .robots-modal {box-sizing: border-box;display: none;position: fixed;top: 50%; left: 50%;transform: translate(-50%, -50%);width: 900px;height: 100%;background-color: #fff;font-family: 'Noto Sans KR';z-index: 9999;overflow-y: auto;-ms-overflow-style: none;}
11  .robots-modal::-webkit-scrollbar {width: 3px;}
12  .robots-modal::-webkit-scrollbar-thumb {background-color: #d1d3d5;}
13  .robots-modal::-webkit-scrollbar-track {background-color: #f3f4f5;}
14  .robots-modal .tab-content {display: none;}
15  .robots-modal .tab-content.active {display: block;}
16
17  .robots-header {padding: 24px 40px 24px;position: sticky;top: 1px;z-index: 10000;background: #fff;height: 142px;border-bottom: 1px solid #e6e8ea;}
18  .robots-header2 {display:flex; justify-content: space-between;}
19  .robots-header__title {height: 35px;font-family: 'Noto Sans KR';font-size: 24px;font-weight: normal;font-stretch: normal;font-style: normal;line-height: 1.46;letter-spacing: normal;text-align: left;color: #222428;margin-bottom:20px;}
20  .robots-header__title-explain {line-height: 1.63;font-size: 12px;color: #696c71;background: #fff;}
21  .robots-header__btns {display: flex;justify-content: space-between;}
22  .robots-header__btn {width: 120px;height: 40px;border-radius: 3px;border: none;color: #696c71;}
23  .robots-header__btn--close {background-color: #f5f5f5;margin-right:10px;}
24  .robots-header__btn--save {background-color: #4789e7;color: #fff;}
25  
26  .robots-site-menu {border-bottom:1px solid #e6e8ea; background-color:#fff; padding: 0 40px; position: sticky; top: 143px; z-index:10000; height: 50px;}
27  .robots-site-menu ul {display: flex;}
28  .robots-site-menu li {display: inline-block; position:relative; padding:0;}
29  .robots-site-menu li a {padding:10px 15px; font-size:14px; color:#222428; min-width:104px; text-align: center; box-sizing: border-box; line-height: 1.43; letter-spacing: -0.35px; height: 50px; display: flex; justify-content: center; align-items: center;}
30  .robots-site-menu li.active a {border-bottom:3px solid #4789e7; color:#222428;}
31  .robots-site-menu li:not(.active) a {opacity: 0.3;}
32  .robots-site-menu li:hover:not(.active) a {border-bottom:3px solid rgba(107,137,231, .15); color:#222428; opacity: 1;}
33  .robots-site-menu .robots-site-menu-container:before, .site-menu .container:after {display:block; content:none;}
34
35  #robots-content .robots-toggle {display: flex;align-items: center;width: 100%;height: 50px;padding: 0 40px;position: sticky;top: 193px;z-index: 10000;background: white;border-bottom: 1px solid #e6e8ea;}
36  #robots-content .robots-toggle__switch {position: relative;display: inline-block;width: 34px;height: 18px;}
37  #robots-content .robots-toggle__switch input {display: none;}
38  #robots-content .robots-toggle__slider {position: absolute;cursor: pointer;background-color: var(--color-bg-disabled, #d6d9dc);border: solid 1px var(--color-content-placeholder, #b4b5b5);border-radius: 50px;width: 34px;height: 18px;transition: background-color 0.2s ease, border-color 0.2s ease;}
39  #robots-content .robots-toggle__slider::before {position: absolute;content: "";height: 16px;width: 16px;background-color: #fff;border-radius: 50%;transition: transform 0.2s ease;left: 0;top: 0;}
40  #robots-content .robots-toggle__slider:hover {border: solid 1px #adcbf3;background-color: #c3dbfc;}
41  #robots-content .robots-toggle__switch input:checked + .robots-toggle__slider {background-color: #3b89ff;border: 1px solid #0e62cd;}
42  #robots-content .robots-toggle__switch input:checked + .robots-toggle__slider::before {transform: translateX(16px);}
43  #robots-content .robots-toggle__switch input:checked + .robots-toggle__slider:hover {background-color: #1b6eeb;border: 1px solid #0759c2;}
44  #robots-content .robots-toggle div {display: flex;align-items: center;width: 164px;}
45  #robots-content .robots-toggle div > span {font-family: 'Noto Sans KR';font-size: 13px;font-weight: normal;font-stretch: normal;font-style: normal;line-height: 1.46;letter-spacing: -0.33px;text-align: left;margin-right: 10px;}
46
47  #robots-content .robots-form {display: flex;flex-direction: column;padding: 0 40px;margin-top: 2px;}
48  #robots-content .robots-form table {width: 100%;border-radius: 3px;border: solid 1px #e6e8ea;border-collapse: collapse;}
49  #robots-content .robots-form table tr {box-sizing: border-box;width: 100%;border-bottom: solid 1px #e6e8ea;}
50  #robots-content .robots-form table th {padding: 10px 0 11px 0;height: 19px;font-family: 'Noto Sans KR';font-size: 13px;font-weight: normal;font-stretch: normal;font-style: normal;line-height: 1.46;letter-spacing: -0.13px;}
51  #robots-content .robots-form table th:first-child,
52  #robots-content .robots-form table td:first-child {width: 38px;text-align: center;vertical-align: middle;position: relative;}
53  #robots-content .robots-form table th:nth-child(2),
54  #robots-content .robots-form table td:nth-child(2) {width: 50px;text-align: center;}
55  #robots-content .robots-form table th:nth-child(3),
56  #robots-content .robots-form table td:nth-child(3) {width: 236px;}
57  #robots-content .robots-form table th:nth-child(4),
58  #robots-content .robots-form table td:nth-child(4) {width: 122px;}
59  #robots-content .robots-form table th:last-child,
60  #robots-content .robots-form table td:last-child {width: auto;}
61  #robots-content .robots-form table td {height: 40px;padding: 0;}
62  #robots-content .robots-form table tbody tr.ui-sortable-helper {display: table;table-layout: fixed;background-color: #fff;box-sizing: border-box;}
63  #robots-content .robots-form table tbody tr.ui-sortable-helper td,
64  #robots-content .robots-form table tbody tr.ui-sortable-helper input {box-sizing: border-box;}
65  #robots-content .robots-form__dragHandle {opacity: 0;transition: opacity 0.5s;cursor: move;display: block;
65width: 20px;height: 20px;margin: 0 auto;fill: #949fab;}
66  #robots-content .robots-form__dragHandle:hover {fill: #4789E7;}
67  #robots-content .robots-form table tr:hover .robots-form__dragHandle {opacity: 1;}
68  #robots-content .robots-form table tbody tr:hover{outline: 1px solid #4789e7;}
69  #robots-content .robots-form table td:nth-child(3) input {color: var(--color-content-secondary);width: 90%;height: 30px;border-radius: 3px;border: solid 1px #dbdcdf;padding: 5px;}
70  #robots-content .robots-form table td:last-child {display: grid;grid-template-columns: minmax(0, 1fr) 32px 32px;align-items: center;column-gap: 8px;padding: 4px 10px;}
71  #robots-content .robots-form table td:last-child input {color: var(--color-content-secondary);width: 100%;height: 30px;border-radius: 3px;border: solid 1px #dbdcdf;padding: 6px 10px;}
72  #robots-content .robots-form table td:last-child button {position: static;width: 32px;height: 32px;display: flex;justify-content: center;align-items: center;}
73  #robots-content .robots-form table td:last-child .robots-form__cancelBtn {background-color: transparent;}
74  #robots-content .robots-form table td:last-child .robots-form__confirmBtn {border-radius: 3px;background-color: #4789e7;fill: #fff;transition: background-color 0.2s ease;}
75  #robots-content .robots-form table td:last-child .robots-form__confirmBtn:hover {background-color: #5997F1;}
76  #robots-content .robots-form table input:focus {border-color: #4789e7;}
77  #robots-content .robots-form table input[name="user-agent"]:not([readonly]):hover,
78  #robots-content .robots-form table input[name="value"]:not([readonly]):hover {border-color: #9bc5ff;}
79  #robots-content .robots-form table input[readonly] {pointer-events: none;background-color: #fafafa;color: #b1b3b7 !important;}
80  #robots-content .robots-form table select {width: 85%;height: 30px;border-radius: 3px;border: solid 1px #e6e8ea;background-color: #fff;}
81  #robots-content .robots-form table select:focus {border-color: #4789e7;}
82  #robots-content .robots-form table .bootstrap-select {width: 85% !important;height: 32px; cursor:default;}
83  #robots-content .robots-form table .bootstrap-select > .dropdown-toggle {height: 32px;padding: 6px 10px;border-radius: 3px;border: solid 1px var(--color-line-primary);background-color: var(--color-bg-white);font-size: 14px;line-height: 1.5;color: #222428;}
84  #robots-content .robots-form table .bootstrap-select:not(.disabled) .filter-option,
85  #robots-content .robots-form table .bootstrap-select:not(.disabled) .filter-option-inner-inner {font-size: 14px;line-height: 1.5;color: #696c71;}
86  #robots-content .robots-form table .bootstrap-select.disabled .filter-option,
87  #robots-content .robots-form table .bootstrap-select.disabled .filter-option-inner-inner {font-size: 14px;line-height: 1.5;color: var(--color-content-placeholder);}
88  #robots-content .robots-form table .bootstrap-select.disabled > .dropdown-toggle,
89  #robots-content .robots-form table .bootstrap-select > .dropdown-toggle.disabled {padding: 6px 10px;border-radius: 3px;border: solid 1px var(--color-line-primary);background-color: var(--color-bg-white);}
90  #robots-content .robots-form table .bootstrap-select .btn.btn-default.dropdown-toggle.disabled {background-color: #ffffff !important;border: solid 1px var(--color-line-primary) !important;opacity: 1;}
91  #robots-content .robots-form table .bootstrap-select.open > .dropdown-toggle,
92  #robots-content .robots-form table .dropdown.bootstrap-select.bs3.open > .dropdown-toggle {border: solid 1px var(--color-brand-primary);background-color: var(--color-bg-white);}
93  #robots-content .robots-form table .bootstrap-select > .dropdown-toggle .bs-caret .caret {border-top-color: #696c71;}
94  #robots-content .robots-form table .bootstrap-select > .dropdown-toggle:focus,
95  #robots-content .robots-form table .bootstrap-select > .dropdown-toggle:active,
96  #robots-content .robots-form table .bootstrap-select.open > .dropdown-toggle,
97  #robots-content .robots-form table .bootstrap-select.open > .dropdown-toggle:focus,
98  #robots-content .robots-form table .bootstrap-select.open > .dropdown-toggle:active {outline: none !important;box-shadow: none !important;}
99  #robots-content .robots-form table .bootstrap-select.open > .dropdown-toggle .bs-caret .caret,
100  #robots-content .robots-form table .dropdown.bootstrap-select.bs3.open > .dropdown-toggle .bs-caret .caret {border-top: 0;border-bottom: 4px dashed #4789e7;border-right: 4px solid transparent;border-left: 4px solid transparent;}
101  .robots-modal .bootstrap-select .dropdown-menu > li > a {padding: 7px 10px;font-size: 14px;line-height: 1.5;}
102  .robots-modal .bootstrap-select .dropdown-menu > li:not(.selected):not(.active) > a {color: #696c71;}
103  .robots-modal .bootstrap-select .dropdown-menu > li > a:hover,
104  .robots-modal .bootstrap-select .dropdown-menu > li > a:focus {background-color: #fafafa;}
105  .robots-modal .bootstrap-select .dropdown-menu > li.selected > a,
106  .robots-modal .bootstrap-select .dropdown-menu > li.active > a {background-color: var(--color-bg-white);color: var(--color-brand-primary);}
107  .robots-modal .bootstrap-select .dropdown-menu > li.selected > a:hover,
108  .robots-modal .bootstrap-select .dropdown-menu > li.active > a:hover,
109  .robots-modal .bootstrap-select .dropdown-menu > li.selected > a:focus,
110  .robots-modal .bootstrap-select .dropdown-menu > li.active > a:focus {background-color: #fafafa;}
111  .robots-modal .bootstrap-select .dropdown-menu.open {z-index: 10002;border-radius: 3px;box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.1);border: solid 1px var(--color-line-primary);background-color: var(--color-bg-white);}
112  #robots-content .robots-form__itemModify,
113  #robots-content .robots-form__itemConfirm,
114  #robots-content .robots-form__itemDelete {cursor: pointer;position: static;justify-self: center;align-self: center;margin: 0;}
115  #robots-content .robots-form__itemModify,
116  #robots-content .robots-form__itemDelete {width: 24px;height: 24px;fill: #b1b3b7;}
117  #robots-content .robots-form__itemCancel {fill: #b1b3b7;}
118  #robots-content .robots-form__itemCancel:hover,
119  #robots-content .robots-form__itemModify:hover,
120  #robots-content .robots-form__itemDelete:hover {fill: #696C71 !important;}
121  #robots-content .robots-form-btns {display: flex;margin-top: 15.5px;margin-bottom: 25px;justify-content: space-between;}
122  #robots-content .robots-form-btns > div {display: flex;}
123  #robots-content .robots-form-btns__btn {display: inline-flex;align-items: center;justify-content: center;gap: 3px;
123height: 36px;border-radius: 3px;border: solid 1px var(--color-line-primary);background-color: var(--color-bg-white);color: #222428;fill: #222428;}
124  #robots-content .robots-form-btns__btn:hover {background-color: #FAFAFA;}
125  #robots-content .robots-form-btns__addItem,
126  #robots-content .robots-form-btns__loadFileBtn {width: 98px;}
127  #robots-content .robots-form-btns__resetItem {width: 82px;}
128  #robots-content .robots-form-btns__btn svg {width: 20px;height: 20px;flex: 0 0 20px;}
129  #robots-content .robots-form-btns > div > .robots-form-btns__addItem {margin-right: 10px;}
130  #robots-content .robots-form-btns__btn span {font-family: 'Noto Sans KR';font-size: 14px;font-weight: normal;font-stretch: normal;font-style: normal;line-height: 1;color: #222428;}
131
132  #robots-content .robots-tooltip + .tooltip > .tooltip-inner {width: 200px;white-space: normal; border-color: #b1b3b7;}
133  #robots-content .robots-preview {display: flex;flex-direction: column;padding: 0 40px 30px 40px;}
134  #robots-content .robots-preview__title {width: 100%;height: 20px;font-family: 'Noto Sans KR';color: #222428;margin: 25px 0 15px 0;font-size: 14px;}
135  #robots-content .robots-preview__content {width: 100%;height: 554px;border: solid 1px var(--color-line-primary); background-color: var(--color-bg-hover-primary);resize: none;padding: 20px;color: #696c71;letter-spacing: -0.13px;line-height: 1.5;font-size: 14px;overflow-y: auto;-ms-overflow-style: none;}
136  #robots-content .robots-preview__content::-webkit-scrollbar {width: 3px;}
137  #robots-content .robots-preview__content::-webkit-scrollbar-thumb {background-color: #d1d3d5; cursor: pointer;}
138  #robots-content .robots-preview__content::-webkit-scrollbar-track {background-color: #f3f4f5;}
139  #robots-content .sortable-placeholder {background-color: #f0f0f0;height: 40px;}
140
141  #sitemap-content .sitemap-btns {padding: 30px 40px 0 40px;}
142  #sitemap-content .sitemap-btns .sitemap-btn {width: 104px; height: 30px; border-radius:3px; border:1px solid #e6e8ea; background-color: #fdfdfd;}
143  #sitemap-content .sitemap-btns .sitemap-btn .robots-svg {transform: translateY(3px);}
144  #sitemap-content .sitemap-btns .sitemap-btn#sitemap-btn__upload {margin-right: 10px;}
145  #sitemap-content .sitemap-btns .sitemap-btn#sitemap-btn__upload + .tooltip > .tooltip-inner {max-width: 130px;white-space: normal; border-color: #b1b3b7;}
146  #sitemap-content .sitemap-preview {display: flex;flex-direction: column;padding: 0 40px 30px 40px;}
147  #sitemap-content .sitemap-preview__title {width: 100%;height: 20px;font-family: 'Noto Sans KR';color: #222428;margin: 25px 0 15px 0;font-size: 14px;}
148  #sitemap-content .sitemap-preview__content {width: 100%;height: 554px;border: solid 1px #e6e8ea;background-color: #fdfdfd;resize: none;padding: 15px 20px;color: #222428;letter-spacing: -0.13px;line-height: 1.46;font-size: 13px;overflow-y: auto;-ms-overflow-style: none;}
149  #sitemap-content .sitemap-preview__content::-webkit-scrollbar {width: 3px;}
150  #sitemap-content .sitemap-preview__content::-webkit-scrollbar-thumb {background-color: #d1d3d5; cursor: pointer;}
151  #sitemap-content .sitemap-preview__content::-webkit-scrollbar-track {background-color: #f3f4f5;}
152  
153  @media (max-width:900px) {
154    .robots-modal {width: 100%;}
155  }
156`;
157
158(function ($) {
159  $.seoConfig = {
160    initialState: {
161      searchBotSwitch: false,
162      aiBotSwitch: false,
163      robotsText: '',
164      sitemapText: ''
165    },
166  }
167
168  $.robots = {
169    isOpen: false,
170    robotsText: '',
171    robotsRules: {},
172    siteMaps: new Set(),
173    domain: '',
174    itemIndex: 1,
175    searchBots: [
176      "AhrefsBot", "Applebot", "archive.org_bot", "Baiduspider", "Bingbot",
177      "CCBot", "DaumBot", "Daumoa", "DotBot", "Exabot", "facebot",
178      "Gigablast", "Googlebot", "ia_archiver", "LinkedInBot", "Magpie-crawler",
179      "MegaIndex", "MJ12bot", "MojeekBot", "NaverBot", "Pinterestbot",
180      "Qwantify", "SerpstatBot", "Slurp", "Twitterbot", "UpdownerBot",
181      "VelenPublicWebCrawler", "Yeti", "ZumBot"
182    ],
183    aiBots: [
184      "AIbot", "Amazonbot", "Bytespider", "ChatGPT-User", "ClaudeBot",
185      "Claude-Web", "Cohere-ai", "DeepSeekBot", "Diffbot", "FacebookBot",
186      "GroqBot", "GPTBot", "Huggingfacebot", "MetaBot", "MistralAI-User/1.0",
187      "OpenAI-User", "OpenRetriever", "Omigili", "OmigiliBot", "PalmBot",
188      "PerplexityBot", "Quora-Quivr", "YouBot"
189    ],
190    daumWebmaster: '',
191    open: function (options = {}) {
192      const {
193        searchBot = false,
194        aiBot = false,
195        isToggle = false,
196      } = options;
197      this.getDaumWebmaster();
198      // 중복오픈 체크
199      if($.robots.isOpen) return;
200      $.robots.isOpen = true;
201
202      $.processON('robots 파일을 가져오는중...');
203      $.ajax({
204        url: '/template/open_robots',
205        type: 'POST',
206        data: {
207          'user_dir': RPATH,
208          'sid': SID,
209          'host': HOST,
210
211        },
212        success: function (response) {
213          $.processOFF();
214          $.robots.display(searchBot, aiBot, isToggle);
215          $.robots.renderInitialRobotsRules(response.content);  // robots.txt 초기 렌더링
216          $.robots.domain = response.domain;
217          if (response.source === "default") {
218            $.robots.siteMaps.add(response.sitemap); // Storage에 robots.txt 없다면 sitemap 추가
219          } else if (response.source === "file") {  // 2025.09.09: 외부 도메인 연결 + robots.txt 수정한 고객의 Sitemap이 기본 도메인이기 때문에 이를 외부 도메인으로 변경하기 위해 추가함
220            let lines = response.content.split('\n');
221            const targetSitemap = ($.robots.domain || "").replace(/\/$/, '') + '/user-sitemap';
222            for (let line of lines) {
223              line = line.trim();
224
225              if (!line.toLowerCase().startsWith('sitemap:') || !line.includes('creatorlink.net')) continue;
226
227              const url = line.replace(/^sitemap:\s*/i, '').trim(); // "Sitemap: " 제거해서 순수 URL을 얻음
228
229              if (url === targetSitemap) break; // 외부 도메인 연결 안했기 때문에 도메인이 기본 도메인이라면
230
231              const arr = [...$.robots.siteMaps];
232              const idx = arr.findIndex(v => v.replace(/\/$/, '') === url.replace(/\/$/, ''));  // Set을 배열로 바꿔 위치(인덱스)를 찾아 같은 자리에 교체
233
234              if (idx !== -1) {
235                arr[idx] = targetSitemap;
236                $.robots.siteMaps = new Set(arr);
237              }
238            }
239          }
240          $.robots.event();
241        },
242        error: function (xhr, status, error) {
243          alert('기존 데이터가 존재하지 않습니다', error);
244          $.processOFF();
245        }
246      });
247      $.sitemap.init();
248      // 초기 상태 저장
249      setTimeout(() => {
250        $.seoConfig.initialState = {
251          searchBotSwitch: $('input[name="searchBot"]').prop('checked'),
252          aiBotSwitch: $('input[name="aiBot"]').prop('checked'),
253          robotsText: $.robots.robotsText || '',
254          sitemapText: $.sitemap.sitemapPreviewText || ''
255        };
256      }, 500);
257    },
258    event: function () {
259      // 사이트맵 파일 input change 이벤트 (한 번만 등록)
260      $(document)
261        .off('change', '#sitemap-content input[type="file"]')
262        .on('change', '#sitemap-content input[type="file"]', function(e) {
263          const file = e.target.files[0];
264          if(!file) return;
265
266          // 확장자 체크
267          if(!file.name.match(/\.xml$/i)){
268            alert($.lang[LANG]['editor.sitemap.upload.error.filename-extension']);
269            $(this).val('');
270            return;
271          }
272          // 파일명 문자 검증 (영문 대소문자, 숫자, 하이픈, 밑줄, 점만 허용)
273          if(!/^[a-zA-Z0-9._-]+$/.test(file.name)){
274            alert($.lang[LANG]['editor.sitemap.upload.error.filename']);
275            $(this).val('');
276            return;
277          }
278          $.sitemap.loadFile(e);
279        });
280      // 사이트맵 업로드 버튼
281      $(document).off('click', '#sitemap-btn__upload').on('click', '#sitemap-btn__upload', function() {
282        const $btn = $(this);
283        $btn.tooltip('hide');
284        $btn.blur();
285        $('#sitemap-content input[type="file"]').click();
286      });
287      // 사이트맵 초기화 버튼
288      $(document).on('click', '#sitemap-btn__reset', function() {
289        $.sitemap.reset();
290      });
291      // 닫기
292      $(document)
293        .off('click', '.robots-header__btn--close')
294        .on('click', '.robots-header__btn--close', function (e) {
295          $.robots.close();
296        });
297      // 저장
298      $(document)
299        .off('click', '.robots-header__btn--save')
300        .on('click', '.robots-header__btn--save', function () {
301          $.robots.save();
302          // if($.sitemap.isFileUploaded) $.sitemap.save();
303          $.sitemap.save();
304        });
305      // 메뉴탭
306      $(document).on('click', '.robots-site-menu li', function(e){
307        e.preventDefault();
308        let tabName = $(this).data('tab');
309        $('.robots-site-menu li').removeClass('active');
310        $('.tab-content').removeClass('active');
311        $(this).addClass('active');
312        $('#' + tabName + '-content').addClass('active');
313      });
314      // 검색봇 스위치 클릭 바인딩
315      $(`.robots-toggle div:first-child input[type='checkbox']`).off('change').on('change', (e) => this.blockSearchBot(e.target.checked));
316      // AI봇 스위치 클릭 바인딩
317      $(`.robots-toggle div:nth-child(2) input[type='checkbox']`).off('change').on('change', (e) => this.blockAIBot(e.target.checked));
318      // Confirm(체크) 버튼 클릭 바인딩
319      $(document)
320        .off('click', '.robots-form__itemConfirm')
321        .on('click', '.robots-form__itemConfirm', () => this.previewText());
322      // 취소버튼
323      $(document)
324        .off('click', '.robots-form__itemCancel')
325        .on('click', '.robots-form__itemCancel', (e) => {
326          $.robots.cancelItem(e);
327        });
328      // 확인버튼
329      $(document)
330        .off('click', '.robots-form__confirmBtn')
331        .on('click', '.robots-form__confirmBtn', (e) => {
332          $.robots.confirmItem(e);
333        });
334      // 수정버튼
335      $(document)
336        .off('click', '.robots-form__itemModify')
337        .on('click', '.robots-form__itemModify', (e) => {
338          $.robots.modifyItem(e);
339        });
340      // 삭제버튼
341      $(document)
342        .off('click', '.robots-form__itemDelete')
343        .on('click', '.robots-form__itemDelete', (e) => {
344          $.robots.deleteItem(e);
345        });
346      // 항목추가 버튼
347      $('.robots-form-btns__addItem')
348        .off('click', '.robots-form-btns__addItem')
349        .on('click', () => {
350          $.robots.addItem();
351        });
352      // 가져오기 버튼
353      $('.robots-form-btns__loadFileBtn').on('click', function () {
354        const $fileInput = $(`.robots-form-btns input[type="file"]`);
355        $fileInput.click();  // 클릭 시 숨겨진 input[type=file] 클릭 유도
356        // 파일 선택 후 처리
357        $fileInput.off('change').on('change', function (e) {
358          $.robots.loadFile(e);
359        });
360      });
361      // 초기화 버튼
362      $('.robots-form-btns__resetItem').on('click', function () {
363        if (confirm($.lang[LANG]['editor.robots.reset.message'])) {
364          $.robots.resetItem();
365        }
366      })
367    },
368    display: function (searchBot, aiBot, isToggle) {
369      $('head').append(`<style data-robots-style>${robots_css}</style>`);
370      // $('head').append(`<script data-robots-script src="https://code.jquery.com/ui/1.13.0/jquery-ui.min.js"></script>`);
371      $('head').append(`<link data-robots-css rel="stylesheet" href="https://code.jquery.com/ui/1.13.0/themes/base/jquery-ui.css" />`);
372      $('body').append(`
373        <div class="robots-background"></div>
374        <div class="robots-modal">
375          <div class="robots-header">
376            <div class="robots-header2">
377              <span class="robots-header__title">` + $.lang[LANG]['editor.robots.advanced-settings'] + `</span>
378              <div class="robots-header__btns">
379                <button class="robots-header__btn robots-header__btn--close">` + $.lang[LANG]['editor.robots.close'] + `</button>
380                <button class="robots-header__btn robots-header__btn--save">` + $.lang[LANG]['editor.robots.save'] + `</button>
381              </div>
382            </div>
383            <div class="robots-header__title-explain">` + $.lang[LANG]['editor.robots.explain'] + `</div>
384          </div>
385
386          <div class="robots-site-menu">
387            <div class="robots-site-menu-container">
388              <ul>
389                <li class="active" data-tab="robots"><a href="#">Robots.txt</a></li>
390                <li class="" data-tab="sitemap"><a href="#">Sitemap</a></li>
391              </ul>
392            </div>
393          </div>
394
395          <div class="tab-content active" id="robots-content">
396            <div class="robots-toggle">
397              <div>
398                <span>` + $.lang[LANG]['editor.robots.block.webcrawler'] + `</span>
399                <label class="robots-toggle__switch" style="margin-bottom: 0;">
400                  <input type="checkbox" name="searchBot" ${searchBot ? 'checked' : ''}>
401                  <span class="robots-toggle__slider"></span>
402                </label>
403              </div>
404              <div>
405                <span>` + $.lang[LANG]['editor.robots.block.aibot'] + `</span>
406                <label class="robots-toggle__switch" style="margin-bottom: 0;">
407                  <input type="checkbox" name="aiBot" ${aiBot ? 'checked' : ''}>
408                  <span class="robots-toggle__slider"></span>
409                </label>
410              </div>
411            </div>
412            <div style="height:30px; background-color: white; position:sticky; top: 243px; z-index: 10000"></div>
413            <div class="robots-form">
414                <table>
415                  <thead>
416                    <tr>
417                      <th></th>
418                      <th>#</th>
419                      <th>User Agent</th>
420                      <th>Directive</th>
421                      <th>Value</th>
422                    </tr>
423                  </thead>
424                  <tbody>
425                  </tbody>
426                </table>
427                <div class="robots-form-btns">
428                  <div>
429                    <button class="robots-form-btns__btn robots-form-btns__addItem">
430                      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="M9.38 2.5v6.88H2.5v1.25h6.88v6.88h1.25v-6.88h6.88V9.38h-6.88V2.5z"/></svg>
431                      <span>` + $.lang[LANG]['editor.robots.additem'] + `</span>
432                    </button>
433                    <button data-html="true" data-placement="bottom" data-toggle="tooltip" data-original-title="` + $.lang[LANG]['editor.robots.tooltip'] + `" class="robots-form-btns__btn robots-form-btns__loadFileBtn robots-tooltip">
434                      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><g data-name="그룹_6486"><path d="M12.38 3H7C5.55 3 4.38 4.18 4.38 5.62v8.75c0 1.45 1.18 2.62 2.62 2.62h6c1.45 0 2.62-1.18 2.62-2.62V6.25L12.37 3Zm-.13 1.64 1.73 1.73H13c-.41 0-.75-.34-.75-.75zm2.13 9.74c0 .76-.62 1.37-1.38 1.38H7c-.76 0-1.37-.62-1.38-1.38V5.62c0-.76.62-1.37 1.38-1.38h4v1.38c0 1.1.9 2 2 2h1.38v6.75Z"/></g></svg>
435                      <span>` + $.lang[LANG]['editor.robots.import'] + `</span>
436                    </button>
437                    <input type="file" accept=".txt" style="display:none" />
438                  </div>
439                  <div>
440                    <button class="robots-form-btns__btn robots-form-btns__resetItem">
441                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="M7.32 5.34c1.66-.95 3.7-.95 5.37 0l.63-1.08a6.68 6.68 0 0 0-6.62 0 6.61 6.61 0 0 0-3.31 5.73h-.88l1.5 2.5 1.5-2.5h-.88c0-1.92 1.03-3.69 2.69-4.65M16 7.49l-1.5 2.5h.88c0 1.92-1.03 3.69-2.69 4.65-1.66.95-3.71.95-5.37 0l-.63 1.08a6.62 6.62 0 0 0 9.05-2.44c.58-1 .88-2.14.88-3.3h.88L16 7.48Z"/></svg>
442                      <span>` + $.lang[LANG]['editor.robots.reset'] + `</span>
443                    </button>
444                  </div>
445                </div>
446            </div>
447            <div class="robots-preview">
448              <div class="robots-divider"></div>
449              <span class="robots-preview__title">` + $.lang[LANG]['editor.robots.preview'] + `</span>
450              <textarea class="robots-preview__content" readonly></textarea>
451            </div>
452          </div>
453
454          <div class="tab-content" id="sitemap-content">
455            <div class="sitemap-btns">
456              <button id="sitemap-btn__upload" class="sitemap-btn" data-html="true" data-placement="bottom" data-toggle="tooltip" data-original-title="` + $.lang[LANG]['editor.sitemap.upload.tooltip'] + `" class="sitemap-tooltip">
457                <svg class="robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M13 9v3H3V9H2v4h12V9z"/><path d="m8 3-.71.71-2.83 2.83.71.71L7.5 4.92v5.09h1V4.92l2.33 2.33.71-.71-2.83-2.83z"/></svg>
458                <span>` + $.lang[LANG]['editor.sitemap.import'] + `</span>
459              </button>
460              <input type="file" accept=".xml" style="display:none" />
461              <button id="sitemap-btn__reset" class="sitemap-btn">
462                <svg class="robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8.83 2.5c-2.46 0-4.62 1.63-5.29 4L2.48 5.09l-.8.6 2.11 2.8.8-.6 2-1.5-.6-.8-1.42 1.06a4.514 4.514 0 0 1 5.65-2.94c2.37.75 3.68 3.28 2.94 5.65a4.514 4.514 0 0 1-5.65 2.94c-.86-.27-1.61-.79-2.17-1.5l-.75.7a5.504 5.504 0 0 0 7.74.76c2.35-1.93 2.68-5.4.76-7.74a5.5 5.5 0 0 0-4.25-2.01"/></svg>
463                <span>` + $.lang[LANG]['editor.sitemap.reset'] + `</span>
464              </button>
465            </div>
466            <div class="sitemap-preview">
467              <span class="sitemap-preview__title">` + $.lang[LANG]['editor.sitemap.preview'] + `</span>
468              <textarea class="sitemap-preview__content" readonly></textarea>
469            </div>
470          </div>
471
472        </div>
473      `);
474      requestAnimationFrame(() => {
475        setTimeout(() => {
476          $('[data-toggle="tooltip"]').tooltip();
477        }, 0);
478      });
479      setTimeout(function (e) {
480        if (!isToggle) {
481          $('.robots-background').show();
482          $('.robots-modal').show();
483          $('body').css('overflow', 'hidden');
484        }
485        // $.robots.updateToggleStatus();
486        $.robots.previewText();
487        $.sitemap.previewText();
488        $.robots.dragAndDrop();
489      }, 50);
490    },
491    renderInitialRobotsRules: function (content) {
492      const lines = content.split('\n');
493      let currentAgent = null;
494
495      lines.forEach(line => {
496        line = line.trim();
497        if (!line || line.startsWith('#')) return;
498
499        if (line.toLowerCase().startsWith('user-agent')) {
500          const idx = line.indexOf(':');
501          currentAgent = $.robots.escapeHtml(line.slice(idx + 1).trim());
502          if (!$.robots.robotsRules[currentAgent]) {
503            $.robots.robotsRules[currentAgent] = [];
504          }
505          return;
506        }
507        if (line.toLowerCase().startsWith('disallow:') || line.toLowerCase().startsWith('allow:')) {
508          if (!currentAgent) return;
509          const idx = line.indexOf(':');
510          let directive = line.slice(0, idx).trim();
511          let value = $.robots.escapeHtml(line.slice(idx + 1).trim());
512          $.robots.robotsRules[currentAgent].push({
513            directive: directive.trim(),
514            value: value.trim()
515          });
516          return;
517        }
518        if (line.toLowerCase().startsWith('sitemap:')) {
519          const sitemapUrl = line.substring(line.indexOf(':') + 1).trim();
520          $.robots.siteMaps.add(sitemapUrl);
521        }
522      });
523
524      for (const userAgent in $.robots.robotsRules) {
525        const rules = $.robots.robotsRules[userAgent];
526        rules.forEach(rule => {
527          const [directive, value] = [rule.directive.toLowerCase(), rule.value];
528          $.robots.addItemRow(userAgent, directive, value);
529        });
530      }
531    },
532    close: function () {
533      $.robots.isOpen = false;
534      // 추가된 head 요소 제거
535      $('head style[data-robots-style]').remove();
536      // $('head script[data-robots-script]').remove();
537      $('head link[data-robots-css]').remove();
538
539      $('.robots-modal').remove();
540      $('.robots-background').remove();
541      $('body').css('overflow', '');  // body 스크롤바 표시
542      $.robots.robotsText = '';
543      $.robots.robotsRules = {};
544      $.robots.itemIndex = 1;
545      $.robots.siteMaps = new Set();
546
547      // 초기 상태 초기화
548      $.seoConfig.initialState = {
549        searchBotSwitch: false,
550        aiBotSwitch: false,
551        robotsText: '',
552        sitemapText: ''
553      };
554    },
555    save: function () {
556      $.processON('robots.txt 파일을 저장중입니다...');
557      $.ajax({
558        url: '/template/save_robots',
559        type: 'POST',
560        data: {
561          'robots_text': $.robots.robotsText,
562          'user_dir': RPATH,
563          'sid': SID,
564          'host': HOST,
565        },
566        success: function (r) {
567          $.processOFF();
568          const isSearchBotToggleChecked = $(".robots-toggle input[name='searchBot']").prop('checked');
569          const isAIBotToggleChecked = $(".robots-toggle input[name='aiBot']").prop('checked');
570          $('#site-searchBot-click').prop('checked', isSearchBotToggleChecked);
571          $('#site-aiBot-click').prop('checked', isAIBotToggleChecked);
572
573          const searchBotChecked = $('#site-searchBot-click').prop('checked');
574          const aiBotchecked = $('#site-aiBot-click').prop('checked');
575          var settings = {
576            searchBotClick: searchBotChecked,
577            aiBotClick: aiBotchecked,
578          };
579
580          $.post('/template/settings', { sid: SID, settings: JSON.stringify(settings), el: 'site_settings' }, function (data) {
581            checkError(data);
582          }, 'json');
583
584          // 변경 사항 확인 후 로그 저장
585          const currentState = {
586            searchBotSwitch: $('input[name="searchBot"]').prop('checked'),
587            aiBotSwitch: $('input[name="aiBot"]').prop('checked'),
588            robotsText: $.robots.robotsText || '',
589            sitemapText: $.sitemap.sitemapPreviewText || ''
590          };
591
592          // 변경된 항목만 changes 객체에 추가
593          const changes = {
594            anything_changed: false,
595            searchBotSwitch_changed: false,
596            aiBotSwitch_changed: false,
597            robotsText_changed: false,
598            sitemapText_changed: false
599          };
600
601           // 검색봇 변경 확인
602          if ($.seoConfig.initialState.searchBotSwitch !== currentState.searchBotSwitch) {
603            changes.anything_changed = true;
604            changes.searchBotSwitch_changed = true;
605            changes.searchBotSwitch_before = $.seoConfig.initialState.searchBotSwitch ? 'ON' : 'OFF';
606            changes.searchBotSwitch_after = currentState.searchBotSwitch ? 'ON' : 'OFF';
607          }
608
609          // AI봇 변경 확인
610          if ($.seoConfig.initialState.aiBotSwitch !== currentState.aiBotSwitch) {
611            changes.anything_changed = true;
612            changes.aiBotSwitch_changed = true;
613            changes.aiBotSwitch_before = $.seoConfig.initialState.aiBotSwitch ? 'ON' : 'OFF';
614            changes.aiBotSwitch_after = currentState.aiBotSwitch ? 'ON' : 'OFF';
615          }
616          
617          // robots.txt 변경 확인
618          if ($.seoConfig.initialState.robotsText !== currentState.robotsText) {
619            changes.anything_changed = true;
620            changes.robotsText_changed = true;
621            changes.robotsText_before = $.seoConfig.initialState.robotsText;
622            changes.robotsText_after = currentState.robotsText;
623          }
624          
625          // sitemap.xml 변경 확인
626          if ($.seoConfig.initialState.sitemapText !== currentState.sitemapText) {
627            changes.anything_changed = true;
628            changes.sitemapText_changed = true;
629            changes.sitemapText_before = btoa(unescape(encodeURIComponent($.robots.escapeHtml($.seoConfig.initialState.sitemapText))));
630            changes.sitemapText_after = btoa(unescape(encodeURIComponent($.robots.escapeHtml(currentState.sitemapText))));
631          }
632
633          // 로그 남기기
634          if(changes.anything_changed) {
635            setSiteLogs(
636                'config.settings.robots-sitemap',     // action
637                'edit',                               // v(val)
638                changes,                              // obj
639                'config'                              // page
640            );
641          }
642
643          $.robots.close();
644        },
645        error: function (xhr, status, error) {
646          alert(error);
647        }
648      });
649    },
650    blockSearchBot: function (on) {
651      if (on) {
652        this.searchBots.forEach(bot => {
653          if (!this.robotsRules[bot] || this.robotsRules[bot].length === 0) {
654            this.robotsRules[bot] = [{ directive: "Disallow", value: "/" }];
655            $.robots.addItemRow(bot, 'disallow', '/');
656          }
657        });
658        // $('input[name="searchBot"]').prop('checked', true);
659        
660      } else {
661        this.searchBots.forEach(bot => {
662          delete this.robotsRules[bot];
663        });
664        $.robots.deleteItemRow(this.searchBots);
665        // $('input[name="searchBot"]').prop('checked', false);
666      }
667      this.previewText();
668    },
669    blockAIBot: function (on) {
670      if (on) {
671        this.aiBots.forEach(bot => {
672          if (!this.robotsRules[bot] || this.robotsRules[bot].length === 0) {
673            this.robotsRules[bot] = [{ directive: "Disallow", value: "/" }];
674            $.robots.addItemRow(bot, 'disallow', '/');
675          }
676        });
677        // $('input[name="aiBot"]').prop('checked', true);
678      } else {
679        this.aiBots.forEach(bot => {
680          delete this.robotsRules[bot];
681        });
682        $.robots.deleteItemRow(this.aiBots);
683        // $('input[name="aiBot"]').prop('checked', false);
684      }
685      this.previewText();
686    },
687    cancelItem: function (e) {
688      const $row = $(e.currentTarget).closest('tr');
689      const $isNew = $row.is('[data-type="new"]');
690
691      // 새로운 항목을 취소하는것이면 -> 그대로 삭제하고 종료(return)
692      if ($isNew) {
693        $.robots.deleteItem(e, true);
694        return;
695      }
696
697      // 기존 항목을 취소하는것이면 -> 편집 취소(현재 입력값을 무시하고 Confirm UI로 복귀)
698      const $userAgent = $row.find('input[name="user-agent"]');
699      const $directive = $row.find('select[name="directive"]');
700      const $value = $row.find('input[name="value"]');
701
702      // 이전 값 복구 (edit 들어갈 때 data-prev-* 저장해놨다고 가정)
703      if ($userAgent.data('prev') !== undefined) $userAgent.val($userAgent.data('prev'));
704      if ($directive.data('prev') !== undefined) $directive.val($directive.data('prev'));
705      if ($value.data('prev') !== undefined) $value.val($value.data('prev'));
706
707      // 규칙 재추가 없이 UI만 Confirm 상태로
708      $.robots.confirmItem(null, $row, $userAgent, $directive, $value, { skipRuleAdd: true });
709    },
710    confirmItem: function (e, $row, $userAgent, $directive, $value, opts) {
711      opts = opts || {};
712      const skipRuleAdd = !!opts.skipRuleAdd;
713
714      // e로 호출된 경우 DOM 다시 찾기
715      if (!$row) {
716        $row = $(e.currentTarget).closest('tr');
717        $userAgent = $row.find('input[name="user-agent"]');
718        $directive = $row.find('select[name="directive"]');
719        $value = $row.find('input[name="value"]');
720      }
721
722      // 모두 다 안 채워졌으면 유효성 검사 후 return
723      if (!$userAgent.val() || !$directive.val() || !$value.val()) {
724        if (!$userAgent.val()) {
725          $userAgent[0].reportValidity();
726          return;
727        }
728        if (!$directive.val()) {
729          $directive[0].reportValidity();
730          return;
731        }
732        if (!$value.val()) {
733          $value[0].reportValidity();
734          return;
735        }
736      }
737
738      // 주석(#)이 있다면 제거
739      let userAgentVal = $userAgent.val();
740      let valueVal = $value.val();
741      userAgentVal = userAgentVal.indexOf('#') === -1 ? userAgentVal : userAgentVal.slice(0, userAgentVal.indexOf('#')).trim();
742      valueVal = valueVal.indexOf('#') === -1 ? valueVal : valueVal.slice(0, valueVal.indexOf('#')).trim();
743      $userAgent.val(userAgentVal);
744      $value.val(valueVal);
745
746      // robotsRules에 아직 없는 규칙인 경우 빈 배열 생성
747      if (!$.robots.robotsRules[$userAgent.val()]) {
748        $.robots.robotsRules[$userAgent.val()] = [];
749      };
750
751      // 이미 규칙이 있다면 알림 띄우고 return
752      // 규칙 없다면 robotsRules에 규칙 추가
753      if (!skipRuleAdd) {
754        const exists = $.robots.robotsRules[$userAgent.val().trim()].some(rule =>
755          rule.directive === $directive.val().charAt(0).toUpperCase() + $directive.val().slice(1) && rule.value === $value.val().trim()
756        );
757        if (exists) {
758          alert("해당 규칙이 이미 등록되어 있습니다.");
759          return;
760        } else {
761          $.robots.robotsRules[$userAgent.val()].push({
762            directive: `${$directive.val().charAt(0).toUpperCase() + $directive.val().slice(1)}`,
763            value: $value.val()
764          });
765        }
766      }
767
768      // 필드 잠그기
769      $userAgent.prop('readonly', true);
770      $directive.prop('disabled', true);
771      $value.prop('readonly', true);
772      $.robots.refreshDirectiveSelects($directive);
773
774      // 버튼 영역 아이콘 변경
775      const $btnCell = $row.find('td').eq(4);
776      $btnCell.find('.robots-form__cancelBtn, .robots-form__confirmBtn').hide();
777      $btnCell.find('.robots-form__itemModify, .robots-form__itemDelete')
778        .show()
779        .css({ 'pointer-events': 'all' });
780
781      // 미리보기 다시 그리기
782      $.robots.previewText();
783
784      // data-type="new" 제거
785      $row.removeAttr('data-type').removeData('type'); // jQuery 캐시도 제거
786
787      // value input 넓이 98%인거 없애기
788      $row.find('td').eq(4).find('input').removeClass('robots-modify__input');
789    },
790    modifyItem: function (e) {
791      const $targetRow = $(e.currentTarget).closest('tr');
792      const $userAgent = $targetRow.find('td').eq(2).find('input');
793      const $directive = $targetRow.find('td').eq(3).find('select');
794      const $value = $targetRow.find('td').eq(4).find('input');
795
796      // CancelItem 메소드 때문에 data-prev에 저장
797      $userAgent.data('prev', $userAgent.val());
798      $directive.data('prev', $directive.val());
799      $value.data('prev', $value.val());
800
801      // 수정, 삭제 버튼 제거
802      $(e.currentTarget).hide();
803      $(e.currentTarget).closest('td').find('.robots-form__itemDelete').hide();
804
805      // 취소, 확인 버튼 표시
806      $(e.currentTarget).closest('td').find('input').addClass('robots-modify__input');
807      $(e.currentTarget).closest('td').find('.robots-form__cancelBtn').css('display', 'flex');
808      $(e.currentTarget).closest('td').find('.robots-form__confirmBtn').css('display', 'flex');
809
810      // 미리보기 다시 그리기(previewText에서는 Confirmed 된것만 그리기 때문에 수정버튼 누른건 먼저 숨기기)
811      $.robots.previewText();
812
813      // 수정안하고 그대로 저장하면 중복값으로 인식하기 때문에
814      // 수정버튼 누르는 순간 해당 값을 규칙에서 제외
815      const rules = $.robots.robotsRules[$userAgent.val()] || [];
816      $.robots.robotsRules[$userAgent.val()] = rules.filter(rule =>
817        !(rule.directive === $directive.val().charAt(0).toUpperCase() + $directive.val().slice(1) && rule.value === $value.val())
818      );
819
820      // 필드 잠그기
821      $userAgent.prop('readonly', false);
822      $directive.prop('disabled', false);
823      $value.prop('readonly', false);
824      $.robots.refreshDirectiveSelects($directive);
825    },
826    resetItem: function () {
827      $.robots.deleteItemRow();
828      this.robotsRules = {
829        '*': [
830          { directive: "Disallow", value: "/_forgot/id" },
831          { directive: "Disallow", value: "/_register" },
832          { directive: "Disallow", value: "/_login" },
833          { directive: "Disallow", value: "/_cart" },
834          { directive: "Disallow", value: "/member/login" },
835          { directive: "Disallow", value: "/member/join" },
836          { directive: "Disallow", value: "/terms" },
837          { directive: "Disallow", value: "/notice" },
838          { directive: "Disallow", value: "/support" },
839          { directive: "Disallow", value: "/lang" },
840          { directive: "Disallow", value: "/contact" },
841          { directive: "Disallow", value: "/privacy" },
842          { directive: "Disallow", value: "/abuse" },
843        ]
844      }
845
846      const rules = this.robotsRules;
847      for (const agent in rules) {
848        rules[agent].forEach(rule => $.robots.addItemRow(agent, rule.directive, rule.value));
849      }
850
851      this.siteMaps = new Set();
852      this.siteMaps.add(`${this.domain}/user-sitemap`);
853      this.previewText();
854      this.updateToggleStatus();
855    },
856    /**
857     * 1. 삭제 클릭하면
858     * 2. 클릭한 svg에서 가장 가까운 tr 삭제
859     * 3. itemIndex 하나 감소하고
860     * 4. 클릭한 itemIndex + 1 부터 끝까지 숫자 하나 감소해서 다시 그리기
861     */
862    deleteItem: function (e, skipConfirm = false) {
863      let isDelete = true;
864      // 삭제 여부 확인
865      if (!skipConfirm) {
866        isDelete = confirm('삭제하시겠습니까?');
867      }
868      if (!isDelete) return;
869
870      const $targetRow = $(e.currentTarget).closest('tr');
871      const removedIndex = parseInt($targetRow.find('td').eq(1).find('span').text(), 10);
872      $targetRow.remove();
873      this.itemIndex--;
874      $('.robots-form table tbody tr').each(function () {
875        const $firstTd = $(this).find('td').eq(1).find('span');
876        const currentIndex = parseInt($firstTd.text(), 10);
877        if (currentIndex > removedIndex) {
878          $firstTd.text(currentIndex - 1);
879        }
880      })
881      this.previewText();
882      // this.updateToggleStatus();
883    },
884    updateToggleStatus: function () {
885      const rules = this.robotsRules || {};
886
887      // searchBots 모두 robotsRules에 존재해야 true
888      const searchBotOn = this.searchBots.every(bot => rules[bot] && rules[bot].length > 0);
889
890      // aiBots 모두 robotsRules에 존재해야 true
891      const aiBotOn = this.aiBots.every(bot => rules[bot] && rules[bot].length > 0);
892
893      $(".robots-toggle input[name='searchBot']").prop('checked', searchBotOn);
894      $(".robots-toggle input[name='aiBot']").prop('checked', aiBotOn);
895    },
896    refreshDirectiveSelects: function ($select) {
897      if (!$.fn.selectpicker) return;
898
899      const $target = $select && $select.length ? $select : $('.robots-form select[name="directive"]');
900      $target.each(function () {
901        const $this = $(this);
902        if (!$this.data('selectpicker')) {
903					$this.selectpicker({
904						width: '85%',
905						dropupAuto: false,
906						container: '.robots-modal'
907					});
908        } else {
909          $this.selectpicker('refresh');
910        }
911      });
912    },
913    addItem: function () {
914      $('.robots-form table tbody').append(`
915          <tr data-type="new">
916            <td>
917              ${clSVG('move_handle', 20, 20, true, 'robots-form__dragHandle robots-svg')}
918            </td>
919            <td>
920              <span>${this.itemIndex++}</span>
921            </td>
922            <td><input required type="text" name="user-agent"></td>
923            <td>
924              <select class="selectpicker" required name="directive">
925                <option value="">` + $.lang[LANG]['editor.robots.select'] + `</option>
926                <option value="allow">allow</option>
927                <option value="disallow">disallow</option>
928              </select>
929            </td>
930            <td>
931              <input class="robots-modify__input" required type="text" name="value" >
932              <button class="robots-form__cancelBtn robots-button">
933                <svg class="robots-form__itemCancel robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="M13.94 5.18 10 9.12 6.06 5.18l-.89.88L9.11 10l-3.94 3.94.89.88L10 10.88l3.94 3.94.88-.88L10.88 10l3.94-3.94-.89-.88Z"/></svg>
934              </button>
935              <button class="robots-form__confirmBtn robots-button">
936                <svg class="robots-form__itemConfirm robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="m15.94 5.25-7.4 7.38-4.47-4.46L3 9.22l5.54 5.53L17 6.31z"/></svg>
937              </button>
938              <svg class="robots-form__itemModify robots-svg" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><g data-name="그룹_6448"><g data-name="그룹_6449"><path d="M18.35 3.56c-.47-.36-1.06-.52-1.64-.44-.68.09-1.3.44-1.72.99L5.45 16.33l-.64 4.58 4.28-1.73 9.54-12.22c.86-1.02.73-2.53-.28-3.39M8.31 18.13l-1.97.79.29-2.1 7.38-9.45 1.67 1.31-7.38 9.45Zm9.34-11.95L16.47 7.7 14.8 6.39l1.18-1.51c.22-.28.53-.47.88-.52.26-.04.52.03.72.19.21.17.34.43.36.7.02.34-.08.67-.29.94"/></g></g></svg>
939              <svg class="robots-form__itemDelete robots-svg" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><g data-name="그룹_7089"><g data-name="그룹_7090"><path d="M20 7.06h-3.38v-3H7.38v3H4v1.25h2.12v9.62c0 1.1.9 2 2 2h7.75c1.1 0 2-.9 2-2V8.31h2.12V7.06ZM8.62 5.31h6.75v1.75H8.62zm8 12.63c0 .41-.34.75-.75.75H8.12c-.41 0-.75-.34-.75-.75V8.32h9.25zM11 16.31H9.75v-5.62H11zm3.25 0H13v-5.62h1.25z"/></g></g></svg>
940            </td>
941          </tr>
942          `);
943      $.robots.refreshDirectiveSelects();
944    },
945    addItemRow: function (userAgent, directive, value) {
946      $('.robots-form table tbody').append(`
947        <tr>
948          <td>
949            ${clSVG('move_handle', 20, 20, true, 'robots-form__dragHandle robots-svg')}
950          </td>
951          <td>
952            <span>${$.robots.itemIndex++}</span>
953          </td>
954          <td><input required readonly type="text" name="user-agent" value="${userAgent}"></td>
955          <td>
956            <select class="selectpicker" required disabled name="directive">
957              <option value="">선택하세요</option>
958              <option value="allow" ${directive.toLowerCase() === 'allow' ? 'selected' : ''}>allow</option>
959              <option value="disallow" ${directive.toLowerCase() === 'disallow' ? 'selected' : ''}>disallow</option>
960            </select>
961          </td>
962          <td>
963            <input required readonly type="text" name="value" value="${value}">
964            <button class="robots-form__cancelBtn robots-button" style="display: none;">
965                <svg class="robots-form__itemCancel robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="M13.94 5.18 10 9.12 6.06 5.18l-.89.88L9.11 10l-3.94 3.94.89.88L10 10.88l3.94 3.94.88-.88L10.88 10l3.94-3.94-.89-.88Z"/></svg>
966            </button>
967            <button class="robots-form__confirmBtn robots-button" style="display: none;">
968              <svg class="robots-form__itemConfirm robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"><path d="m15.94 5.25-7.4 7.38-4.47-4.46L3 9.22l5.54 5.53L17 6.31z"/></svg>
969            </button>
970            <svg class="robots-form__itemModify robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><g data-name="그룹_6448"><g data-name="그룹_6449"><path d="M18.35 3.56c-.47-.36-1.06-.52-1.64-.44-.68.09-1.3.44-1.72.99L5.45 16.33l-.64 4.58 4.28-1.73 9.54-12.22c.86-1.02.73-2.53-.28-3.39M8.31 18.13l-1.97.79.29-2.1 7.38-9.45 1.67 1.31-7.38 9.45Zm9.34-11.95L16.47 7.7 14.8 6.39l1.18-1.51c.22-.28.53-.47.88-.52.26-.04.52.03.72.19.21.17.34.43.36.7.02.34-.08.67-.29.94"/></g></g></svg>
971            <svg class="robots-form__itemDelete robots-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><g data-name="그룹_7089"><g data-name="그룹_7090"><path d="M20 7.06h-3.38v-3H7.38v3H4v1.25h2.12v9.62c0 1.1.9 2 2 2h7.75c1.1 0 2-.9 2-2V8.31h2.12V7.06ZM8.62 5.31h6.75v1.75H8.62zm8 12.63c0 .41-.34.75-.75.75H8.12c-.41 0-.75-.34-.75-.75V8.32h9.25zM11 16.31H9.7
9715v-5.62H11zm3.25 0H13v-5.62h1.25z"/></g></g></svg>
972          </td>
973        </tr>
974      `);
975      $.robots.refreshDirectiveSelects();
976    },
977    deleteItemRow: function (bots) {
978      const self = this;
979
980      if (!bots || bots.length === 0) {
981        $('.robots-form table tbody tr').remove();
982      } else {
983        bots.forEach(bot => {
984          $('.robots-form table tbody tr').each(function () {
985            const userAgent = $(this).find('input[name="user-agent"]').val()?.trim();
986            if (userAgent === bot) {
987              $(this).remove();
988            }
989          });
990        });
991      }
992
993      // 인덱스 다시 매기기
994      self.itemIndex = 1;
995      $('.robots-form table tbody tr').each(function () {
996        $(this).find('td').eq(1).find('span').text(self.itemIndex++);
997      });
998      this.previewText();
999    },
1000    loadFile: function (e) {
1001      // 파일 선택 후 처리
1002      const file = e.target.files[0];
1003      if (!file) return;
1004      if (file.type !== 'text/plain') {
1005        $('#robots-loadFile-error').remove();
1006        const robotsLoadFileDialog = `
1007          <div id="robots-loadFile-error" title="` + $.lang[LANG]['editor.robots.load.error'] + `">
1008            <p>` + $.lang[LANG]['editor.robots.load.message'] + `</p>
1009          </div>`;
1010        $('body').append(robotsLoadFileDialog);
1011
1012        // 다이얼로그 실행
1013        $('#robots-loadFile-error').dialog({
1014          modal: true,
1015          buttons: {
1016            [$.lang[LANG]['editor.robots.ok']]: function () {
1017              $(this).dialog("close").remove();
1018            }
1019          },
1020          open: function () {
1021            // z-index 조정
1022            $('.ui-dialog').css('z-index', 9999);
1023
1024            // 다이얼로그 타이틀바 스타일
1025            $('.ui-dialog-titlebar').css({
1026              background: '#949fab',
1027              color: '#fff'
1028            });
1029
1030            // 버튼 스타일
1031            $('.ui-dialog-buttonpane button').css({
1032              background: '#f5f5f5',
1033              color: '#222428',
1034              border: '0.5px solid #e6e8ea',
1035            });
1036
1037            // 버튼 스타일: 호버
1038            $('.ui-dialog-buttonpane button').hover(
1039              function () {
1040                // 마우스 올라갈 때
1041                $(this).css({
1042                  background: '#4789e7',
1043                  color: '#fff',
1044                  border: '0.5px solid #e6e8ea',
1045                });
1046              },
1047              function () {
1048                // 마우스 나갈 때
1049                $(this).css({
1050                  background: '#f5f5f5',
1051                  color: '#222428',
1052                  border: '0.5px solid #e6e8ea',
1053                });
1054              }
1055            );
1056
1057            // 닫기 버튼 스타일
1058            $('.ui-dialog-titlebar-close').css({
1059              background: 'none',
1060              border: 'none',
1061            });
1062
1063            // 줄 없애기
1064            $('.ui-dialog-buttonpane.ui-widget-content').css({
1065              border: 'none',
1066            })
1067
1068            // 본문 상단 여백
1069            $('.ui-dialog .ui-dialog-content').css({
1070              'margin-top': '10px',
1071            })
1072          },
1073          close: function () {
1074            $(this).remove();
1075          }
1076        });
1077
1078        e.target.value = '';
1079        return;
1080      }
1081
1082      const reader = new FileReader();
1083      reader.onload = (event) => {
1084        const text = event.target.result;
1085        const lines = text.split('\n');
1086
1087        let rawUserAgent = null;
1088        let currentUserAgent = null;
1089        let directive = null;
1090        let rawValue = null;
1091        let value = null;
1092
1093        lines.forEach(line => {
1094          line = line.trim();
1095          if (!line || line.startsWith('#')) return;
1096          
1097          const idx = line.indexOf(':');
1098          if (line.toLowerCase().startsWith('user-agent:')) {
1099            rawUserAgent = line.slice(idx + 1).trim();
1100            rawUserAgent = rawUserAgent.indexOf('#') === -1 ? rawUserAgent : rawUserAgent.slice(0, rawUserAgent.indexOf('#')).trim(); // User-agent 주석(#) 이후 값 제거
1101            currentUserAgent = $.robots.escapeHtml(rawUserAgent);
1102          } else if (currentUserAgent && (line.toLowerCase().startsWith('allow:') || line.toLowerCase().startsWith('disallow:'))) {
1103            directive = line.slice(0, idx).trim();
1104            rawValue = line.slice(idx + 1).trim();
1105            rawValue = rawValue.indexOf('#') === -1 ? rawValue : rawValue.slice(0, rawValue.indexOf('#')).trim();  // value에 주석(#) 이후 값 제거
1106            value = $.robots.escapeHtml(rawValue);
1107
1108            // robotsRules에도 저장
1109            if (!$.robots.robotsRules[rawUserAgent]) {
1110              $.robots.robotsRules[rawUserAgent] = [];
1111            }
1112
1113            // tr 추가(하지만 로드할때 중복값 추가 전에 return)
1114            const isDuplicate = $.robots.robotsRules[rawUserAgent].some(rule =>
1115              rule.directive.toLowerCase() ===  directive.toLowerCase() && rule.value ===  rawValue
1116            );
1117            if (isDuplicate) return;
1118            
1119            $.robots.addItemRow(currentUserAgent, directive, value);
1120            
1121            $.robots.robotsRules[rawUserAgent].push({
1122              directive: directive.charAt(0).toUpperCase() + directive.slice(1),
1123              value: rawValue
1124            });
1125          } else if (line.toLowerCase().startsWith('sitemap:')) {
1126            const sitemapUrl = line.slice(line.indexOf(':') + 1).trim();
1127            $.robots.siteMaps.add(sitemapUrl);
1128          }
1129        })
1130        $.robots.previewText();
1131        e.target.value = '';
1132      };
1133      reader.readAsText(file);
1134    },
1135    escapeHtml: function (text) {
1136      return text
1137      .replace(/&/g, "&amp;")      // &
1138      .replace(/</g, "&lt;")       // <
1139      .replace(/>/g, "&gt;")       // >
1140      .replace(/"/g, "&quot;")     // "
1141      .replace(/'/g, "&#039;")     // '
1142      .replace(/\//g, "&#x2F;")    // /
1143      .replace(/`/g, "&#x60;")     // `
1144      .replace(/=/g, "&#x3D;");    // =
1145    },
1146    unescapeHtml: function (text) {
1147      return text
1148        .replace(/&amp;/g, "&")       // &
1149        .replace(/&lt;/g, "<")        // <
1150        .replace(/&gt;/g, ">")        // >
1151        .replace(/&quot;/g, "\"")     // "
1152        .replace(/&#039;/g, "'")      // '
1153        .replace(/&#x2F;/g, "/")      // /
1154        .replace(/&#x60;/g, "`")      // `
1155        .replace(/&#x3D;/g, "=");     // =
1156    },
1157    dragAndDrop: function () {
1158      // 이미 sortable 적용돼 있으면 먼저 제거
1159      if ($('.robots-form tbody').data('ui-sortable')) {
1160        console.log('이미 sortable이라 destroy 실행');
1161        $('.robots-form tbody').sortable('destroy');
1162      }
1163
1164      const fixRowHelper = function (event, $row) {
1165        const $cells = $row.children();
1166        const $valueCell = $cells.last();
1167        const $valueInput = $valueCell.find('input[name="value"]');
1168        const valueInputWidth = $valueInput.outerWidth();
1169
1170        $row.css('width', $row.closest('table').outerWidth() + 'px');
1171        $cells.each(function () {
1172          const $cell = $(this);
1173          $cell.css('width', $cell.outerWidth() + 'px');
1174        });
1175        $valueCell.css('grid-template-columns', valueInputWidth + 'px 32px 32px');
1176        $valueInput.css('width', valueInputWidth + 'px');
1177        return $row;
1178      };
1179
1180      $('.robots-form tbody').sortable({
1181        start: function (event, ui) {
1182          const $draggingRow = ui.item;
1183          const $select = $draggingRow.find('select');
1184          $select.prop('disabled', false);
1185          const $isConfirmBtnVisible = $draggingRow.find('.robots-form__confirmBtn').is(':visible');
1186          $isConfirmBtnVisible ? $select.css('color', '') : $select.css('color', '#aeaeae');
1187          $.robots.refreshDirectiveSelects($select);
1188        },
1189        stop: function (event, ui) {
1190          const $draggingRow = ui.item;
1191          const $select = $draggingRow.find('select');
1192          const $isConfirmBtnVisible = $draggingRow.find('.robots-form__confirmBtn').is(':visible');
1193          if($isConfirmBtnVisible) {
1194            $select.css('color', '');
1195          } else {
1196            $select.css('color', '');
1197            $select.prop('disabled', true);
1198          }
1199          $draggingRow.css('width', '');
1200          $draggingRow.children().css('width', '');
1201          $draggingRow.children().last().css('grid-template-columns', '');
1202          $draggingRow.find('input[name="value"]').css('width', '');
1203          $.robots.refreshDirectiveSelects($select);
1204        },
1205        handle: '.robots-form__dragHandle',
1206        axis: 'y',
1207        helper: fixRowHelper,
1208        forceHelperSize: true,
1209        forcePlaceholderSize: true,
1210        cursor: 'move',
1211        revert: 100,
1212        placeholder: 'sortable-placeholder',
1213        update: function (event, ui) {
1214          $.robots.previewText();
1215          $.robots.updateItemIndex();
1216        }
1217      });
1218    },
1219    updateItemIndex: function () {
1220      let index = 1;
1221      $('.robots-form table tbody tr').each(function () {
1222        $(this).find('td').eq(1).find('span').text(index++);
1223      });
1224    },
1225    getDaumWebmaster: function () {
1226      const self = this;
1227      $.ajax({
1228        url: '/template/get_daum_webmaster',
1229        type: 'POST',
1230        data: {
1231          'sid': SID,
1232        },
1233        success: function (r) {
1234          if(!r || !r['settings']) return;
1235
1236          const settings = JSON.parse(r['settings']);
1237          if(settings['daum_webmaster']) {
1238            const daumWebmaster = settings['daum_webmaster']
1239            self.daumWebmaster = daumWebmaster;
1240          };
1241        },
1242        error: function (xhr, status, error) {
1243          alert(error);
1244        }
1245      });
1246    },
1247    previewText: function () {
1248      let content = '';
1249      this.robotsRules = {};
1250      // 각 항목을 순회해서 robotsRules 객체에 넣기
1251      $('.robots-form table tbody tr').each(function () {
1252        // Confirm된 것만(즉, 화면에 확인버튼 없는것들만) robotRules에 삽입
1253        const isConfirmed = $(this).find('td').eq(4).find('.robots-form__confirmBtn').css('display') === 'none';
1254        if (!isConfirmed) return;
1255        const userAgent = $(this).find('input[name="user-agent"]').val()?.trim();
1256        const directive = $(this).find('select[name="directive"]').val();
1257        const value = $(this).find('input[name="value"]').val()?.trim();
1258
1259        // 모두 다 안 채워졌으면 유효성 검사 후 return
1260        if (!userAgent || !directive || !value) {
1261          if (!userAgent) {
1262            $(this).find('input[name="user-agent"]')[0].reportValidity();
1263          }
1264          if (!directive) {
1265            $(this).find('select[name="directive"]')[0].reportValidity();
1266          }
1267          if (!value) {
1268            $(this).find('input[name="value"]')[0].reportValidity();
1269          }
1270          return;
1271        }
1272
1273        // user-agent 키값이 객체에 없다면 새로 생성
1274        if (!$.robots.robotsRules[userAgent]) {
1275          $.robots.robotsRules[userAgent] = [];
1276        }
1277
1278        // 이미 존재하는지 확인
1279        const exists = $.robots.robotsRules[userAgent].some(rule =>
1280          rule.directive === directive.charAt(0).toUpperCase() + directive.slice(1) && rule.value === value
1281        );
1282
1283        if (!exists) {
1284          $.robots.robotsRules[userAgent].push({
1285            directive: `${directive.charAt(0).toUpperCase() + directive.slice(1)}`,
1286            value: value
1287          });
1288        }
1289      })
1290
1291      // 완성된 robotsRules의 값들을 꺼내서 content에 넣기
1292      for (const userAgent in this.robotsRules) {
1293        content += `User-agent: ${userAgent}\n`;
1294        const rules = this.robotsRules[userAgent];
1295        rules.forEach(rule => {
1296          content += `${rule.directive}: ${rule.value}\n`;
1297        });
1298        content += '\n';
1299      }
1300
1301      // 마지막으로 Sitemap 넣기
1302      this.siteMaps.forEach(siteMap => content += `Sitemap: ${siteMap}\n`);
1303
1304      // 추가: 다음 웹마스터도구
1305      if($.robots.daumWebmaster !== ''){
1306        content += $.robots.daumWebmaster;
1307      }
1308
1309      // textarea 태그에 값 넣기
1310      $('.robots-preview__content').val(content);
1311
1312      // robotsText 변수에 content 넣기
1313      $.robots.robotsText = content;
1314    },
1315  }
1316
1317  $.sitemap = {
1318    sitemapPreviewText: '',
1319    defaultGeneratedSitemap: '',
1320    isFileUploaded: false,
1321    XMLFileContent: '',
1322    init: function () {
1323      $.ajax({
1324        url: '/user_sitemap/get_sitemap',
1325        type: 'POST',
1326        data: {
1327          'sid': SID,
1328        },
1329        success: function (r) {
1330          $.sitemap.sitemapPreviewText = r.message;
1331
1332          if(r.source === 'generated') {
1333            $.sitemap.defaultGeneratedSitemap = r.message;
1334          } else if (r.source === 'uploaded') {
1335            $.sitemap.loadDefaultSitemap();
1336          }
1337
1338          $.sitemap.previewText();
1339        },
1340        error: function (xhr, status, error) {
1341          alert('기존 사이트맵 데이터가 존재하지 않습니다', error);
1342        }
1343      });
1344    },
1345    previewText: function () {
1346      $('.sitemap-preview__content').val($.sitemap.sitemapPreviewText);
1347    },
1348    loadDefaultSitemap: function () {
1349      $.ajax({
1350        url: '/user_sitemap/get_default_sitemap',
1351        type: 'POST',
1352        data: {
1353          'sid': SID,
1354        },
1355        success: function (r) {
1356          $.sitemap.defaultGeneratedSitemap = r.message;
1357        }
1358      });
1359    },
1360    loadFile: function (e) {
1361      const file = e.target.files[0];
1362      if (!file) return;
1363
1364      const reader = new FileReader();
1365      reader.onload = function(event) {
1366        $.sitemap.XMLFileContent = event.target.result;
1367
1368        // 검증 실패 시 초기화
1369        if (!$.sitemap.validateAll()) {
1370          $.sitemap.XMLFileContent = '';
1371          $.sitemap.isFileUploaded = false;
1372          $(e.target).val('');
1373          return;
1374        }
1375
1376        // 검증 통과 시 sitemapPreviewText에 저장
1377        $.sitemap.sitemapPreviewText = $.sitemap.XMLFileContent;
1378
1379        $.processON();
1380        $.sitemap.previewText();
1381        setTimeout(() => {
1382          $.processOFF();
1383        }, 1000);
1384        $.sitemap.isFileUploaded = true;
1385        alert('파일이 성공적으로 로드되었습니다.\n저장 버튼을 눌러 업로드하세요.');
1386        $(e.target).val(''); // 파일 로드 후 input 초기화(재업로드 가능하게)
1387      };
1388      reader.readAsText(file);
1389    },
1390    save: function () {
1391      // 검증 통과 후 인코딩 및 전송
1392      const encodedSitemap = btoa(encodeURIComponent($.sitemap.sitemapPreviewText).replace(/%([0-9A-F]{2})/g, function(match, p1) {
1393        return String.fromCharCode(parseInt(p1, 16));
1394      }));
1395
1396      $.ajax({
1397        url: '/user_sitemap/save',
1398        type: 'POST',
1399        contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
1400        data: {
1401          'sitemap_text': encodedSitemap,
1402          'is_encoded': '1',  // 인코딩 여부 표시
1403          'user_dir': RPATH,
1404          'sid': SID,
1405          'host': HOST,
1406        },
1407        success: function (r) {
1408          if(r.success) {
1409            $.robots.close();
1410          } else {
1411            alert(r.error || '저장에 실패했습니다.');
1412          }
1413        },
1414        error: function (xhr, status, error) {
1415          try {
1416            const response = JSON.parse(xhr.responseText);
1417            alert(response.error || '저장 중 오류가 발생했습니다.');
1418          } catch(e) {
1419            alert('저장 중 오류가 발생했습니다.\n\n' + xhr.responseText);
1420          }
1421        }
1422      });
1423    },
1424    reset: function () {
1425      $.sitemap.sitemapPreviewText = $.sitemap.defaultGeneratedSitemap;
1426      $.sitemap.isFileUploaded = false;
1427      $('#sitemap-content input[type="file"]').val('');
1428      $.sitemap.previewText();
1429    },
1430    validateEmpty: function() {
1431      if (!$.sitemap.XMLFileContent || $.sitemap.XMLFileContent.trim() === '') {
1432        alert($.lang[LANG]['editor.sitemap.error.message.empty-content']);
1433        return false;
1434      }
1435      return true;
1436    },
1437    validateFileSize: function() {
1438      const maxSize = 2 * 1024 * 1024; // 2MB
1439      const textSize = new Blob([$.sitemap.XMLFileContent]).size;
1440      if (textSize > maxSize) {
1441        alert($.lang[LANG]['editor.sitemap.error.message.file-size']);
1442        return false;
1443      }
1444      return true;
1445    },
1446    validateXMLStructure: function() {
1447      try {
1448        const parser = new DOMParser();
1449        const xmlDoc = parser.parseFromString($.sitemap.XMLFileContent, 'text/xml');
1450        
1451        // 파싱 에러 체크
1452        const parseError = xmlDoc.getElementsByTagName('parsererror');
1453        if (parseError.length > 0) {
1454          alert($.lang[LANG]['editor.sitemap.error.message.invalid-content'] + '\n\n' + parseError[0].textContent);
1455          return false;
1456        }
1457
1458        return xmlDoc;
1459      } catch (e) {
1460        alert('XML 검증 중 오류가 발생했습니다.\n\n' + e.message);
1461        return false;
1462      }
1463    },
1464    validateUrlsetElement: function(xmlDoc) {
1465      const urlset = xmlDoc.getElementsByTagName('urlset')[0];
1466      if (!urlset) {
1467        alert($.lang[LANG]['editor.sitemap.error.message.url-set']);
1468        return false;
1469      }
1470
1471      // namespace 확인
1472      const xmlns = urlset.getAttribute('xmlns');
1473      if (xmlns !== 'http://www.sitemaps.org/schemas/sitemap/0.9') {
1474        alert($.lang[LANG]['editor.sitemap.error.message.url-namespace'] + '\n\n' + $.lang[LANG]['editor.sitemap.error.message.valid-format'] +':\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
1475        return false;
1476      }
1477
1478      return urlset;
1479    },
1480    validateUrlCount: function(xmlDoc) {
1481      const urls = xmlDoc.getElementsByTagName('url');
1482      
1483      if (urls.length === 0) {
1484        alert($.lang[LANG]['editor.sitemap.error.message.url-required']);
1485        return false;
1486      }
1487      
1488      if (urls.length > 50000) {
1489        alert($.lang[LANG]['editor.sitemap.error.message.url-max']);
1490        return false;
1491      }
1492
1493      return urls;
1494    },
1495    validateUrlFormat: function(urlText, index) {
1496      try {
1497        const urlObj = new URL(urlText);
1498        return urlObj;
1499      } catch (e) {
1500        alert($.lang[LANG]['editor.sitemap.error.message.url-invalid'] + '\n\nURL ' + (index + 1) + ': ' + urlText);
1501        return false;
1502      }
1503    },
1504    validateDomain: function(urlObj, index, urlText) {
1505      const hostDomain = new URL(HOST).hostname;
1506      
1507      if (urlObj.hostname !== hostDomain) {
1508        alert('다른 도메인의 URL은 허용되지 않습니다.\n\n현재 도메인: ' + hostDomain + '\n포함된 URL: ' + urlText);
1509        return false;
1510      }
1511      return true;
1512    },
1513    validateLastmod: function(url, index) {
1514      const lastmod = url.getElementsByTagName('lastmod')[0];
1515      if (lastmod) {
1516        const lastmodValue = lastmod.textContent.trim();
1517        if (!/^\d{4}-\d{2}-\d{2}/.test(lastmodValue)) {
1518          alert($.lang[LANG]['editor.sitemap.error.message.lastmod'] + '\n\nURL ' + (index + 1) + ': ' + lastmodValue);
1519          return false;
1520        }
1521      }
1522      return true;
1523    },
1524    validatePriority: function(url, index) {
1525      const priority = url.getElementsByTagName('priority')[0];
1526      if (priority) {
1527        const priorityValue = parseFloat(priority.textContent);
1528        if (isNaN(priorityValue) || priorityValue < 0.0 || priorityValue > 1.0) {
1529          alert($.lang[LANG]['editor.sitemap.error.message.priority'] + '\n\nURL ' + (index + 1) + ': ' + priorityValue);
1530          return false;
1531        }
1532      }
1533      return true;
1534    },
1535    validateChangefreq: function(url, index) {
1536      const changefreq = url.getElementsByTagName('changefreq')[0];
1537      if (changefreq) {
1538        const validFreq = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
1539        const freqValue = changefreq.textContent.trim().toLowerCase();
1540        if (!validFreq.includes(freqValue)) {
1541          alert($.lang[LANG]['editor.sitemap.error.message.changefreq'] + '\n\n' + $.lang[LANG]['editor.sitemap.error.message.allowed'] + ': ' + validFreq.join(', ') + '\n' + $.lang[LANG]['editor.sitemap.error.message.input'] + ': ' + freqValue);
1542          return false;
1543        }
1544      }
1545      return true;
1546    },
1547    validateUrls: function(urls) {
1548      for (let i = 0; i < urls.length; i++) {
1549        const url = urls[i];
1550        const loc = url.getElementsByTagName('loc')[0];
1551        
1552        // loc 요소 존재 확인
1553        if (!loc || !loc.textContent.trim()) {
1554          let msg = $.lang[LANG]['editor.sitemap.error.message.loc'];
1555          msg = msg.replace('{0}', i + 1);
1556          alert(msg);
1557          return false;
1558        }
1559
1560        const urlText = loc.textContent.trim();
1561        
1562        // URL 형식 검증
1563        const urlObj = this.validateUrlFormat(urlText, i);
1564        if (!urlObj) return false;
1565
1566        // 도메인 검증
1567        // if (!this.validateDomain(urlObj, i, urlText)) return false;
1568
1569        // lastmod 검증
1570        if (!this.validateLastmod(url, i)) return false;
1571
1572        // priority 검증
1573        if (!this.validatePriority(url, i)) return false;
1574
1575        // changefreq 검증
1576        if (!this.validateChangefreq(url, i)) return false;
1577      }
1578      return true;
1579    },
1580    validateDangerousPatterns: function() {
1581      const dangerousPatterns = [
1582        { pattern: /<!ENTITY/i, message: $.lang[LANG]['editor.sitemap.error.message.entity'] },
1583        { pattern: /<!DOCTYPE[^>]*\[/i, message: $.lang[LANG]['editor.sitemap.error.message.doctype'] },
1584        { pattern: /<\?php/i, message: $.lang[LANG]['editor.sitemap.error.message.php'] },
1585        { pattern: /<script/i, message: $.lang[LANG]['editor.sitemap.error.message.script'] },
1586        { pattern: /javascript:/i, message: $.lang[LANG]['editor.sitemap.error.message.javascript'] }
1587      ];
1588
1589      for (let check of dangerousPatterns) {
1590        if (check.pattern.test($.sitemap.XMLFileContent)) {
1591          alert(check.message);
1592          return false;
1593        }
1594      }
1595      return true;
1596    },
1597    validateAll: function() {
1598      if (!this.validateEmpty()) return false;                // 1. 빈 내용 체크
1599      if (!this.validateFileSize()) return false;             // 2. 파일 크기 체크
1600      const xmlDoc = this.validateXMLStructure();             // 3. XML 구조 검증
1601      if (!xmlDoc) return false;
1602      if (!this.validateUrlsetElement(xmlDoc)) return false;  // 4. urlset 요소 검증
1603      const urls = this.validateUrlCount(xmlDoc);             // 5. URL 개수 체크
1604      if (!urls) return false;
1605      if (!this.validateUrls(urls)) return false;             // 6. 각 URL 검증
1606      if (!this.validateDangerousPatterns()) return false;    // 7. 위험한 패턴 체크
1607      return true;
1608    },
1609  }
1610
1611})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.