Merge branch 'feature/crypto-module' into master

This commit is contained in:
curry772
2026-05-07 15:39:13 +09:00
11 changed files with 1149 additions and 0 deletions
@@ -0,0 +1,153 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: {
cmd: 'LIST',
searchName: $('input[name=searchName]').val(),
algType: $('select[name=algType]').val(),
keySourceType: $('select[name=keySourceType]').val(),
useYn: $('select[name=useYn]').val()
},
colNames: ['ID', '모듈명', '설명', '알고리즘', '운영모드', '키 소스', '사용'],
colModel: [
{ name: 'CRYPTO_ID', hidden: true },
{ name: 'CRYPTO_NAME', align: 'left' },
{ name: 'CRYPTO_DESC', align: 'left', sortable: false },
{ name: 'ALG_TYPE', align: 'center', width: 80 },
{ name: 'CIPHER_MODE', align: 'center', width: 80 },
{ name: 'KEY_SOURCE_TYPE', align: 'center', width: 100 },
{ name: 'USE_YN', align: 'center', width: 60 }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var cryptoId = rowData['CRYPTO_ID'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&searchName=' + $("input[name=searchName]").val();
url2 += '&cryptoId=' + cryptoId;
goNav(url2);
},
gridComplete: function() {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
},
loadError: function(jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
$("#btn_search").click(function() {
var postData = getSearchForJqgrid("cmd", "LIST");
postData['algType'] = $('select[name=algType]').val();
postData['keySourceType'] = $('select[name=keySourceType]').val();
postData['useYn'] = $('select[name=useYn]').val();
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
});
$("#btn_new").click(function() {
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $("#grid").getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
goNav(url2);
});
$("input[name=searchName]").keydown(function(key) {
if (key.keyCode == 13) { $("#btn_search").click(); }
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> 신규</button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> 검색</button>
</div>
<div class="title">암호화 모듈 설정</div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">모듈명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:100px;">알고리즘</th>
<td>
<select name="algType">
<option value="">전체</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
<th style="width:100px;">키 소스</th>
<td>
<select name="keySourceType">
<option value="">전체</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
<th style="width:80px;">사용</th>
<td>
<select name="useYn">
<option value="">전체</option>
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,365 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
var isDetail = false;
function isValid() {
if ($('input[name=cryptoName]').val().trim() === '') {
alert('모듈명을 입력하세요.');
$('input[name=cryptoName]').focus();
return false;
}
if ($('select[name=algType]').val() === '') {
alert('알고리즘을 선택하세요.');
return false;
}
if ($('select[name=cipherMode]').val() === '') {
alert('운영모드를 선택하세요.');
return false;
}
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === '') {
alert('키 소스 유형을 선택하세요.');
return false;
}
if (keySourceType === 'STATIC') {
if ($('input[name=encKeyHex]').val().trim() === '') {
alert('암호화 키(Hex)를 입력하세요.');
$('input[name=encKeyHex]').focus();
return false;
}
}
if (keySourceType === 'DYNAMIC') {
if ($('input[name=keyDerivStrategy]').val().trim() === '') {
alert('키 도출 전략 FQCN을 입력하세요.');
$('input[name=keyDerivStrategy]').focus();
return false;
}
if ($('textarea[name=keyDerivParams]').val().trim() === '') {
alert('키 도출 파라미터(JSON)를 입력하세요.');
$('textarea[name=keyDerivParams]').focus();
return false;
}
}
return true;
}
function textToHex(text) {
if (!text) return '';
var encoder = new TextEncoder();
var bytes = encoder.encode(text);
return Array.from(bytes).map(function(b) {
return b.toString(16).padStart(2, '0');
}).join('').toUpperCase();
}
function updateHexFromText(textEl, hexName, lenSpanId) {
var hex = textToHex($(textEl).val());
$('input[name=' + hexName + ']').val(hex);
updateHexLength(hex, lenSpanId);
}
function updateHexLength(hex, lenSpanId) {
var charLen = hex ? hex.replace(/\s/g, '').length : 0;
var byteLen = Math.floor(charLen / 2);
var msg = charLen > 0 ? byteLen + ' bytes (' + charLen + '자)' : '';
$('#' + lenSpanId).text(msg);
}
function toggleKeySourceFields() {
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === 'STATIC') {
$('.static-section').show();
$('.dynamic-section').hide();
} else if (keySourceType === 'DYNAMIC') {
$('.static-section').hide();
$('.dynamic-section').show();
} else {
$('.static-section').show();
$('.dynamic-section').show();
}
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DETAIL', cryptoId: key },
success: function(data) {
$('input[name=cryptoId]').val(data['CRYPTO_ID']);
$('input[name=cryptoName]').val(data['CRYPTO_NAME']).attr('readonly', true);
$('input[name=cryptoDesc]').val(data['CRYPTO_DESC']);
$('select[name=algType]').val(data['ALG_TYPE']);
$('select[name=cipherMode]').val(data['CIPHER_MODE']);
$('select[name=padding]').val(data['PADDING'] || '');
$('input[name=ivHex]').val(data['IV_HEX']);
$('select[name=keySourceType]').val(data['KEY_SOURCE_TYPE']);
$('input[name=encKeyHex]').val(data['ENC_KEY_HEX']);
$('input[name=decKeyHex]').val(data['DEC_KEY_HEX']);
$('input[name=keyDerivStrategy]').val(data['KEY_DERIV_STRATEGY']);
$('textarea[name=keyDerivParams]').val(data['KEY_DERIV_PARAMS']);
$('select[name=cacheYn]').val(data['CACHE_YN']);
$('input[name=cacheTtlSec]').val(data['CACHE_TTL_SEC']);
$('select[name=useYn]').val(data['USE_YN']);
updateHexLength(data['ENC_KEY_HEX'], 'encKeyLen');
updateHexLength(data['DEC_KEY_HEX'], 'decKeyLen');
updateHexLength(data['IV_HEX'], 'ivHexLen');
if (data['MODIFIED_BY'] || data['MODIFIED_AT']) {
$('#span_modified_by').text(data['MODIFIED_BY'] || '');
$('#span_modified_at').text((data['MODIFIED_AT'] || '').replace('T', ' '));
$('#row_modified_info').show();
}
toggleKeySourceFields();
},
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = '${param.cryptoId}';
if (key !== '' && key !== 'null') {
isDetail = true;
}
detail(url, key);
$('select[name=keySourceType]').change(function() {
toggleKeySourceFields();
});
$('input[name=encKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'encKeyLen');
});
$('input[name=decKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'decKeyLen');
});
$('input[name=ivHex]').on('input', function() {
updateHexLength($(this).val(), 'ivHexLen');
});
toggleKeySourceFields();
$('#btn_modify').click(function() {
if (!isValid()) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: 'cmd', value: isDetail ? 'UPDATE' : 'INSERT' });
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: postData,
success: function(data) {
var msg = '<%= localeMessage.getString("common.saveMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_delete').click(function() {
if (confirm('<%= localeMessage.getString("common.confirmMsg") %>') !== true) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DELETE', cryptoId: $('input[name=cryptoId]').val() },
success: function(data) {
var msg = '<%= localeMessage.getString("common.deleteMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_previous').click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> 삭제</button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> 저장</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> 이전</button>
</div>
<div class="title">암호화 모듈 설정 상세</div>
<form id="ajaxForm">
<input type="hidden" name="cryptoId" />
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;">모듈명 *</th>
<td><input type="text" name="cryptoName" style="width:300px;" /></td>
</tr>
<tr>
<th>설명</th>
<td><input type="text" name="cryptoDesc" style="width:400px;" /></td>
</tr>
<tr>
<th>알고리즘 *</th>
<td>
<select name="algType">
<option value="">선택</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
</tr>
<tr>
<th>운영모드 *</th>
<td>
<select name="cipherMode">
<option value="">선택</option>
<option value="CBC">CBC</option>
<option value="GCM">GCM</option>
<option value="ECB">ECB</option>
<option value="FF1">FF1</option>
<option value="FF3-1">FF3-1</option>
</select>
</td>
</tr>
<tr>
<th>패딩</th>
<td>
<select name="padding">
<option value="">없음</option>
<option value="PKCS5Padding">PKCS5Padding</option>
<option value="NoPadding">NoPadding</option>
</select>
</td>
</tr>
<tr>
<th>IV (Hex)</th>
<td>
<input type="text" name="ivHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="ivHexLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="ivHexText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'ivHex', 'ivHexLen')" />
<span class="help-inline">CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용</span>
</td>
</tr>
<tr>
<th>키 소스 유형 *</th>
<td>
<select name="keySourceType">
<option value="">선택</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
</tr>
<!-- STATIC 전용 -->
<tr class="static-section">
<th>암호화 키 (Hex) *</th>
<td>
<input type="text" name="encKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="encKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="encKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'encKeyHex', 'encKeyLen')" />
<span class="help-inline">AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자)</span>
</td>
</tr>
<tr class="static-section">
<th>복호화 키 (Hex)</th>
<td>
<input type="text" name="decKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="decKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="decKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'decKeyHex', 'decKeyLen')" />
<span class="help-inline">미입력 시 암호화 키와 동일</span>
</td>
</tr>
<!-- DYNAMIC 전용 -->
<tr class="dynamic-section">
<th>키 도출 전략 (FQCN) *</th>
<td><input type="text" name="keyDerivStrategy" style="width:500px;" /></td>
</tr>
<tr class="dynamic-section">
<th>키 도출 파라미터 (JSON) *</th>
<td><textarea name="keyDerivParams" style="width:500px; height:80px;"></textarea></td>
</tr>
<tr>
<th>동적 키 캐시 *</th>
<td>
<select name="cacheYn">
<option value="N">N</option>
<option value="Y">Y</option>
</select>
</td>
</tr>
<tr>
<th>캐시 TTL (초)</th>
<td>
<input type="number" name="cacheTtlSec" style="width:100px;" />
<span class="help-inline">기본값 300</span>
</td>
</tr>
<tr>
<th>사용 여부 *</th>
<td>
<select name="useYn">
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
<tr id="row_modified_info" style="display:none;">
<th>수정자 / 수정일시</th>
<td>
<span id="span_modified_by" style="font-weight:bold;"></span>
<span style="margin:0 8px; color:#ccc;">|</span>
<span id="span_modified_at" style="color:#555;"></span>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.eactive.eai.data.DataService;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
public interface CryptoModuleConfigDataService extends DataService<CryptoModuleConfig, String> {
Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn);
}
@@ -0,0 +1,38 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.entity.onl.security.QCryptoModuleConfig;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional
public class CryptoModuleConfigDataServiceImpl
extends AbstractDataService<CryptoModuleConfig, String, CryptoModuleConfigRepository>
implements CryptoModuleConfigDataService {
@Override
public Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType,
String keySourceType, String useYn) {
QCryptoModuleConfig q = QCryptoModuleConfig.cryptoModuleConfig;
BooleanExpression predicate = q.cryptoName.containsIgnoreCase(searchName != null ? searchName : "");
if (algType != null && !algType.isEmpty()) {
predicate = predicate.and(q.algType.eq(algType));
}
if (keySourceType != null && !keySourceType.isEmpty()) {
predicate = predicate.and(q.keySourceType.eq(keySourceType));
}
if (useYn != null && !useYn.isEmpty()) {
predicate = predicate.and(q.useYn.eq(useYn));
}
return repository.findAll(predicate, pageable);
}
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.jpa.BaseRepository;
interface CryptoModuleConfigRepository
extends BaseRepository<CryptoModuleConfig, String>, QuerydslPredicateExecutor<CryptoModuleConfig> {
}
@@ -0,0 +1,125 @@
package com.eactive.eai.rms.onl.manage.crypto;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.SortDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.agent.command.CommandResult;
import com.eactive.eai.agent.command.CommonCommand;
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
import com.eactive.eai.rms.common.login.SessionManager;
import com.eactive.eai.rms.common.vo.GridResponse;
@Controller
public class CryptoModuleConfigManController extends OnlBaseAnnotationController {
private static final String RELOAD_CRYPTO_MODULE_COMMAND = "com.eactive.eai.agent.security.ReloadCryptoModuleCommand";
private final CryptoModuleConfigManService service;
@Autowired
public CryptoModuleConfigManController(CryptoModuleConfigManService service) {
this.service = service;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
}
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view")
public String viewList() {
return "/onl/admin/security/cryptoModuleMan";
}
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view", params = "cmd=DETAIL")
public String detailView() {
return "/onl/admin/security/cryptoModuleManDetail";
}
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<CryptoModuleConfigUI>> selectList(
@SortDefault("cryptoName") Pageable pageable,
String searchName, String algType, String keySourceType, String useYn) {
Page<CryptoModuleConfigUI> page = service.selectList(pageable, searchName, algType, keySourceType, useYn);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DETAIL")
public ResponseEntity<CryptoModuleConfigUI> selectDetail(String cryptoId) {
return ResponseEntity.ok(service.selectDetail(cryptoId));
}
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=INSERT")
public ResponseEntity<Map<String, String>> insert(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
String cryptoId = service.insert(ui, SessionManager.getUserId(request));
CommandResult broadcastResult = CommonCommand.builder()
.name(RELOAD_CRYPTO_MODULE_COMMAND)
.args(cryptoId)
.build()
.broadcast(agentUtilService);
Map<String, String> body = new HashMap<>();
body.put("broadcastResult", broadcastResult.getMessage(true));
return ResponseEntity.ok(body);
}
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=UPDATE")
public ResponseEntity<Map<String, String>> update(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
service.update(ui, SessionManager.getUserId(request));
CommandResult broadcastResult = CommonCommand.builder()
.name(RELOAD_CRYPTO_MODULE_COMMAND)
.args(ui.getCryptoId())
.build()
.broadcast(agentUtilService);
Map<String, String> body = new HashMap<>();
body.put("broadcastResult", broadcastResult.getMessage(true));
return ResponseEntity.ok(body);
}
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DELETE")
public ResponseEntity<Map<String, String>> delete(String cryptoId) {
service.delete(cryptoId);
CommandResult broadcastResult = CommonCommand.builder()
.name(RELOAD_CRYPTO_MODULE_COMMAND)
.args(cryptoId)
.build()
.broadcast(agentUtilService);
Map<String, String> body = new HashMap<>();
body.put("broadcastResult", broadcastResult.getMessage(true));
return ResponseEntity.ok(body);
}
}
@@ -0,0 +1,64 @@
package com.eactive.eai.rms.onl.manage.crypto;
import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.data.entity.onl.security.CryptoModuleConfigDataService;
import com.eactive.eai.rms.onl.common.exception.BizException;
@Service
@Transactional
public class CryptoModuleConfigManService extends BaseService {
private final CryptoModuleConfigDataService dataService;
private final CryptoModuleConfigUiMapper mapper;
@Autowired
public CryptoModuleConfigManService(CryptoModuleConfigDataService dataService,
CryptoModuleConfigUiMapper mapper) {
this.dataService = dataService;
this.mapper = mapper;
}
public Page<CryptoModuleConfigUI> selectList(Pageable pageable, String searchName,
String algType, String keySourceType, String useYn) {
Page<CryptoModuleConfig> page = dataService.findAll(pageable, searchName, algType, keySourceType, useYn);
return page.map(mapper::toVo);
}
public CryptoModuleConfigUI selectDetail(String cryptoId) {
CryptoModuleConfig entity = dataService.findById(cryptoId)
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + cryptoId));
return mapper.toVo(entity);
}
public String insert(CryptoModuleConfigUI ui, String loginId) {
CryptoModuleConfig entity = mapper.toEntity(ui);
entity.setCryptoId(null);
entity.setModifiedBy(loginId);
entity.setModifiedAt(LocalDateTime.now());
dataService.save(entity);
return entity.getCryptoId();
}
public void update(CryptoModuleConfigUI ui, String loginId) {
CryptoModuleConfig entity = dataService.findById(ui.getCryptoId())
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + ui.getCryptoId()));
mapper.updateToEntity(ui, entity);
entity.setModifiedBy(loginId);
entity.setModifiedAt(LocalDateTime.now());
dataService.save(entity);
}
public void delete(String cryptoId) {
dataService.deleteById(cryptoId);
}
}
@@ -0,0 +1,71 @@
package com.eactive.eai.rms.onl.manage.crypto;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class CryptoModuleConfigUI {
@JsonProperty("CRYPTO_ID")
private String cryptoId;
@JsonProperty("CRYPTO_NAME")
@NotNull
@Size(max = 100)
private String cryptoName;
@JsonProperty("CRYPTO_DESC")
private String cryptoDesc;
@JsonProperty("ALG_TYPE")
@NotNull
private String algType;
@JsonProperty("CIPHER_MODE")
@NotNull
private String cipherMode;
@JsonProperty("PADDING")
private String padding;
@JsonProperty("IV_HEX")
private String ivHex;
@JsonProperty("KEY_SOURCE_TYPE")
@NotNull
private String keySourceType;
@JsonProperty("ENC_KEY_HEX")
private String encKeyHex;
@JsonProperty("DEC_KEY_HEX")
private String decKeyHex;
@JsonProperty("KEY_DERIV_STRATEGY")
private String keyDerivStrategy;
@JsonProperty("KEY_DERIV_PARAMS")
private String keyDerivParams;
@JsonProperty("CACHE_YN")
@NotNull
private String cacheYn;
@JsonProperty("CACHE_TTL_SEC")
private Integer cacheTtlSec;
@JsonProperty("USE_YN")
@NotNull
private String useYn;
@JsonProperty("MODIFIED_BY")
private String modifiedBy;
@JsonProperty("MODIFIED_AT")
private String modifiedAt;
}
@@ -0,0 +1,18 @@
package com.eactive.eai.rms.onl.manage.crypto;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.ReportingPolicy;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.mapper.GenericMapper;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CryptoModuleConfigUiMapper extends GenericMapper<CryptoModuleConfigUI, CryptoModuleConfig> {
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateToEntity(CryptoModuleConfigUI ui, @MappingTarget CryptoModuleConfig entity);
}
@@ -0,0 +1,143 @@
# 작업내역 — feature/crypto-module (eapim-admin)
> 작업일: 2026-05-06
> 브랜치: `feature/crypto-module`
> 작업자: curry772
> 프로젝트: `eapim-admin`
---
## 작업 목적
`eapim-online`에서 구현된 암호화모듈 프레임워크(`CryptoModuleManager`)의 설정 데이터(`crypto_module_config`)를
관리자 화면에서 CRUD할 수 있는 관리 화면 개발.
설정 변경 즉시 게이트웨이 엔진에 동적 반영되어야 한다.
---
## 변경 파일 목록
### JPA 데이터 레이어 — `com.eactive.eai.rms.data.entity.onl.security`
| 파일 | 설명 |
|---|---|
| `CryptoModuleConfigRepository.java` | `BaseRepository` + `QuerydslPredicateExecutor` 조합 Repository |
| `CryptoModuleConfigDataService.java` | 데이터 서비스 인터페이스. `findAll(pageable, searchName, algType, keySourceType, useYn)` |
| `CryptoModuleConfigDataServiceImpl.java` | QueryDSL predicate 기반 검색 구현. `QCryptoModuleConfig` 사용 |
### 관리 화면 레이어 — `com.eactive.eai.rms.onl.manage.crypto`
| 파일 | 설명 |
|---|---|
| `CryptoModuleConfigUI.java` | UI VO (`@Data`, `@JsonProperty` 대문자+언더스코어 규칙). `@NotNull` 검증 포함 |
| `CryptoModuleConfigUiMapper.java` | MapStruct Entity↔UI 매퍼. `updateToEntity` (null 무시) 포함 |
| `CryptoModuleConfigManService.java` | CRUD 비즈니스 서비스. insert/update 시 `modifiedBy`·`modifiedAt` 자동 설정 |
| `CryptoModuleConfigManController.java` | Spring MVC Controller. INSERT/UPDATE/DELETE 후 엔진 재로드 커맨드 broadcast |
### JSP 화면 — `WebContent/jsp/onl/admin/security/`
| 파일 | 설명 |
|---|---|
| `cryptoModuleMan.jsp` | 목록 화면. jqGrid, 4가지 검색 조건(모듈명·알고리즘·키소스·사용여부) |
| `cryptoModuleManDetail.jsp` | 상세/등록/수정 화면. `keySourceType` 선택에 따라 STATIC/DYNAMIC 섹션 토글 |
---
## URL 구조
| 용도 | URL |
|---|---|
| 목록 View | `GET /onl/admin/security/cryptoModuleMan.view` |
| 상세 View | `GET /onl/admin/security/cryptoModuleMan.view?cmd=DETAIL&cryptoId={id}` |
| 목록 조회 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=LIST` |
| 상세 조회 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=DETAIL` |
| 등록 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=INSERT` |
| 수정 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=UPDATE` |
| 삭제 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=DELETE` |
---
## 엔진 동적 반영 구조
```
Controller (INSERT / UPDATE / DELETE)
└─ service.insert() / update() / delete() ← DB 반영
└─ CommonCommand.builder()
.name("com.eactive.eai.agent.security.ReloadCryptoModuleCommand")
.args(cryptoId)
.build()
.broadcast(agentUtilService) ← 게이트웨이 전 인스턴스 broadcast
└─ CryptoModuleManager.reload(cryptoId)
├─ configMap 에서 cryptoId 항목 제거
├─ dynamicKeyCache 에서 관련 항목 제거
└─ DB 재조회 → useYn=Y 이면 configMap 재등록
(DELETE 후 호출 시: DB에 없으므로 제거만 수행)
```
- `RELOAD_CRYPTO_MODULE_COMMAND` 상수는 `CommonCommand.java`에 추가하지 않고
`CryptoModuleConfigManController` 내부에 `private static final String`으로 선언
---
## 화면 설계
### 목록 화면 (`cryptoModuleMan.jsp`)
**검색 조건**
| 항목 | 유형 | 비고 |
|---|---|---|
| 모듈명 | text | 부분 일치 (containsIgnoreCase) |
| 알고리즘 | select | 전체 / AES / ARIA |
| 키 소스 | select | 전체 / STATIC / DYNAMIC |
| 사용 여부 | select | 전체 / Y / N |
**그리드 컬럼**: 모듈명 · 설명 · 알고리즘 · 운영모드 · 키 소스 · 사용여부
(CRYPTO_ID는 hidden 컬럼으로 포함, 더블클릭 시 상세 화면 이동에 사용)
### 상세 화면 (`cryptoModuleManDetail.jsp`)
**공통 필드**: 모듈명\* · 설명 · 알고리즘\* · 운영모드\* · 패딩 · IV(Hex) · 키소스유형\* · 동적키캐시\* · 캐시TTL · 사용여부\*
**STATIC 섹션** (`keySourceType = STATIC` 일 때만 표시):
- 암호화 키(Hex) \*
- 복호화 키(Hex) (미입력 시 암호화 키와 동일)
**DYNAMIC 섹션** (`keySourceType = DYNAMIC` 일 때만 표시):
- 키 도출 전략 FQCN \*
- 키 도출 파라미터 JSON \*
`keySourceType` select 변경 시 JavaScript `toggleKeySourceFields()`로 즉시 토글.
---
## 주요 설계 결정
### 1. `@InitBinder` — Integer 빈 문자열 처리
`cacheTtlSec`(Integer)에 빈 문자열이 전송될 수 있으므로 Controller에 `@InitBinder` 등록:
```java
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
```
### 2. 로그인 사용자 — `SessionManager.getUserId(request)`
`HttpServletRequest`를 Controller에서 받아 `SessionManager.getUserId(request)`로 추출,
Service의 `insert()` / `update()``loginId`로 전달 → `modifiedBy` 설정.
### 3. JSON 필드명 규칙
`@JsonProperty`는 DB 컬럼명 기반 대문자+언더스코어 (`CRYPTO_ID`, `KEY_SOURCE_TYPE` 등).
JSP `detail()` 함수에서 제네릭 루프 대신 명시적 필드 매핑으로 처리
(기존 `name.toUpperCase()` 패턴은 언더스코어 있는 컬럼명과 불일치하므로).
### 4. DELETE 후 엔진 반영
`CryptoModuleManager.reload(cryptoId)` — DB에서 삭제된 후 호출하면
`loader.findById(cryptoId)` 가 empty를 반환 → `configMap` 에서 제거만 수행.
별도 RemoveCommand 없이 동일한 `ReloadCryptoModuleCommand` 재사용.
---
## 미완료 / 다음 작업
- [ ] `kjb-gradle.sh compileJava` 실행 → `QCryptoModuleConfig` Q-class 생성 확인
- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`)
- [ ] 화면 접근 권한(ACL) 등록
- [ ] 전체 파일 커밋 (eapim-admin)
@@ -0,0 +1,149 @@
# 작업내역 — feature/crypto-module (eapim-admin)
> 작업일: 2026-05-07
> 브랜치: `feature/crypto-module`
> 작업자: curry772
> 프로젝트: `eapim-admin`
---
## 작업 목적
2026-05-06 작업 미완료 항목 처리 및 기능 테스트 중 발견된 버그 수정, UX 개선.
---
## 변경 파일 목록
### DB DDL
| 항목 | 내용 |
|---|---|
| `EMSADM.CRYPTO_MODULE_CONFIG` 테이블 컬럼 추가 | 기존 DDL에서 누락된 `MODIFIED_BY`, `MODIFIED_AT` 컬럼 추가 |
```sql
ALTER TABLE EMSADM.CRYPTO_MODULE_CONFIG
ADD (
MODIFIED_BY VARCHAR2(50),
MODIFIED_AT TIMESTAMP
);
```
> 전체 DDL (신규 환경용)
```sql
CREATE TABLE EMSADM.CRYPTO_MODULE_CONFIG (
CRYPTO_ID VARCHAR2(36) NOT NULL,
CRYPTO_NAME VARCHAR2(100) NOT NULL,
CRYPTO_DESC VARCHAR2(500),
ALG_TYPE VARCHAR2(10) NOT NULL,
CIPHER_MODE VARCHAR2(20) NOT NULL,
PADDING VARCHAR2(30),
IV_HEX VARCHAR2(200),
KEY_SOURCE_TYPE VARCHAR2(20) NOT NULL,
ENC_KEY_HEX VARCHAR2(200),
DEC_KEY_HEX VARCHAR2(200),
KEY_DERIV_STRATEGY VARCHAR2(200),
KEY_DERIV_PARAMS VARCHAR2(2000),
CACHE_YN CHAR(1) NOT NULL,
CACHE_TTL_SEC NUMBER(10),
USE_YN CHAR(1) NOT NULL,
MODIFIED_BY VARCHAR2(50),
MODIFIED_AT TIMESTAMP,
CONSTRAINT PK_CRYPTO_MODULE_CONFIG PRIMARY KEY (CRYPTO_ID),
CONSTRAINT UQ_CRYPTO_MODULE_CONFIG_NAME UNIQUE (CRYPTO_NAME)
);
```
---
### 비즈니스 레이어 — `CryptoModuleConfigManService.java`
#### 버그 수정: detached entity passed to persist
**원인**
- `AbstractEntity``Persistable<I>`를 구현하고 `@Transient boolean isNew = true`로 선언
- `@PostLoad`에서 `isNew = false`로 전환하는 구조
- `AbstractDataService.getById(id)``repository.getReferenceById(id)` (lazy proxy) 반환
- lazy proxy는 실제 DB 조회를 하지 않아 `@PostLoad`가 호출되지 않음 → `isNew = true` 유지
- `save(proxy)` 호출 시 `isNew() = true``em.persist(proxy)` → detached entity 에러
**수정 내용**
| 메서드 | 변경 전 | 변경 후 |
|---|---|---|
| `selectDetail()` | `dataService.getById(cryptoId)` | `dataService.findById(cryptoId).orElseThrow(...)` |
| `update()` | `dataService.getById(ui.getCryptoId())` | `dataService.findById(ui.getCryptoId()).orElseThrow(...)` |
| `insert()` | — | `entity.setCryptoId(null)` 추가 |
- `findById()`는 실제 DB 조회 → `@PostLoad` 호출 → `isNew = false``merge()` 정상 동작
- `insert()` 에서 `setCryptoId(null)` 추가: JSP `<input type="hidden" name="cryptoId" />`가 빈 문자열 `""`로 전송될 때 UUID 생성기가 정상 동작하도록 보장
---
### 목록 화면 — `cryptoModuleMan.jsp`
#### 버그 수정: 검색 조건 누락
**원인**
- 공통 함수 `getSearchForJqgrid()``name^=search` 패턴 필드만 자동 수집
- `algType`, `keySourceType`, `useYn`은 해당 패턴이 아니어서 검색 버튼 클릭 시 postData에서 누락
**수정 내용**
- 검색 버튼 클릭 핸들러에서 세 필드를 명시적으로 postData에 추가
```javascript
var postData = getSearchForJqgrid("cmd", "LIST");
postData['algType'] = $('select[name=algType]').val();
postData['keySourceType'] = $('select[name=keySourceType]').val();
postData['useYn'] = $('select[name=useYn]').val();
```
---
### 상세 화면 — `cryptoModuleManDetail.jsp`
#### 기능 추가 1: 텍스트 → HEX 변환 보조 입력 + 키 길이 표시
**배경**
- AES/ARIA 암호화 키, 복호화 키, IV를 HEX 값으로 직접 입력하는 것은 불편
- 단, HEX → 텍스트 역변환이 항상 가능하지 않음 (랜덤 바이트 키는 유효하지 않은 UTF-8 시퀀스 포함 가능)
- 텍스트 변환 기능은 개발/테스트 편의용, 운영 환경에서는 랜덤 키 생성 권장
**추가 내용**
- `encKeyHex`, `decKeyHex`, `ivHex` 각 필드에 텍스트 입력 보조 필드 추가
- 텍스트 입력 시 `TextEncoder`(UTF-8)로 HEX 자동 변환 → HEX 필드에 반영
- 보조 입력 필드는 `name` 속성 없음 → form submit에 포함되지 않음
- HEX 필드 옆에 키 길이 실시간 표시 (`N bytes (M자)` 형식)
- HEX 직접 입력 시 및 조회 시에도 즉시 반영
- 참고 기준: AES-128: 16bytes / AES-192: 24bytes / AES-256: 32bytes
**추가 함수**
```javascript
textToHex(text) // TextEncoder UTF-8 기반 HEX 변환
updateHexFromText(...) // 텍스트 입력 → HEX 필드 갱신 + 길이 표시
updateHexLength(hex, id) // HEX 문자열 길이 계산 및 표시
```
#### 기능 추가 2: 수정자 / 수정일시 정보 표시
- 테이블 하단에 수정자/수정일시 행 추가 (read-only `<span>` 표시)
- 신규 등록 화면: 행 숨김 (`display:none`)
- 상세 조회 화면: 값이 있을 때만 행 표시
- `MODIFIED_AT` 포맷: `LocalDateTime.toString()` 결과의 `T` → 공백 치환 (`2026-05-07 09:30:00`)
---
## 주요 설계 결정
### `getById()` vs `findById()`
이 프로젝트에서 `AbstractDataService.getById()``getReferenceById()`를 래핑한다.
`AbstractEntity``Persistable<isNew>`를 구현하므로, lazy proxy 상태에서는 `isNew = true`가 유지되어 `save()``persist()`가 호출되는 문제가 있다.
**수정/조회 후 저장이 필요한 경우 반드시 `findById()`를 사용해야 한다.**
---
## 미완료 / 다음 작업
- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`)
- [ ] 화면 접근 권한(ACL) 등록
- [ ] 전체 파일 커밋 (eapim-admin + eapim-online 양쪽)