From ac43b2757f251751277b8b8a5355d9fa9a6993ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A4=80=EC=8B=9D=28PJS=29?= Date: Mon, 23 Feb 2026 14:17:21 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctrl/MedicalCategoryController.java | 27 +++ .../service/MedicalCategoryService.java | 70 ++++++++ .../medicalcategory/medicalCategoryList.js | 162 +++++++++++++----- 3 files changed, 212 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/madeu/crm/settings/medicalcategory/ctrl/MedicalCategoryController.java b/src/main/java/com/madeu/crm/settings/medicalcategory/ctrl/MedicalCategoryController.java index 8a11722..45f16d7 100644 --- a/src/main/java/com/madeu/crm/settings/medicalcategory/ctrl/MedicalCategoryController.java +++ b/src/main/java/com/madeu/crm/settings/medicalcategory/ctrl/MedicalCategoryController.java @@ -1,6 +1,8 @@ package com.madeu.crm.settings.medicalcategory.ctrl; import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -182,4 +184,29 @@ public class MedicalCategoryController extends ManagerDraftAction { log.debug("MedicalCategoryController delMedicalCategory END"); return map; } + + /** + * 진료유형 다건 삭제 + */ + @PostMapping("/delMultiMedicalCategory.do") + public HashMap delMultiMedicalCategory(@RequestBody Map> body) { + log.debug("MedicalCategoryController delMultiMedicalCategory START"); + HashMap map = new HashMap<>(); + + try { + List pidList = body.get("pidList"); + if (pidList == null || pidList.isEmpty()) { + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "삭제할 항목이 선택되지 않았습니다."); + return map; + } + map = medicalCategoryService.deleteMultiMedicalCategory(pidList); + } catch (Exception e) { + log.error("delMultiMedicalCategory : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController delMultiMedicalCategory END"); + return map; + } } diff --git a/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalCategoryService.java b/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalCategoryService.java index 2dfd351..ab3ae83 100644 --- a/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalCategoryService.java +++ b/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalCategoryService.java @@ -212,4 +212,74 @@ public class MedicalCategoryService { return map; } + /** + * 카테고리 다건 삭제 (논리 삭제 처리) + * 하위 항목이 있는 카테고리는 건너뛰고, 나머지만 삭제 처리 + * + * @param pidList 삭제 대상 pid 목록 + * @return + * @throws Exception + */ + public HashMap deleteMultiMedicalCategory(List pidList) throws Exception { + HashMap map = new HashMap<>(); + int successCount = 0; + int skipCount = 0; + List skippedNames = new ArrayList<>(); + + try { + for (String pidStr : pidList) { + HashMap paramMap = new HashMap<>(); + paramMap.put("pid", pidStr); + + // 하위 카테고리 존재 여부 확인 + int childCount = medicalCategoryMapper.getChildCategoryCount(paramMap); + if (childCount > 0) { + // 하위 항목이 있으면 건너뜀 + Map info = medicalCategoryMapper.getMedicalCategory(paramMap); + if (info != null && info.get("divi_name") != null) { + skippedNames.add(String.valueOf(info.get("divi_name"))); + } + skipCount++; + continue; + } + + // 삭제 처리 + int result = medicalCategoryMapper.deleteMedicalCategory(paramMap); + if (result > 0) { + successCount++; + } + } + + if (successCount > 0) { + map.put("msgCode", Constants.OK); + map.put("success", true); + } else { + map.put("msgCode", Constants.FAIL); + } + + // 결과 메시지 구성 + StringBuilder msg = new StringBuilder(); + if (successCount > 0) { + msg.append(successCount + "건 삭제되었습니다."); + } + if (skipCount > 0) { + if (msg.length() > 0) + msg.append("\n"); + msg.append(skipCount + "건은 하위 항목이 존재하여 건너뛰었습니다."); + if (!skippedNames.isEmpty()) { + msg.append("\n(" + String.join(", ", skippedNames) + ")"); + } + } + if (successCount == 0 && skipCount == 0) { + msg.append("삭제할 항목이 없습니다."); + } + map.put("msgDesc", msg.toString()); + + } catch (Exception e) { + log.error("deleteMultiMedicalCategory Error: ", e); + throw e; + } + return map; + } + } diff --git a/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js b/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js index 73a0e94..c98f3c5 100644 --- a/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js +++ b/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js @@ -30,47 +30,77 @@ $(document).ready(function () { } }); - // 메뉴 [삭제] 클릭 이벤트 + // 메뉴 [삭제] 클릭 이벤트 - 멀티셀렉트 지원 $(document).on('click', '#customContextMenu .delete', function () { var menu = $('#customContextMenu'); - var pid = menu.data('pid'); - var name = menu.data('name'); menu.hide(); - if (!pid) return; - if (!confirm("'" + name + "' 항목을 삭제하시겠습니까?\n하위 항목이 있을 경우 삭제되지 않습니다.")) return; + if (!window.currentContextRow) return; - $.ajax({ - url: '/settings/medicalCategory/delMedicalCategory.do', - type: 'POST', - data: { pid: pid }, - success: function (res) { - alert(res.msgDesc); - if (res.msgCode === '0' && window.currentContextRow) { - var tableId = window.currentContextRow.getTable().element.id; - var isSelected = window.currentContextRow.isSelected(); + var contextTable = window.currentContextRow.getTable(); + var contextData = window.currentContextRow.getData(); + var selectedRows = contextTable.getSelectedRows(); - // 1. 그리드에서 행 삭제 - window.currentContextRow.delete(); - - // 2. 만약 선택된 행이었다면, 우측 하위 패널 초기화 - if (isSelected) { - if (tableId === 'gridDepth2') { - table3.setData([]); - table4.setData([]); - $("#btnArea3").empty(); - $("#btnArea4").empty(); - } else if (tableId === 'gridDepth3') { - table4.setData([]); - $("#btnArea4").empty(); - } - } - } - }, - error: function () { - alert("삭제 중 오류가 발생했습니다."); - } + // 우클릭한 행이 선택된 행 목록에 포함되어 있는지 확인 + var isContextRowSelected = selectedRows.some(function (r) { + return r.getData().pid === contextData.pid; }); + + var targetRows; + if (isContextRowSelected && selectedRows.length > 1) { + // 멀티 선택 상태에서 선택된 행 중 하나를 우클릭한 경우 → 전체 선택 삭제 + targetRows = selectedRows; + } else { + // 단일 행 또는 선택되지 않은 행을 우클릭한 경우 → 해당 행만 삭제 + targetRows = [window.currentContextRow]; + } + + var pidList = targetRows.map(function (r) { return r.getData().pid; }); + var nameList = targetRows.map(function (r) { return r.getData().divi_name; }); + + var confirmMsg; + if (pidList.length === 1) { + confirmMsg = "'" + nameList[0] + "' 항목을 삭제하시겠습니까?\n하위 항목이 있을 경우 삭제되지 않습니다."; + } else { + confirmMsg = pidList.length + "개 항목을 삭제하시겠습니까?\n(" + nameList.join(', ') + ")\n하위 항목이 있는 경우 해당 항목은 건너뜁니다."; + } + + if (!confirm(confirmMsg)) return; + + if (pidList.length === 1) { + // 단건 삭제 - 기존 API 사용 + $.ajax({ + url: '/settings/medicalCategory/delMedicalCategory.do', + type: 'POST', + data: { pid: pidList[0] }, + success: function (res) { + alert(res.msgDesc); + if (res.msgCode === '0') { + loadData(true); + } + }, + error: function () { + alert("삭제 중 오류가 발생했습니다."); + } + }); + } else { + // 멀티 삭제 + $.ajax({ + url: '/settings/medicalCategory/delMultiMedicalCategory.do', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ pidList: pidList }), + success: function (res) { + alert(res.msgDesc); + if (res.msgCode === '0') { + loadData(true); + } + }, + error: function () { + alert("삭제 중 오류가 발생했습니다."); + } + }); + } }); } @@ -94,7 +124,7 @@ $(document).ready(function () { index: "pid", layout: "fitColumns", placeholder: "상위 항목을 선택하세요.", - selectable: 1, + selectable: true, height: "100%", columnDefaults: { headerTooltip: true, @@ -102,18 +132,17 @@ $(document).ready(function () { } }; - // Depth 2 Grid - 순번, 명칭, 순서, 관리, 하위 + // Depth 2 Grid - 순번, 명칭, 순서, 하위 table2 = new Tabulator("#gridDepth2", Object.assign({}, commonOpts, { columns: [ { title: "순번", formatter: "rownum", width: 38, hozAlign: "center", headerSort: false, cssClass: "col-sm" }, { title: "명칭", field: "divi_name", formatter: categoryNameFormatter, tooltip: true }, { title: "순서", field: "divi_sort", width: 38, hozAlign: "center", cssClass: "col-sm" }, - { title: "관리", width: 50, hozAlign: "center", headerSort: false, formatter: editFormatter, cellClick: function (e, cell) { e.stopPropagation(); editCategory(cell.getRow().getData().pid); } }, { title: "하위", width: 50, hozAlign: "center", headerSort: false, formatter: function (c) { return addDescendantFormatter(c, 2); }, cellClick: function (e, cell) { e.stopPropagation(); var d = cell.getRow().getData(); addChildCategory(d.pid, 3, d.divi_name); } } ] })); - // Depth 3 Grid - 순번, 명칭, 거래처, 담당자(연락처), 단가, 순서, 관리, 하위 + // Depth 3 Grid - 순번, 명칭, 거래처, 담당자(연락처), 단가, 순서, 하위 table3 = new Tabulator("#gridDepth3", Object.assign({}, commonOpts, { columns: [ { title: "순번", formatter: "rownum", width: 38, hozAlign: "center", headerSort: false, cssClass: "col-sm" }, @@ -122,24 +151,65 @@ $(document).ready(function () { { title: "담당자(연락처)", field: "cust_contact", width: 110, hozAlign: "center", tooltip: true }, { title: "단가", field: "kind_cost", width: 80, hozAlign: "right", formatter: costFormatter }, { title: "순서", field: "divi_sort", width: 38, hozAlign: "center", cssClass: "col-sm" }, - { title: "관리", width: 50, hozAlign: "center", headerSort: false, formatter: editFormatter, cellClick: function (e, cell) { e.stopPropagation(); editCategory(cell.getRow().getData().pid); } }, { title: "하위", width: 50, hozAlign: "center", headerSort: false, formatter: function (c) { return addDescendantFormatter(c, 3); }, cellClick: function (e, cell) { e.stopPropagation(); var d = cell.getRow().getData(); addChildCategory(d.pid, 4, d.divi_name); } } ] })); - // Depth 4 Grid - 순번, 명칭, 순서, 관리 (하위 없음) + // Depth 4 Grid - 순번, 명칭, 순서 (하위 없음) table4 = new Tabulator("#gridDepth4", Object.assign({}, commonOpts, { columns: [ { title: "순번", formatter: "rownum", width: 38, hozAlign: "center", headerSort: false, cssClass: "col-sm" }, { title: "명칭", field: "divi_name", formatter: categoryNameFormatter, tooltip: true }, - { title: "순서", field: "divi_sort", width: 38, hozAlign: "center", cssClass: "col-sm" }, - { title: "관리", width: 50, hozAlign: "center", headerSort: false, formatter: editFormatter, cellClick: function (e, cell) { e.stopPropagation(); editCategory(cell.getRow().getData().pid); } } + { title: "순서", field: "divi_sort", width: 38, hozAlign: "center", cssClass: "col-sm" } ] })); - table2.on("rowClick", function (e, row) { clickRow(row, 2); }); - table3.on("rowClick", function (e, row) { clickRow(row, 3); }); - table4.on("rowClick", function (e, row) { clickRow(row, 4); }); + // Shift+Click 범위 선택을 위한 마지막 클릭 행 추적 + var lastClickedRow = {}; + + function handleRowClick(e, row, table, depth) { + var tableId = table.element.id; + + if (e.shiftKey && lastClickedRow[tableId]) { + // Shift+Click: 범위 선택 + var allRows = table.getRows(); + var startIdx = allRows.indexOf(lastClickedRow[tableId]); + var endIdx = allRows.indexOf(row); + if (startIdx === -1) startIdx = 0; + + var from = Math.min(startIdx, endIdx); + var to = Math.max(startIdx, endIdx); + + table.deselectRow(); + for (var i = from; i <= to; i++) { + allRows[i].select(); + } + } else if (e.ctrlKey || e.metaKey) { + // Ctrl+Click: 토글 선택 + row.toggleSelect(); + } else { + // 일반 클릭: 단일 선택 + table.deselectRow(); + row.select(); + } + + lastClickedRow[tableId] = row; + clickRow(row, depth); + } + + table2.on("rowClick", function (e, row) { handleRowClick(e, row, table2, 2); }); + table3.on("rowClick", function (e, row) { handleRowClick(e, row, table3, 3); }); + table4.on("rowClick", function (e, row) { handleRowClick(e, row, table4, 4); }); + + + function onCellDblClick(e, cell) { + if (cell.getField() === "divi_name") { + editCategory(cell.getRow().getData().pid); + } + } + table2.on("cellDblClick", onCellDblClick); + table3.on("cellDblClick", onCellDblClick); + table4.on("cellDblClick", onCellDblClick); table2.on("rowContext", onRowContextMenu); table3.on("rowContext", onRowContextMenu); @@ -195,8 +265,6 @@ $(document).ready(function () { var data = row.getData(); var children = data._children || []; - row.getTable().deselectRow(); - row.select(); if (depth === 2) { table3.setData(children).then(function () {