diff --git a/sql/alter_medical_divi_list.sql b/sql/alter_medical_divi_list.sql new file mode 100644 index 0000000..ef0a60c --- /dev/null +++ b/sql/alter_medical_divi_list.sql @@ -0,0 +1,11 @@ +-- ===================================================== +-- medical_divi_list 테이블 엔진 & 캐릭터셋 변경 +-- MyISAM → InnoDB (FK 지원을 위해) +-- utf8mb3 → utf8mb4 (medical_divi_product와 일치시키기 위해) +-- ===================================================== + +-- 1) 엔진 변경: MyISAM → InnoDB +ALTER TABLE `medical_divi_list` ENGINE = InnoDB; + +-- 2) 캐릭터셋 변경: utf8mb3 → utf8mb4 +ALTER TABLE `medical_divi_list` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; diff --git a/sql/create_medical_divi_product.sql b/sql/create_medical_divi_product.sql new file mode 100644 index 0000000..bcccaae --- /dev/null +++ b/sql/create_medical_divi_product.sql @@ -0,0 +1,24 @@ +-- ===================================================== +-- medical_divi_product : 용량/출력(Depth4) 카테고리별 약품 매핑 테이블 +-- 참조: MU_TREATMENT_PROCEDURE_PRODUCT +-- ===================================================== + +CREATE TABLE `medical_divi_product` ( + `pid` INT(11) NOT NULL AUTO_INCREMENT COMMENT '고유 식별자', + `store_pid` INT(11) NOT NULL DEFAULT 1 COMMENT '병원(지점) 식별자', + `divi_pid` INT(11) NOT NULL COMMENT '진료유형 카테고리 pid (medical_divi_list.pid, Depth4 기준)', + `product_name` VARCHAR(200) NOT NULL COMMENT '약품/제품 명칭', + `product_code` VARCHAR(100) DEFAULT NULL COMMENT '약품 코드 (재고관리용)', + `volume` DECIMAL(10,2) DEFAULT 0 COMMENT '제품 1개당 용량', + `use_volume` DECIMAL(10,2) DEFAULT 0 COMMENT '1회 사용량', + `unit_cd` VARCHAR(50) DEFAULT NULL COMMENT '단위 코드 (UNIT_CD 공통코드)', + `unit_nm` VARCHAR(100) DEFAULT NULL COMMENT '단위 명칭', + `price` INT(11) DEFAULT 0 COMMENT '입고 단가', + `order_number` INT(11) DEFAULT 0 COMMENT '정렬 순서', + `list_use` CHAR(1) DEFAULT 'y' COMMENT '사용여부 (y/n)', + `reg_date` DATETIME DEFAULT CURRENT_TIMESTAMP() COMMENT '등록일시', + `up_date` DATETIME DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP() COMMENT '수정일시', + PRIMARY KEY (`pid`), + KEY `idx_divi_pid` (`divi_pid`), + CONSTRAINT `fk_mdp_divi_pid` FOREIGN KEY (`divi_pid`) REFERENCES `medical_divi_list` (`pid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='진료유형 카테고리별 약품/제품 매핑 (재고관리)'; 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 4b3e471..4198574 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 @@ -15,6 +15,7 @@ import org.springframework.web.servlet.ModelAndView; import com.madeu.constants.Constants; import com.madeu.crm.settings.medicalcategory.dto.MedicalCategoryDTO; import com.madeu.crm.settings.medicalcategory.service.MedicalCategoryService; +import com.madeu.crm.settings.medicalcategory.service.MedicalDiviProductService; import com.madeu.init.ManagerDraftAction; import com.madeu.util.HttpUtil; @@ -30,6 +31,9 @@ public class MedicalCategoryController extends ManagerDraftAction { @Autowired private MedicalCategoryService medicalCategoryService; + @Autowired + private MedicalDiviProductService diviProductService; + // ==================== 뷰 반환 메서드 ==================== /** @@ -288,4 +292,127 @@ public class MedicalCategoryController extends ManagerDraftAction { log.debug("MedicalCategoryController updateBatchMedicalCategory END"); return map; } + + // ==================== 약품 관련 API ==================== + + /** + * 약품 리스트 조회 + */ + @PostMapping("/getDiviProductList.do") + public HashMap getDiviProductList(HttpServletRequest request, HttpServletResponse response) { + log.debug("MedicalCategoryController getDiviProductList START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.getDiviProductList(paramMap); + } catch (Exception e) { + log.error("getDiviProductList : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController getDiviProductList END"); + return map; + } + + /** + * 약품 등록 + */ + @PostMapping("/putDiviProduct.do") + public HashMap putDiviProduct(@RequestBody HashMap paramMap, + HttpServletRequest request) { + log.debug("MedicalCategoryController putDiviProduct START"); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.insertDiviProduct(paramMap); + } catch (Exception e) { + log.error("putDiviProduct : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController putDiviProduct END"); + return map; + } + + /** + * 약품 삭제 + */ + @PostMapping("/delDiviProduct.do") + public HashMap delDiviProduct(HttpServletRequest request, HttpServletResponse response) { + log.debug("MedicalCategoryController delDiviProduct START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.deleteDiviProduct(paramMap); + } catch (Exception e) { + log.error("delDiviProduct : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController delDiviProduct END"); + return map; + } + + /** + * 약품 수정 + */ + @PostMapping("/modDiviProduct.do") + public HashMap modDiviProduct(@RequestBody HashMap paramMap, + HttpServletRequest request) { + log.debug("MedicalCategoryController modDiviProduct START"); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.updateDiviProduct(paramMap); + } catch (Exception e) { + log.error("modDiviProduct : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController modDiviProduct END"); + return map; + } + + /** + * 약품 일괄 저장 (등록/수정/삭제) + */ + + @PostMapping("/saveDiviProductBatch.do") + public HashMap saveDiviProductBatch(@RequestBody List> list, + HttpServletRequest request) { + log.debug("MedicalCategoryController saveDiviProductBatch START"); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.saveDiviProductBatch(list); + } catch (Exception e) { + log.error("saveDiviProductBatch : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController saveDiviProductBatch END"); + return map; + } + + /** + * 제품(약품) 검색 (MU_PRODUCT 기반) + */ + @PostMapping("/searchProductList.do") + public HashMap searchProductList(HttpServletRequest request, HttpServletResponse response) { + log.debug("MedicalCategoryController searchProductList START"); + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + map = diviProductService.searchProductList(paramMap); + } catch (Exception e) { + log.error("searchProductList : ", e); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } + log.debug("MedicalCategoryController searchProductList END"); + return map; + } } diff --git a/src/main/java/com/madeu/crm/settings/medicalcategory/mapper/MedicalDiviProductMapper.java b/src/main/java/com/madeu/crm/settings/medicalcategory/mapper/MedicalDiviProductMapper.java new file mode 100644 index 0000000..aff485c --- /dev/null +++ b/src/main/java/com/madeu/crm/settings/medicalcategory/mapper/MedicalDiviProductMapper.java @@ -0,0 +1,51 @@ +package com.madeu.crm.settings.medicalcategory.mapper; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MedicalDiviProductMapper { + + /** + * 특정 카테고리(divi_pid)에 연결된 약품 리스트 조회 + * + * @param paramMap (diviPid, storePid) + * @return 약품 리스트 + */ + List> getDiviProductList(HashMap paramMap); + + /** + * 약품 등록 + * + * @param paramMap + * @return + */ + int insertDiviProduct(HashMap paramMap); + + /** + * 약품 수정 + * + * @param paramMap + * @return + */ + int updateDiviProduct(HashMap paramMap); + + /** + * 약품 삭제 (논리 삭제) + * + * @param paramMap (pid) + * @return + */ + int deleteDiviProduct(HashMap paramMap); + + /** + * 제품(약품) 목록 검색 (MU_PRODUCT 기준) + * + * @param paramMap (keyword) + * @return 제품 리스트 + */ + List> searchProductList(HashMap paramMap); +} diff --git a/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalDiviProductService.java b/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalDiviProductService.java new file mode 100644 index 0000000..e9eea1d --- /dev/null +++ b/src/main/java/com/madeu/crm/settings/medicalcategory/service/MedicalDiviProductService.java @@ -0,0 +1,120 @@ +package com.madeu.crm.settings.medicalcategory.service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.madeu.constants.Constants; +import com.madeu.crm.settings.medicalcategory.mapper.MedicalDiviProductMapper; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class MedicalDiviProductService { + + @Autowired + private MedicalDiviProductMapper diviProductMapper; + + /** + * 약품 리스트 조회 + */ + public HashMap getDiviProductList(HashMap paramMap) { + HashMap returnMap = new HashMap<>(); + List> rows = diviProductMapper.getDiviProductList(paramMap); + returnMap.put("msgCode", Constants.OK); + returnMap.put("rows", rows); + return returnMap; + } + + /** + * 약품 등록 + */ + public HashMap insertDiviProduct(HashMap paramMap) throws Exception { + HashMap returnMap = new HashMap<>(); + + int result = diviProductMapper.insertDiviProduct(paramMap); + if (result > 0) { + returnMap.put("msgCode", Constants.OK); + returnMap.put("msgDesc", "약품이 등록되었습니다."); + } else { + returnMap.put("msgCode", Constants.FAIL); + returnMap.put("msgDesc", "등록에 실패했습니다."); + } + return returnMap; + } + + /** + * 약품 수정 + */ + public HashMap updateDiviProduct(HashMap paramMap) throws Exception { + HashMap returnMap = new HashMap<>(); + + int result = diviProductMapper.updateDiviProduct(paramMap); + if (result > 0) { + returnMap.put("msgCode", Constants.OK); + returnMap.put("msgDesc", "약품 정보가 수정되었습니다."); + } else { + returnMap.put("msgCode", Constants.FAIL); + returnMap.put("msgDesc", "수정에 실패했습니다."); + } + return returnMap; + } + + /** + * 약품 삭제 (논리 삭제) + */ + public HashMap deleteDiviProduct(HashMap paramMap) throws Exception { + HashMap returnMap = new HashMap<>(); + + int result = diviProductMapper.deleteDiviProduct(paramMap); + if (result > 0) { + returnMap.put("msgCode", Constants.OK); + returnMap.put("msgDesc", "약품이 삭제되었습니다."); + } else { + returnMap.put("msgCode", Constants.FAIL); + returnMap.put("msgDesc", "삭제에 실패했습니다."); + } + return returnMap; + } + + /** + * 약품 일괄 저장 (등록/수정/삭제) + */ + public HashMap saveDiviProductBatch(List> list) throws Exception { + HashMap returnMap = new HashMap<>(); + int successCnt = 0; + + for (HashMap item : list) { + String action = (String) item.getOrDefault("_action", ""); + if ("insert".equals(action)) { + diviProductMapper.insertDiviProduct(item); + successCnt++; + } else if ("update".equals(action)) { + diviProductMapper.updateDiviProduct(item); + successCnt++; + } else if ("delete".equals(action)) { + diviProductMapper.deleteDiviProduct(item); + successCnt++; + } + } + + returnMap.put("msgCode", Constants.OK); + returnMap.put("msgDesc", successCnt + "건이 처리되었습니다."); + return returnMap; + } + + /** + * 제품(약품) 목록 검색 + */ + public HashMap searchProductList(HashMap paramMap) { + HashMap returnMap = new HashMap<>(); + List> rows = diviProductMapper.searchProductList(paramMap); + returnMap.put("msgCode", Constants.OK); + returnMap.put("rows", rows); + return returnMap; + } +} diff --git a/src/main/resources/mappers/crm/settings/medicalcategory/MedicalCategoryMapper.xml b/src/main/resources/mappers/crm/settings/medicalcategory/MedicalCategoryMapper.xml index 65ec9cd..883259f 100644 --- a/src/main/resources/mappers/crm/settings/medicalcategory/MedicalCategoryMapper.xml +++ b/src/main/resources/mappers/crm/settings/medicalcategory/MedicalCategoryMapper.xml @@ -33,8 +33,10 @@ DATE_FORMAT(a.reg_date, '%Y-%m-%d %H:%i:%s') AS reg_date, DATE_FORMAT(a.up_date, '%Y-%m-%d %H:%i:%s') AS up_date, a.store_pid, - a.npay_use + a.npay_use, + ucl.code_nm AS kind_unit_nm FROM medical_divi_list a + LEFT JOIN crm_code_list ucl ON ucl.grp_cd = 'UNIT_CD' AND ucl.code_cd = a.kind_unit AND ucl.store_pid = a.store_pid AND ucl.list_use = 'y' WHERE a.list_use = 'y' AND a.store_pid = #{storePid} @@ -79,9 +81,11 @@ DATE_FORMAT(a.up_date, '%Y-%m-%d %H:%i:%s') AS up_date, a.store_pid, a.npay_use, - p.divi_name AS parent_name + p.divi_name AS parent_name, + ucl.code_nm AS kind_unit_nm FROM medical_divi_list a LEFT JOIN medical_divi_list p ON a.divi_parent = p.pid + LEFT JOIN crm_code_list ucl ON ucl.grp_cd = 'UNIT_CD' AND ucl.code_cd = a.kind_unit AND ucl.store_pid = a.store_pid AND ucl.list_use = 'y' WHERE a.pid = #{pid} diff --git a/src/main/resources/mappers/crm/settings/medicalcategory/MedicalDiviProductMapper.xml b/src/main/resources/mappers/crm/settings/medicalcategory/MedicalDiviProductMapper.xml new file mode 100644 index 0000000..2a8cb56 --- /dev/null +++ b/src/main/resources/mappers/crm/settings/medicalcategory/MedicalDiviProductMapper.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + INSERT INTO medical_divi_product ( + store_pid, + divi_pid, + product_name, + product_code, + volume, + use_volume, + unit_cd, + unit_nm, + price, + order_number, + list_use, + reg_date, + up_date + ) VALUES ( + #{storePid}, + #{diviPid}, + #{productName}, + #{productCode, jdbcType=VARCHAR}, + IFNULL(#{volume}, 0), + IFNULL(#{useVolume}, 0), + #{unitCd, jdbcType=VARCHAR}, + #{unitNm, jdbcType=VARCHAR}, + IFNULL(#{price}, 0), + IFNULL(#{orderNumber}, 0), + 'y', + NOW(), + NOW() + ) + + + + + UPDATE medical_divi_product + + + product_name = #{productName}, + + + product_code = #{productCode}, + + + volume = #{volume}, + + + use_volume = #{useVolume}, + + + unit_cd = #{unitCd}, + + + unit_nm = #{unitNm}, + + + price = #{price}, + + + order_number = #{orderNumber}, + + up_date = NOW() + + WHERE pid = #{pid} + + + + + UPDATE medical_divi_product + SET list_use = 'n', up_date = NOW() + WHERE pid = #{pid} + + + + + + diff --git a/src/main/resources/static/image/web/settings_36px.svg b/src/main/resources/static/image/web/settings_36px.svg new file mode 100644 index 0000000..86712df --- /dev/null +++ b/src/main/resources/static/image/web/settings_36px.svg @@ -0,0 +1 @@ + \ No newline at end of file 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 a011b9e..f85e51b 100644 --- a/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js +++ b/src/main/resources/static/js/web/settings/medicalcategory/medicalCategoryList.js @@ -598,7 +598,8 @@ $(document).ready(function () { let height = 300; // Depth 2, 3, 5는 기본 300px if (diviDept == 4) { - height = 500; // Depth 4 (용량/출력)는 입력 필드가 많아 500px + width = 780; // 약품 관리 그리드 포함으로 넓힘 + height = 700; // Depth 4 (용량/출력 + 약품 관리) } let left = (screen.width - width) / 2; @@ -651,7 +652,13 @@ $(document).ready(function () { { title: "깊이", field: "divi_dept", width: "4%", hozAlign: "center", editor: "number", editorParams: { min: 1, max: 5, step: 1 } }, { title: "단가", field: "kind_cost", width: "10%", formatter: costFormatter, hozAlign: "right", editor: "number" }, { title: "할인가", field: "dc_cost", width: "10%", formatter: costFormatter, hozAlign: "right", editor: "number" }, - { title: "단위", field: "kind_unit", width: "8%", hozAlign: "center", editor: "input" }, + { + title: "단위", field: "kind_unit_nm", width: "8%", hozAlign: "center", + formatter: function (cell) { + var data = cell.getRow().getData(); + return data.kind_unit_nm || data.kind_unit || ''; + } + }, { title: "사용여부", field: "list_use", width: "6%", formatter: ynFormatter, hozAlign: "center", editor: "list", editorParams: { values: { "y": "Y", "n": "N" } } }, { title: "면세여부", field: "tax_free", width: "6%", formatter: ynFormatter, hozAlign: "center", editor: "list", editorParams: { values: { "y": "Y", "n": "N" } } } ] diff --git a/src/main/resources/static/js/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.js b/src/main/resources/static/js/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.js index 77362ec..048b2b1 100644 --- a/src/main/resources/static/js/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.js +++ b/src/main/resources/static/js/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.js @@ -1,5 +1,5 @@ /** - * 카테고리 Depth 4 팝업 스크립트 + * 카테고리 Depth 4 팝업 스크립트 (약품 관리 그리드 포함) */ $(document).ready(function () { const params = new URLSearchParams(window.location.search); @@ -9,10 +9,16 @@ $(document).ready(function () { const diviParent = params.get('diviParent'); const parentName = params.get('parentName'); - initForm(); + var productTable = null; // Tabulator 인스턴스 + var searchResultTable = null; // 검색결과 Tabulator 인스턴스 + var unitCodeCache = []; // 단위 코드 캐시 + + loadUnitCodes(null, function () { + initForm(); + }); bindEvents(); - // 금액 포맷팅 함수 + // ====== 금액 포맷팅 ====== function formatNumber(num) { if (!num) return '0'; return num.toString().replace(/[^0-9]/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ","); @@ -23,12 +29,61 @@ $(document).ready(function () { return parseFloat(str.toString().replace(/,/g, '')) || 0; } + // ====== 공통코드 단위 로드 ====== + function loadUnitCodes(selectedVal, callback) { + $.ajax({ + url: '/settings/code/getCodeList.do', + type: 'POST', + data: { grpCd: 'UNIT_CD', storePid: '1' }, + success: function (res) { + if (res.msgCode === '0' && res.rows) { + unitCodeCache = res.rows; + + // 메인 단위 셀렉트 세팅 + var $sel = $('#kindUnit'); + $sel.find('option:not(:first)').remove(); + + $.each(res.rows, function (i, item) { + $sel.append(''); + }); + + if (selectedVal) { + $sel.val(selectedVal); + } + } + if (typeof callback === 'function') callback(); + }, + error: function () { + console.error('단위 코드 목록 로드 실패'); + if (typeof callback === 'function') callback(); + } + }); + } + + // 단위코드 → 단위명 변환 헬퍼 + function getUnitName(unitCd) { + if (!unitCd) return ''; + for (var i = 0; i < unitCodeCache.length; i++) { + if (unitCodeCache[i].code_cd === unitCd) return unitCodeCache[i].code_nm; + } + return unitCd; + } + + // ====== 폼 초기화 ====== function initForm() { if (mode === 'add') { $("#popTitle").text('용량/출력 신규 등록'); $("#pid").val(''); $("#diviParent").val(diviParent); $("#btn_delete").hide(); + $("#productSection").show(); + $("#btn_add_product").prop('disabled', true).css('opacity', '0.5'); + initProductGrid(); + // 저장 전 안내 placeholder + if (productTable) { + productTable.options.placeholder = "카테고리 저장 후 약품을 추가할 수 있습니다."; + productTable.setData([]); + } if (diviParent !== '0' && parentName) { $("#parentNameRow").show(); @@ -38,10 +93,13 @@ $(document).ready(function () { $("#popTitle").text('용량/출력 정보 수정'); $("#pid").val(pid); $("#btn_delete").show(); + $("#productSection").show(); loadDetail(pid); + initProductGrid(); } } + // ====== 상세 정보 로드 ====== function loadDetail(id) { $.ajax({ url: '/settings/medicalCategory/getMedicalCategory.do', @@ -55,13 +113,12 @@ $(document).ready(function () { $("#diviSort").val(data.divi_sort); $("#diviColor").val(data.divi_color || '#000000'); - // 단가/제품 정보 (복구) $("#kindCost").val(formatNumber(data.kind_cost)); $("#dcCost").val(formatNumber(data.dc_cost)); - $("#kindUnit").val(data.kind_unit || ''); $("#kindUnitVol").val(data.kind_unit_vol || 0); - // 상위 카테고리 명칭 표시 (Edit 모드) + loadUnitCodes(data.kind_unit || ''); + if (data.parent_name) { $("#parentNameRow").show(); $("#parentNameTxt").text(data.parent_name); @@ -81,6 +138,274 @@ $(document).ready(function () { }); } + // ====== 약품 그리드 초기화 ====== + function initProductGrid() { + // 단위코드 셀렉트용 values 생성 (배열: 정렬 보장) + var unitValuesMap = {}; + unitValuesMap[''] = '선택'; + + // code_cd 오름차순 정렬 + var sortedUnits = unitCodeCache.slice().sort(function (a, b) { + return (a.code_cd || '').localeCompare(b.code_cd || ''); + }); + + var unitSelectValues = [{ label: '선택', value: '' }]; + for (var i = 0; i < sortedUnits.length; i++) { + unitSelectValues.push({ label: sortedUnits[i].code_nm, value: sortedUnits[i].code_cd }); + unitValuesMap[sortedUnits[i].code_cd] = sortedUnits[i].code_nm; + } + + productTable = new Tabulator("#productGrid", { + layout: "fitColumns", + placeholder: "등록된 약품이 없습니다.", + height: "200px", + columnDefaults: { + headerHozAlign: "center", + headerSort: false, + tooltip: true + }, + columns: [ + { title: "거래처", field: "company_name", width: 90, hozAlign: "center" }, + { title: "약품명", field: "product_name", minWidth: 120 }, + { + title: "재고", field: "stock_quantity", width: 65, hozAlign: "right" + }, + { title: "재고단위", field: "stock_unit_nm", width: 70, hozAlign: "center" }, + { + title: "사용량", field: "use_volume", width: 70, hozAlign: "right", + editor: "number", + editorParams: { step: 0.1, min: 0 }, + formatter: function (cell) { + var v = cell.getValue(); + return (v && Number(v) > 0) ? Number(v).toFixed(1) : '0'; + }, + cssClass: "editable-cell" + }, + { + title: "사용단위", field: "unit_cd", width: 80, hozAlign: "center", + editor: "list", + editorParams: { values: unitSelectValues, defaultValue: '' }, + formatter: function (cell) { + var val = cell.getValue(); + if (!val || val === '') return '선택'; + if (unitValuesMap[val]) return unitValuesMap[val]; + // CRM 코드에 없으면 unit_nm(단위명) 표시 + var rowData = cell.getRow().getData(); + return rowData.unit_nm || '선택'; + }, + cssClass: "editable-cell" + }, + { + title: "삭제", width: 45, hozAlign: "center", + formatter: function () { + return ''; + }, + cellClick: function (e, cell) { + e.stopPropagation(); + var data = cell.getRow().getData(); + if (!confirm("'" + data.product_name + "' 약품을 삭제하시겠습니까?")) return; + deleteProduct(data.pid, cell.getRow()); + } + } + ] + }); + + // 셀 수정 시 자동 저장 + productTable.on("cellEdited", function (cell) { + var data = cell.getRow().getData(); + var field = cell.getField(); + + var updateObj = { pid: data.pid }; + + if (field === 'use_volume') { + updateObj.useVolume = parseFloat(data.use_volume || 0); + } else if (field === 'unit_cd') { + updateObj.unitCd = data.unit_cd || ''; + updateObj.unitNm = getUnitName(data.unit_cd); + } + + $.ajax({ + url: '/settings/medicalCategory/modDiviProduct.do', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(updateObj), + success: function (res) { + if (res.msgCode !== '0') { + alert(res.msgDesc || "수정에 실패했습니다."); + loadProductList(); + } + }, + error: function () { + alert("수정 중 오류가 발생했습니다."); + loadProductList(); + } + }); + }); + + // 그리드 데이터 로드 + loadProductList(); + } + + // ====== 약품 리스트 로드 ====== + function loadProductList() { + if (!pid) return; + $.ajax({ + url: '/settings/medicalCategory/getDiviProductList.do', + type: 'POST', + data: { diviPid: pid, storePid: '1' }, + success: function (res) { + if (res.msgCode === '0' && productTable) { + productTable.setData(res.rows || []); + } + }, + error: function () { + console.error('약품 리스트 로드 실패'); + } + }); + } + + // ====== 약품 등록 (검색 팝업에서 선택 시 호출) ====== + function selectAndAddProduct(productData) { + if (!productData) return; + + var submitObj = { + storePid: $("#storePid").val(), + diviPid: pid, + productName: productData.productName || '', + productCode: productData.muProductId || '', + volume: parseFloat(productData.volume || '0'), + useVolume: 0, + unitCd: null, + unitNm: null, + price: parseInt((productData.price || '0').toString().replace(/,/g, ''), 10), + orderNumber: 0 + }; + + $.ajax({ + url: '/settings/medicalCategory/putDiviProduct.do', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(submitObj), + success: function (res) { + if (res.msgCode === '0') { + loadProductList(); + closeProductSearchPopup(); + } else { + alert(res.msgDesc || "등록에 실패했습니다."); + } + }, + error: function () { + alert("약품 등록 중 오류가 발생했습니다."); + } + }); + } + + // ====== 약품 삭제 ====== + function deleteProduct(productPid, row) { + $.ajax({ + url: '/settings/medicalCategory/delDiviProduct.do', + type: 'POST', + data: { pid: productPid }, + success: function (res) { + if (res.msgCode === '0') { + loadProductList(); + } else { + alert(res.msgDesc || "삭제에 실패했습니다."); + } + }, + error: function () { + alert("약품 삭제 중 오류가 발생했습니다."); + } + }); + } + + // ====== 제품 검색 팝업 열기 ====== + function openProductSearchPopup() { + $("#searchProductKeyword").val(''); + $("#productSearchOverlay").addClass('active'); + initSearchResultGrid(); + // 팝업 열릴 때 전체 목록 바로 조회 + searchProducts(); + setTimeout(function () { + $("#searchProductKeyword").focus(); + }, 200); + } + + // ====== 제품 검색 팝업 닫기 ====== + function closeProductSearchPopup() { + $("#productSearchOverlay").removeClass('active'); + } + + // ====== 검색 결과 그리드 초기화 ====== + function initSearchResultGrid() { + if (searchResultTable) return; // 이미 초기화됨 + + searchResultTable = new Tabulator("#searchResultGrid", { + layout: "fitColumns", + placeholder: "검색 결과가 없습니다.", + height: "300px", + columnDefaults: { + headerHozAlign: "center", + headerSort: true, + tooltip: true + }, + columns: [ + { title: "No", formatter: "rownum", width: 35, hozAlign: "center", headerSort: false }, + { title: "재고구분", field: "treatmentName", width: 90, hozAlign: "center" }, + { title: "거래처", field: "companyName", width: 100 }, + { + title: "제품명", field: "productName", minWidth: 140, + formatter: function (cell) { + return '' + (cell.getValue() || '') + ''; + }, + cellClick: function (e, cell) { + e.stopPropagation(); + var data = cell.getRow().getData(); + if (confirm("'" + data.productName + "' 제품을 약품으로 추가하시겠습니까?")) { + selectAndAddProduct(data); + } + } + }, + { + title: "용량", field: "volume", width: 60, hozAlign: "right", + formatter: function (cell) { + var v = cell.getValue(); + return (v && Number(v) > 0) ? Number(v).toFixed(1) : ''; + } + }, + { title: "단위", field: "unitName", width: 55, hozAlign: "center" }, + { + title: "단가(원)", field: "price", width: 80, hozAlign: "right" + }, + { + title: "재고", field: "quantity", width: 60, hozAlign: "right" + } + ] + }); + } + + // ====== 제품 검색 실행 ====== + function searchProducts() { + var keyword = $.trim($("#searchProductKeyword").val()); + + $.ajax({ + url: '/settings/medicalCategory/searchProductList.do', + type: 'POST', + data: { keyword: keyword }, + success: function (res) { + if (res.msgCode === '0' && searchResultTable) { + searchResultTable.setData(res.rows || []); + } else { + if (searchResultTable) searchResultTable.setData([]); + } + }, + error: function () { + alert("제품 검색 중 오류가 발생했습니다."); + } + }); + } + + // ====== 이벤트 바인딩 ====== function bindEvents() { $("#btn_close").on("click", function () { window.close(); @@ -94,12 +419,41 @@ $(document).ready(function () { deleteCategory(); }); - // 금액 콤마 자동 입력 이벤트 + // 금액 콤마 자동 입력 $("#kindCost, #dcCost").on("keyup", function () { $(this).val(formatNumber($(this).val())); }); + + // 약품 추가 (검색 팝업 열기) + $("#btn_add_product").on("click", function () { + openProductSearchPopup(); + }); + + // 검색 팝업 닫기 + $("#btn_close_search").on("click", function () { + closeProductSearchPopup(); + }); + + // 오버레이 클릭 시 닫기 + $("#productSearchOverlay").on("click", function (e) { + if (e.target === this) closeProductSearchPopup(); + }); + + // 검색 버튼 + $("#btn_search_product").on("click", function () { + searchProducts(); + }); + + // 검색 Enter 키 + $("#searchProductKeyword").on("keydown", function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + searchProducts(); + } + }); } + // ====== 카테고리 저장 ====== function saveCategory() { const dName = $("#diviName").val().trim(); if (!dName) { @@ -112,12 +466,10 @@ $(document).ready(function () { pid: $("#pid").val() || null, storePid: $("#storePid").val(), diviName: dName, - diviDept: $("#diviDept").val(), // hidden = 4 + diviDept: $("#diviDept").val(), diviParent: $("#diviParent").val(), diviSort: parseInt($("#diviSort").val() || "0", 10), diviColor: $("#diviColor").val(), - - // 단가/제품 정보 (복구 및 포맷팅 처리) kindCost: unformatNumber($("#kindCost").val()), dcCost: unformatNumber($("#dcCost").val()), kindUnit: $("#kindUnit").val(), @@ -137,7 +489,16 @@ $(document).ready(function () { if (window.opener && typeof window.opener.loadData === 'function') { window.opener.loadData(true); } - window.close(); + // 신규 등록 후 수정 모드로 전환하여 약품 관리 가능하게 + if (mode === 'add' && res.pid) { + window.location.href = window.location.pathname + + '?mode=edit&pid=' + res.pid + + '&diviDept=' + diviDept + + '&diviParent=' + diviParent + + '&parentName=' + encodeURIComponent(parentName || ''); + } else { + window.close(); + } } }, error: function () { @@ -146,6 +507,7 @@ $(document).ready(function () { }); } + // ====== 카테고리 삭제 ====== function deleteCategory() { const pidVal = $("#pid").val(); if (!pidVal) return; diff --git a/src/main/resources/templates/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.html b/src/main/resources/templates/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.html index c9dd219..5eac3c4 100644 --- a/src/main/resources/templates/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.html +++ b/src/main/resources/templates/web/settings/medicalcategory/popup/medicalCategoryInfoPopDept4.html @@ -4,10 +4,12 @@ 진료유형 정보 + + @@ -176,7 +320,7 @@ - +
용량/출력 정보
@@ -202,19 +346,43 @@
+ + + + +
+
+
+

약품(제품) 검색

+ +
+
+
+ + +
+
+

* 제품명을 클릭하면 약품이 자동 추가됩니다.

+
+
+
+