From 49fdc3b577e8b111d8a3371a16672d0913be6108 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 7 May 2026 10:51:39 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=A0=95=20=EA=B4=80=EB=A6=AC=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=B6=94=EA=B0=80=20(feature/crypto-module)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CryptoModuleConfig CRUD 관리 화면 신규 개발 - JPA DataService/Repository, MapStruct UiMapper, ManService, Controller - 목록(jqGrid, 4가지 검색조건), 상세/등록/수정/삭제 화면 - INSERT/UPDATE/DELETE 후 게이트웨이 전 인스턴스에 ReloadCryptoModuleCommand broadcast - 버그 수정 - getById(getReferenceById) → findById 교체: Persistable.isNew=true 유지로 인한 persist 에러 - insert 시 entity.setCryptoId(null): 빈 문자열 cryptoId 전송 시 UUID 생성기 오동작 방지 - 검색 버튼 클릭 시 algType/keySourceType/useYn getSearchForJqgrid 누락 수정 - UX 개선 - 암호화키/복호화키/IV 필드에 텍스트→HEX 변환 보조 입력 및 키 길이 실시간 표시 - 상세 화면에 수정자/수정일시 정보성 표시 - DB: EMSADM.CRYPTO_MODULE_CONFIG 테이블 (MODIFIED_BY/MODIFIED_AT 포함 전체 DDL) Co-Authored-By: Claude Sonnet 4.6 --- .../onl/admin/security/cryptoModuleMan.jsp | 153 ++++++++ .../admin/security/cryptoModuleManDetail.jsp | 355 ++++++++++++++++++ .../CryptoModuleConfigDataService.java | 13 + .../CryptoModuleConfigDataServiceImpl.java | 38 ++ .../CryptoModuleConfigRepository.java | 10 + .../CryptoModuleConfigManController.java | 118 ++++++ .../crypto/CryptoModuleConfigManService.java | 64 ++++ .../manage/crypto/CryptoModuleConfigUI.java | 71 ++++ .../crypto/CryptoModuleConfigUiMapper.java | 18 + ..._20260506_eapim-admin_feature-crypto-module.md | 143 +++++++ ..._20260507_eapim-admin_feature-crypto-module.md | 149 ++++++++ 11 files changed, 1132 insertions(+) create mode 100644 WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp create mode 100644 WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java create mode 100644 작업내역_20260506_eapim-admin_feature-crypto-module.md create mode 100644 작업내역_20260507_eapim-admin_feature-crypto-module.md diff --git a/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp b/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp new file mode 100644 index 0000000..dd6278e --- /dev/null +++ b/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp @@ -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"); +%> + + + + + + + + + +
+
+ +
+
+
+ + +
+
암호화 모듈 설정
+
+ + + + + + + + + + + + + +
모듈명알고리즘 + + 키 소스 + + 사용 + +
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp b/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp new file mode 100644 index 0000000..20a5285 --- /dev/null +++ b/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp @@ -0,0 +1,355 @@ +<%@ 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"); +%> + + + + + + + + + +
+
+ +
+
+
+ + + +
+
암호화 모듈 설정 상세
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
모듈명 *
설명
알고리즘 * + +
운영모드 * + +
패딩 + +
IV (Hex) + + +
+ + CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용 +
키 소스 유형 * + +
암호화 키 (Hex) * + + +
+ + AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자) +
복호화 키 (Hex) + + +
+ + 미입력 시 암호화 키와 동일 +
키 도출 전략 (FQCN) *
키 도출 파라미터 (JSON) *
동적 키 캐시 * + +
캐시 TTL (초) + + 기본값 300 +
사용 여부 * + +
+
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java new file mode 100644 index 0000000..a921e6d --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java @@ -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 { + + Page findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn); + +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java new file mode 100644 index 0000000..f7166a1 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java @@ -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 + implements CryptoModuleConfigDataService { + + @Override + public Page 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); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java new file mode 100644 index 0000000..297ecae --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java @@ -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, QuerydslPredicateExecutor { +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java new file mode 100644 index 0000000..9a8a1a0 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java @@ -0,0 +1,118 @@ +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.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> selectList( + @SortDefault("cryptoName") Pageable pageable, + String searchName, String algType, String keySourceType, String useYn) { + Page 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 selectDetail(String cryptoId) { + return ResponseEntity.ok(service.selectDetail(cryptoId)); + } + + @PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=INSERT") + public ResponseEntity> insert(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult, + HttpServletRequest request) { + if (bindingResult.hasErrors()) { + Map 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)); + CommonCommand.builder() + .name(RELOAD_CRYPTO_MODULE_COMMAND) + .args(cryptoId) + .build() + .broadcast(agentUtilService); + return ResponseEntity.ok().build(); + } + + @PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=UPDATE") + public ResponseEntity> update(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult, + HttpServletRequest request) { + if (bindingResult.hasErrors()) { + Map 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)); + CommonCommand.builder() + .name(RELOAD_CRYPTO_MODULE_COMMAND) + .args(ui.getCryptoId()) + .build() + .broadcast(agentUtilService); + return ResponseEntity.ok().build(); + } + + @PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DELETE") + public ResponseEntity delete(String cryptoId) { + service.delete(cryptoId); + CommonCommand.builder() + .name(RELOAD_CRYPTO_MODULE_COMMAND) + .args(cryptoId) + .build() + .broadcast(agentUtilService); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java new file mode 100644 index 0000000..e93bdbb --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java @@ -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 selectList(Pageable pageable, String searchName, + String algType, String keySourceType, String useYn) { + Page 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); + } + +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java new file mode 100644 index 0000000..1e4d24a --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java @@ -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; + +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java new file mode 100644 index 0000000..d801005 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java @@ -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 { + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateToEntity(CryptoModuleConfigUI ui, @MappingTarget CryptoModuleConfig entity); + +} diff --git a/작업내역_20260506_eapim-admin_feature-crypto-module.md b/작업내역_20260506_eapim-admin_feature-crypto-module.md new file mode 100644 index 0000000..e69b861 --- /dev/null +++ b/작업내역_20260506_eapim-admin_feature-crypto-module.md @@ -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) diff --git a/작업내역_20260507_eapim-admin_feature-crypto-module.md b/작업내역_20260507_eapim-admin_feature-crypto-module.md new file mode 100644 index 0000000..5628e3d --- /dev/null +++ b/작업내역_20260507_eapim-admin_feature-crypto-module.md @@ -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`를 구현하고 `@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 ``가 빈 문자열 `""`로 전송될 때 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 `` 표시) +- 신규 등록 화면: 행 숨김 (`display:none`) +- 상세 조회 화면: 값이 있을 때만 행 표시 +- `MODIFIED_AT` 포맷: `LocalDateTime.toString()` 결과의 `T` → 공백 치환 (`2026-05-07 09:30:00`) + +--- + +## 주요 설계 결정 + +### `getById()` vs `findById()` +이 프로젝트에서 `AbstractDataService.getById()`는 `getReferenceById()`를 래핑한다. +`AbstractEntity`가 `Persistable`를 구현하므로, lazy proxy 상태에서는 `isNew = true`가 유지되어 `save()` 시 `persist()`가 호출되는 문제가 있다. +**수정/조회 후 저장이 필요한 경우 반드시 `findById()`를 사용해야 한다.** + +--- + +## 미완료 / 다음 작업 + +- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`) +- [ ] 화면 접근 권한(ACL) 등록 +- [ ] 전체 파일 커밋 (eapim-admin + eapim-online 양쪽)