function getEditorBlocks(editor) { var body = editor.document.getBody().$; var result = []; for (var i = 0; i < body.children.length; i++) { var el = body.children[i]; // 커서 표시용 요소는 콘텐츠 블록에서 제외 if (!el.getAttribute("data-remote-cursor")) { result.push(el.outerHTML); } } return result; } function computeBlockDiff(oldBlocks, newBlocks) { var m = oldBlocks.length; var n = newBlocks.length; var i, j; var dp = []; for (i = 0; i <= m; i++) { dp[i] = []; for (j = 0; j <= n; j++) { dp[i][j] = 0; } } for (i = 1; i <= m; i++) { for (j = 1; j <= n; j++) { dp[i][j] = oldBlocks[i - 1] === newBlocks[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); } } var rawOps = []; i = m; j = n; while (i > 0 || j > 0) { if (i > 0 && j > 0 && oldBlocks[i - 1] === newBlocks[j - 1]) { rawOps.unshift({ op: "retain" }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { rawOps.unshift({ op: "insert", html: newBlocks[j - 1] }); j--; } else { rawOps.unshift({ op: "delete" }); i--; } } // 인접한 delete + insert 를 replace 로 병합 var ops = []; for (var k = 0; k < rawOps.length; k++) { if (rawOps[k].op === "delete" && k + 1 < rawOps.length && rawOps[k + 1].op === "insert") { ops.push({ op: "replace", html: rawOps[k + 1].html }); k++; } else { ops.push(rawOps[k]); } } return ops; } function applyBlockDiff(editor, ops) { var editorDoc = editor.document.$; var body = editor.document.getBody().$; // 커서 요소(data-remote-cursor)는 콘텐츠 블록이 아니므로 제외 var children = Array.prototype.filter.call(body.children, function (el) { return !el.getAttribute("data-remote-cursor"); }); var pos = 0; for (var i = 0; i < ops.length; i++) { var op = ops[i]; if (op.op === "retain") { pos++; } else if (op.op === "delete") { if (children[pos] && children[pos].parentNode) { body.removeChild(children[pos]); } pos++; } else if (op.op === "insert") { var tmpIns = editorDoc.createElement("div"); tmpIns.innerHTML = op.html; var newElIns = tmpIns.firstChild; if (newElIns) { if (children[pos]) { body.insertBefore(newElIns, children[pos]); } else { body.appendChild(newElIns); } } } else if (op.op === "replace") { if (children[pos] && children[pos].parentNode) { var tmpRep = editorDoc.createElement("div"); tmpRep.innerHTML = op.html; var newElRep = tmpRep.firstChild; if (newElRep) { body.replaceChild(newElRep, children[pos]); } } pos++; } } } function getCursorPath(editor) { var selection = editor.getSelection(); if (!selection) return null; var native = selection.getNative(); if (!native || native.rangeCount === 0) return null; var range = native.getRangeAt(0); var body = editor.document.getBody().$; var node = range.startContainer; var path = []; while (node && node !== body) { var parent = node.parentNode; if (!parent) return null; path.unshift(Array.prototype.indexOf.call(parent.childNodes, node)); node = parent; } if (node !== body) return null; return { path: path, offset: range.startOffset }; } function showRemoteCursor(editor, userInfo, cursorData) { var editorDoc = editor.document.$; var body = editor.document.getBody().$; var userId = userInfo.id; removeRemoteCursor(userId); var node = body; for (var i = 0; i < cursorData.path.length; i++) { if (cursorData.path[i] >= node.childNodes.length) return; node = node.childNodes[cursorData.path[i]]; } var range = editorDoc.createRange(); try { var maxOffset = node.nodeType === 3 ? node.length : node.childNodes.length; range.setStart(node, Math.min(cursorData.offset, maxOffset)); range.collapse(true); } catch (e) { return; } var rect = range.getBoundingClientRect(); if (!rect || rect.height === 0) return; var scrollTop = editorDoc.documentElement.scrollTop || editorDoc.body.scrollTop || 0; var scrollLeft = editorDoc.documentElement.scrollLeft || editorDoc.body.scrollLeft || 0; var cursor = editorDoc.createElement("span"); cursor.setAttribute("data-remote-cursor", userId); cursor.style.cssText = "position:absolute;" + "left:" + (rect.left + scrollLeft) + "px;" + "top:" + (rect.top + scrollTop) + "px;" + "width:2px;" + "height:" + rect.height + "px;" + "background-color:" + userInfo.color + ";" + "pointer-events:none;" + "z-index:9999;"; var label = editorDoc.createElement("span"); label.style.cssText = "position:absolute;" + "top:-18px;" + "left:0;" + "background-color:" + userInfo.color + ";" + "color:#fff;" + "font-size:11px;" + "padding:1px 5px;" + "border-radius:3px 3px 3px 0;" + "white-space:nowrap;"; label.textContent = userInfo.name; cursor.appendChild(label); editorDoc.body.appendChild(cursor); window._remoteCursors = window._remoteCursors || {}; window._remoteCursors[userId] = cursor; } function removeRemoteCursor(userId) { window._remoteCursors = window._remoteCursors || {}; var el = window._remoteCursors[userId]; if (el && el.parentNode) { el.parentNode.removeChild(el); } delete window._remoteCursors[userId]; } //////////////////////////////////////////////////////////////////////////////////////// //Global Variable //////////////////////////////////////////////////////////////////////////////////////// var dbName = "drawDB"; var storeName = "armsDiagrams"; var reqStatus = {}; var reqStateMap = {}; function execDocReady() { var pluginGroups = [ [ "../reference/lightblue4/docs/lib/widgster/widgster.js", "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.js", "../reference/jquery-plugins/jstree-v.pre1.0/_lib/jquery.cookie.js", "../reference/jquery-plugins/jstree-v.pre1.0/_lib/jquery.hotkeys.js", "../reference/jquery-plugins/jstree-v.pre1.0/jquery.jstree.js", "../reference/jquery-plugins/stompjs-develop/bundles/stomp.umd.min.js", "../reference/jquery-plugins/sockjs-client-main/dist/sockjs.min.js", "../reference/jquery-plugins/datetimepicker-2.5.20/build/jquery.datetimepicker.min.css", "../reference/light-blue/lib/bootstrap-datepicker.js", "../reference/jquery-plugins/datetimepicker-2.5.20/build/jquery.datetimepicker.full.min.js" ], [ "../reference/jquery-plugins/dataTables-1.10.16/media/css/jquery.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/css/responsive.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/css/select.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/media/js/jquery.dataTables.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/js/dataTables.responsive.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/js/dataTables.select.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/RowGroup/js/dataTables.rowsGroup.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/dataTables.buttons.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.html5.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.print.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/jszip.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/pdfmake.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/vfs_fonts.js" ], [ "../reference/jquery-plugins/select2-4.0.2/dist/css/select2_lightblue4.css", "../reference/jquery-plugins/select2-4.0.2/dist/js/select2.min.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/css/multiselect-lightblue4.css", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.quicksearch.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.multi-select.js", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select-bluelight.css", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select.min.js" ], [ "../reference/jquery-plugins/swiper-11.1.4/swiper-bundle.min.js", "../reference/jquery-plugins/swiper-11.1.4/swiper-bundle.min.css", "./js/common/swiperHelper.js", "./css/customSwiper.css" ], [ "./js/common/table_new.js", "./js/adms/session-manager.js", "./js/adms/wiki-list.js", "./js/adms/editor-operation.js", "./js/adms/vesion-control.js" ] ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { console.log("모든 플러그인 로드 완료"); $(".widget").widgster(); setSideMenu("sidebar_large_menu_ai", "sidebar_medium_menu_ai_adms", "sidebar_small_menu_ai_adms"); //Select2 makePdServiceSelectBox(); $(".multiple-select").multipleSelect(); var waitCKEDITOR = setInterval(function () { try { if (window.CKEDITOR) { if (window.CKEDITOR.status === "loaded") { CKEDITOR.replace("add_tabmodal_editor"); clearInterval(waitCKEDITOR); } } } catch (err) { console.log("CKEDITOR 로드가 완료되지 않아서 초기화 재시도 중..."); } }, 313 /*milli*/); $("#editor").trigger("init.editor"); $("#wiki_tree").slimScroll({ height: "500px" }); $("#btn_copy_link").on("click", function () { var link = location.origin + location.pathname + "?page=adms&pdServiceId=" + $("#selected_pdService").val() + "&wikiId=" + $("#wiki_tree").jstree("get_selected").attr("id").replace("node_", "").replace("copy_", ""); if (typeof navigator.clipboard == "undefined") { var textArea = document.createElement("textarea"); textArea.value = link; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); return; } navigator.clipboard.writeText(link); }); var lastBlocks = []; var isComposing = false; CKEDITOR.instances["editor"].on("contentDom", function (event) { var editor = event.editor; var editable = editor.editable(); lastBlocks = getEditorBlocks(editor); function sendDiffIfChanged() { var currentBlocks = getEditorBlocks(editor); var ops = computeBlockDiff(lastBlocks, currentBlocks); var hasChanges = ops.some(function (op) { return op.op !== "retain"; }); if (hasChanges) { lastBlocks = currentBlocks.slice(); sessionManager.remoteUserUpdate(JSON.stringify({ type: "diff", ops: ops })); } buildTableOfContents(editor); } // IME 조합 시작 (한글 등) - 조합 중 diff 전송 차단 editable.attachListener(editor.document, "compositionstart", function () { isComposing = true; }); // IME 조합 완료 - 완성된 내용으로 diff 전송 editable.attachListener(editor.document, "compositionend", function () { isComposing = false; sendDiffIfChanged(); }); editable.attachListener(editor.document, "keyup", function () { // IME 조합 중에는 keyup 무시 (한글 입력 방해 방지) if (isComposing) return; sendDiffIfChanged(); }); }); // selectionChange는 contentDom 외부에서 한 번만 등록 (중복 방지) var lastCursorSend = 0; CKEDITOR.instances["editor"].on("selectionChange", function (event) { var editor = event.editor; // 편집 모드일 때만 커서 전송 (읽기 전용 수신측이 커서를 보내지 않도록) if (editor.readOnly) return; var now = Date.now(); if (now - lastCursorSend < 50) return; lastCursorSend = now; var cursorPath = getCursorPath(editor); if (cursorPath) { sessionManager.remoteUserUpdate( JSON.stringify({ type: "cursor", path: cursorPath.path, offset: cursorPath.offset }) ); } }); // 목차 토글 버튼 $("#btn_table_of_contents").on("click", function () { var $tocSection = $("#table_of_contents").closest(".col-md-2"); var $btn = $(this); if ($tocSection.is(":visible")) { $btn.html(' 목차 보기'); } else { $btn.html(' 목차 숨기기'); } $tocSection.toggleClass("hidden"); $("#editor_wrapper").toggleClass("col-md-10 col-md-12"); }); var userInfo = { userId: userID, userName: userName, userColor: generateRandomHexColor() }; var sessionManager = $.sessionManager(StompJs.Client, SockJS, userInfo); sessionManager.onContentsChange = function (contents) { var data = contents.selection.message; if (typeof data === "string") { try { var parsed = JSON.parse(data); if (parsed && parsed.type === "diff") { applyBlockDiff(CKEDITOR.instances["editor"], parsed.ops); return; } if (parsed && parsed.type === "cursor") { // 자신의 커서 메시지는 무시 (서버 브로드캐스트 시 자신에게도 올 수 있음) if (String(contents.id) !== String(userInfo.userId)) { showRemoteCursor(CKEDITOR.instances["editor"], contents, parsed); } return; } } catch (e) { // JSON이 아니면 HTML로 처리 } CKEDITOR.instances["editor"].setData(data); } }; var $userList = $("#user_list"); sessionManager.onStateChange = function (participants) { var labelVariants = ["default", "primary", "success", "info", "warning", "danger"]; $userList.empty(); participants.forEach(function (participant, i) { $userList.append( $("") .addClass("ml-0 mr-xs label label-" + labelVariants[i % labelVariants.length]) .append($("").addClass("fa fa-user mr-xs")) .append(participant.name) ); }); // 퇴장한 사용자의 커서 제거 var activeIds = {}; participants.forEach(function (p) { if (p.id) activeIds[p.id] = true; }); window._remoteCursors = window._remoteCursors || {}; Object.keys(window._remoteCursors).forEach(function (userId) { if (!activeIds[userId]) { removeRemoteCursor(userId); } }); }; window.addEventListener("beforeunload", function (event) { if (!$("#btn_save_contents").hasClass("hidden")) event.preventDefault(); }); $("#btn_save_contents").on("click", function () { $(CKEDITOR.instances["editor"].document.getBody().$) .find(".panel.panel-default") .each(function () { var $self = $(this); var $panelBody = $self.find(".panel-body"); var $collapse = $self.find(".collapse"); $panelBody.empty(); $collapse.collapse("hide"); }); $.ajax({ type: "PUT", url: "/auth-user/api/arms/wiki/updateWiki.do", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ wikiId: getWikiId(), author: userID, contents: CKEDITOR.instances["editor"].getData() }), success: function () { $("#btn_save_contents").addClass("hidden"); $("#btn_cancel").addClass("hidden"); $("#btn_edit_contents").removeClass("hidden"); $("#btn_version").removeClass("hidden"); CKEDITOR.instances["editor"].setReadOnly(true); $("#user_list").empty(); sessionManager.closeRoom(); } }); }); $("#btn_cancel").on("click", function () { $.ajax({ type: "GET", url: "/auth-user/api/arms/wiki/" + getWikiId() + "/getWiki.do", contentType: "application/json;charset=UTF-8", dataType: "json", success: function (data) { $("#btn_save_contents").addClass("hidden"); $("#btn_cancel").addClass("hidden"); $("#btn_edit_contents").removeClass("hidden"); $("#btn_version").removeClass("hidden"); $("#user_list").empty(); CKEDITOR.instances["editor"].setData(data.contents); CKEDITOR.instances["editor"].setReadOnly(true); sessionManager.closeRoom(); } }); }); $("#btn_edit_contents").on("click", function () { $(this).addClass("hidden"); $("#btn_version").addClass("hidden"); $("#btn_save_contents").removeClass("hidden"); $("#btn_cancel").removeClass("hidden"); CKEDITOR.instances["editor"].setReadOnly(false); sessionManager.setRoom(getWikiId()).done(function () { sessionManager.openWebsocket(); }); }); var $wikiTree = $("#wiki_tree"); $("#btn_create_file").on("click", function () { var selectedWiki = $wikiTree.jstree("get_selected"); if (selectedWiki.length === 0) { selectedWiki = $("#node_2"); } $wikiTree.jstree("create", selectedWiki, "last", { attr: { rel: "default" } }); }); $("#btn_create_folder").on("click", function (obj) { var selectedWiki = $wikiTree.jstree("get_selected"); if (selectedWiki.length === 0) { selectedWiki = $("#node_2"); } $wikiTree.jstree("create", selectedWiki, "last", { attr: { rel: "folder" } }); }); $("#btn_collapse_all").on("click", function () { $wikiTree.jstree("close_all"); }); $("#mmenu .form-search").submit(function (event) { event.preventDefault(); $wikiTree.jstree("search", document.getElementById("text").value); }); resizePanel(); $("#version_table").on("click", ".btn-restore", function (clickEvent) { var $btn = $(clickEvent.target); var row = $btn.data("row"); $.ajax({ type: "GET", url: "/auth-user/api/arms/wiki/" + row.wikiId + "/" + row.version + "/getWiki.do", contentType: "application/json;charset=UTF-8", dataType: "json", success: function (data) { closePanel(); $("#btn_edit_contents").addClass("hidden"); $("#version_warning").removeClass("hidden"); $("#btn_restore_version").data("version", row.version); CKEDITOR.instances["editor"].setData(data.contents); } }); }); $("#btn_latest_version").on("click", function () { $("#btn_save_contents").addClass("hidden"); $("#btn_cancel").addClass("hidden"); var $btnVersion = $("#btn_version"); $btnVersion.addClass("hidden"); $.ajax({ type: "GET", url: "/auth-user/api/arms/wiki/" + getWikiId() + "/getWiki.do", contentType: "application/json;charset=UTF-8", dataType: "json", success: function (data) { $("#btn_edit_contents").removeClass("hidden"); $btnVersion.removeClass("hidden"); $("#version_warning").addClass("hidden"); CKEDITOR.instances["editor"].setData(data.contents); } }); }); $("#btn_restore_version").on("click", function () { var version = $(this).data("version"); $.ajax({ type: "PUT", url: "/auth-user/api/arms/wiki/changeRecent.do", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ wikiId: getWikiId(), version: version }), success: function () { $("#btn_version").removeClass("hidden"); $("#btn_edit_contents").removeClass("hidden"); $("#btn_cancel").addClass("hidden"); $("#btn_save_contents").addClass("hidden"); $("#version_warning").addClass("hidden"); $("#user_list").empty(); CKEDITOR.instances["editor"].setReadOnly(true); sessionManager.closeRoom(); } }); }); initPriorityCalculation(); switch_action_for_mode(); get_arms_req_state_list() .then((state_list) => { console.log(state_list); let reqStateList = []; for (let k in state_list) { let state = state_list[k]; console.log(state); //--- 테이블 보기에서 사용하는 전역변수 reqStateMap[state.c_id] = state; reqStatus[state.c_title] = state.c_id; reqStateList.push(state); } console.log(reqStateList); binding_state_list("addview_req_state", reqStateList, true); }) .catch((error) => { console.error("Error fetching data:", error); reject(error); // 에러 발생 시 프라미스를 거부 }); click_btn_for_req_save(); autoCompleteForUser(); drawio(); drawdb(); // 스크립트 실행 로직을 이곳에 추가합니다. var 라따적용_클래스이름_배열 = [".ladda_save_req"]; laddaBtnSetting(라따적용_클래스이름_배열); }) .catch(function () { console.error("플러그인 로드 중 오류 발생"); }); } function getReviewer(index, req_reviewers_id) { var reviewer = "none"; if ($("#" + req_reviewers_id).select2("data")[index] != undefined) { reviewer = $("#" + req_reviewers_id).select2("data")[0].text; } return reviewer; } function getWikiId() { var selectedJsTreeId = $("#wiki_tree").jstree("get_selected").attr("id").replace("node_", "").replace("copy_", ""); return "WIKI_" + $("#selected_pdService").val() + "_" + selectedJsTreeId; } function generateRandomHexColor() { var randomColor = Math.floor(Math.random() * 16777216); var hexColor = randomColor.toString(16); while (hexColor.length < 6) { hexColor = "0" + hexColor; } return "#" + hexColor; } function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + $("#selected_pdService").val(), type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// for (var k in data.response) { var obj = data.response[k]; var $opt = $("