feat: 코드 도메인·캐시 서비스·조회 API 추가
CodeService가 기동 후 첫 조회에 코드를 한 번 읽어 메모리에 들고 있는다. 지연 로딩인 이유는 Flyway 마이그레이션보다 빈 초기화가 먼저 돌 수 있기 때문이다. 활성 목록과 전체 목록을 나눠 들고 있는다. 화면 선택지에는 활성만 나가지만, 이미 저장된 과거 값의 라벨을 그리려면 비활성도 조회돼야 한다. GET /api/meta/codes는 아직 소비자가 없다. 화면 전환은 Phase 2다. Co-Authored-By: Claude Opus 5 (1M context)
@bc077ed9588946d201348cc9f6ddfad3acfd53b9
+++ src/main/java/kr/itn/itnhub/code/Code.java
... | ... | @@ -0,0 +1,24 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import java.util.Map; | |
| 4 | + | |
| 5 | +/** attrs까지 파싱된 코드. 화면과 서비스가 쓰는 형태다. */ | |
| 6 | +public record Code( | |
| 7 | + String groupId, | |
| 8 | + String code, | |
| 9 | + String label, | |
| 10 | + int sortOrder, | |
| 11 | + boolean active, | |
| 12 | + Map<String, Object> attrs) { | |
| 13 | + | |
| 14 | + /** attrs의 문자열 속성. 없으면 null. */ | |
| 15 | + public String attr(String key) { | |
| 16 | + Object value = attrs.get(key); | |
| 17 | + return value == null ? null : String.valueOf(value); | |
| 18 | + } | |
| 19 | + | |
| 20 | + /** attrs의 불리언 속성. 없으면 false. */ | |
| 21 | + public boolean flag(String key) { | |
| 22 | + return Boolean.TRUE.equals(attrs.get(key)); | |
| 23 | + } | |
| 24 | +} |
+++ src/main/java/kr/itn/itnhub/code/CodeController.java
... | ... | @@ -0,0 +1,26 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 4 | +import org.springframework.web.bind.annotation.RestController; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | +import java.util.Map; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 프론트가 로그인 직후 한 번 불러 모든 선택지를 받아가는 엔드포인트. 화면마다 따로 부르지 | |
| 11 | + * 않는 이유는 코드가 자주 안 바뀌고, 화면이 옵션을 기다리느라 깜빡이는 것을 피하기 위해서다. | |
| 12 | + */ | |
| 13 | +@RestController | |
| 14 | +public class CodeController { | |
| 15 | + | |
| 16 | + private final CodeService codeService; | |
| 17 | + | |
| 18 | + public CodeController(CodeService codeService) { | |
| 19 | + this.codeService = codeService; | |
| 20 | + } | |
| 21 | + | |
| 22 | + @GetMapping("/api/meta/codes") | |
| 23 | + public Map<String, List<Code>> codes() { | |
| 24 | + return codeService.all(); | |
| 25 | + } | |
| 26 | +} |
+++ src/main/java/kr/itn/itnhub/code/CodeGroup.java
... | ... | @@ -0,0 +1,12 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 코드 그룹. {@code editable}이 false면 관리 화면에서 코드를 추가·삭제할 수 없다 - | |
| 5 | + * 진행단계처럼 로직과 강하게 묶인 그룹은 표시명·순서만 바꾸게 한다. | |
| 6 | + */ | |
| 7 | +public record CodeGroup( | |
| 8 | + String groupId, | |
| 9 | + String groupName, | |
| 10 | + String description, | |
| 11 | + boolean editable) { | |
| 12 | +} |
+++ src/main/java/kr/itn/itnhub/code/CodeMapper.java
... | ... | @@ -0,0 +1,14 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import org.apache.ibatis.annotations.Mapper; | |
| 4 | + | |
| 5 | +import java.util.List; | |
| 6 | + | |
| 7 | +@Mapper | |
| 8 | +public interface CodeMapper { | |
| 9 | + | |
| 10 | + /** 비활성 코드까지 전부. 활성 필터는 {@link CodeService}가 용도에 따라 건다. */ | |
| 11 | + List<CodeRow> findAllCodes(); | |
| 12 | + | |
| 13 | + List<CodeGroup> findAllGroups(); | |
| 14 | +} |
+++ src/main/java/kr/itn/itnhub/code/CodeRow.java
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * DB에서 그대로 읽은 코드 한 줄. attrs는 jsonb를 문자열로 받는다 - 커스텀 TypeHandler를 | |
| 5 | + * 만들면 매퍼마다 등록을 신경써야 하는데, 코드는 기동 후 한 번만 읽으므로 | |
| 6 | + * {@link CodeService}가 그때 한 번 파싱하는 편이 단순하다. | |
| 7 | + */ | |
| 8 | +public record CodeRow( | |
| 9 | + String groupId, | |
| 10 | + String code, | |
| 11 | + String label, | |
| 12 | + int sortOrder, | |
| 13 | + boolean active, | |
| 14 | + String attrsJson) { | |
| 15 | +} |
+++ src/main/java/kr/itn/itnhub/code/CodeService.java
... | ... | @@ -0,0 +1,115 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.core.type.TypeReference; | |
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 5 | +import org.slf4j.Logger; | |
| 6 | +import org.slf4j.LoggerFactory; | |
| 7 | +import org.springframework.stereotype.Service; | |
| 8 | + | |
| 9 | +import java.util.ArrayList; | |
| 10 | +import java.util.Collections; | |
| 11 | +import java.util.LinkedHashMap; | |
| 12 | +import java.util.List; | |
| 13 | +import java.util.Map; | |
| 14 | +import java.util.Optional; | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * 코드값을 기동 후 첫 조회에 한 번 읽어 메모리에 들고 있는다. 코드는 관리 화면에서 가끔 | |
| 18 | + * 바뀔 뿐이라 매 요청 조회가 낭비이고, 단일 인스턴스 운영이라 분산 캐시는 필요 없다. | |
| 19 | + * | |
| 20 | + * <p>지연 로딩을 쓰는 이유는 Flyway 마이그레이션이 끝나기 전에 빈 초기화가 돌 수 있기 | |
| 21 | + * 때문이다. 첫 조회 시점에는 마이그레이션이 반드시 끝나 있다.</p> | |
| 22 | + */ | |
| 23 | +@Service | |
| 24 | +public class CodeService { | |
| 25 | + | |
| 26 | + private static final Logger log = LoggerFactory.getLogger(CodeService.class); | |
| 27 | + | |
| 28 | + private final CodeMapper mapper; | |
| 29 | + private final ObjectMapper json = new ObjectMapper(); | |
| 30 | + | |
| 31 | + /** 그룹ID -> 활성 코드(정렬순). 화면 선택지가 여기서 나간다. */ | |
| 32 | + private volatile Map<String, List<Code>> activeByGroup; | |
| 33 | + /** 그룹ID -> 코드ID -> 코드(비활성 포함). 과거 데이터를 화면에 그릴 때 라벨이 필요하다. */ | |
| 34 | + private volatile Map<String, Map<String, Code>> allByGroup; | |
| 35 | + | |
| 36 | + public CodeService(CodeMapper mapper) { | |
| 37 | + this.mapper = mapper; | |
| 38 | + } | |
| 39 | + | |
| 40 | + /** 관리 화면에서 코드를 고친 뒤 부른다. */ | |
| 41 | + public synchronized void reload() { | |
| 42 | + Map<String, List<Code>> active = new LinkedHashMap<>(); | |
| 43 | + Map<String, Map<String, Code>> all = new LinkedHashMap<>(); | |
| 44 | + | |
| 45 | + for (CodeRow row : mapper.findAllCodes()) { | |
| 46 | + Code code = toCode(row); | |
| 47 | + all.computeIfAbsent(code.groupId(), key -> new LinkedHashMap<>()) | |
| 48 | + .put(code.code(), code); | |
| 49 | + if (code.active()) { | |
| 50 | + active.computeIfAbsent(code.groupId(), key -> new ArrayList<>()).add(code); | |
| 51 | + } | |
| 52 | + } | |
| 53 | + | |
| 54 | + this.activeByGroup = active; | |
| 55 | + this.allByGroup = all; | |
| 56 | + } | |
| 57 | + | |
| 58 | + private Code toCode(CodeRow row) { | |
| 59 | + Map<String, Object> attrs; | |
| 60 | + try { | |
| 61 | + attrs = json.readValue(row.attrsJson(), new TypeReference<>() { | |
| 62 | + }); | |
| 63 | + } catch (Exception e) { | |
| 64 | + // 잘못된 JSON 하나가 전체 코드 로딩을 막지는 않게 한다. 해당 코드만 속성이 빈다. | |
| 65 | + log.warn("코드 attrs 파싱 실패 - {}:{} 의 속성을 비운다", row.groupId(), row.code(), e); | |
| 66 | + attrs = Map.of(); | |
| 67 | + } | |
| 68 | + return new Code(row.groupId(), row.code(), row.label(), row.sortOrder(), row.active(), attrs); | |
| 69 | + } | |
| 70 | + | |
| 71 | + private void ensureLoaded() { | |
| 72 | + if (activeByGroup == null) { | |
| 73 | + reload(); | |
| 74 | + } | |
| 75 | + } | |
| 76 | + | |
| 77 | + /** 활성 코드만, 그룹별 정렬순. */ | |
| 78 | + public Map<String, List<Code>> all() { | |
| 79 | + ensureLoaded(); | |
| 80 | + return activeByGroup; | |
| 81 | + } | |
| 82 | + | |
| 83 | + /** 활성 코드만. 없는 그룹이면 빈 목록. */ | |
| 84 | + public List<Code> group(String groupId) { | |
| 85 | + ensureLoaded(); | |
| 86 | + return activeByGroup.getOrDefault(groupId, List.of()); | |
| 87 | + } | |
| 88 | + | |
| 89 | + /** 비활성 코드도 찾는다 - 과거 데이터의 라벨을 그려야 하기 때문이다. */ | |
| 90 | + public Optional<Code> find(String groupId, String code) { | |
| 91 | + ensureLoaded(); | |
| 92 | + return Optional.ofNullable(allByGroup.getOrDefault(groupId, Map.of()).get(code)); | |
| 93 | + } | |
| 94 | + | |
| 95 | + /** attrs.scopes에 해당 자리가 든 활성 코드만. 공공누리 유형이 이걸 쓴다. */ | |
| 96 | + public List<Code> scoped(String groupId, String scope) { | |
| 97 | + return group(groupId).stream().filter(code -> scopes(code).contains(scope)).toList(); | |
| 98 | + } | |
| 99 | + | |
| 100 | + @SuppressWarnings("unchecked") | |
| 101 | + private List<String> scopes(Code code) { | |
| 102 | + Object raw = code.attrs().get(Codes.ATTR_SCOPES); | |
| 103 | + return raw instanceof List<?> list ? (List<String>) list : Collections.emptyList(); | |
| 104 | + } | |
| 105 | + | |
| 106 | + /** 저장 요청 값이 이 그룹의 활성 코드인지. */ | |
| 107 | + public boolean isValid(String groupId, String code) { | |
| 108 | + return group(groupId).stream().anyMatch(candidate -> candidate.code().equals(code)); | |
| 109 | + } | |
| 110 | + | |
| 111 | + /** 코드 관리 화면이 쓰는 그룹 메타. 캐시하지 않는다 - 호출 빈도가 낮다. */ | |
| 112 | + public List<CodeGroup> groups() { | |
| 113 | + return mapper.findAllGroups(); | |
| 114 | + } | |
| 115 | +} |
+++ src/main/java/kr/itn/itnhub/code/Codes.java
... | ... | @@ -0,0 +1,39 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 코드 그룹 ID와, 로직이 직접 참조해야 하는 attrs 키·scope 상수. 소스에 한글 리터럴을 | |
| 5 | + * 흩뿌리지 않기 위한 단일 지점이다. | |
| 6 | + * | |
| 7 | + * <p>화면에 그리는 선택지 자체는 여기가 아니라 DB(code 테이블)에서 온다. 여기 있는 것은 | |
| 8 | + * "코드체계를 가리키는 이름"뿐이고 "코드값 목록"이 아니다.</p> | |
| 9 | + */ | |
| 10 | +public final class Codes { | |
| 11 | + | |
| 12 | + private Codes() { | |
| 13 | + } | |
| 14 | + | |
| 15 | + public static final String STAGE = "STAGE"; | |
| 16 | + public static final String STAGE_GROUP = "STAGE_GROUP"; | |
| 17 | + public static final String REVIEW_MAJOR = "REVIEW_MAJOR"; | |
| 18 | + public static final String REVIEW_MINOR = "REVIEW_MINOR"; | |
| 19 | + public static final String REVIEW_RESULT = "REVIEW_RESULT"; | |
| 20 | + public static final String KOGL_TYPE = "KOGL_TYPE"; | |
| 21 | + public static final String PROCESS_STATUS = "PROCESS_STATUS"; | |
| 22 | + public static final String CONTRACT_DOC = "CONTRACT_DOC"; | |
| 23 | + public static final String CONTACT_METHOD = "CONTACT_METHOD"; | |
| 24 | + public static final String CONTACT_CATEGORY = "CONTACT_CATEGORY"; | |
| 25 | + public static final String ATTACHMENT_YN = "ATTACHMENT_YN"; | |
| 26 | + public static final String KOGL_ATTACHED = "KOGL_ATTACHED"; | |
| 27 | + | |
| 28 | + /** 공공누리 유형이 쓰이는 자리. 같은 코드체계라도 자리마다 노출 목록이 다르다. */ | |
| 29 | + public static final String SCOPE_REVIEW = "review"; | |
| 30 | + public static final String SCOPE_PROCESS = "process"; | |
| 31 | + public static final String SCOPE_PRIOR = "prior"; | |
| 32 | + | |
| 33 | + /** attrs 키. */ | |
| 34 | + public static final String ATTR_SCOPES = "scopes"; | |
| 35 | + public static final String ATTR_PROGRESS = "progress"; | |
| 36 | + public static final String ATTR_KPI_FROM = "kpiFrom"; | |
| 37 | + public static final String ATTR_GROUP_KEY = "groupKey"; | |
| 38 | + public static final String ATTR_DONE = "done"; | |
| 39 | +} |
+++ src/main/resources/mapper/CodeMapper.xml
... | ... | @@ -0,0 +1,22 @@ |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" | |
| 3 | + "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |
| 4 | +<mapper namespace="kr.itn.itnhub.code.CodeMapper"> | |
| 5 | + | |
| 6 | + <!-- | |
| 7 | + attrs는 jsonb지만 ::text로 문자열로 받는다. 커스텀 TypeHandler를 두면 매퍼마다 | |
| 8 | + 등록을 신경써야 하는데, 코드는 기동 후 한 번만 읽으므로 서비스에서 파싱하는 편이 단순하다. | |
| 9 | + --> | |
| 10 | + <select id="findAllCodes" resultType="kr.itn.itnhub.code.CodeRow"> | |
| 11 | + select group_id, code, label, sort_order, active, attrs::text as attrs_json | |
| 12 | + from code | |
| 13 | + order by group_id, sort_order | |
| 14 | + </select> | |
| 15 | + | |
| 16 | + <select id="findAllGroups" resultType="kr.itn.itnhub.code.CodeGroup"> | |
| 17 | + select group_id, group_name, description, editable | |
| 18 | + from code_group | |
| 19 | + order by group_id | |
| 20 | + </select> | |
| 21 | + | |
| 22 | +</mapper> |
+++ src/test/java/kr/itn/itnhub/code/CodeControllerTest.java
... | ... | @@ -0,0 +1,44 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import kr.itn.itnhub.mattermost.MattermostClient; | |
| 5 | +import org.junit.jupiter.api.Test; | |
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | |
| 8 | +import org.springframework.boot.test.mock.mockito.MockBean; | |
| 9 | +import org.springframework.security.test.context.support.WithMockUser; | |
| 10 | +import org.springframework.test.web.servlet.MockMvc; | |
| 11 | + | |
| 12 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | |
| 13 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | |
| 14 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | |
| 15 | + | |
| 16 | +@AutoConfigureMockMvc | |
| 17 | +class CodeControllerTest extends AbstractDbTest { | |
| 18 | + | |
| 19 | + @Autowired | |
| 20 | + MockMvc mvc; | |
| 21 | + | |
| 22 | + @MockBean | |
| 23 | + MattermostClient mattermost; | |
| 24 | + | |
| 25 | + @Test | |
| 26 | + @WithMockUser(roles = "ADMIN") | |
| 27 | + void 전체_코드를_그룹별로_내려준다() throws Exception { | |
| 28 | + mvc.perform(get("/api/meta/codes")) | |
| 29 | + .andExpect(status().isOk()) | |
| 30 | + .andExpect(jsonPath("$.STAGE.length()").value(13)) | |
| 31 | + .andExpect(jsonPath("$.STAGE[12].code").value("13")) | |
| 32 | + .andExpect(jsonPath("$.STAGE[12].label").value("보고서 제출 완료")) | |
| 33 | + .andExpect(jsonPath("$.STAGE[12].attrs.progress").value("done")) | |
| 34 | + .andExpect(jsonPath("$.KOGL_TYPE.length()").value(7)) | |
| 35 | + .andExpect(jsonPath("$.CONTACT_CATEGORY[0].code").value("APPLICANT")) | |
| 36 | + .andExpect(jsonPath("$.CONTACT_CATEGORY[0].label").value("신청기관")); | |
| 37 | + } | |
| 38 | + | |
| 39 | + @Test | |
| 40 | + void 인증이_없으면_401이다() throws Exception { | |
| 41 | + mvc.perform(get("/api/meta/codes")) | |
| 42 | + .andExpect(status().isUnauthorized()); | |
| 43 | + } | |
| 44 | +} |
+++ src/test/java/kr/itn/itnhub/code/CodeMapperTest.java
... | ... | @@ -0,0 +1,63 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import org.junit.jupiter.api.Test; | |
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | + | |
| 9 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 10 | + | |
| 11 | +class CodeMapperTest extends AbstractDbTest { | |
| 12 | + | |
| 13 | + @Autowired | |
| 14 | + CodeMapper mapper; | |
| 15 | + | |
| 16 | + @Test | |
| 17 | + void 모든_코드를_그룹과_정렬순서대로_읽는다() { | |
| 18 | + List<CodeRow> rows = mapper.findAllCodes(); | |
| 19 | + | |
| 20 | + assertThat(rows).isNotEmpty(); | |
| 21 | + List<CodeRow> stages = rows.stream().filter(r -> r.groupId().equals("STAGE")).toList(); | |
| 22 | + assertThat(stages).hasSize(13); | |
| 23 | + assertThat(stages.get(0).code()).isEqualTo("1"); | |
| 24 | + assertThat(stages.get(12).label()).isEqualTo("보고서 제출 완료"); | |
| 25 | + } | |
| 26 | + | |
| 27 | + @Test | |
| 28 | + void attrs를_JSON_문자열로_읽는다() { | |
| 29 | + CodeRow stage13 = mapper.findAllCodes().stream() | |
| 30 | + .filter(r -> r.groupId().equals("STAGE") && r.code().equals("13")) | |
| 31 | + .findFirst().orElseThrow(); | |
| 32 | + | |
| 33 | + assertThat(stage13.attrsJson()) | |
| 34 | + .contains("progress") | |
| 35 | + .contains("done") | |
| 36 | + .contains("completed"); | |
| 37 | + } | |
| 38 | + | |
| 39 | + @Test | |
| 40 | + void 비활성_코드도_함께_읽는다() { | |
| 41 | + // 활성 필터는 CodeService가 용도에 따라 건다. 매퍼는 전부 내려줘야 | |
| 42 | + // 과거 데이터의 라벨을 그릴 수 있다. | |
| 43 | + List<CodeRow> rows = mapper.findAllCodes(); | |
| 44 | + | |
| 45 | + assertThat(rows).allSatisfy(r -> assertThat(r.groupId()).isNotBlank()); | |
| 46 | + assertThat(rows).anySatisfy(r -> assertThat(r.active()).isTrue()); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @Test | |
| 50 | + void 그룹_목록을_읽는다() { | |
| 51 | + List<CodeGroup> groups = mapper.findAllGroups(); | |
| 52 | + | |
| 53 | + assertThat(groups).hasSize(12); | |
| 54 | + assertThat(groups).anySatisfy(g -> { | |
| 55 | + assertThat(g.groupId()).isEqualTo("STAGE"); | |
| 56 | + assertThat(g.editable()).isFalse(); | |
| 57 | + }); | |
| 58 | + assertThat(groups).anySatisfy(g -> { | |
| 59 | + assertThat(g.groupId()).isEqualTo("CONTACT_METHOD"); | |
| 60 | + assertThat(g.editable()).isTrue(); | |
| 61 | + }); | |
| 62 | + } | |
| 63 | +} |
+++ src/test/java/kr/itn/itnhub/code/CodeServiceTest.java
... | ... | @@ -0,0 +1,119 @@ |
| 1 | +package kr.itn.itnhub.code; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import org.junit.jupiter.api.AfterEach; | |
| 5 | +import org.junit.jupiter.api.Test; | |
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 8 | + | |
| 9 | +import java.util.List; | |
| 10 | + | |
| 11 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 12 | + | |
| 13 | +class CodeServiceTest extends AbstractDbTest { | |
| 14 | + | |
| 15 | + @Autowired | |
| 16 | + CodeService service; | |
| 17 | + | |
| 18 | + @Autowired | |
| 19 | + JdbcTemplate jdbc; | |
| 20 | + | |
| 21 | + @AfterEach | |
| 22 | + void restore() { | |
| 23 | + jdbc.update("update code set active = true where group_id = 'CONTACT_METHOD'"); | |
| 24 | + service.reload(); | |
| 25 | + } | |
| 26 | + | |
| 27 | + @Test | |
| 28 | + void 그룹을_정렬순서대로_돌려준다() { | |
| 29 | + List<Code> stages = service.group(Codes.STAGE); | |
| 30 | + | |
| 31 | + assertThat(stages).hasSize(13); | |
| 32 | + assertThat(stages.get(0).code()).isEqualTo("1"); | |
| 33 | + assertThat(stages.get(12).label()).isEqualTo("보고서 제출 완료"); | |
| 34 | + } | |
| 35 | + | |
| 36 | + @Test | |
| 37 | + void attrs가_맵으로_파싱된다() { | |
| 38 | + Code stage13 = service.find(Codes.STAGE, "13").orElseThrow(); | |
| 39 | + | |
| 40 | + assertThat(stage13.attr("progress")).isEqualTo("done"); | |
| 41 | + assertThat(stage13.attr("kpiFrom")).isEqualTo("completed"); | |
| 42 | + assertThat(stage13.attr("groupKey")).isEqualTo("closing"); | |
| 43 | + assertThat(stage13.attr("없는속성")).isNull(); | |
| 44 | + } | |
| 45 | + | |
| 46 | + @Test | |
| 47 | + void 불리언_속성을_읽는다() { | |
| 48 | + assertThat(service.find(Codes.PROCESS_STATUS, "처리완료").orElseThrow().flag("done")).isTrue(); | |
| 49 | + assertThat(service.find(Codes.PROCESS_STATUS, "미처리").orElseThrow().flag("done")).isFalse(); | |
| 50 | + // 속성이 아예 없는 코드도 false여야 한다. | |
| 51 | + assertThat(service.find(Codes.CONTRACT_DOC, "공문").orElseThrow().flag("freeText")).isFalse(); | |
| 52 | + assertThat(service.find(Codes.CONTRACT_DOC, "기타").orElseThrow().flag("freeText")).isTrue(); | |
| 53 | + } | |
| 54 | + | |
| 55 | + @Test | |
| 56 | + void 사용처별로_공공누리_유형을_거른다() { | |
| 57 | + assertThat(service.scoped(Codes.KOGL_TYPE, Codes.SCOPE_REVIEW)).hasSize(7); | |
| 58 | + assertThat(service.scoped(Codes.KOGL_TYPE, Codes.SCOPE_PROCESS)) | |
| 59 | + .hasSize(6) | |
| 60 | + .noneMatch(c -> c.code().equals("보류")); | |
| 61 | + assertThat(service.scoped(Codes.KOGL_TYPE, Codes.SCOPE_PRIOR)) | |
| 62 | + .extracting(Code::code) | |
| 63 | + .containsExactly("0유형", "1유형", "2유형", "3유형", "4유형"); | |
| 64 | + } | |
| 65 | + | |
| 66 | + @Test | |
| 67 | + void scopes가_없는_그룹은_빈_결과다() { | |
| 68 | + assertThat(service.scoped(Codes.CONTACT_METHOD, Codes.SCOPE_REVIEW)).isEmpty(); | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Test | |
| 72 | + void 비활성_코드는_목록에서_빠지지만_조회는_된다() { | |
| 73 | + jdbc.update("update code set active = false " | |
| 74 | + + "where group_id = 'CONTACT_METHOD' and code = '방문'"); | |
| 75 | + service.reload(); | |
| 76 | + | |
| 77 | + assertThat(service.group(Codes.CONTACT_METHOD)) | |
| 78 | + .extracting(Code::code) | |
| 79 | + .containsExactly("전화", "메일", "기타"); | |
| 80 | + // 이미 '방문'으로 저장된 과거 데이터를 화면에 그리려면 조회는 되어야 한다. | |
| 81 | + assertThat(service.find(Codes.CONTACT_METHOD, "방문")).isPresent(); | |
| 82 | + } | |
| 83 | + | |
| 84 | + @Test | |
| 85 | + void 유효성_검사는_활성_코드만_통과시킨다() { | |
| 86 | + assertThat(service.isValid(Codes.PROCESS_STATUS, "처리완료")).isTrue(); | |
| 87 | + assertThat(service.isValid(Codes.PROCESS_STATUS, "없는값")).isFalse(); | |
| 88 | + | |
| 89 | + jdbc.update("update code set active = false " | |
| 90 | + + "where group_id = 'CONTACT_METHOD' and code = '방문'"); | |
| 91 | + service.reload(); | |
| 92 | + | |
| 93 | + assertThat(service.isValid(Codes.CONTACT_METHOD, "방문")).isFalse(); | |
| 94 | + } | |
| 95 | + | |
| 96 | + @Test | |
| 97 | + void 전체_조회는_활성_코드만_그룹별로_담는다() { | |
| 98 | + assertThat(service.all()) | |
| 99 | + .containsKeys(Codes.STAGE, Codes.KOGL_TYPE, Codes.CONTACT_CATEGORY) | |
| 100 | + .hasSize(12); | |
| 101 | + } | |
| 102 | + | |
| 103 | + @Test | |
| 104 | + void 없는_그룹은_빈_목록이고_없는_코드는_빈_결과다() { | |
| 105 | + assertThat(service.group("NO_SUCH_GROUP")).isEmpty(); | |
| 106 | + assertThat(service.find("NO_SUCH_GROUP", "A")).isEmpty(); | |
| 107 | + assertThat(service.find(Codes.STAGE, "999")).isEmpty(); | |
| 108 | + } | |
| 109 | + | |
| 110 | + @Test | |
| 111 | + void 그룹_메타를_돌려준다() { | |
| 112 | + assertThat(service.groups()) | |
| 113 | + .hasSize(12) | |
| 114 | + .anySatisfy(g -> { | |
| 115 | + assertThat(g.groupId()).isEqualTo("STAGE"); | |
| 116 | + assertThat(g.groupName()).isEqualTo("진행단계"); | |
| 117 | + }); | |
| 118 | + } | |
| 119 | +} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?