feat: 공통코드 12개 그룹 시드
진행단계는 요구사항 [7][11]대로 13단계로 넣고, 진행상태(신청/진행중/완료)와 KPI 임계값을 attrs로 데이터화했다. 요구사항 [2](보고서가 올라와야 최종완료)는 completed 임계값을 13번에 두는 것으로 해결된다. 공공누리 유형은 scopes로 사용처(review/process/prior)를 구분한다. 세 화면의 목록이 7/5/6개로 달랐던 것이 실수가 아니라 자리 차이였기 때문이고, 이 구분 없이 합치면 권리처리 화면에 보류가 잘못 노출된다. Co-Authored-By: Claude Opus 5 (1M context)
@db5a94789772a206fff769c2c08878e4872c55f5
--- src/main/resources/db/migration/V13__common_code.sql
+++ src/main/resources/db/migration/V13__common_code.sql
... | ... | @@ -29,3 +29,131 @@ |
| 29 | 29 |
); |
| 30 | 30 |
|
| 31 | 31 |
create index idx_code_group_sort on code (group_id, sort_order); |
| 32 |
+ |
|
| 33 |
+ |
|
| 34 |
+-- ============================================================ |
|
| 35 |
+-- 시드 |
|
| 36 |
+-- ============================================================ |
|
| 37 |
+ |
|
| 38 |
+insert into code_group (group_id, group_name, description, editable) values |
|
| 39 |
+ ('STAGE', '진행단계', 'organization.stage에 번호로 저장. 단계 추가는 마이그레이션으로 한다', false),
|
|
| 40 |
+ ('STAGE_GROUP', '업무구간', '칸반 컬럼 색과 카드 모양을 결정한다', false),
|
|
| 41 |
+ ('REVIEW_MAJOR', '권리확인 대분류', null, true),
|
|
| 42 |
+ ('REVIEW_MINOR', '권리확인 세부', '제3자 권리를 골랐을 때만 노출된다', true),
|
|
| 43 |
+ ('REVIEW_RESULT', '권리확인 처리결과', null, true),
|
|
| 44 |
+ ('KOGL_TYPE', '공공누리 유형', 'scopes로 사용처를 구분한다', true),
|
|
| 45 |
+ ('PROCESS_STATUS', '권리처리 상태', null, false),
|
|
| 46 |
+ ('CONTRACT_DOC', '계약 서류', null, true),
|
|
| 47 |
+ ('CONTACT_METHOD', '연락 방법', null, true),
|
|
| 48 |
+ ('CONTACT_CATEGORY','담당자 구분', null, false),
|
|
| 49 |
+ ('ATTACHMENT_YN', '첨부파일 여부', null, false),
|
|
| 50 |
+ ('KOGL_ATTACHED', '기존 공공누리 부착 여부', null, false);
|
|
| 51 |
+ |
|
| 52 |
+-- 진행단계. progress는 대시보드 진행상태 필터(신청/진행중/완료)를, |
|
| 53 |
+-- kpiFrom은 "이 단계부터 해당 KPI에 집계"라는 임계값을 뜻한다. |
|
| 54 |
+-- 원문자(①②)는 저장하지 않는다 - 단계 번호에서 계산한다. |
|
| 55 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 56 |
+ ('STAGE','1', '신청', 1, '{"groupKey":"intake","progress":"applied"}'::jsonb),
|
|
| 57 |
+ ('STAGE','2', '목록접수', 2, '{"groupKey":"intake","progress":"active","kpiFrom":"docSubmitted"}'::jsonb),
|
|
| 58 |
+ ('STAGE','3', '예비검토', 3, '{"groupKey":"intake","progress":"active"}'::jsonb),
|
|
| 59 |
+ ('STAGE','4', '변호사 배당', 4, '{"groupKey":"assign","progress":"active"}'::jsonb),
|
|
| 60 |
+ ('STAGE','5', '법률검토(권리확인)', 5, '{"groupKey":"check","progress":"active"}'::jsonb),
|
|
| 61 |
+ ('STAGE','6', 'RE:확인', 6, '{"groupKey":"check","progress":"active"}'::jsonb),
|
|
| 62 |
+ ('STAGE','7', '법률검토(권리확인) 완료', 7, '{"groupKey":"check","progress":"active"}'::jsonb),
|
|
| 63 |
+ ('STAGE','8', '법률검토(권리처리)', 8, '{"groupKey":"process","progress":"active"}'::jsonb),
|
|
| 64 |
+ ('STAGE','9', 'RE:처리', 9, '{"groupKey":"process","progress":"active"}'::jsonb),
|
|
| 65 |
+ ('STAGE','10','법률검토(권리처리) 완료',10, '{"groupKey":"process","progress":"active"}'::jsonb),
|
|
| 66 |
+ ('STAGE','11','법률검토 최종완료', 11, '{"groupKey":"closing","progress":"active"}'::jsonb),
|
|
| 67 |
+ ('STAGE','12','보고서 작성', 12, '{"groupKey":"closing","progress":"active","kpiFrom":"reportWriting"}'::jsonb),
|
|
| 68 |
+ ('STAGE','13','보고서 제출 완료', 13, '{"groupKey":"closing","progress":"done","kpiFrom":"completed"}'::jsonb);
|
|
| 69 |
+ |
|
| 70 |
+-- 업무구간. tone은 의미 토큰이며 실제 CSS 클래스는 프론트가 갖는다 - DB에 Tailwind 클래스를 |
|
| 71 |
+-- 넣으면 화면 스타일을 바꿀 때마다 DB를 고쳐야 한다. |
|
| 72 |
+-- cardBody는 칸반 카드에 진척 막대를 몇 줄 그릴지를 뜻한다. |
|
| 73 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 74 |
+ ('STAGE_GROUP','intake', '접수', 1, '{"tone":"slate","cardBody":"plain"}'::jsonb),
|
|
| 75 |
+ ('STAGE_GROUP','assign', '배당', 2, '{"tone":"violet","cardBody":"assign"}'::jsonb),
|
|
| 76 |
+ ('STAGE_GROUP','check', '권리확인',3, '{"tone":"sky","cardBody":"review1"}'::jsonb),
|
|
| 77 |
+ ('STAGE_GROUP','process','권리처리',4, '{"tone":"amber","cardBody":"review2"}'::jsonb),
|
|
| 78 |
+ ('STAGE_GROUP','closing','마감', 5, '{"tone":"emerald","cardBody":"plain"}'::jsonb);
|
|
| 79 |
+ |
|
| 80 |
+-- 권리확인 대분류. suggest는 이 대분류를 고르면 자동으로 채워줄 처리결과, |
|
| 81 |
+-- hasMinor는 세부항목을 노출할지를 뜻한다(제3자 권리에서만 세부가 나온다). |
|
| 82 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 83 |
+ ('REVIEW_MAJOR','만료 저작물', '만료 저작물', 1, '{"suggest":"신유형 개방","hasMinor":false}'::jsonb),
|
|
| 84 |
+ ('REVIEW_MAJOR','업무상 저작물','업무상 저작물',2, '{"suggest":"신유형 개방","hasMinor":false}'::jsonb),
|
|
| 85 |
+ ('REVIEW_MAJOR','제3자 권리', '제3자 권리', 3, '{"hasMinor":true}'::jsonb),
|
|
| 86 |
+ ('REVIEW_MAJOR','개인정보 포함','개인정보 포함',4, '{"suggest":"개방불가","hasMinor":false}'::jsonb);
|
|
| 87 |
+ |
|
| 88 |
+-- 권리확인 세부. code는 저장값 원문이고 label은 화면 표시용(짧은 형태)이다. |
|
| 89 |
+-- note는 라벨 아래 회색 부연설명, allowed는 고를 수 있는 처리결과 제한이다 |
|
| 90 |
+-- (2026-07-20 정책변경: 3·4번은 추진/미희망 택일, 초상권은 개방불가까지 3택). |
|
| 91 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 92 |
+ ('REVIEW_MINOR','1. 원시적 권리 전부 보유','1. 원시적 권리 전부 보유',1,
|
|
| 93 |
+ '{"suggest":"신유형 개방"}'::jsonb),
|
|
| 94 |
+ ('REVIEW_MINOR','2. 후천적 권리 전부 보유(계약서 확인 필요)','2. 후천적 권리 전부 보유',2,
|
|
| 95 |
+ '{"note":"(계약에 의한 전부 양수)","suggest":"계약서 등 재확인"}'::jsonb),
|
|
| 96 |
+ ('REVIEW_MINOR','3. 권리 일부(공동) 보유','3. 권리 일부(공동) 보유',3,
|
|
| 97 |
+ '{"note":"(보도자료·제3자저작물, 계약에 의한 전부/공동 보유 등)","suggest":"권리처리 추진","allowed":["권리처리 추진","권리처리 추진 미희망"]}'::jsonb),
|
|
| 98 |
+ ('REVIEW_MINOR','4. 권리 미보유','4. 권리 미보유',4,
|
|
| 99 |
+ '{"note":"(공모전 수상작 등)","suggest":"권리처리 추진","allowed":["권리처리 추진","권리처리 추진 미희망"]}'::jsonb),
|
|
| 100 |
+ ('REVIEW_MINOR','초상권 포함','초상권 포함',5,
|
|
| 101 |
+ '{"suggest":"권리처리 추진","allowed":["권리처리 추진","권리처리 추진 미희망","개방불가"]}'::jsonb);
|
|
| 102 |
+ |
|
| 103 |
+-- 권리확인 처리결과. needsProcessing/koglSet은 값이고, 실제로 폼을 어떻게 채울지는 코드가 정한다. |
|
| 104 |
+-- koglCopiesPrior가 참이면 기존 부착 유형을 복사하고 비고에 안내문구를 덧붙인다 - 이 "동작"까지 |
|
| 105 |
+-- 데이터로 옮기면 attrs 안에 규칙 언어를 만들게 되므로 코드에 남긴다. |
|
| 106 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 107 |
+ ('REVIEW_RESULT','신유형 개방', '신유형 개방', 1,'{"tone":"emerald","openable":"Y"}'::jsonb),
|
|
| 108 |
+ ('REVIEW_RESULT','계약서 등 재확인', '계약서 등 재확인', 2,'{"tone":"amber"}'::jsonb),
|
|
| 109 |
+ ('REVIEW_RESULT','권리처리 추진', '권리처리 추진', 3,'{"tone":"blue","needsProcessing":true,"koglSet":"보류"}'::jsonb),
|
|
| 110 |
+ ('REVIEW_RESULT','개방불가', '개방불가', 4,'{"tone":"red","needsProcessing":false,"koglSet":"개방불가"}'::jsonb),
|
|
| 111 |
+ ('REVIEW_RESULT','권리처리 추진 미희망','권리처리 추진 미희망',5,'{"tone":"slate","needsProcessing":false,"koglCopiesPrior":true}'::jsonb);
|
|
| 112 |
+ |
|
| 113 |
+-- 공공누리 유형. 세 화면의 목록이 서로 달랐던 이유는 쓰이는 자리가 달라서다. |
|
| 114 |
+-- review = 변호사 판정값 / process = 권리처리 판정값 / prior = 기존에 부착돼 있던 유형 |
|
| 115 |
+-- 이 구분 없이 하나로 합치면 권리처리 화면에 '보류'가 잘못 노출된다. |
|
| 116 |
+-- firstRow는 라디오 첫 줄에 놓을 자리를 뜻한다. |
|
| 117 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 118 |
+ ('KOGL_TYPE','0유형', '0유형', 1,'{"scopes":["review","process","prior"],"firstRow":["review","process"]}'::jsonb),
|
|
| 119 |
+ ('KOGL_TYPE','1유형', '1유형', 2,'{"scopes":["review","process","prior"],"firstRow":[]}'::jsonb),
|
|
| 120 |
+ ('KOGL_TYPE','2유형', '2유형', 3,'{"scopes":["review","process","prior"],"firstRow":[]}'::jsonb),
|
|
| 121 |
+ ('KOGL_TYPE','3유형', '3유형', 4,'{"scopes":["review","process","prior"],"firstRow":[]}'::jsonb),
|
|
| 122 |
+ ('KOGL_TYPE','4유형', '4유형', 5,'{"scopes":["review","process","prior"],"firstRow":[]}'::jsonb),
|
|
| 123 |
+ ('KOGL_TYPE','개방불가','개방불가',6,'{"scopes":["review","process"],"firstRow":["review","process"]}'::jsonb),
|
|
| 124 |
+ ('KOGL_TYPE','보류', '보류', 7,'{"scopes":["review"],"firstRow":["review"]}'::jsonb);
|
|
| 125 |
+ |
|
| 126 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 127 |
+ ('PROCESS_STATUS','미처리', '미처리', 1,'{"done":false}'::jsonb),
|
|
| 128 |
+ ('PROCESS_STATUS','처리완료','처리완료',2,'{"done":true,"stampsProcessedAt":true}'::jsonb);
|
|
| 129 |
+ |
|
| 130 |
+-- freeText가 참인 코드는 '기타:자유텍스트' 형태로 직렬화된다(직렬화 규칙 자체는 코드). |
|
| 131 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 132 |
+ ('CONTRACT_DOC','양도계약서', '양도계약서', 1,'{}'::jsonb),
|
|
| 133 |
+ ('CONTRACT_DOC','제안요청서', '제안요청서', 2,'{}'::jsonb),
|
|
| 134 |
+ ('CONTRACT_DOC','초상이용동의서','초상이용동의서',3,'{}'::jsonb),
|
|
| 135 |
+ ('CONTRACT_DOC','공공누리동의서','공공누리동의서',4,'{}'::jsonb),
|
|
| 136 |
+ ('CONTRACT_DOC','공문', '공문', 5,'{}'::jsonb),
|
|
| 137 |
+ ('CONTRACT_DOC','기타', '기타', 6,'{"freeText":true}'::jsonb);
|
|
| 138 |
+ |
|
| 139 |
+-- 요구사항 [24]는 미응답이라 현행 4종을 그대로 옮긴다. 답이 오면 데이터만 바꾸면 된다. |
|
| 140 |
+-- 지금까지 서버 검증이 없어 아무 값이나 저장됐는데, 코드테이블 도입으로 검증도 생긴다. |
|
| 141 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 142 |
+ ('CONTACT_METHOD','전화','전화',1,'{}'::jsonb),
|
|
| 143 |
+ ('CONTACT_METHOD','메일','메일',2,'{}'::jsonb),
|
|
| 144 |
+ ('CONTACT_METHOD','방문','방문',3,'{}'::jsonb),
|
|
| 145 |
+ ('CONTACT_METHOD','기타','기타',4,'{}'::jsonb);
|
|
| 146 |
+ |
|
| 147 |
+-- importAlias는 담당자 명부 엑셀의 한글 구분값이다(MemberDirectoryParser가 쓰던 매핑). |
|
| 148 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 149 |
+ ('CONTACT_CATEGORY','APPLICANT','신청기관', 1,'{"tone":"sky","importAlias":"신청기관"}'::jsonb),
|
|
| 150 |
+ ('CONTACT_CATEGORY','MJ', '주관기관', 2,'{"tone":"violet","importAlias":"주관기관"}'::jsonb),
|
|
| 151 |
+ ('CONTACT_CATEGORY','LAWYER', '변호사', 3,'{"tone":"emerald","importAlias":"변호사"}'::jsonb),
|
|
| 152 |
+ ('CONTACT_CATEGORY','OPERATOR', '수행기관', 4,'{"tone":"amber","importAlias":"수행기관"}'::jsonb),
|
|
| 153 |
+ ('CONTACT_CATEGORY','ITN', '아이티앤 담당자',5,'{"tone":"slate","importAlias":"아이티앤 담당자"}'::jsonb);
|
|
| 154 |
+ |
|
| 155 |
+insert into code (group_id, code, label, sort_order, attrs) values |
|
| 156 |
+ ('ATTACHMENT_YN','있음','있음',1,'{}'::jsonb),
|
|
| 157 |
+ ('ATTACHMENT_YN','없음','없음',2,'{}'::jsonb),
|
|
| 158 |
+ ('KOGL_ATTACHED','부착', '부착', 1,'{}'::jsonb),
|
|
| 159 |
+ ('KOGL_ATTACHED','미부착','미부착',2,'{}'::jsonb);
|
+++ src/test/java/kr/itn/itnhub/code/CodeSeedTest.java
... | ... | @@ -0,0 +1,151 @@ |
| 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 | + */ | |
| 16 | +class CodeSeedTest extends AbstractDbTest { | |
| 17 | + | |
| 18 | + @Autowired | |
| 19 | + JdbcTemplate jdbc; | |
| 20 | + | |
| 21 | + @Test | |
| 22 | + void 진행단계는_13개이며_13번이_보고서_제출_완료다() { | |
| 23 | + List<String> labels = jdbc.queryForList( | |
| 24 | + "select label from code where group_id = 'STAGE' order by sort_order", String.class); | |
| 25 | + | |
| 26 | + assertThat(labels).hasSize(13); | |
| 27 | + assertThat(labels.get(0)).isEqualTo("신청"); | |
| 28 | + assertThat(labels.get(11)).isEqualTo("보고서 작성"); | |
| 29 | + assertThat(labels.get(12)).isEqualTo("보고서 제출 완료"); | |
| 30 | + } | |
| 31 | + | |
| 32 | + @Test | |
| 33 | + void 진행상태는_1번만_신청이고_13번만_완료다() { | |
| 34 | + List<String> applied = jdbc.queryForList( | |
| 35 | + "select code from code where group_id = 'STAGE' and attrs->>'progress' = 'applied'", | |
| 36 | + String.class); | |
| 37 | + List<String> done = jdbc.queryForList( | |
| 38 | + "select code from code where group_id = 'STAGE' and attrs->>'progress' = 'done'", | |
| 39 | + String.class); | |
| 40 | + Integer active = jdbc.queryForObject( | |
| 41 | + "select count(*) from code where group_id = 'STAGE' and attrs->>'progress' = 'active'", | |
| 42 | + Integer.class); | |
| 43 | + | |
| 44 | + assertThat(applied).containsExactly("1"); | |
| 45 | + assertThat(done).containsExactly("13"); | |
| 46 | + assertThat(active).isEqualTo(11); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @Test | |
| 50 | + void KPI_임계값이_세_개_지정되어_있다() { | |
| 51 | + assertThat(kpiStage("docSubmitted")).isEqualTo("2"); | |
| 52 | + assertThat(kpiStage("reportWriting")).isEqualTo("12"); | |
| 53 | + // 요구사항 [2]: 보고서가 실제로 올라온 기관만 완료로 센다. | |
| 54 | + assertThat(kpiStage("completed")).isEqualTo("13"); | |
| 55 | + } | |
| 56 | + | |
| 57 | + private String kpiStage(String kpi) { | |
| 58 | + return jdbc.queryForObject( | |
| 59 | + "select code from code where group_id = 'STAGE' and attrs->>'kpiFrom' = ?", | |
| 60 | + String.class, kpi); | |
| 61 | + } | |
| 62 | + | |
| 63 | + @Test | |
| 64 | + void 모든_단계가_업무구간에_속한다() { | |
| 65 | + Integer orphan = jdbc.queryForObject( | |
| 66 | + "select count(*) from code s where s.group_id = 'STAGE' " | |
| 67 | + + "and not exists (select 1 from code g where g.group_id = 'STAGE_GROUP' " | |
| 68 | + + " and g.code = s.attrs->>'groupKey')", | |
| 69 | + Integer.class); | |
| 70 | + | |
| 71 | + assertThat(orphan).isZero(); | |
| 72 | + } | |
| 73 | + | |
| 74 | + @Test | |
| 75 | + void 공공누리_유형은_사용처별로_다르게_노출된다() { | |
| 76 | + // 세 화면의 목록이 7개/5개/6개로 달랐던 이유는 쓰이는 자리가 달라서다. | |
| 77 | + assertThat(koglScope("review")).hasSize(7); | |
| 78 | + assertThat(koglScope("process")).hasSize(6).doesNotContain("보류"); | |
| 79 | + assertThat(koglScope("prior")).containsExactly("0유형", "1유형", "2유형", "3유형", "4유형"); | |
| 80 | + } | |
| 81 | + | |
| 82 | + private List<String> koglScope(String scope) { | |
| 83 | + return jdbc.queryForList( | |
| 84 | + "select code from code c where c.group_id = 'KOGL_TYPE' " | |
| 85 | + + " and exists (select 1 from jsonb_array_elements_text(c.attrs->'scopes') s(v) " | |
| 86 | + + " where s.v = ?) " | |
| 87 | + + "order by sort_order", | |
| 88 | + String.class, scope); | |
| 89 | + } | |
| 90 | + | |
| 91 | + @Test | |
| 92 | + void 세부항목의_허용_처리결과는_실제_처리결과_코드다() { | |
| 93 | + Integer orphan = jdbc.queryForObject( | |
| 94 | + "select count(*) from code m, " | |
| 95 | + + " lateral jsonb_array_elements_text(coalesce(m.attrs->'allowed', '[]'::jsonb)) a(v) " | |
| 96 | + + "where m.group_id = 'REVIEW_MINOR' " | |
| 97 | + + " and not exists (select 1 from code r " | |
| 98 | + + " where r.group_id = 'REVIEW_RESULT' and r.code = a.v)", | |
| 99 | + Integer.class); | |
| 100 | + | |
| 101 | + assertThat(orphan).isZero(); | |
| 102 | + } | |
| 103 | + | |
| 104 | + @Test | |
| 105 | + void 대분류와_세부의_추천_처리결과도_실제_코드다() { | |
| 106 | + Integer orphan = jdbc.queryForObject( | |
| 107 | + "select count(*) from code c " | |
| 108 | + + "where c.group_id in ('REVIEW_MAJOR', 'REVIEW_MINOR') " | |
| 109 | + + " and c.attrs->>'suggest' is not null " | |
| 110 | + + " and not exists (select 1 from code r " | |
| 111 | + + " where r.group_id = 'REVIEW_RESULT' and r.code = c.attrs->>'suggest')", | |
| 112 | + Integer.class); | |
| 113 | + | |
| 114 | + assertThat(orphan).isZero(); | |
| 115 | + } | |
| 116 | + | |
| 117 | + @Test | |
| 118 | + void 처리결과가_지정하는_공공누리_유형도_실제_코드다() { | |
| 119 | + Integer orphan = jdbc.queryForObject( | |
| 120 | + "select count(*) from code c " | |
| 121 | + + "where c.group_id = 'REVIEW_RESULT' " | |
| 122 | + + " and c.attrs->>'koglSet' is not null " | |
| 123 | + + " and not exists (select 1 from code k " | |
| 124 | + + " where k.group_id = 'KOGL_TYPE' and k.code = c.attrs->>'koglSet')", | |
| 125 | + Integer.class); | |
| 126 | + | |
| 127 | + assertThat(orphan).isZero(); | |
| 128 | + } | |
| 129 | + | |
| 130 | + @Test | |
| 131 | + void 열두_그룹이_모두_시드된다() { | |
| 132 | + Integer groups = jdbc.queryForObject("select count(*) from code_group", Integer.class); | |
| 133 | + Integer empty = jdbc.queryForObject( | |
| 134 | + "select count(*) from code_group g " | |
| 135 | + + "where not exists (select 1 from code c where c.group_id = g.group_id)", | |
| 136 | + Integer.class); | |
| 137 | + | |
| 138 | + assertThat(groups).isEqualTo(12); | |
| 139 | + assertThat(empty).isZero(); | |
| 140 | + } | |
| 141 | + | |
| 142 | + @Test | |
| 143 | + void 그룹마다_정렬순서가_중복되지_않는다() { | |
| 144 | + Integer dup = jdbc.queryForObject( | |
| 145 | + "select count(*) from (select group_id, sort_order from code " | |
| 146 | + + " group by group_id, sort_order having count(*) > 1) t", | |
| 147 | + Integer.class); | |
| 148 | + | |
| 149 | + assertThat(dup).isZero(); | |
| 150 | + } | |
| 151 | +} |
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?