feat: 기관 코드를 연번1_연번2 합성 코드로 재구성
경찰청 계열처럼 연번1(기관번호) 하나로 하위기관을 대표기관에 흡수하던 모델을 폐기하고, 시트의 모든 행(연번1_연번2)이 각자 독립된 기관·채널 쌍을 갖도록 바꿨다. 경찰청_치안정책연구소에 대한 -policy- 특례는 연번 자체가 하위기관까지 구분하므로 더 이상 필요 없어 제거했다. 기존 채널 ID는 V5 마이그레이션이 org_no만 변환해 그대로 보존한다.
@aa053cd166cf3e996242ee51863e8a4ae213f873
--- src/main/java/kr/itn/itnhub/provision/ChannelNaming.java
+++ src/main/java/kr/itn/itnhub/provision/ChannelNaming.java
... | ... | @@ -3,11 +3,12 @@ |
| 3 | 3 |
/** |
| 4 | 4 |
* 채널 이름 규칙. VBA M06/M07의 이식이며 기존에 만들어진 채널과 반드시 일치해야 한다. |
| 5 | 5 |
* 규칙이 바뀌면 기존 채널을 찾지 못하고 중복 생성된다. |
| 6 |
+ * |
|
| 7 |
+ * 기관 코드({@code orgNo})는 {연번1}_{연번2} 합성 코드다(예: 008_00 경찰청,
|
|
| 8 |
+ * 008_01 경찰청_치안정책연구소, 008_02 서울청 …). 연번 자체가 하위기관까지 구분하므로 |
|
| 9 |
+ * 기관명에 따른 별도 특례는 없다. |
|
| 6 | 10 |
*/ |
| 7 | 11 |
public final class ChannelNaming {
|
| 8 |
- |
|
| 9 |
- /** 같은 연번을 쓰지만 별도 기관으로 취급하는 유일한 예외. */ |
|
| 10 |
- public static final String SPECIAL_ORG_NAME = "경찰청_치안정책연구소"; |
|
| 11 | 12 |
|
| 12 | 13 |
private ChannelNaming() {
|
| 13 | 14 |
} |
... | ... | @@ -22,9 +23,6 @@ |
| 22 | 23 |
.replace("_", "-")
|
| 23 | 24 |
.replace(" ", "-");
|
| 24 | 25 |
|
| 25 |
- if (SPECIAL_ORG_NAME.equalsIgnoreCase(orgName.trim())) {
|
|
| 26 |
- return "org-" + slug + "-policy-" + kind.code(); |
|
| 27 |
- } |
|
| 28 | 26 |
return "org-" + slug + "-" + kind.code(); |
| 29 | 27 |
} |
| 30 | 28 |
} |
--- src/main/java/kr/itn/itnhub/seed/SeedParser.java
+++ src/main/java/kr/itn/itnhub/seed/SeedParser.java
... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 |
package kr.itn.itnhub.seed; |
| 2 | 2 |
|
| 3 |
-import kr.itn.itnhub.provision.ChannelNaming; |
|
| 4 | 3 |
import org.apache.poi.ss.usermodel.Cell; |
| 5 | 4 |
import org.apache.poi.ss.usermodel.CellType; |
| 6 | 5 |
import org.apache.poi.ss.usermodel.Row; |
... | ... | @@ -15,15 +14,16 @@ |
| 15 | 14 |
import java.util.List; |
| 16 | 15 |
|
| 17 | 16 |
/** |
| 18 |
- * 신유형 신청목록 시트를 대표기관 단위로 축약한다. |
|
| 17 |
+ * 신유형 신청목록 시트의 모든 행을 개별 기관으로 읽는다. |
|
| 19 | 18 |
* |
| 20 | 19 |
* 시트 배치 (1행 그룹제목, 2행 헤더, 3행부터 데이터): |
| 21 | 20 |
* B=연번1 C=연번2 F=기관명 G=부서명 H=담당자명 I=직급/직함 J=유선전화 K=이메일 |
| 22 | 21 |
* |
| 23 |
- * 축약 규칙: |
|
| 24 |
- * - 연번2가 "00"인 행이 대표기관이며 담당자 정보를 갖는다 |
|
| 25 |
- * - 나머지 하위기관 행은 대표기관에 흡수한다 (008 경찰청의 서울청·부산청 등) |
|
| 26 |
- * - 단 경찰청_치안정책연구소만 예외로 별도 기관으로 남긴다 |
|
| 22 |
+ * 기관 코드는 {연번1}_{연번2} 합성 코드다(예: 001_00, 008_02). 연번1+연번2+기관명이
|
|
| 23 |
+ * 모두 있는 행은 전부 별도 기관이 된다 - 예전처럼 연번2="00" 대표기관에 하위기관을 |
|
| 24 |
+ * 흡수하지 않는다(경찰청_치안정책연구소 같은 예외도 더 이상 필요 없다 - 연번 자체가 |
|
| 25 |
+ * 하위기관까지 구분하기 때문). 담당자 정보가 없는 행은 담당자 필드만 null이 되고 |
|
| 26 |
+ * 행 자체는 그대로 만들어진다. |
|
| 27 | 27 |
*/ |
| 28 | 28 |
@Component |
| 29 | 29 |
public class SeedParser {
|
... | ... | @@ -62,23 +62,19 @@ |
| 62 | 62 |
continue; |
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 |
- String orgNo = orgNo(row); |
|
| 65 |
+ String orgNo1 = orgNo1(row); |
|
| 66 |
+ String subNo = subNo(row); |
|
| 66 | 67 |
String orgName = string(row, COL_ORG_NAME); |
| 67 |
- if (orgNo.isBlank() || orgName.isBlank()) {
|
|
| 68 |
+ if (orgNo1.isBlank() || subNo.isBlank() || orgName.isBlank()) {
|
|
| 68 | 69 |
continue; |
| 69 | 70 |
} |
| 70 | 71 |
|
| 71 |
- boolean representative = "00".equals(subNo(row)); |
|
| 72 |
- boolean special = ChannelNaming.SPECIAL_ORG_NAME.equalsIgnoreCase(orgName); |
|
| 73 |
- |
|
| 74 |
- if (!representative && !special) {
|
|
| 75 |
- continue; // 하위기관은 대표기관에 흡수된다 |
|
| 76 |
- } |
|
| 72 |
+ String orgNo = orgNo1 + "_" + subNo; |
|
| 77 | 73 |
|
| 78 | 74 |
result.add(new SeedRow( |
| 79 | 75 |
orgNo, |
| 80 | 76 |
orgName, |
| 81 |
- special ? orgNo + "-policy" : orgNo, |
|
| 77 |
+ orgNo, |
|
| 82 | 78 |
nullIfBlank(string(row, COL_DEPT)), |
| 83 | 79 |
nullIfBlank(string(row, COL_MANAGER)), |
| 84 | 80 |
nullIfBlank(string(row, COL_TITLE)), |
... | ... | @@ -88,20 +84,21 @@ |
| 88 | 84 |
return result; |
| 89 | 85 |
} |
| 90 | 86 |
|
| 91 |
- /** 연번은 항상 3자리 문자열이다. 숫자로 저장된 셀도 앞자리 0을 복원해 준다. */ |
|
| 92 |
- private String orgNo(Row row) {
|
|
| 87 |
+ /** 연번1은 항상 3자리 문자열이다. 숫자로 저장된 셀도 앞자리 0을 복원해 준다. */ |
|
| 88 |
+ private String orgNo1(Row row) {
|
|
| 93 | 89 |
String raw = string(row, COL_ORG_NO); |
| 94 |
- return raw.isBlank() ? "" : pad3(raw); |
|
| 90 |
+ return raw.isBlank() ? "" : pad(raw, 3); |
|
| 95 | 91 |
} |
| 96 | 92 |
|
| 93 |
+ /** 연번2도 항상 2자리 문자열이다("0" → "00", "1" → "01"). */
|
|
| 97 | 94 |
private String subNo(Row row) {
|
| 98 | 95 |
String raw = string(row, COL_SUB_NO); |
| 99 |
- return raw.isBlank() ? "" : (raw.length() == 1 ? "0" + raw : raw); |
|
| 96 |
+ return raw.isBlank() ? "" : pad(raw, 2); |
|
| 100 | 97 |
} |
| 101 | 98 |
|
| 102 |
- private String pad3(String value) {
|
|
| 99 |
+ private String pad(String value, int width) {
|
|
| 103 | 100 |
String v = value.trim(); |
| 104 |
- return v.length() >= 3 ? v : "0".repeat(3 - v.length()) + v; |
|
| 101 |
+ return v.length() >= width ? v : "0".repeat(width - v.length()) + v; |
|
| 105 | 102 |
} |
| 106 | 103 |
|
| 107 | 104 |
private String string(Row row, int columnIndex) {
|
+++ src/main/resources/db/migration/V5__org_codes.sql
... | ... | @@ -0,0 +1,10 @@ |
| 1 | +-- 기관 코드를 {연번1}_{연번2} 합성 코드로 바꾼다. 예전에는 연번1(기관번호) 하나로 | |
| 2 | +-- 대표기관에 하위기관을 흡수했지만, 이제는 시트의 모든 행(연번1+연번2)이 각각 | |
| 3 | +-- 독립된 기관이자 채널 쌍이다. 기존에 만들어진 채널 ID는 org_no만 바꿔서 그대로 보존한다. | |
| 4 | + | |
| 5 | +-- 치안정책연구소만 예외적으로 하위 연번을 받는다 (구 모델에서 유일하게 분리돼 있던 하위기관) | |
| 6 | +update organization set org_no = '008_01', channel_slug = '008_01' | |
| 7 | + where org_name = '경찰청_치안정책연구소' and org_no not like '%\_%' escape '\'; | |
| 8 | + | |
| 9 | +update organization set org_no = org_no || '_00', channel_slug = org_no || '_00' | |
| 10 | + where org_no not like '%\_%' escape '\'; |
--- src/test/java/kr/itn/itnhub/provision/ChannelNamingTest.java
+++ src/test/java/kr/itn/itnhub/provision/ChannelNamingTest.java
... | ... | @@ -8,35 +8,41 @@ |
| 8 | 8 |
|
| 9 | 9 |
@Test |
| 10 | 10 |
void 표시명은_연번_기관명_라벨을_조합한다() {
|
| 11 |
- assertThat(ChannelNaming.displayName("001", "국제방송교류재단", "문정원"))
|
|
| 12 |
- .isEqualTo("001_국제방송교류재단 (문정원)");
|
|
| 11 |
+ assertThat(ChannelNaming.displayName("001_00", "국제방송교류재단", "문정원"))
|
|
| 12 |
+ .isEqualTo("001_00_국제방송교류재단 (문정원)");
|
|
| 13 | 13 |
|
| 14 |
- assertThat(ChannelNaming.displayName("001", "국제방송교류재단", "법률검토"))
|
|
| 15 |
- .isEqualTo("001_국제방송교류재단 (법률검토)");
|
|
| 14 |
+ assertThat(ChannelNaming.displayName("001_00", "국제방송교류재단", "법률검토"))
|
|
| 15 |
+ .isEqualTo("001_00_국제방송교류재단 (법률검토)");
|
|
| 16 | 16 |
} |
| 17 | 17 |
|
| 18 | 18 |
@Test |
| 19 | 19 |
void 내부명은_org_연번_구분_형식이다() {
|
| 20 |
- assertThat(ChannelNaming.internalName("001", "국제방송교류재단", ChannelKind.MJ))
|
|
| 21 |
- .isEqualTo("org-001-mj");
|
|
| 20 |
+ assertThat(ChannelNaming.internalName("001_00", "국제방송교류재단", ChannelKind.MJ))
|
|
| 21 |
+ .isEqualTo("org-001-00-mj");
|
|
| 22 | 22 |
|
| 23 |
- assertThat(ChannelNaming.internalName("001", "국제방송교류재단", ChannelKind.LAW))
|
|
| 24 |
- .isEqualTo("org-001-law");
|
|
| 23 |
+ assertThat(ChannelNaming.internalName("001_00", "국제방송교류재단", ChannelKind.LAW))
|
|
| 24 |
+ .isEqualTo("org-001-00-law");
|
|
| 25 | 25 |
} |
| 26 | 26 |
|
| 27 | 27 |
@Test |
| 28 |
- void 치안정책연구소는_policy_구분자가_들어간다() {
|
|
| 29 |
- assertThat(ChannelNaming.internalName("008", "경찰청_치안정책연구소", ChannelKind.MJ))
|
|
| 30 |
- .isEqualTo("org-008-policy-mj");
|
|
| 28 |
+ void 치안정책연구소는_합성코드로만_구분되며_더_이상_특별취급하지_않는다() {
|
|
| 29 |
+ assertThat(ChannelNaming.displayName("008_01", "경찰청_치안정책연구소", "문정원"))
|
|
| 30 |
+ .isEqualTo("008_01_경찰청_치안정책연구소 (문정원)");
|
|
| 31 | 31 |
|
| 32 |
- assertThat(ChannelNaming.internalName("008", "경찰청_치안정책연구소", ChannelKind.LAW))
|
|
| 33 |
- .isEqualTo("org-008-policy-law");
|
|
| 32 |
+ assertThat(ChannelNaming.internalName("008_01", "경찰청_치안정책연구소", ChannelKind.MJ))
|
|
| 33 |
+ .isEqualTo("org-008-01-mj");
|
|
| 34 |
+ |
|
| 35 |
+ assertThat(ChannelNaming.internalName("008_01", "경찰청_치안정책연구소", ChannelKind.LAW))
|
|
| 36 |
+ .isEqualTo("org-008-01-law");
|
|
| 34 | 37 |
} |
| 35 | 38 |
|
| 36 | 39 |
@Test |
| 37 |
- void 같은_연번의_대표기관은_policy가_붙지_않는다() {
|
|
| 38 |
- assertThat(ChannelNaming.internalName("008", "경찰청", ChannelKind.MJ))
|
|
| 39 |
- .isEqualTo("org-008-mj");
|
|
| 40 |
+ void 대표기관과_하위기관은_연번_뒷자리로만_구분된다() {
|
|
| 41 |
+ assertThat(ChannelNaming.internalName("008_00", "경찰청", ChannelKind.MJ))
|
|
| 42 |
+ .isEqualTo("org-008-00-mj");
|
|
| 43 |
+ |
|
| 44 |
+ assertThat(ChannelNaming.internalName("008_02", "서울청", ChannelKind.MJ))
|
|
| 45 |
+ .isEqualTo("org-008-02-mj");
|
|
| 40 | 46 |
} |
| 41 | 47 |
|
| 42 | 48 |
@Test |
... | ... | @@ -50,7 +56,7 @@ |
| 50 | 56 |
|
| 51 | 57 |
@Test |
| 52 | 58 |
void 연번의_앞자리_영은_보존된다() {
|
| 53 |
- assertThat(ChannelNaming.internalName("001", "기관", ChannelKind.MJ))
|
|
| 54 |
- .startsWith("org-001-");
|
|
| 59 |
+ assertThat(ChannelNaming.internalName("001_00", "기관", ChannelKind.MJ))
|
|
| 60 |
+ .startsWith("org-001-00-");
|
|
| 55 | 61 |
} |
| 56 | 62 |
} |
--- src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
+++ src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
... | ... | @@ -180,11 +180,11 @@ |
| 180 | 180 |
} |
| 181 | 181 |
|
| 182 | 182 |
@Test |
| 183 |
- void 치안정책연구소는_policy_내부명으로_생성한다() {
|
|
| 183 |
+ void 치안정책연구소는_합성코드_008_01로_내부명을_만든다() {
|
|
| 184 | 184 |
Organization special = new Organization(); |
| 185 |
- special.setOrgNo("008");
|
|
| 185 |
+ special.setOrgNo("008_01");
|
|
| 186 | 186 |
special.setOrgName("경찰청_치안정책연구소");
|
| 187 |
- special.setChannelSlug("008-policy");
|
|
| 187 |
+ special.setChannelSlug("008_01");
|
|
| 188 | 188 |
mapper.upsertBySeed(special); |
| 189 | 189 |
|
| 190 | 190 |
Long specialId = mapper.findAll().stream() |
... | ... | @@ -199,9 +199,9 @@ |
| 199 | 199 |
service.provision(specialId); |
| 200 | 200 |
|
| 201 | 201 |
verify(mattermost).createPrivateChannel( |
| 202 |
- "org-008-policy-mj", "008_경찰청_치안정책연구소 (문정원)"); |
|
| 202 |
+ "org-008-01-mj", "008_01_경찰청_치안정책연구소 (문정원)"); |
|
| 203 | 203 |
verify(mattermost).createPrivateChannel( |
| 204 |
- "org-008-policy-law", "008_경찰청_치안정책연구소 (법률검토)"); |
|
| 204 |
+ "org-008-01-law", "008_01_경찰청_치안정책연구소 (법률검토)"); |
|
| 205 | 205 |
} |
| 206 | 206 |
|
| 207 | 207 |
@Test |
--- src/test/java/kr/itn/itnhub/seed/SeedParserTest.java
+++ src/test/java/kr/itn/itnhub/seed/SeedParserTest.java
... | ... | @@ -69,9 +69,9 @@ |
| 69 | 69 |
|
| 70 | 70 |
assertThat(rows).hasSize(1); |
| 71 | 71 |
SeedRow row = rows.get(0); |
| 72 |
- assertThat(row.orgNo()).isEqualTo("001");
|
|
| 72 |
+ assertThat(row.orgNo()).isEqualTo("001_00");
|
|
| 73 | 73 |
assertThat(row.orgName()).isEqualTo("국제방송교류재단");
|
| 74 |
- assertThat(row.channelSlug()).isEqualTo("001");
|
|
| 74 |
+ assertThat(row.channelSlug()).isEqualTo("001_00");
|
|
| 75 | 75 |
assertThat(row.deptName()).isEqualTo("데이터정보화팀");
|
| 76 | 76 |
assertThat(row.managerName()).isEqualTo("송민지");
|
| 77 | 77 |
assertThat(row.managerTitle()).isEqualTo("과장");
|
... | ... | @@ -80,7 +80,7 @@ |
| 80 | 80 |
} |
| 81 | 81 |
|
| 82 | 82 |
@Test |
| 83 |
- void 하위기관은_대표기관으로_흡수되어_행이_늘지_않는다() throws Exception {
|
|
| 83 |
+ void 하위기관도_각각_별도_기관으로_읽힌다() throws Exception {
|
|
| 84 | 84 |
byte[] xlsx = workbook( |
| 85 | 85 |
new String[]{"008", "00", "경찰청", "데이터정책계", "박진우", "경위", "02-3150-3205", "hi@police.go.kr"},
|
| 86 | 86 |
new String[]{"008", "02", "서울청", null, null, null, null, null},
|
... | ... | @@ -88,12 +88,17 @@ |
| 88 | 88 |
|
| 89 | 89 |
List<SeedRow> rows = parser.parse(new ByteArrayInputStream(xlsx)); |
| 90 | 90 |
|
| 91 |
- assertThat(rows).hasSize(1); |
|
| 92 |
- assertThat(rows.get(0).orgName()).isEqualTo("경찰청");
|
|
| 91 |
+ assertThat(rows).hasSize(3); |
|
| 92 |
+ assertThat(rows).extracting(SeedRow::orgNo) |
|
| 93 |
+ .containsExactly("008_00", "008_02", "008_03");
|
|
| 94 |
+ assertThat(rows).extracting(SeedRow::orgName) |
|
| 95 |
+ .containsExactly("경찰청", "서울청", "부산청");
|
|
| 96 |
+ assertThat(rows).extracting(SeedRow::channelSlug) |
|
| 97 |
+ .containsExactly("008_00", "008_02", "008_03");
|
|
| 93 | 98 |
} |
| 94 | 99 |
|
| 95 | 100 |
@Test |
| 96 |
- void 치안정책연구소는_하위기관이지만_별도_기관으로_남는다() throws Exception {
|
|
| 101 |
+ void 치안정책연구소는_더_이상_예외가_아니라_연번2로만_구분되는_평범한_행이다() throws Exception {
|
|
| 97 | 102 |
byte[] xlsx = workbook( |
| 98 | 103 |
new String[]{"008", "00", "경찰청", "데이터정책계", "박진우", "경위", "02-3150-3205", "hi@police.go.kr"},
|
| 99 | 104 |
new String[]{"008", "01", "경찰청_치안정책연구소", null, null, null, null, null},
|
... | ... | @@ -101,11 +106,11 @@ |
| 101 | 106 |
|
| 102 | 107 |
List<SeedRow> rows = parser.parse(new ByteArrayInputStream(xlsx)); |
| 103 | 108 |
|
| 104 |
- assertThat(rows).hasSize(2); |
|
| 109 |
+ assertThat(rows).hasSize(3); |
|
| 105 | 110 |
assertThat(rows).extracting(SeedRow::orgName) |
| 106 |
- .containsExactly("경찰청", "경찰청_치안정책연구소");
|
|
| 107 |
- assertThat(rows).extracting(SeedRow::channelSlug) |
|
| 108 |
- .containsExactly("008", "008-policy");
|
|
| 111 |
+ .containsExactly("경찰청", "경찰청_치안정책연구소", "서울청");
|
|
| 112 |
+ assertThat(rows).extracting(SeedRow::orgNo) |
|
| 113 |
+ .containsExactly("008_00", "008_01", "008_02");
|
|
| 109 | 114 |
} |
| 110 | 115 |
|
| 111 | 116 |
@Test |
... | ... | @@ -115,11 +120,21 @@ |
| 115 | 120 |
"정보화담당관", "임난주", "공무직", "044-201-6431", "envlib@korea.kr"}); |
| 116 | 121 |
|
| 117 | 122 |
assertThat(parser.parse(new ByteArrayInputStream(xlsx)).get(0).orgNo()) |
| 118 |
- .isEqualTo("007");
|
|
| 123 |
+ .isEqualTo("007_00");
|
|
| 119 | 124 |
} |
| 120 | 125 |
|
| 121 | 126 |
@Test |
| 122 |
- void 연번이_숫자로_저장돼_있어도_세자리로_복원한다() throws Exception {
|
|
| 127 |
+ void 연번2가_한_자리_숫자여도_두_자리로_복원한다() throws Exception {
|
|
| 128 |
+ byte[] xlsx = workbook(new String[]{
|
|
| 129 |
+ "008", "1", "치안정책연구소", |
|
| 130 |
+ null, null, null, null, null}); |
|
| 131 |
+ |
|
| 132 |
+ assertThat(parser.parse(new ByteArrayInputStream(xlsx)).get(0).orgNo()) |
|
| 133 |
+ .isEqualTo("008_01");
|
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ @Test |
|
| 137 |
+ void 연번이_숫자로_저장돼_있어도_세자리_두자리로_복원한다() throws Exception {
|
|
| 123 | 138 |
try (XSSFWorkbook wb = new XSSFWorkbook(); |
| 124 | 139 |
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
| 125 | 140 |
|
... | ... | @@ -134,7 +149,7 @@ |
| 134 | 149 |
|
| 135 | 150 |
List<SeedRow> rows = parser.parse(new ByteArrayInputStream(out.toByteArray())); |
| 136 | 151 |
|
| 137 |
- assertThat(rows.get(0).orgNo()).isEqualTo("007");
|
|
| 152 |
+ assertThat(rows.get(0).orgNo()).isEqualTo("007_00");
|
|
| 138 | 153 |
} |
| 139 | 154 |
} |
| 140 | 155 |
|
... | ... | @@ -165,7 +180,7 @@ |
| 165 | 180 |
|
| 166 | 181 |
List<SeedRow> rows = parser.parse(new ByteArrayInputStream(out.toByteArray())); |
| 167 | 182 |
|
| 168 |
- assertThat(rows.get(0).orgNo()).isEqualTo("007");
|
|
| 183 |
+ assertThat(rows.get(0).orgNo()).isEqualTo("007_00");
|
|
| 169 | 184 |
} |
| 170 | 185 |
} |
| 171 | 186 |
|
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?