package com.snp.batch.service; import com.snp.batch.global.dto.screening.ChangeHistoryResponse; import com.snp.batch.global.dto.screening.CompanyInfoResponse; import com.snp.batch.global.dto.screening.ComplianceCategoryResponse; import com.snp.batch.global.dto.screening.ComplianceIndicatorResponse; import com.snp.batch.global.dto.screening.IndicatorStatusResponse; import com.snp.batch.global.dto.screening.MethodologyHistoryResponse; import com.snp.batch.global.dto.screening.RiskCategoryResponse; import com.snp.batch.global.dto.screening.RiskIndicatorResponse; import com.snp.batch.global.dto.screening.ShipInfoResponse; import com.snp.batch.global.model.screening.ChangeTypeLang; import com.snp.batch.global.model.screening.CompanyDetailInfo; import com.snp.batch.global.model.screening.ShipCountryCode; import com.snp.batch.global.model.screening.CompanyComplianceHistory; import com.snp.batch.global.model.screening.ComplianceCategoryLang; import com.snp.batch.global.model.screening.ComplianceIndicator; import com.snp.batch.global.model.screening.ComplianceIndicatorLang; import com.snp.batch.global.model.screening.MethodologyHistory; import com.snp.batch.global.model.screening.MethodologyHistoryLang; import com.snp.batch.global.model.screening.RiskIndicator; import com.snp.batch.global.model.screening.RiskIndicatorCategoryLang; import com.snp.batch.global.model.screening.RiskIndicatorLang; import com.snp.batch.global.model.screening.ShipComplianceHistory; import com.snp.batch.global.repository.screening.ChangeTypeLangRepository; import com.snp.batch.global.repository.screening.CompanyComplianceHistoryRepository; import com.snp.batch.global.repository.screening.CompanyDetailInfoRepository; import com.snp.batch.global.repository.screening.ComplianceCategoryLangRepository; import com.snp.batch.global.repository.screening.ComplianceIndicatorLangRepository; import com.snp.batch.global.repository.screening.ComplianceIndicatorRepository; import com.snp.batch.global.repository.screening.MethodologyHistoryLangRepository; import com.snp.batch.global.repository.screening.MethodologyHistoryRepository; import com.snp.batch.global.repository.screening.RiskIndicatorCategoryLangRepository; import com.snp.batch.global.repository.screening.RiskIndicatorLangRepository; import com.snp.batch.global.repository.screening.RiskIndicatorRepository; import com.snp.batch.global.repository.screening.ShipComplianceHistoryRepository; import com.snp.batch.global.repository.screening.ShipCountryCodeRepository; import com.snp.batch.global.repository.screening.ShipInfoMasterRepository; import com.snp.batch.global.repository.screening.ShipRiskDetailHistoryRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class ScreeningGuideService { private static final DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final RiskIndicatorRepository riskIndicatorRepo; private final RiskIndicatorLangRepository riskIndicatorLangRepo; private final RiskIndicatorCategoryLangRepository riskCategoryLangRepo; private final ComplianceIndicatorRepository complianceIndicatorRepo; private final ComplianceIndicatorLangRepository complianceIndicatorLangRepo; private final ComplianceCategoryLangRepository complianceCategoryLangRepo; private final MethodologyHistoryRepository methodologyHistoryRepo; private final MethodologyHistoryLangRepository methodologyHistoryLangRepo; private final ChangeTypeLangRepository changeTypeLangRepo; private final ShipRiskDetailHistoryRepository shipRiskDetailHistoryRepo; private final ShipComplianceHistoryRepository shipComplianceHistoryRepo; private final CompanyComplianceHistoryRepository companyComplianceHistoryRepo; private final ShipInfoMasterRepository shipInfoMasterRepo; private final CompanyDetailInfoRepository companyDetailInfoRepo; private final ShipCountryCodeRepository shipCountryCodeRepo; private final JdbcTemplate jdbcTemplate; /** * 카테고리별 Risk 지표 목록 조회 * * @param lang 언어 코드 (예: KO, EN) * @return 카테고리별 Risk 지표 응답 목록 */ @Transactional(readOnly = true) public List getRiskIndicators(String lang) { Map catNameMap = riskCategoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(RiskIndicatorCategoryLang::getCategoryCode, RiskIndicatorCategoryLang::getCategoryName)); Map langMap = riskIndicatorLangRepo.findByLangCodeOrderByIndicatorIdAsc(lang).stream() .collect(Collectors.toMap(RiskIndicatorLang::getIndicatorId, Function.identity())); List indicators = riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc(); Map> grouped = indicators.stream() .collect(Collectors.groupingBy(RiskIndicator::getCategoryCode, LinkedHashMap::new, Collectors.toList())); return grouped.entrySet().stream().map(entry -> { String catCode = entry.getKey(); List indicatorResponses = entry.getValue().stream().map(ri -> { RiskIndicatorLang langData = langMap.get(ri.getIndicatorId()); return RiskIndicatorResponse.builder() .indicatorId(ri.getIndicatorId()) .fieldKey(ri.getFieldKey()) .fieldName(langData != null ? langData.getFieldName() : ri.getFieldKey()) .description(langData != null ? langData.getDescription() : "") .conditionRed(langData != null ? langData.getConditionRed() : "") .conditionAmber(langData != null ? langData.getConditionAmber() : "") .conditionGreen(langData != null ? langData.getConditionGreen() : "") .dataType(ri.getDataTypeCode()) .build(); }).toList(); return RiskCategoryResponse.builder() .categoryCode(catCode) .categoryName(catNameMap.getOrDefault(catCode, catCode)) .indicators(indicatorResponses) .build(); }).toList(); } /** * 카테고리별 Compliance 지표 목록 조회 * * @param lang 언어 코드 (예: KO, EN) * @param type 지표 유형 필터 (SHIP/COMPANY, null 또는 빈 문자열이면 전체) * @return 카테고리별 Compliance 지표 응답 목록 */ @Transactional(readOnly = true) public List getComplianceIndicators(String lang, String type) { Map catNameMap = complianceCategoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(ComplianceCategoryLang::getCategoryCode, ComplianceCategoryLang::getCategoryName)); List indicators = (type != null && !type.isBlank()) ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); Map langMap = complianceIndicatorLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(ComplianceIndicatorLang::getIndicatorId, Function.identity())); Map> grouped = indicators.stream() .collect(Collectors.groupingBy(ComplianceIndicator::getCategoryCode, LinkedHashMap::new, Collectors.toList())); return grouped.entrySet().stream().map(entry -> { String catCode = entry.getKey(); List indicatorResponses = entry.getValue().stream().map(ci -> { ComplianceIndicatorLang langData = langMap.get(ci.getIndicatorId()); return ComplianceIndicatorResponse.builder() .indicatorId(ci.getIndicatorId()) .fieldKey(ci.getFieldKey()) .fieldName(langData != null ? langData.getFieldName() : ci.getFieldKey()) .description(langData != null ? langData.getDescription() : "") .conditionRed(langData != null ? langData.getConditionRed() : "") .conditionAmber(langData != null ? langData.getConditionAmber() : "") .conditionGreen(langData != null ? langData.getConditionGreen() : "") .dataType(ci.getDataTypeCode()) .build(); }).toList(); return ComplianceCategoryResponse.builder() .categoryCode(catCode) .categoryName(catNameMap.getOrDefault(catCode, catCode)) .indicatorType(type) .indicators(indicatorResponses) .build(); }).toList(); } /** * 방법론 변경 이력 조회 * * @param lang 언어 코드 (예: KO, EN) * @return 방법론 변경 이력 응답 목록 */ @Transactional(readOnly = true) public List getMethodologyHistory(String lang) { Map changeTypeMap = changeTypeLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(ChangeTypeLang::getTypeCode, ChangeTypeLang::getTypeName)); Map langMap = methodologyHistoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(MethodologyHistoryLang::getHistoryId, Function.identity())); List histories = methodologyHistoryRepo.findAllByOrderByChangeDateDescSortOrderAsc(); return histories.stream() .filter(mh -> !"BANNER".equals(mh.getChangeTypeCode())) .map(mh -> { MethodologyHistoryLang langData = langMap.get(mh.getHistoryId()); return MethodologyHistoryResponse.builder() .historyId(mh.getHistoryId()) .changeDate(mh.getChangeDate() != null ? mh.getChangeDate().toString() : "") .changeTypeCode(mh.getChangeTypeCode()) .changeType(changeTypeMap.getOrDefault(mh.getChangeTypeCode(), mh.getChangeTypeCode())) .description(langData != null ? langData.getDescription() : "") .build(); }).toList(); } /** * 방법론 배너 텍스트 조회 */ @Transactional(readOnly = true) public MethodologyHistoryResponse getMethodologyBanner(String lang) { Map langMap = methodologyHistoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(MethodologyHistoryLang::getHistoryId, Function.identity())); return methodologyHistoryRepo.findAllByOrderByChangeDateDescSortOrderAsc().stream() .filter(mh -> "BANNER".equals(mh.getChangeTypeCode())) .findFirst() .map(mh -> { MethodologyHistoryLang langData = langMap.get(mh.getHistoryId()); return MethodologyHistoryResponse.builder() .historyId(mh.getHistoryId()) .changeTypeCode(mh.getChangeTypeCode()) .description(langData != null ? langData.getDescription() : "") .build(); }) .orElse(null); } /** * 선박 위험지표 값 변경 이력 조회 * * @param imoNo IMO 번호 * @param lang 언어 코드 (예: KO, EN) * @return 변경 이력 응답 목록 */ @Transactional(readOnly = true) public List getShipRiskDetailHistory(String imoNo, String lang) { Map fieldNameMap = getRiskFieldNameMap(lang); Map sortOrderMap = getRiskSortOrderMap(); List rows = shipRiskDetailHistoryRepo.findRiskHistoryWithNarrative(imoNo); return rows.stream().map(row -> { String colName = (String) row[3]; return ChangeHistoryResponse.builder() .rowIndex(((Number) row[0]).longValue()) .searchKey((String) row[1]) .lastModifiedDate(row[2] != null ? row[2].toString().substring(0, Math.min(row[2].toString().length(), 19)) : "") .changedColumnName(colName) .beforeValue((String) row[4]) .afterValue((String) row[5]) .narrative((String) row[6]) .prevNarrative((String) row[7]) .fieldName(fieldNameMap.getOrDefault(colName, colName)) .sortOrder(sortOrderMap.getOrDefault(colName, Integer.MAX_VALUE)) .build(); }).toList(); } /** * 선박 제재 값 변경 이력 조회 * * @param imoNo IMO 번호 * @param lang 언어 코드 (예: KO, EN) * @return 변경 이력 응답 목록 */ @Transactional(readOnly = true) public List getShipComplianceHistory(String imoNo, String lang) { Map fieldNameMap = getComplianceFieldNameMap(lang, "SHIP"); Map sortOrderMap = getComplianceSortOrderMap("SHIP"); return shipComplianceHistoryRepo.findByImoNoOrderByLastModifiedDateDesc(imoNo).stream() .map(h -> ChangeHistoryResponse.builder() .rowIndex(h.getRowIndex()) .searchKey(h.getImoNo()) .lastModifiedDate(h.getLastModifiedDate() != null ? h.getLastModifiedDate().format(DT_FORMAT) : "") .changedColumnName(h.getChangedColumnName()) .beforeValue(h.getBeforeValue()) .afterValue(h.getAfterValue()) .fieldName(fieldNameMap.getOrDefault(h.getChangedColumnName(), h.getChangedColumnName())) .sortOrder(sortOrderMap.getOrDefault(h.getChangedColumnName(), Integer.MAX_VALUE)) .build()) .toList(); } /** * 회사 제재 값 변경 이력 조회 * * @param companyCode 회사 코드 * @param lang 언어 코드 (예: KO, EN) * @return 변경 이력 응답 목록 */ @Transactional(readOnly = true) public List getCompanyComplianceHistory(String companyCode, String lang) { Map fieldNameMap = getComplianceFieldNameMap(lang, "COMPANY"); Map sortOrderMap = getComplianceSortOrderMap("COMPANY"); return companyComplianceHistoryRepo.findByCompanyCodeOrderByLastModifiedDateDesc(companyCode).stream() .map(h -> ChangeHistoryResponse.builder() .rowIndex(h.getRowIndex()) .searchKey(h.getCompanyCode()) .lastModifiedDate(h.getLastModifiedDate() != null ? h.getLastModifiedDate().format(DT_FORMAT) : "") .changedColumnName(h.getChangedColumnName()) .beforeValue(h.getBeforeValue()) .afterValue(h.getAfterValue()) .fieldName(fieldNameMap.getOrDefault(h.getChangedColumnName(), h.getChangedColumnName())) .sortOrder(sortOrderMap.getOrDefault(h.getChangedColumnName(), Integer.MAX_VALUE)) .build()) .toList(); } /** * 선박 기본 정보 조회 */ @Transactional(readOnly = true) public ShipInfoResponse getShipInfo(String imoNo) { return shipInfoMasterRepo.findByImoNo(imoNo) .map(s -> ShipInfoResponse.builder() .imoNo(s.getImoNo()) .shipName(s.getShipName()) .shipStatus(s.getShipStatus()) .nationalityCode(s.getNationalityCode()) .nationalityIsoCode(resolveIsoTwoCode(s.getNationalityCode())) .nationality(s.getNationality()) .shipType(s.getShipType()) .dwt(s.getDwt()) .gt(s.getGt()) .buildYear(s.getBuildYear()) .mmsiNo(s.getMmsiNo()) .callSign(s.getCallSign()) .shipTypeGroup(s.getShipTypeGroup()) .build()) .orElse(null); } /** * 회사 기본 정보 조회 */ @Transactional(readOnly = true) public CompanyInfoResponse getCompanyInfo(String companyCode) { return companyDetailInfoRepo.findByCompanyCode(companyCode) .map(c -> { String parentCompanyName = null; if (c.getParentCompanyCode() != null && !c.getParentCompanyCode().isBlank()) { parentCompanyName = companyDetailInfoRepo.findByCompanyCode(c.getParentCompanyCode()) .map(CompanyDetailInfo::getFullName) .orElse("UNKNOWN"); } return CompanyInfoResponse.builder() .companyCode(c.getCompanyCode()) .fullName(c.getFullName()) .abbreviation(c.getAbbreviation()) .status(c.getStatus()) .parentCompanyCode(c.getParentCompanyCode()) .parentCompanyName(parentCompanyName) .registrationCountry(c.getRegistrationCountry()) .registrationCountryCode(c.getRegistrationCountryCode()) .registrationCountryIsoCode(resolveIsoTwoCode(c.getRegistrationCountryCode())) .controlCountry(c.getControlCountry()) .controlCountryCode(c.getControlCountryCode()) .controlCountryIsoCode(resolveIsoTwoCode(c.getControlCountryCode())) .foundedDate(c.getFoundedDate()) .email(c.getEmail()) .phone(c.getPhone()) .website(c.getWebsite()) .build(); }) .orElse(null); } /** * 선박 현재 Risk 지표 상태 조회 */ @Transactional(readOnly = true) public List getShipRiskStatus(String imoNo, String lang) { Map fieldNameMap = getRiskFieldNameMap(lang); Map sortOrderMap = getRiskSortOrderMap(); Map categoryMap = getRiskCategoryMap(lang); Map categoryCodeMap = getRiskCategoryCodeMap(); try { Map row = jdbcTemplate.queryForMap( "SELECT * FROM std_snp_svc.tb_ship_risk_detail_info WHERE imo_no = ?", imoNo); List result = new ArrayList<>(); for (Map.Entry entry : sortOrderMap.entrySet()) { String colName = entry.getKey(); Object codeVal = row.get(colName); String descColName = "ais_up_imo_desc".equals(colName) ? "ais_up_imo_desc_val" : colName + "_desc"; Object descVal = row.get(descColName); result.add(IndicatorStatusResponse.builder() .columnName(colName) .fieldName(fieldNameMap.getOrDefault(colName, colName)) .categoryCode(categoryCodeMap.getOrDefault(colName, "")) .category(categoryMap.getOrDefault(colName, "")) .value(codeVal != null ? codeVal.toString() : null) .narrative(descVal != null ? descVal.toString() : null) .sortOrder(entry.getValue()) .build()); } result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder)); return result; } catch (EmptyResultDataAccessException e) { return List.of(); } } /** * 선박 현재 Compliance 상태 조회 */ @Transactional(readOnly = true) public List getShipComplianceStatus(String imoNo, String lang) { Map fieldNameMap = getComplianceFieldNameMap(lang, "SHIP"); Map sortOrderMap = getComplianceSortOrderMap("SHIP"); Map categoryMap = getComplianceCategoryMap("SHIP", lang); Map categoryCodeMap = getComplianceCategoryCodeMap("SHIP"); try { Map row = jdbcTemplate.queryForMap( "SELECT * FROM std_snp_svc.tb_ship_compliance_info WHERE imo_no = ?", imoNo); List result = new ArrayList<>(); for (Map.Entry entry : sortOrderMap.entrySet()) { String colName = entry.getKey(); Object codeVal = row.get(colName); result.add(IndicatorStatusResponse.builder() .columnName(colName) .fieldName(fieldNameMap.getOrDefault(colName, colName)) .categoryCode(categoryCodeMap.getOrDefault(colName, "")) .category(categoryMap.getOrDefault(colName, "")) .value(codeVal != null ? codeVal.toString() : null) .sortOrder(entry.getValue()) .build()); } result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder)); return result; } catch (EmptyResultDataAccessException e) { return List.of(); } } /** * 회사 현재 Compliance 상태 조회 */ @Transactional(readOnly = true) public List getCompanyComplianceStatus(String companyCode, String lang) { Map fieldNameMap = getComplianceFieldNameMap(lang, "COMPANY"); Map sortOrderMap = getComplianceSortOrderMap("COMPANY"); Map categoryMap = getComplianceCategoryMap("COMPANY", lang); Map categoryCodeMap = getComplianceCategoryCodeMap("COMPANY"); try { Map row = jdbcTemplate.queryForMap( "SELECT * FROM std_snp_svc.tb_company_compliance_info WHERE company_cd = ?", companyCode); List result = new ArrayList<>(); for (Map.Entry entry : sortOrderMap.entrySet()) { String colName = entry.getKey(); Object codeVal = row.get(colName); result.add(IndicatorStatusResponse.builder() .columnName(colName) .fieldName(fieldNameMap.getOrDefault(colName, colName)) .categoryCode(categoryCodeMap.getOrDefault(colName, "")) .category(categoryMap.getOrDefault(colName, "")) .value(codeVal != null ? codeVal.toString() : null) .sortOrder(entry.getValue()) .build()); } result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder)); return result; } catch (EmptyResultDataAccessException e) { return List.of(); } } private String resolveIsoTwoCode(String shipCountryCode) { if (shipCountryCode == null || shipCountryCode.isBlank()) return null; return shipCountryCodeRepo.findByShipCountryCode(shipCountryCode) .map(ShipCountryCode::getIsoTwoCode) .orElse(null); } /** * Risk 지표 컬럼명 → 필드명 매핑 조회 */ private Map getRiskFieldNameMap(String lang) { Map langMap = riskIndicatorLangRepo.findByLangCodeOrderByIndicatorIdAsc(lang).stream() .collect(Collectors.toMap(RiskIndicatorLang::getIndicatorId, RiskIndicatorLang::getFieldName)); return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream() .filter(ri -> ri.getColumnName() != null) .collect(Collectors.toMap( RiskIndicator::getColumnName, ri -> langMap.getOrDefault(ri.getIndicatorId(), ri.getFieldKey()), (a, b) -> a)); } private Map getRiskCategoryMap(String lang) { Map catNameMap = riskCategoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(RiskIndicatorCategoryLang::getCategoryCode, RiskIndicatorCategoryLang::getCategoryName)); return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream() .filter(ri -> ri.getColumnName() != null) .collect(Collectors.toMap( RiskIndicator::getColumnName, ri -> catNameMap.getOrDefault(ri.getCategoryCode(), ri.getCategoryCode()), (a, b) -> a)); } private Map getRiskCategoryCodeMap() { return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream() .filter(ri -> ri.getColumnName() != null) .collect(Collectors.toMap( RiskIndicator::getColumnName, RiskIndicator::getCategoryCode, (a, b) -> a)); } private Map getRiskSortOrderMap() { return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream() .filter(ri -> ri.getColumnName() != null) .collect(Collectors.toMap( RiskIndicator::getColumnName, RiskIndicator::getSortOrder, (a, b) -> a)); } /** * Compliance 지표 컬럼명 → 필드명 매핑 조회 */ private Map getComplianceFieldNameMap(String lang, String type) { Map langMap = complianceIndicatorLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(ComplianceIndicatorLang::getIndicatorId, ComplianceIndicatorLang::getFieldName)); List indicators = (type != null && !type.isBlank()) ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); return indicators.stream() .filter(ci -> ci.getColumnName() != null) .collect(Collectors.toMap( ComplianceIndicator::getColumnName, ci -> langMap.getOrDefault(ci.getIndicatorId(), ci.getFieldKey()), (a, b) -> a)); } private Map getComplianceCategoryMap(String type, String lang) { Map catNameMap = complianceCategoryLangRepo.findByLangCode(lang).stream() .collect(Collectors.toMap(ComplianceCategoryLang::getCategoryCode, ComplianceCategoryLang::getCategoryName)); List indicators = (type != null && !type.isBlank()) ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); return indicators.stream() .filter(ci -> ci.getColumnName() != null) .collect(Collectors.toMap( ComplianceIndicator::getColumnName, ci -> catNameMap.getOrDefault(ci.getCategoryCode(), ci.getCategoryCode()), (a, b) -> a)); } private Map getComplianceCategoryCodeMap(String type) { List indicators = (type != null && !type.isBlank()) ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); return indicators.stream() .filter(ci -> ci.getColumnName() != null) .collect(Collectors.toMap( ComplianceIndicator::getColumnName, ComplianceIndicator::getCategoryCode, (a, b) -> a)); } private Map getComplianceSortOrderMap(String type) { List indicators = (type != null && !type.isBlank()) ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); return indicators.stream() .filter(ci -> ci.getColumnName() != null) .collect(Collectors.toMap( ComplianceIndicator::getColumnName, ComplianceIndicator::getSortOrder, (a, b) -> a)); } }