사용약품 저장기능

This commit is contained in:
pjs
2026-02-25 23:40:23 +09:00
parent da3e497e9c
commit 1b4e1d78de
11 changed files with 1048 additions and 24 deletions

View File

@@ -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;

View File

@@ -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='진료유형 카테고리별 약품/제품 매핑 (재고관리)';

View File

@@ -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<String, Object> getDiviProductList(HttpServletRequest request, HttpServletResponse response) {
log.debug("MedicalCategoryController getDiviProductList START");
HashMap<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> 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<String, Object> putDiviProduct(@RequestBody HashMap<String, Object> paramMap,
HttpServletRequest request) {
log.debug("MedicalCategoryController putDiviProduct START");
HashMap<String, Object> 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<String, Object> delDiviProduct(HttpServletRequest request, HttpServletResponse response) {
log.debug("MedicalCategoryController delDiviProduct START");
HashMap<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> 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<String, Object> modDiviProduct(@RequestBody HashMap<String, Object> paramMap,
HttpServletRequest request) {
log.debug("MedicalCategoryController modDiviProduct START");
HashMap<String, Object> 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<String, Object> saveDiviProductBatch(@RequestBody List<HashMap<String, Object>> list,
HttpServletRequest request) {
log.debug("MedicalCategoryController saveDiviProductBatch START");
HashMap<String, Object> 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<String, Object> searchProductList(HttpServletRequest request, HttpServletResponse response) {
log.debug("MedicalCategoryController searchProductList START");
HashMap<String, Object> paramMap = HttpUtil.getParameterMap(request);
HashMap<String, Object> 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;
}
}

View File

@@ -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<Map<String, Object>> getDiviProductList(HashMap<String, Object> paramMap);
/**
* 약품 등록
*
* @param paramMap
* @return
*/
int insertDiviProduct(HashMap<String, Object> paramMap);
/**
* 약품 수정
*
* @param paramMap
* @return
*/
int updateDiviProduct(HashMap<String, Object> paramMap);
/**
* 약품 삭제 (논리 삭제)
*
* @param paramMap (pid)
* @return
*/
int deleteDiviProduct(HashMap<String, Object> paramMap);
/**
* 제품(약품) 목록 검색 (MU_PRODUCT 기준)
*
* @param paramMap (keyword)
* @return 제품 리스트
*/
List<Map<String, Object>> searchProductList(HashMap<String, Object> paramMap);
}

View File

@@ -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<String, Object> getDiviProductList(HashMap<String, Object> paramMap) {
HashMap<String, Object> returnMap = new HashMap<>();
List<Map<String, Object>> rows = diviProductMapper.getDiviProductList(paramMap);
returnMap.put("msgCode", Constants.OK);
returnMap.put("rows", rows);
return returnMap;
}
/**
* 약품 등록
*/
public HashMap<String, Object> insertDiviProduct(HashMap<String, Object> paramMap) throws Exception {
HashMap<String, Object> 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<String, Object> updateDiviProduct(HashMap<String, Object> paramMap) throws Exception {
HashMap<String, Object> 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<String, Object> deleteDiviProduct(HashMap<String, Object> paramMap) throws Exception {
HashMap<String, Object> 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<String, Object> saveDiviProductBatch(List<HashMap<String, Object>> list) throws Exception {
HashMap<String, Object> returnMap = new HashMap<>();
int successCnt = 0;
for (HashMap<String, Object> 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<String, Object> searchProductList(HashMap<String, Object> paramMap) {
HashMap<String, Object> returnMap = new HashMap<>();
List<Map<String, Object>> rows = diviProductMapper.searchProductList(paramMap);
returnMap.put("msgCode", Constants.OK);
returnMap.put("rows", rows);
return returnMap;
}
}

View File

@@ -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'
<if test='storePid != null and storePid != ""'>
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}
</select>

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.madeu.crm.settings.medicalcategory.mapper.MedicalDiviProductMapper">
<!-- 1. 특정 카테고리(divi_pid)에 연결된 약품 리스트 조회 -->
<select id="getDiviProductList" parameterType="java.util.HashMap" resultType="java.util.HashMap">
SELECT a.pid,
a.store_pid,
a.divi_pid,
a.product_name,
a.product_code,
a.volume,
a.use_volume,
a.unit_cd,
IFNULL(ucl.code_nm, a.unit_nm) AS unit_nm,
a.price,
a.order_number,
a.list_use,
IFNULL(mc.COMPANY_NAME, '') AS company_name,
IFNULL(mp.VOLUME, 0) AS stock_volume,
IFNULL(mp.UNIT_NAME, '') AS stock_unit_nm,
IFNULL((SELECT FORMAT(SUM(IFNULL(mss.QUANTITY,0)),1)
FROM MU_STOCK_SUM mss
WHERE mss.MU_PRODUCT_ID = a.product_code
AND mss.USE_YN = 'Y'), '0') AS stock_quantity,
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
FROM medical_divi_product a
LEFT JOIN crm_code_list ucl ON ucl.grp_cd = 'UNIT_CD' AND ucl.code_cd = a.unit_cd AND ucl.store_pid = a.store_pid AND ucl.list_use = 'y'
LEFT JOIN MU_PRODUCT mp ON mp.MU_PRODUCT_ID = a.product_code AND mp.USE_YN = 'Y'
LEFT JOIN MU_COMPANY_PRODUCT mcp ON mcp.MU_PRODUCT_ID = a.product_code
LEFT JOIN MU_COMPANY mc ON mc.MU_COMPANY_ID = mcp.MU_COMPANY_ID
WHERE a.list_use = 'y'
AND a.divi_pid = #{diviPid}
<if test='storePid != null and storePid != ""'>
AND a.store_pid = #{storePid}
</if>
GROUP BY a.pid
ORDER BY a.order_number ASC, a.pid ASC
</select>
<!-- 2. 약품 등록 -->
<insert id="insertDiviProduct" parameterType="java.util.HashMap" useGeneratedKeys="true" keyProperty="pid">
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()
)
</insert>
<!-- 3. 약품 수정 -->
<update id="updateDiviProduct" parameterType="java.util.HashMap">
UPDATE medical_divi_product
<set>
<if test='productName != null and productName != ""'>
product_name = #{productName},
</if>
<if test='productCode != null'>
product_code = #{productCode},
</if>
<if test='volume != null'>
volume = #{volume},
</if>
<if test='useVolume != null'>
use_volume = #{useVolume},
</if>
<if test='unitCd != null'>
unit_cd = #{unitCd},
</if>
<if test='unitNm != null'>
unit_nm = #{unitNm},
</if>
<if test='price != null'>
price = #{price},
</if>
<if test='orderNumber != null'>
order_number = #{orderNumber},
</if>
up_date = NOW()
</set>
WHERE pid = #{pid}
</update>
<!-- 4. 약품 삭제 (논리 삭제) -->
<update id="deleteDiviProduct" parameterType="java.util.HashMap">
UPDATE medical_divi_product
SET list_use = 'n', up_date = NOW()
WHERE pid = #{pid}
</update>
<!-- 5. 제품(약품) 목록 검색 (MU_PRODUCT 기준) -->
<select id="searchProductList" parameterType="java.util.HashMap" resultType="java.util.HashMap">
SELECT MP.MU_PRODUCT_ID AS "muProductId"
,MP.PRODUCT_NAME AS "productName"
,MP.PRODUCT_CODE AS "productCode"
,IFNULL(MC.COMPANY_NAME,'') AS "companyName"
,IFNULL(MCI.CATEGORY_ITEM_NAME,'') AS "treatmentName"
,MP.VOLUME AS "volume"
,MP.UNIT_CODE AS "unitCode"
,MP.UNIT_NAME AS "unitName"
,FORMAT(IFNULL(MSS.PRICE,0),0) AS "price"
,CASE
WHEN FLOOR(SUM(IFNULL(MSS.QUANTITY,0))) = SUM(IFNULL(MSS.QUANTITY,0))
THEN FORMAT(SUM(IFNULL(MSS.QUANTITY,0)),0)
ELSE TRIM(TRAILING '0' FROM CAST(FORMAT(SUM(IFNULL(MSS.QUANTITY,0)), 2) AS CHAR))
END AS "quantity"
FROM MU_PRODUCT AS MP
LEFT JOIN MU_COMPANY_PRODUCT AS MCP
ON MP.MU_PRODUCT_ID = MCP.MU_PRODUCT_ID
LEFT JOIN MU_STOCK_SUM MSS
ON MP.MU_PRODUCT_ID = MSS.MU_PRODUCT_ID AND MSS.USE_YN='Y'
LEFT JOIN MU_CATEGORY_ITEM AS MCI
ON MCI.MU_CATEGORY_ITEM_ID = MP.MU_TREATMENT_ID AND MCI.USE_YN = 'Y'
LEFT JOIN MU_COMPANY AS MC
ON MC.MU_COMPANY_ID = MCP.MU_COMPANY_ID
WHERE MP.USE_YN = 'Y'
<if test='keyword != null and keyword != ""'>
AND MP.PRODUCT_NAME LIKE CONCAT('%', TRIM(#{keyword}), '%')
</if>
GROUP BY MP.MU_PRODUCT_ID
ORDER BY MCI.CATEGORY_ITEM_NAME ASC, MP.PRODUCT_NAME ASC
</select>
</mapper>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#1f1f1f"><path d="m382-80-18.67-126.67q-17-6.33-34.83-16.66-17.83-10.34-32.17-21.67L178-192.33 79.33-365l106.34-78.67q-1.67-8.33-2-18.16-.34-9.84-.34-18.17 0-8.33.34-18.17.33-9.83 2-18.16L79.33-595 178-767.67 296.33-715q14.34-11.33 32.34-21.67 18-10.33 34.66-16L382-880h196l18.67 126.67q17 6.33 35.16 16.33 18.17 10 31.84 22L782-767.67 880.67-595l-106.34 77.33q1.67 9 2 18.84.34 9.83.34 18.83 0 9-.34 18.5Q776-452 774-443l106.33 78-98.66 172.67-118-52.67q-14.34 11.33-32 22-17.67 10.67-35 16.33L578-80H382Zm55.33-66.67h85l14-110q32.34-8 60.84-24.5T649-321l103.67 44.33 39.66-70.66L701-415q4.33-16 6.67-32.17Q710-463.33 710-480q0-16.67-2-32.83-2-16.17-7-32.17l91.33-67.67-39.66-70.66L649-638.67q-22.67-25-50.83-41.83-28.17-16.83-61.84-22.83l-13.66-110h-85l-14 110q-33 7.33-61.5 23.83T311-639l-103.67-44.33-39.66 70.66L259-545.33Q254.67-529 252.33-513 250-497 250-480q0 16.67 2.33 32.67 2.34 16 6.67 32.33l-91.33 67.67 39.66 70.66L311-321.33q23.33 23.66 51.83 40.16 28.5 16.5 60.84 24.5l13.66 110Zm43.34-200q55.33 0 94.33-39T614-480q0-55.33-39-94.33t-94.33-39q-55.67 0-94.5 39-38.84 39-38.84 94.33t38.84 94.33q38.83 39 94.5 39ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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" } } }
]

View File

@@ -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('<option value="' + item.code_cd + '">' + item.code_nm + '</option>');
});
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 '<span style="color:#ff4444; cursor:pointer; font-weight:600;">✕</span>';
},
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 '<span style="color:#3985EA; cursor:pointer; font-weight:600;">' + (cell.getValue() || '') + '</span>';
},
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;

View File

@@ -4,10 +4,12 @@
<head>
<title>진료유형 정보</title>
<link rel="stylesheet" th:href="@{/css/common.css}">
<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.6.1/dist/css/tabulator.min.css">
<script th:src="@{/js/web/jquery.min.js}"></script>
<script src="https://unpkg.com/tabulator-tables@5.6.1/dist/js/tabulator.min.js"></script>
<style>
.pop_wrap {
max-width: 600px;
max-width: 750px;
margin: 0 auto;
}
@@ -137,6 +139,148 @@
padding: 8px 0 4px 0;
border-bottom: 1px solid #e9ecf0;
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title .btn_sm {
padding: 3px 10px;
font-size: 11px;
font-weight: 600;
background: #3985EA;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
.section-title .btn_sm:hover {
background: #2c6fd1;
}
/* 약품 그리드 영역 */
#productGridWrap {
margin-top: 8px;
border: 1px solid #e9ecf0;
border-radius: 4px;
overflow: hidden;
}
#productGrid {
font-size: 12px;
}
/* 편집 가능한 셀 스타일 */
.editable-cell {
background-color: #f0f7ff !important;
cursor: pointer;
}
.editable-cell:hover {
background-color: #e0efff !important;
}
.no-product-msg {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
/* ===== 제품 검색 레이어팝업 ===== */
.search-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 9999;
justify-content: center;
align-items: center;
}
.search-overlay.active {
display: flex;
}
.search-popup {
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
width: 680px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.search-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: #3985EA;
color: #fff;
}
.search-popup-header h4 {
margin: 0;
font-size: 14px;
font-weight: 700;
}
.search-popup-header .popup-close {
background: none;
border: none;
color: #fff;
font-size: 20px;
cursor: pointer;
line-height: 1;
}
.search-popup-body {
padding: 14px 18px;
flex: 1;
overflow-y: auto;
}
.search-input-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.search-input-row input {
flex: 1;
height: 34px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 10px;
font-size: 13px;
}
.search-input-row button {
padding: 0 18px;
height: 34px;
background: #3985EA;
color: #fff;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.search-input-row button:hover {
background: #2c6fd1;
}
#searchResultGrid {
font-size: 12px;
}
</style>
</head>
@@ -176,7 +320,7 @@
</tbody>
</table>
<!-- 용량/출력 정보 (Depth 4 전용) 복구 -->
<!-- 용량/출력 정보 (Depth 4 전용) -->
<div class="section-title">용량/출력 정보</div>
<table class="board_write">
<colgroup>
@@ -202,19 +346,43 @@
<input type="number" id="kindUnitVol" class="w120" value="0" step="0.1" placeholder="용량">
<select id="kindUnit" style="width: 100px; margin-left: 5px;">
<option value="">선택</option>
<!-- 추후 공통코드 연동을 통해 데이터 바인딩 -->
<option value="CC">CC</option>
<option value="ML">ML</option>
<option value="샷"></option>
<option value="회"></option>
<option value="V">V</option>
<option value="줄"></option>
</select>
</td>
</tr>
</tbody>
</table>
<!-- 약품 관리 (재고관리용) - 수정 모드에서만 표시 -->
<div id="productSection" style="display:none;">
<div class="section-title">
<span>사용 약품 관리 (재고관리)</span>
<button type="button" class="btn_sm" id="btn_add_product">+ 약품 추가 (검색)</button>
</div>
<!-- 약품 그리드 -->
<div id="productGridWrap">
<div id="productGrid"></div>
</div>
</div>
<!-- ===== 제품 검색 레이어 팝업 ===== -->
<div id="productSearchOverlay" class="search-overlay">
<div class="search-popup">
<div class="search-popup-header">
<h4>약품(제품) 검색</h4>
<button type="button" class="popup-close" id="btn_close_search">&times;</button>
</div>
<div class="search-popup-body">
<div class="search-input-row">
<input type="text" id="searchProductKeyword" placeholder="제품명을 입력하세요">
<button type="button" id="btn_search_product">검색</button>
</div>
<div id="searchResultGrid"></div>
<p style="margin-top:8px; font-size:11px; color:#999;">* 제품명을 클릭하면 약품이 자동 추가됩니다.</p>
</div>
</div>
</div>
<div class="pop_btn_area">
<button type="button" class="btn_blue" id="btn_save">저장</button>
<button type="button" class="btn_red" id="btn_delete" style="display:none;">삭제</button>