11 Commits

Author SHA1 Message Date
Rinjae(gf63) 4bf7f661e3 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-11 18:38:59 +09:00
Rinjae(gf63) 124df00f51 - Gradle 실행 설정 추가 (clean, compileJava)
- API 편집기 닫기 전 변경사항 알림/확인 로직 추가
- 저장 성공/초기 로드 시점 기준 스냅샷 로직 및 UI 처리를 반영
2026-09-11 18:38:50 +09:00
eastargh e4de8ebc8b 조회시간 최대 59분이 되도록 변경
eapim-admin CI / build (push) Has been cancelled
2026-09-11 17:35:47 +09:00
eastargh 52e8c3be39 조회시간 최대 59분이 되도록 변경 2026-09-11 17:35:23 +09:00
eastargh 136626e7c4 사용자 권한변경시 거래현황-업무구분 기본 선택되도록 변경
eapim-admin CI / build (push) Has been cancelled
2026-09-11 14:03:16 +09:00
eastargh 0848cd3469 UMS 카톡 템플릿 적용
eapim-admin CI / build (push) Has been cancelled
2026-09-10 13:19:00 +09:00
eastargh 0b6f273132 UMS 응답 저장 및 상세조회
eapim-admin CI / build (push) Has been cancelled
2026-09-10 09:56:50 +09:00
eastargh 266644798f 이메일발송 설정 상수로 원복
eapim-admin CI / build (push) Has been cancelled
2026-09-09 15:28:29 +09:00
eastargh 2e8b45c69e email 발송 변수설정
eapim-admin CI / build (push) Has been cancelled
2026-09-09 14:07:54 +09:00
Rinjae ea3d23424c Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-09 11:05:51 +09:00
Rinjae 2a9dfaa093 API Spec 상태 조회 및 관리 기능 추가
- ApiSpecStatusRepository 신규 추가
- 스펙 상태 검색 조건/그룹 조회 및 UI 반영
2026-09-09 11:05:46 +09:00
20 changed files with 609 additions and 73 deletions
+11 -1
View File
@@ -29,6 +29,14 @@
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
var djbAuthType = '';
// 마지막 저장(또는 최초 로드) 시점의 state.data 스냅샷 — 닫기 전 미저장 변경 여부 판별용.
var savedSnapshot = null;
function isDirty() { return savedSnapshot !== null && JSON.stringify(state.data) !== savedSnapshot; }
function closeEditor() {
if (isDirty() && !window.confirm('저장하지 않은 변경사항이 있습니다. 저장하지 않고 닫으시겠습니까?')) return;
window.close();
}
// ============================================================
// 2. 유틸
// ============================================================
@@ -1703,7 +1711,7 @@
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', () => window.close());
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', closeEditor);
// Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단)
document.addEventListener('keydown', function (e) {
@@ -2066,6 +2074,7 @@
.then(function (res) {
if (res && res.status === 'success') {
toast(temp ? '임시저장되었습니다' : '저장되었습니다');
savedSnapshot = JSON.stringify(state.data); // 저장 성공 시점을 새 저장 기준선으로 갱신
var badge = $('#hdr-source'); if (badge) { badge.textContent = '저장본'; badge.className = 'ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200'; badge.classList.remove('hidden'); }
} else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error');
})
@@ -2102,6 +2111,7 @@
// ============================================================
function init() {
loadInitialData().then(function () {
savedSnapshot = JSON.stringify(state.data); // 초기 로드(자동생성 포함) 완료 시점을 저장 기준선으로 설정
renderStepper();
renderForm();
bindGlobalEvents();
@@ -47,6 +47,8 @@
$('#sentDate').text(data.sentDateFull || '');
// 서버에서 마스킹+escape 된 안전 HTML
$('#messageHtml').html(data.messageHtml || '');
// 응답내용(RESPONSE_DATA) — 원문이므로 text 로만 출력
$('#responseData').text(data.responseData || '');
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
if (data.requestStatus !== 'PENDING') {
@@ -142,6 +144,10 @@
<th>메세지 내용</th>
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
</tr>
<tr>
<th>응답내용</th>
<td colspan="3"><div id="responseData" class="msg-detail"></div></td>
</tr>
</table>
</div>
</div>
@@ -52,6 +52,8 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var url_left_menu = '<c:url value="/leftMenu.do"/>';
var DASHBOARD_MENU_ID = '0305001'; // apiStatsMinuteMan(대시보드) 메뉴 ID
var totalDonutChart, callChart;
@@ -162,23 +164,20 @@
var hour = parseInt(statTime.substring(8, 10));
var startDate = year + '-' + month + '-' + day;
var startTime = (hour < 10 ? '0' + hour : hour) + ':00';
// 1시간 후 계산 (23시인 경우 다음날 00시로 자동 계산)
var d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour, 0, 0);
d.setHours(d.getHours() + 1);
var endDate = d.getFullYear() + '-' +
(d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1) + '-' +
(d.getDate() < 10 ? '0' : '') + d.getDate();
var endTime = (d.getHours() < 10 ? '0' : '') + d.getHours() + ':00';
var hourStr = (hour < 10 ? '0' + hour : hour);
var startTime = hourStr + ':00';
// 해당 시간대 끝(정시~59분)으로 이동 (예: 10:00~10:59)
var endTime = hourStr + ':59';
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
+ '&searchEndDate=' + endDate + '&searchEndTime=' + endTime;
params += '&menuId='+'${param.menuId}';
+ '&searchEndDate=' + startDate + '&searchEndTime=' + endTime;
params += '&menuId='+DASHBOARD_MENU_ID;
params += '&cmd='+'LIST';
params += '&serviceType='+'APIGW';
// 왼쪽 메뉴도 대시보드(분단위 통계) 메뉴로 전환
parent.leftFrame.location.href = url_left_menu + '?menuId=' + DASHBOARD_MENU_ID
+ '&serviceType=' + sessionStorage["serviceType"];
location.href = url_minute_view + params;
}
@@ -262,14 +262,14 @@
var diffMinutes = (end - start) / (1000 * 60);
if (diffMinutes > 60) {
// 1시간 초과 시 시작 + 1시간으로 조정
endDateTime = updateEndTime(new Date(start.getTime() + 60 * 60 * 1000));
// 1시간 초과 시 시작시각 기준 1시간치(정시~59분)로 조정
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
} else {
endDateTime = formatDateTime(end);
}
} else {
// 종료시간 미입력 시 시작 + 1시간으로 설정
endDateTime = updateEndTime(new Date(start.getTime() + 60 * 60 * 1000));
// 종료시간 미입력 시 시작시각 기준 1시간치(정시~59분)로 설정
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
}
return { start: startDateTime, end: endDateTime };
@@ -325,8 +325,8 @@
}
// 조회기간을 정시 단위 1시간 구간으로 이동 (direction: -1 이전, 1 이후)
// ex) 시작 08:35, 이전 클릭 -> 08:00~09:00, 다시 -> 07:00~08:00
// ex) 시작 08:35, 이후 클릭 -> 09:00~10:00, 다시 -> 10:00~11:00
// ex) 시작 08:35, 이전 클릭 -> 08:00~08:59, 다시 -> 07:00~07:59
// ex) 시작 08:35, 이후 클릭 -> 09:00~09:59, 다시 -> 10:00~10:59
function shiftTimeRange(direction) {
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
@@ -350,7 +350,8 @@
start.setHours(start.getHours() + 1);
}
var end = new Date(start.getTime() + 60 * 60 * 1000);
// 종료시각은 같은 시간대의 59분으로 설정 (예: 10:00~10:59)
var end = new Date(start.getTime() + 59 * 60 * 1000);
function pad(n) { return String(n).padStart(2, '0'); }
@@ -580,9 +581,9 @@
$("input[name=searchStartDate]").datepicker();
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분))
// 기본값 설정 (현재 시간(분) 기준 59분 전 ~ 현재 시간(분))
var now = new Date();
var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
var oneHourAgo = new Date(now.getTime() - 59 * 60 * 1000);
function formatDate(d) {
return d.getFullYear() + '-' +
@@ -38,6 +38,13 @@
success: function (json) {
new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
var apiGroupSelect = $('select[name=searchApiGroupId]').empty();
apiGroupSelect.append($('<option>').val('').text('전체'));
apiGroupSelect.append($('<option>').val('UNREGISTERED').text('미등록'));
(json.apiGroupList || []).forEach(function (group) {
apiGroupSelect.append($('<option>').val(group.id).text(group.groupName));
});
setSearchable(selectName); // 콤보에 searchable 설정
putSelectFromParam();
@@ -86,6 +93,17 @@
}
}
function specStatusFormatter(cellvalue, options, rowObject) {
switch (cellvalue) {
case 'PUBLISHED':
var groupNames = rowObject.publishedGroupNames || [];
var tooltip = groupNames.length ? groupNames.join('\n') : '게시된 그룹 없음';
return $('<span>').text('게시(' + groupNames.length + ')').attr('title', tooltip).css('cursor', 'help').prop('outerHTML');
case 'REGISTERED': return '등록';
default: return '';
}
}
function list() {
detail()
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
@@ -101,6 +119,7 @@
'요청',
'응답',
'상태',
'스펙 상태',
'작성자',
'가상응답여부',
'변경일시(*)',
@@ -113,6 +132,7 @@
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
{name: 'specStatus', align: 'center', width: '40', formatter: specStatusFormatter, sortable: false, title: false},
{name: 'author', align: 'center', width: '40', sortable: false},
{name : 'simYn', align : 'center' , width:'40', hidden: true },
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
@@ -421,6 +441,14 @@
<td>
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
</td>
<th style="width:180px;">당타발 구분</th>
<td>
<select name="searchInOutType" style="width: 100%">
<option value="">전체</option>
<option value="1">당발</option>
<option value="2">타발</option>
</select>
</td>
<th style="width:180px;">상태</th>
<td>
<select name="searchStatusCode" style="width: 100%">
@@ -431,6 +459,24 @@
<option value="C">점검</option>
</select>
</td>
</tr>
<tr>
<th style="width:180px;">API 스펙 상태</th>
<td>
<select name="searchSpecStatus" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</option>
<option value="REGISTERED">등록</option>
<option value="PUBLISHED">게시</option>
</select>
</td>
<th style="width:180px;">API 그룹</th>
<td>
<select name="searchApiGroupId" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</option>
</select>
</td>
<th></th><td></td>
</tr>
</tbody>
@@ -467,4 +513,4 @@
</div>
</div>
</body>
</html>
</html>
@@ -22,6 +22,9 @@ import com.eactive.eai.rms.common.util.CommonConstants;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusiness;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessId;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilter;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterId;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterService;
import com.eactive.eai.rms.data.entity.onl.unifbwk.UnifBwkTpService;
@Service
@@ -40,6 +43,9 @@ public class BizManService extends BaseService {
@Autowired
private UserBusinessService userBusinessService;
@Autowired
private TransactionTrackingFilterService transactionTrackingFilterService;
@Autowired
private UserBizUIMapper userBizUIMapper;
@@ -101,7 +107,14 @@ public class BizManService extends BaseService {
}).collect(Collectors.toList());
userBusinessService.deleteOrSave(userId, userBusinesses);
transactionTrackingFilterService.deleteAllByUserId(userId);
List<TransactionTrackingFilter> filters = userBusinesses.stream()
.map(ub -> new TransactionTrackingFilter(
new TransactionTrackingFilterId(userId, ub.getId().getEaiBzwkDstcd())))
.collect(Collectors.toList());
transactionTrackingFilterService.saveAll(filters);
String userBusinessServiceLists = userBusinesses.stream()
.map(e -> e.getId().getEaiBzwkDstcd())
.collect(Collectors.joining(","));
@@ -8,9 +8,12 @@ import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.Tuple;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
@@ -47,6 +50,24 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
});
}
/** 검색 콤보용 그룹 ID/명칭. 게시 여부와 관계없이 모든 그룹을 제공한다. */
public List<Map<String, String>> findGroupOptions() {
QApiGroup group = QApiGroup.apiGroup;
List<Tuple> rows = getJPAQueryFactory()
.select(group.id, group.groupName)
.from(group)
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
List<Map<String, String>> options = new ArrayList<>();
for (Tuple row : rows) {
Map<String, String> option = new HashMap<>();
option.put("id", row.get(group.id));
option.put("groupName", StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
options.add(option);
}
return options;
}
public void deleteApiGroupApiByApiId(String apiId) {
List<ApiGroup> apiGroups = repository.findAll();
for (ApiGroup apiGroup : apiGroups) {
@@ -77,6 +98,28 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
.fetch();
}
/** 목록 툴팁용 게시 그룹명. Oracle IN 절 제한에 맞춰 페이지의 API들을 일괄 조회한다. */
public Map<String, List<String>> findPublishedGroupNamesByApiIds(List<String> apiIds) {
Map<String, List<String>> groupNames = new HashMap<>();
QApiGroup group = QApiGroup.apiGroup;
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
for (int from = 0; from < apiIds.size(); from += 1000) {
List<String> chunk = apiIds.subList(from, Math.min(from + 1000, apiIds.size()));
List<Tuple> rows = getJPAQueryFactory()
.select(membership.id.apiId, group.groupName)
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.in(chunk), group.displayYn.eq("1"))
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
for (Tuple row : rows) {
groupNames.computeIfAbsent(row.get(membership.id.apiId), key -> new ArrayList<>())
.add(StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
}
}
return groupNames;
}
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
deleteApiGroupApiByApiId(apiId);
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
import java.util.Collection;
import java.util.List;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
/** EAI 목록과 스키마가 다른 EMS 스펙의 ID/게시 여부만 조회한다. */
@EMSDataSource
public interface ApiSpecStatusRepository extends Repository<ApiSpecInfo, String> {
@Query("SELECT a.apiId FROM ApiSpecInfo a WHERE "
+ "(:status = 'PUBLISHED' AND a.displayYn = 'Y') OR "
+ "(:status = 'REGISTERED' AND (a.displayYn IS NULL OR a.displayYn <> 'Y'))")
List<String> findApiIdsByStatus(@Param("status") String status);
@Query("SELECT a.apiId AS apiId, a.displayYn AS displayYn FROM ApiSpecInfo a WHERE a.apiId IN :apiIds")
List<SpecStatus> findStatusesByApiIds(@Param("apiIds") Collection<String> apiIds);
interface SpecStatus {
String getApiId();
String getDisplayYn();
}
}
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.eaimsg;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.eai.rms.data.entity.onl.apim.apispec.ApiSpecStatusRepository;
import com.eactive.eai.data.entity.onl.adapter.QAdapter;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
@@ -51,6 +53,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
private final ApiSpecInfoRepository apiSpecInfoRepository;
private final ApiSpecStatusRepository apiSpecStatusRepository;
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
@@ -201,6 +204,32 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
predicate
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
}
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchInOutType())) {
predicate.and(qeaiMessageEntity.eaisvcname.endsWith(eaiMessageUISearch.getSearchInOutType()));
}
String specStatus = eaiMessageUISearch.getSearchSpecStatus();
if ("UNREGISTERED".equals(specStatus)) {
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname).not());
} else if ("REGISTERED".equals(specStatus) || "PUBLISHED".equals(specStatus)) {
predicate.and(apiIdInExpression(qeaiMessageEntity.eaisvcname,
apiSpecStatusRepository.findApiIdsByStatus(specStatus)));
}
String apiGroupId = eaiMessageUISearch.getSearchApiGroupId();
if (StringUtils.isNotBlank(apiGroupId)) {
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
QApiGroup group = QApiGroup.apiGroup;
JPQLQuery<Integer> groupQuery = JPAExpressions.selectOne()
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.eq(qeaiMessageEntity.eaisvcname));
if ("UNREGISTERED".equals(apiGroupId)) {
predicate.and(groupQuery.notExists());
} else {
predicate.and(groupQuery.where(group.id.eq(apiGroupId)).exists());
}
}
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) {
String statusCode = eaiMessageUISearch.getSearchStatusCode();
@@ -230,7 +259,10 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
*/
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
List<String> specApiIds = apiSpecInfoRepository.findAllApiIds();
return apiIdInExpression(eaiSvcNamePath, apiSpecInfoRepository.findAllApiIds());
}
private BooleanExpression apiIdInExpression(StringPath eaiSvcNamePath, List<String> specApiIds) {
if (specApiIds.isEmpty()) {
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
return Expressions.ONE.eq(Expressions.TWO);
@@ -245,6 +277,18 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
return expression;
}
/** 현재 페이지의 스펙 상태를 일괄 조회한다. Oracle IN 절은 1,000개씩 나눈다. */
public Map<String, String> findApiSpecStatuses(List<String> apiIds) {
Map<String, String> statuses = new HashMap<>();
for (int from = 0; from < apiIds.size(); from += ORACLE_IN_LIMIT) {
int to = Math.min(from + ORACLE_IN_LIMIT, apiIds.size());
for (ApiSpecStatusRepository.SpecStatus spec : apiSpecStatusRepository.findStatusesByApiIds(apiIds.subList(from, to))) {
statuses.put(spec.getApiId(), "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED");
}
}
return statuses;
}
/** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */
private JPQLQuery<String> displayedGroupApiIdSubQuery() {
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
@@ -19,6 +19,12 @@ public class EAIMessageUISearch {
private String searchRefKey;
private String searchApiFullPath;
/** API ID 마지막 자리 (1: 당발, 2: 타발) */
private String searchInOutType;
/** UNREGISTERED: 미등록, REGISTERED: 등록(비게시), PUBLISHED: 게시 */
private String searchSpecStatus;
/** 그룹 ID. UNREGISTERED이면 소속 그룹이 없는 API */
private String searchApiGroupId;
/** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */
private String searchStatusCode;
@@ -336,12 +336,12 @@ public class ApiStatsMinuteService
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER);
// 1시간(60분) 초과 시 시작시간 기준 1시간로 강제 설정
// 1시간(60분) 초과 시 시작시간 기준 1시간치(정시~59분, statTime 조건이 양끝 포함이므로 -1분)로 강제 설정
if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) {
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
}
} else {
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
}
search.setParsedStartDateTime(startDateTime);
@@ -20,4 +20,12 @@ public class TransactionTrackingFilterService extends
.fetch();
}
public void deleteAllByUserId(String userId) {
repository.deleteAll(findAllByUserId(userId));
}
public void saveAll(List<TransactionTrackingFilter> filters) {
repository.saveAll(filters);
}
}
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.ext.djb.ums.service;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
@@ -11,6 +12,7 @@ import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -42,13 +44,11 @@ public class UmsService {
private static final Gson gson = new Gson();
private static final String SYS_DVCD = "API"; //시스템코드
private static final String BZWK_DVCD = "95"; //업무구분코드
private static final String BZWK_CD = "999"; //업무코드
private static final String BZWK_DCLS_DVCD = "9019"; //세부구분코드
private static final String SUB_BZWK_CD = "999"; //서브업무코드 TODO: 변경할것
private static final String TEAM_DVCD = "99"; //팀구분코드 TODO: 변경해야함
private static final String BZWK_CD = "EBK"; //업무코드
private static final String SUB_BZWK_CD = "EBG"; //서브업무코드
private final MonitoringPropertyService propertyService;
private final MessageRequestService messageRequestService;
@@ -62,27 +62,35 @@ public class UmsService {
throw new IllegalArgumentException("messageRequest is null");
}
this.httpConnection(messageRequest);
UmsResponse response = this.httpConnection(messageRequest);
return true;
// HTTP 응답코드가 2xx 일 때만 SENT, 나머지는 FAILED
boolean success = response.getHttpStatus() >= 200 && response.getHttpStatus() < 300;
messageRequest.setRequestStatus(success ? "SENT" : "FAILED");
messageRequest.setSentDate(LocalDateTime.now());
messageRequest.setResponseData(response.getResponseData());
messageRequestService.save(messageRequest);
return success;
}
private void httpConnection(MessageRequest messageRequest) throws Exception {
private UmsResponse httpConnection(MessageRequest messageRequest) throws Exception {
HttpURLConnection conn = null;
BufferedReader br = null;
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
try {
String host = propertyService.getPropertyValue("Monitoring", "djb.ums."+messageRequest.getMessageType().toLowerCase()+".url", "");
URL url = new URL(host);
int timeoutValue = 5 * 1000; // 타임아웃 설정을 위한 값 (단위: ms)
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.forName("MS949");
StringBuffer sb = null;
String bodyData = this.makeBodyData(messageRequest);
String responseData = "";
String method = bodyData.isEmpty() ? "GET" : "POST";
conn = (HttpURLConnection) url.openConnection();
@@ -92,7 +100,7 @@ public class UmsService {
conn.setConnectTimeout(timeoutValue); // 연결 타임아웃 설정(5초)
conn.setReadTimeout(timeoutValue); // 읽기 타임아웃 설정(5초)
conn.setDoOutput(true);
// POst 방식인 경우에만
if (method.equals("POST")) {
OutputStream os = conn.getOutputStream();
@@ -100,29 +108,32 @@ public class UmsService {
os.write(requestData);
os.close();
}
int responseCode = conn.getResponseCode();
if (log.isDebugEnabled()) {
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
log.debug("getResponsecode();" + conn.getResponseCode()); // 응답 코드 구하기
log.debug("getResponsecode();" + responseCode); // 응답 코드 구하기
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
}
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), charset));
InputStream is = (responseCode >= 200 && responseCode < 300) ? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
br = new BufferedReader(new InputStreamReader(is, charset));
String inputLine;
sb = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
responseData = sb.toString();
}
String inputLine;
sb = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
responseData = sb.toString();
log.debug("UMS responseData:" + responseData);
return new UmsResponse(responseCode, responseData);
} finally {
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
if (br != null) {
@@ -133,6 +144,25 @@ public class UmsService {
}
}
}
/** UMS HTTP 호출 결과(응답코드 + 응답본문) 보관용. */
private static class UmsResponse {
private final int httpStatus;
private final String responseData;
UmsResponse(int httpStatus, String responseData) {
this.httpStatus = httpStatus;
this.responseData = responseData;
}
int getHttpStatus() {
return httpStatus;
}
String getResponseData() {
return responseData;
}
}
@@ -203,7 +233,7 @@ public class UmsService {
stdHeadVO.setChnl_tycd("EAI"); // 채널유형코드
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
stdHeadVO.setTx_id(messageRequest.getEaiTxId()); // 거래ID
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
@@ -247,7 +277,7 @@ public class UmsService {
// Sms 데이터
SmsDataVO dataVO = new SmsDataVO();
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest));
dataVO.setSnd_dvcd("EAI");
dataVO.setSms_msg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
dataVO.setSms_msg_ctnt(messageRequest.getMessage());
@@ -257,18 +287,20 @@ public class UmsService {
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
//ADMIN으로 시작하면 제주은행 임직원, 나머지는 제주은행 고객용
//ADMIN으로 시작하면 제주은행 내부용, 나머지는 제주은행 고객용
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
dataVO.setSms_trnm_bzwk_dvcd("99"); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.intra", "")); // 제주은행 임직원 플러스친구아이디
} else {
dataVO.setSms_trnm_bzwk_dvcd("95"); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.public", "")); // 제주은행 플러스친구아이디
}
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + BZWK_DCLS_DVCD + "001"); // 템플릿코드
dataVO.setAlmtk_tmplt_cd(messageRequest.getServiceId()); // 템플릿코드
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
dataVO.setSms_trnm_bzwk_dvcd(BZWK_DVCD); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
dataVO.setSms_sys_dvcd("85"); //시스템코드
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
@@ -293,7 +325,7 @@ public class UmsService {
EmailDataVO dataVO = new EmailDataVO();
dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수)
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest));
dataVO.setDman_seqno(1); // 발송요청순번
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수)
@@ -305,7 +337,7 @@ public class UmsService {
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
dataVO.setBzwk_cd(BZWK_CD); // 업무코드
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 서브업무코드
dataVO.setSys_dvcd(SYS_DVCD); // 시스템구분코드
dataVO.setSys_dvcd("85"); // 시스템구분코드
dataVO.setTx_dt(jsonData[0]); // 거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); // 거래시간
@@ -399,20 +431,24 @@ public class UmsService {
return buffer.toString();
}
private String getSndDmanId(String messageType) {
private String getSndDmanId(MessageRequest messageRequest) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String yyyymmdd = sdf.format(new Date());
String uuid = "";
if ("EMAIL".equals(messageType)) {
uuid = SYS_DVCD + BZWK_CD + SUB_BZWK_CD + yyyymmdd + "O" + "E" ;
if ("EMAIL".equals(messageRequest.getMessageType())) {
uuid = SYS_DVCD + BZWK_CD + SUB_BZWK_CD + yyyymmdd + "O" + "E" ;
long seq = messageRequestService.countEmailRequestsOnDate(LocalDate.now()) + 1;
uuid += String.format("%013d", seq);
} else if ("SMS".equals(messageType)) { //kakao 알림톡
uuid = SYS_DVCD + BZWK_DVCD + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K";
} else if ("SMS".equals(messageRequest.getMessageType())) { //kakao 알림톡
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
uuid = SYS_DVCD + "99" + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K"; //내부용
} else {
uuid = SYS_DVCD + "95" + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K"; //대고객
}
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
uuid += String.format("%013d", seq);
@@ -72,6 +72,7 @@ public class MessageRequestManService extends BaseService {
ui.setUmsUid(e.getUmsUid());
ui.setRequestStatus(e.getRequestStatus());
ui.setResponseData(e.getResponseData());
ui.setEaiInterfaceId(e.getEaiInterfaceId());
ui.setServiceId(e.getServiceId());
@@ -22,6 +22,9 @@ public class MessageRequestUI {
/* 본문 — 패턴 마스킹 + span 색상강조된 안전 HTML */
private String messageHtml;
/* UMS 응답 내용 (RESPONSE_DATA) — 원문 그대로, 화면에서는 text 로만 출력 */
private String responseData;
/* 수신자 (마스킹) */
private String username;
private String email;
@@ -147,8 +147,8 @@ public class UmsDispatchService {
try {
transactionTemplate.execute(status -> {
try {
// 상태(SENT/FAILED) 및 RESPONSE_DATA 저장은 umsService.send() 내부에서 처리한다.
umsService.send(message);
updateMessageStatus(message, "SENT");
return null;
} catch (Exception e) {
// checked exception을 RuntimeException으로 감싸 전파 → TransactionTemplate이 rollback 후 re-throw
@@ -159,7 +159,8 @@ public class UmsDispatchService {
log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e);
try {
transactionTemplate.execute(s -> {
updateMessageStatus(message, "FAILED");
// 응답 자체를 받지 못한 경우(네트워크/타임아웃 등) 예외 메시지를 RESPONSE_DATA 에 남긴다.
updateMessageStatus(message, "FAILED", toResponseData(e));
return null;
});
} catch (Exception ex) {
@@ -171,10 +172,19 @@ public class UmsDispatchService {
public void updateMessageStatus(MessageRequest message, String status) {
public void updateMessageStatus(MessageRequest message, String status, String responseData) {
message.setRequestStatus(status);
message.setSentDate(LocalDateTime.now());
if (responseData != null) {
message.setResponseData(responseData);
}
entityManager.merge(message);
entityManager.flush();
}
/** 예외를 RESPONSE_DATA 컬럼에 저장할 문자열로 변환한다. (RuntimeException 래퍼는 원인 예외로 풀어서 사용) */
private static String toResponseData(Throwable t) {
Throwable root = (t instanceof RuntimeException && t.getCause() != null) ? t.getCause() : t;
return String.valueOf(root);
}
}
@@ -135,6 +135,15 @@ public class ApiInterfaceService extends OnlBaseService {
public Page<ApiInterfaceUI> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable,String sortname, String sortorder) {
Page<Tuple> tuples = eaiMessageQueryService.selectList(eaiMessageUISearch, pageable, sortname, sortorder );
List<String> apiIds = tuples.getContent().stream()
.map(tuple -> tuple.get(QEAIMessageEntity.eAIMessageEntity).getEaisvcname())
.distinct()
.collect(Collectors.toList());
Map<String, String> specStatuses = eaiMessageQueryService.findApiSpecStatuses(apiIds);
List<String> publishedApiIds = apiIds.stream()
.filter(apiId -> "PUBLISHED".equals(specStatuses.get(apiId)))
.collect(Collectors.toList());
Map<String, List<String>> publishedGroupNames = apiGroupService.findPublishedGroupNamesByApiIds(publishedApiIds);
return tuples.map(tuple -> {
EAIMessageEntity eaiMessageEntity = tuple.get(QEAIMessageEntity.eAIMessageEntity);
@@ -153,6 +162,8 @@ public class ApiInterfaceService extends OnlBaseService {
apiInterfaceUI.setApiFullPath(apiFullPath);
apiInterfaceUI.setBzwksvckeyname(bzwksvckeyname);
apiInterfaceUI.setStatusCode(statusCode);
apiInterfaceUI.setSpecStatus(specStatuses.getOrDefault(eaiMessageEntity.getEaisvcname(), "UNREGISTERED"));
apiInterfaceUI.setPublishedGroupNames(publishedGroupNames.getOrDefault(eaiMessageEntity.getEaisvcname(), Collections.emptyList()));
return apiInterfaceUI;
});
@@ -164,6 +175,7 @@ public class ApiInterfaceService extends OnlBaseService {
// 전체 목록수 얻기
HashMap map = new HashMap();
map.put("bizList", voList);
map.put("apiGroupList", apiGroupService.findGroupOptions());
return map;
}
@@ -54,6 +54,8 @@ public class ApiInterfaceUI {
private String apiFullPath;
private String bzwksvckeyname;
private String statusCode;
private String specStatus;
private List<String> publishedGroupNames;
//ASYNC-SYNC 용
private String inboundResponseHttpMethod;
@@ -0,0 +1,184 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10 seconds">
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
<property name="PREFIX" value="${inst.Type:-ems}" />
<property name="LOG_HOME" value="/work/eactive/djb-eapim/workspace/eapim-admin/logs" />
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>utf-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_stdout.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_HOME}/${PREFIX}_stdout.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="ACCESS_APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_access.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_HOME}/${PREFIX}_access.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="SMS_APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/sms.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_HOME}/sms.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="QUERY_APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_query.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_HOME}/${PREFIX}_query.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="DYNAMIC_APPENDER"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_dynamic.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_HOME}/${PREFIX}_dynamic.log-%d{yyyy-MM-dd}.%i
</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_hibernate.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${PREFIX}_hibernate.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="HOTSWAP_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${PREFIX}_hotswap.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${PREFIX}_hotswap.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<charset>utf-8</charset>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<logger name="com.eactive.eai.rms.onl.server" level="ERROR" />
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
<logger name="com.eactive.eai.rms.ext.djb.job" level="DEBUG" />
<logger name="com.eactive.eai.rms.ext.djb.apistatus" level="DEBUG" />
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />
<logger name="org.springframework" level="INFO" />
<logger name="net.sf.ehcache" level="INFO" />
<logger name="ACCESS_LOGGER" level="DEBUG" >
<appender-ref ref="ACCESS_APPENDER" />
</logger>
<logger name="SMS_LOGGER" level="DEBUG" >
<appender-ref ref="SMS_APPENDER" />
</logger>
<logger name="DYNAMIC_LOGGER" level="DEBUG" >
<appender-ref ref="DYNAMIC_APPENDER" />
</logger>
<!-- QUERY_APPENDER start -->
<!-- Hibernate SQL & 파라미터 바인딩 -->
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
<appender-ref ref="QUERY_APPENDER" />
<appender-ref ref="HIBERNATE_APPENDER" />
</logger>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
<appender-ref ref="QUERY_APPENDER" />
<appender-ref ref="HIBERNATE_APPENDER" />
</logger>
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
<appender-ref ref="QUERY_APPENDER" />
<appender-ref ref="HIBERNATE_APPENDER" />
</logger>
<!-- Hibernate 일반 로그 (DDL/init/connection 등) -->
<logger name="org.hibernate" level="INFO" additivity="false">
<appender-ref ref="HIBERNATE_APPENDER" />
<appender-ref ref="STDOUT" />
</logger>
<!-- iBATIS 2.3.4 (commons-logging 또는 log4j-over-slf4j 통해 SLF4J 도달) -->
<logger name="com.ibatis" level="DEBUG" additivity="false">
<appender-ref ref="QUERY_APPENDER" />
</logger>
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
<appender-ref ref="QUERY_APPENDER" />
</logger>
<!-- QUERY_APPENDER end -->
<!-- HotswapAgent는 자체 logger를 사용하므로 logback에 안 잡힘. hotswap-agent.properties의 LOGFILE로 직접 파일 출력 -->
<root>
<level value="INFO" />
<appender-ref ref="STDOUT" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>
@@ -0,0 +1,84 @@
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
class ApiSpecStatusRepositoryTest {
private static SessionFactory sessionFactory;
private static EntityManager entityManager;
private static ApiSpecStatusRepository repository;
@BeforeAll
static void setUp() {
sessionFactory = new Configuration()
.addAnnotatedClass(ApiSpecInfo.class)
.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
.setProperty("hibernate.connection.driver_class", "org.h2.Driver")
.setProperty("hibernate.connection.url", "jdbc:h2:mem:apiSpecStatus;DB_CLOSE_DELAY=-1")
.setProperty("hibernate.hbm2ddl.auto", "create-drop")
.buildSessionFactory();
entityManager = sessionFactory.createEntityManager();
repository = new JpaRepositoryFactory(entityManager).getRepository(ApiSpecStatusRepository.class);
entityManager.getTransaction().begin();
save("PUBLISHED1", "Y");
save("REGISTERED1", "N");
save("LEGACY2", null);
entityManager.getTransaction().commit();
entityManager.clear();
}
private static void save(String apiId, String displayYn) {
ApiSpecInfo spec = new ApiSpecInfo();
spec.setApiId(apiId);
spec.setDisplayYn(displayYn);
entityManager.persist(spec);
}
@AfterAll
static void tearDown() {
if (entityManager != null) entityManager.close();
if (sessionFactory != null) sessionFactory.close();
}
@Test
void publishedIncludesOnlyDisplayedSpecs() {
assertEquals(Arrays.asList("PUBLISHED1"), repository.findApiIdsByStatus("PUBLISHED"));
}
@Test
void registeredIncludesNonDisplayedAndLegacyNullSpecs() {
assertEquals(new HashSet<>(Arrays.asList("REGISTERED1", "LEGACY2")),
new HashSet<>(repository.findApiIdsByStatus("REGISTERED")));
}
@Test
void statusProjectionIncludesOnlyRequestedExistingSpecs() {
Map<String, String> statuses = repository.findStatusesByApiIds(
Arrays.asList("PUBLISHED1", "LEGACY2", "MISSING2")).stream()
.collect(Collectors.toMap(ApiSpecStatusRepository.SpecStatus::getApiId,
spec -> "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED"));
assertEquals(2, statuses.size());
assertEquals("PUBLISHED", statuses.get("PUBLISHED1"));
assertEquals("REGISTERED", statuses.get("LEGACY2"));
assertEquals("UNREGISTERED", statuses.getOrDefault("MISSING2", "UNREGISTERED"));
}
@Test
void missingApiHasNoSpecStatus() {
assertTrue(repository.findStatusesByApiIds(Arrays.asList("MISSING2")).isEmpty());
}
}