feat: 기존 데이터와 코드테이블 정합성 점검
운영 DB에는 정책 변경 전 값이나 공백만 다른 값이 남아 있을 수 있다. Phase 2에서 화면이 코드테이블만 보게 되면 그런 값은 선택지에서 조용히 사라지므로, 배포 전에 찾아내 active=false 코드로 보존해야 한다. Phase 1 회귀 확인: 백엔드 255건(기존 221 + 신규 34), 프론트 191건 전부 통과. 기존 동작은 바뀌지 않았다. Co-Authored-By: Claude Opus 5 (1M context)
@f7e17eaf80cd42e083dc161e008768c2b6fcc57c
+++ src/main/resources/db/consistency/check-orphan-codes.sql
... | ... | @@ -0,0 +1,76 @@ |
| 1 | +-- 운영 DB에 Phase 2(화면이 코드테이블만 보게 되는 단계)를 배포하기 전에 실행한다. | |
| 2 | +-- | |
| 3 | +-- 결과가 한 줄이라도 나오면, 그 값을 active=false 코드로 code 테이블에 추가한 뒤 | |
| 4 | +-- 배포해야 한다. 값을 지우거나 다른 값으로 바꾸지 않는 이유는 이미 그 값으로 저장된 | |
| 5 | +-- 데이터가 있어서, 지우면 기존 화면에서 값이 조용히 사라지기 때문이다. | |
| 6 | +-- | |
| 7 | +-- 실행 예: | |
| 8 | +-- psql -h 192.168.0.60 -U itnhub -d itnhub -f check-orphan-codes.sql | |
| 9 | +-- | |
| 10 | +-- 고아값을 발견했을 때의 보존 예: | |
| 11 | +-- insert into code (group_id, code, label, sort_order, active) | |
| 12 | +-- values ('CONTACT_METHOD', '문자', '문자', 90, false); | |
| 13 | + | |
| 14 | +select 'review_item.review_major' as source, review_major as value, count(*) as cnt | |
| 15 | + from review_item | |
| 16 | + where review_major is not null and review_major <> '' | |
| 17 | + and review_major not in (select code from code where group_id = 'REVIEW_MAJOR') | |
| 18 | + group by review_major | |
| 19 | + | |
| 20 | +union all | |
| 21 | +select 'review_item.review_minor', review_minor, count(*) | |
| 22 | + from review_item | |
| 23 | + where review_minor is not null and review_minor <> '' | |
| 24 | + and review_minor not in (select code from code where group_id = 'REVIEW_MINOR') | |
| 25 | + group by review_minor | |
| 26 | + | |
| 27 | +union all | |
| 28 | +select 'review_item.review_result', review_result, count(*) | |
| 29 | + from review_item | |
| 30 | + where review_result is not null and review_result <> '' | |
| 31 | + and review_result not in (select code from code where group_id = 'REVIEW_RESULT') | |
| 32 | + group by review_result | |
| 33 | + | |
| 34 | +union all | |
| 35 | +select 'review_item.judged_kogl_type', judged_kogl_type, count(*) | |
| 36 | + from review_item | |
| 37 | + where judged_kogl_type is not null and judged_kogl_type <> '' | |
| 38 | + and judged_kogl_type not in (select code from code where group_id = 'KOGL_TYPE') | |
| 39 | + group by judged_kogl_type | |
| 40 | + | |
| 41 | +union all | |
| 42 | +select 'review_item.kogl_type', kogl_type, count(*) | |
| 43 | + from review_item | |
| 44 | + where kogl_type is not null and kogl_type <> '' | |
| 45 | + and kogl_type not in (select code from code where group_id = 'KOGL_TYPE') | |
| 46 | + group by kogl_type | |
| 47 | + | |
| 48 | +union all | |
| 49 | +select 'process_item.process_status', process_status, count(*) | |
| 50 | + from process_item | |
| 51 | + where process_status is not null and process_status <> '' | |
| 52 | + and process_status not in (select code from code where group_id = 'PROCESS_STATUS') | |
| 53 | + group by process_status | |
| 54 | + | |
| 55 | +union all | |
| 56 | +select 'process_item.judged_kogl_type', judged_kogl_type, count(*) | |
| 57 | + from process_item | |
| 58 | + where judged_kogl_type is not null and judged_kogl_type <> '' | |
| 59 | + and judged_kogl_type not in (select code from code where group_id = 'KOGL_TYPE') | |
| 60 | + group by judged_kogl_type | |
| 61 | + | |
| 62 | +union all | |
| 63 | +select 'process_item.prior_kogl_type', prior_kogl_type, count(*) | |
| 64 | + from process_item | |
| 65 | + where prior_kogl_type is not null and prior_kogl_type <> '' | |
| 66 | + and prior_kogl_type not in (select code from code where group_id = 'KOGL_TYPE') | |
| 67 | + group by prior_kogl_type | |
| 68 | + | |
| 69 | +union all | |
| 70 | +select 'contact_log.method', method, count(*) | |
| 71 | + from contact_log | |
| 72 | + where method is not null and method <> '' | |
| 73 | + and method not in (select code from code where group_id = 'CONTACT_METHOD') | |
| 74 | + group by method | |
| 75 | + | |
| 76 | +order by 1, 2; |
+++ src/test/java/kr/itn/itnhub/code/CodeConsistencyTest.java
... | ... | @@ -0,0 +1,73 @@ |
| 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 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * 업무 테이블에 저장된 값이 코드테이블에 다 있는지 확인한다. | |
| 14 | + * | |
| 15 | + * <p>운영 DB에는 정책 변경 전 값이나 공백만 다른 값이 남아 있을 수 있다. Phase 2에서 | |
| 16 | + * 화면이 코드테이블만 보게 바뀌면 그런 값은 선택지에서 조용히 사라진다. 배포 전에 | |
| 17 | + * {@code db/consistency/check-orphan-codes.sql}을 돌려 발견된 값을 active=false 코드로 | |
| 18 | + * 추가해야 한다 - 지우지 않고 보존하는 이유는 이미 그 값으로 저장된 데이터가 있기 때문이다.</p> | |
| 19 | + */ | |
| 20 | +class CodeConsistencyTest extends AbstractDbTest { | |
| 21 | + | |
| 22 | + @Autowired | |
| 23 | + JdbcTemplate jdbc; | |
| 24 | + | |
| 25 | + @Autowired | |
| 26 | + CodeService codeService; | |
| 27 | + | |
| 28 | + @Test | |
| 29 | + void 저장된_권리확인_값이_모두_코드테이블에_있다() { | |
| 30 | + assertThat(orphans("review_item", "review_major", Codes.REVIEW_MAJOR)).isEmpty(); | |
| 31 | + assertThat(orphans("review_item", "review_minor", Codes.REVIEW_MINOR)).isEmpty(); | |
| 32 | + assertThat(orphans("review_item", "review_result", Codes.REVIEW_RESULT)).isEmpty(); | |
| 33 | + assertThat(orphans("review_item", "judged_kogl_type", Codes.KOGL_TYPE)).isEmpty(); | |
| 34 | + } | |
| 35 | + | |
| 36 | + @Test | |
| 37 | + void 저장된_권리처리_값이_모두_코드테이블에_있다() { | |
| 38 | + assertThat(orphans("process_item", "process_status", Codes.PROCESS_STATUS)).isEmpty(); | |
| 39 | + assertThat(orphans("process_item", "judged_kogl_type", Codes.KOGL_TYPE)).isEmpty(); | |
| 40 | + } | |
| 41 | + | |
| 42 | + @Test | |
| 43 | + void 저장된_연락방법이_모두_코드테이블에_있다() { | |
| 44 | + assertThat(orphans("contact_log", "method", Codes.CONTACT_METHOD)).isEmpty(); | |
| 45 | + } | |
| 46 | + | |
| 47 | + @Test | |
| 48 | + void 점검이_고아값을_실제로_찾아낸다() { | |
| 49 | + jdbc.update("insert into organization (org_no, org_name, channel_slug) " | |
| 50 | + + "values ('900', '정합성테스트기관', '900')"); | |
| 51 | + Long orgId = jdbc.queryForObject( | |
| 52 | + "select id from organization where org_no = '900'", Long.class); | |
| 53 | + jdbc.update("insert into contact_log (org_id, contacted_on, method, summary, author) " | |
| 54 | + + "values (?, current_date, '텔레그램', '코드테이블에 없는 값', 'admin')", orgId); | |
| 55 | + | |
| 56 | + assertThat(orphans("contact_log", "method", Codes.CONTACT_METHOD)) | |
| 57 | + .containsExactly("텔레그램"); | |
| 58 | + | |
| 59 | + jdbc.update("delete from contact_log where org_id = ?", orgId); | |
| 60 | + jdbc.update("delete from organization where id = ?", orgId); | |
| 61 | + } | |
| 62 | + | |
| 63 | + /** 해당 컬럼에 저장돼 있으나 코드테이블에 없는 값. */ | |
| 64 | + private List<String> orphans(String table, String column, String groupId) { | |
| 65 | + List<String> stored = jdbc.queryForList( | |
| 66 | + "select distinct " + column + " from " + table | |
| 67 | + + " where " + column + " is not null and " + column + " <> ''", | |
| 68 | + String.class); | |
| 69 | + return stored.stream() | |
| 70 | + .filter(value -> codeService.find(groupId, value).isEmpty()) | |
| 71 | + .toList(); | |
| 72 | + } | |
| 73 | +} |
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?