package kr.itn.itnhub.code; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; /** * 코드값을 기동 후 첫 조회에 한 번 읽어 메모리에 들고 있는다. 코드는 관리 화면에서 가끔 * 바뀔 뿐이라 매 요청 조회가 낭비이고, 단일 인스턴스 운영이라 분산 캐시는 필요 없다. * *

지연 로딩을 쓰는 이유는 Flyway 마이그레이션이 끝나기 전에 빈 초기화가 돌 수 있기 * 때문이다. 첫 조회 시점에는 마이그레이션이 반드시 끝나 있다.

*/ @Service public class CodeService { private static final Logger log = LoggerFactory.getLogger(CodeService.class); private final CodeMapper mapper; private final ObjectMapper json = new ObjectMapper(); /** 그룹ID -> 활성 코드(정렬순). 화면 선택지가 여기서 나간다. */ private volatile Map> activeByGroup; /** 그룹ID -> 코드ID -> 코드(비활성 포함). 과거 데이터를 화면에 그릴 때 라벨이 필요하다. */ private volatile Map> allByGroup; public CodeService(CodeMapper mapper) { this.mapper = mapper; } /** 관리 화면에서 코드를 고친 뒤 부른다. */ public synchronized void reload() { Map> active = new LinkedHashMap<>(); Map> all = new LinkedHashMap<>(); for (CodeRow row : mapper.findAllCodes()) { Code code = toCode(row); all.computeIfAbsent(code.groupId(), key -> new LinkedHashMap<>()) .put(code.code(), code); if (code.active()) { active.computeIfAbsent(code.groupId(), key -> new ArrayList<>()).add(code); } } this.activeByGroup = active; this.allByGroup = all; } private Code toCode(CodeRow row) { Map attrs; try { attrs = json.readValue(row.attrsJson(), new TypeReference<>() { }); } catch (Exception e) { // 잘못된 JSON 하나가 전체 코드 로딩을 막지는 않게 한다. 해당 코드만 속성이 빈다. log.warn("코드 attrs 파싱 실패 - {}:{} 의 속성을 비운다", row.groupId(), row.code(), e); attrs = Map.of(); } return new Code(row.groupId(), row.code(), row.label(), row.sortOrder(), row.active(), attrs); } private void ensureLoaded() { if (activeByGroup == null) { reload(); } } /** 활성 코드만, 그룹별 정렬순. */ public Map> all() { ensureLoaded(); return activeByGroup; } /** 활성 코드만. 없는 그룹이면 빈 목록. */ public List group(String groupId) { ensureLoaded(); return activeByGroup.getOrDefault(groupId, List.of()); } /** 비활성 코드도 찾는다 - 과거 데이터의 라벨을 그려야 하기 때문이다. */ public Optional find(String groupId, String code) { ensureLoaded(); return Optional.ofNullable(allByGroup.getOrDefault(groupId, Map.of()).get(code)); } /** attrs.scopes에 해당 자리가 든 활성 코드만. 공공누리 유형이 이걸 쓴다. */ public List scoped(String groupId, String scope) { return group(groupId).stream().filter(code -> scopes(code).contains(scope)).toList(); } @SuppressWarnings("unchecked") private List scopes(Code code) { Object raw = code.attrs().get(Codes.ATTR_SCOPES); return raw instanceof List list ? (List) list : Collections.emptyList(); } /** 저장 요청 값이 이 그룹의 활성 코드인지. */ public boolean isValid(String groupId, String code) { return group(groupId).stream().anyMatch(candidate -> candidate.code().equals(code)); } /** 코드 관리 화면이 쓰는 그룹 메타. 캐시하지 않는다 - 호출 빈도가 낮다. */ public List groups() { return mapper.findAllGroups(); } }