fix: 권리처리 엑셀을 내려받아 다시 올리면 기존 증빙자료가 사라지던 문제
설계서에서 "업로드와 다운로드가 따로 정의돼 어긋날 수 있다"고 적었던 그 결함이 실제로 나 있었다. 다운로드 기존 증빙자료를 헤더 자리인 T열(19)에 쓴다 업로드 U열(20)을 읽는다 기관 원본 파일은 헤더가 T인데 데이터가 U에 있어서(T에는 엑셀 화면용 '수정' 링크가 들어 있다) 파서가 U를 읽도록 돼 있었는데, 이 시스템이 만들어 주는 다운로드 파일은 T에 쓴다. 그래서 내려받아 고쳐서 다시 올리면 그 칸만 조용히 비워졌다. 파서가 U를 먼저 보고 비어 있으면 T를 보게 했다. 한쪽만 맞추면 다른 쪽이 깨지므로 둘 다 읽는다. 왕복 테스트를 먼저 만들어 결함을 재현한 뒤 고쳤다. 헤더 이름과 파서 컬럼이 같은 자리를 가리키는지도 24개 필드 전부 확인한다 - 앞으로 한쪽만 고치면 여기서 깨진다. 덤: PowerShell로 파일을 손보다 들어간 BOM 14건을 제거했다. 백엔드 270건, 프론트 208건 통과. Co-Authored-By: Claude Opus 5 (1M context)
@b0bdae23a864dc019623dcf97ad2f885ca108410
--- frontend/src/components/ContactsPage.test.tsx
+++ frontend/src/components/ContactsPage.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ContactsPage from './ContactsPage' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/Dashboard.test.tsx
+++ frontend/src/components/Dashboard.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import Dashboard from './Dashboard' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/OrgOverview.test.tsx
+++ frontend/src/components/OrgOverview.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import OrgOverview from './OrgOverview' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/ProcessBoard.test.tsx
+++ frontend/src/components/ProcessBoard.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ProcessBoard from './ProcessBoard' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 1 |
+import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 2 | 2 |
import {
|
| 3 | 3 |
bulkDeleteProcess, |
| 4 | 4 |
bulkUpdateProcess, |
--- frontend/src/components/ProjectsPage.test.tsx
+++ frontend/src/components/ProjectsPage.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ProjectsPage from './ProjectsPage' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/ReviewBoard.test.tsx
+++ frontend/src/components/ReviewBoard.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ReviewBoard from './ReviewBoard' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 1 |
+import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 2 | 2 |
import {
|
| 3 | 3 |
bulkDeleteReview, |
| 4 | 4 |
bulkUpdateReview, |
--- frontend/src/components/SystemPage.test.tsx
+++ frontend/src/components/SystemPage.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import SystemPage from './SystemPage' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- frontend/src/components/Timeline.test.tsx
+++ frontend/src/components/Timeline.test.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import Timeline from './Timeline' |
| 4 | 4 |
import { withCodes } from '../codes/fixtures'
|
--- src/main/java/kr/itn/itnhub/process/ProcessParser.java
+++ src/main/java/kr/itn/itnhub/process/ProcessParser.java
... | ... | @@ -43,35 +43,36 @@ |
| 43 | 43 |
|
| 44 | 44 |
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
| 45 | 45 |
|
| 46 |
- private static final int COL_SEQ = 0; // A |
|
| 47 |
- private static final int COL_SITE_NAME = 1; // B |
|
| 48 |
- private static final int COL_CATEGORY = 2; // C |
|
| 49 |
- private static final int COL_BOARD_NAME = 3; // D |
|
| 50 |
- private static final int COL_POST_TITLE = 4; // E |
|
| 51 |
- private static final int COL_URL = 5; // F |
|
| 52 |
- private static final int COL_DESCRIPTION = 6; // G |
|
| 53 |
- private static final int COL_HAS_ATTACHMENT = 7; // H |
|
| 54 |
- private static final int COL_PRIOR_KOGL_TYPE = 8; // I |
|
| 55 |
- private static final int COL_CONTRACT_DOCS = 9; // J |
|
| 56 |
- private static final int COL_PRODUCED_DATE = 10; // K |
|
| 57 |
- private static final int COL_PUBLISHED_DATE = 11; // L |
|
| 58 |
- private static final int COL_REVIEW_MAJOR = 12; // M |
|
| 59 |
- private static final int COL_REVIEW_MINOR = 13; // N |
|
| 60 |
- private static final int COL_REVIEW_RESULT = 14; // O |
|
| 61 |
- private static final int COL_REVIEW_KOGL_TYPE = 15; // P |
|
| 62 |
- private static final int COL_REVIEW_AI_TYPE = 16; // Q |
|
| 63 |
- private static final int COL_REVIEW_OPINION = 17; // R |
|
| 64 |
- private static final int COL_REVIEW_NOTE = 18; // S |
|
| 46 |
+ static final int COL_SEQ = 0; // A |
|
| 47 |
+ static final int COL_SITE_NAME = 1; // B |
|
| 48 |
+ static final int COL_CATEGORY = 2; // C |
|
| 49 |
+ static final int COL_BOARD_NAME = 3; // D |
|
| 50 |
+ static final int COL_POST_TITLE = 4; // E |
|
| 51 |
+ static final int COL_URL = 5; // F |
|
| 52 |
+ static final int COL_DESCRIPTION = 6; // G |
|
| 53 |
+ static final int COL_HAS_ATTACHMENT = 7; // H |
|
| 54 |
+ static final int COL_PRIOR_KOGL_TYPE = 8; // I |
|
| 55 |
+ static final int COL_CONTRACT_DOCS = 9; // J |
|
| 56 |
+ static final int COL_PRODUCED_DATE = 10; // K |
|
| 57 |
+ static final int COL_PUBLISHED_DATE = 11; // L |
|
| 58 |
+ static final int COL_REVIEW_MAJOR = 12; // M |
|
| 59 |
+ static final int COL_REVIEW_MINOR = 13; // N |
|
| 60 |
+ static final int COL_REVIEW_RESULT = 14; // O |
|
| 61 |
+ static final int COL_REVIEW_KOGL_TYPE = 15; // P |
|
| 62 |
+ static final int COL_REVIEW_AI_TYPE = 16; // Q |
|
| 63 |
+ static final int COL_REVIEW_OPINION = 17; // R |
|
| 64 |
+ static final int COL_REVIEW_NOTE = 18; // S |
|
| 65 | 65 |
// 실파일에서 헤더는 T열에 '기존 증빙자료'라고 적혀 있지만 실제 데이터는 U열에 있다 |
| 66 | 66 |
// (T열에는 엑셀 화면용 '수정' 링크 텍스트가 들어 있음 - 데이터 기준으로 U를 읽는다) |
| 67 |
- private static final int COL_PRIOR_EVIDENCE = 20; // U |
|
| 68 |
- // 20 = U, 수정 링크 열, 건너뜀 |
|
| 69 |
- private static final int COL_JUDGED_KOGL_TYPE = 21; // V |
|
| 70 |
- private static final int COL_JUDGED_AI_TYPE = 22; // W |
|
| 71 |
- private static final int COL_FINAL_OPINION = 23; // X |
|
| 67 |
+ static final int COL_PRIOR_EVIDENCE = 20; // U |
|
| 68 |
+ /** 헤더('기존 증빙자료')가 적힌 열. 이 시스템이 만드는 다운로드 파일은 여기에 값을 쓴다. */
|
|
| 69 |
+ static final int COL_PRIOR_EVIDENCE_HEADER = 19; // T |
|
| 70 |
+ static final int COL_JUDGED_KOGL_TYPE = 21; // V |
|
| 71 |
+ static final int COL_JUDGED_AI_TYPE = 22; // W |
|
| 72 |
+ static final int COL_FINAL_OPINION = 23; // X |
|
| 72 | 73 |
// 24 = Y, 빈 열, 건너뜀 |
| 73 |
- private static final int COL_JUDGMENT_BASIS = 25; // Z |
|
| 74 |
- private static final int COL_PROCESS_STATUS = 26; // AA |
|
| 74 |
+ static final int COL_JUDGMENT_BASIS = 25; // Z |
|
| 75 |
+ static final int COL_PROCESS_STATUS = 26; // AA |
|
| 75 | 76 |
|
| 76 | 77 |
public List<ProcessRow> parse(InputStream xlsx) {
|
| 77 | 78 |
try (Workbook wb = WorkbookFactory.create(xlsx)) {
|
... | ... | @@ -124,7 +125,7 @@ |
| 124 | 125 |
nullIfBlank(string(row, COL_REVIEW_AI_TYPE)), |
| 125 | 126 |
nullIfBlank(string(row, COL_REVIEW_OPINION)), |
| 126 | 127 |
nullIfBlank(string(row, COL_REVIEW_NOTE)), |
| 127 |
- nullIfBlank(string(row, COL_PRIOR_EVIDENCE)), |
|
| 128 |
+ priorEvidence(row), |
|
| 128 | 129 |
nullIfBlank(string(row, COL_JUDGED_KOGL_TYPE)), |
| 129 | 130 |
nullIfBlank(string(row, COL_JUDGED_AI_TYPE)), |
| 130 | 131 |
nullIfBlank(string(row, COL_FINAL_OPINION)), |
... | ... | @@ -134,6 +135,21 @@ |
| 134 | 135 |
return result; |
| 135 | 136 |
} |
| 136 | 137 |
|
| 138 |
+ /** |
|
| 139 |
+ * 기존 증빙자료는 파일마다 값이 있는 열이 다르다. |
|
| 140 |
+ * |
|
| 141 |
+ * <p>기관이 보내오는 원본은 헤더가 T열인데 데이터가 U열에 있고(T에는 엑셀 화면용 '수정' |
|
| 142 |
+ * 링크 텍스트가 들어 있다), 이 시스템이 만들어 주는 다운로드 파일은 헤더 자리인 T열에 |
|
| 143 |
+ * 값을 쓴다. 한쪽만 읽으면 "내려받아 고쳐서 다시 올리는" 흐름에서 값이 조용히 사라지므로 |
|
| 144 |
+ * U를 먼저 보고, 비어 있으면 T를 본다.</p> |
|
| 145 |
+ */ |
|
| 146 |
+ private String priorEvidence(Row row) {
|
|
| 147 |
+ String fromDataColumn = nullIfBlank(string(row, COL_PRIOR_EVIDENCE)); |
|
| 148 |
+ return fromDataColumn != null |
|
| 149 |
+ ? fromDataColumn |
|
| 150 |
+ : nullIfBlank(string(row, COL_PRIOR_EVIDENCE_HEADER)); |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 137 | 153 |
private String string(Row row, int columnIndex) {
|
| 138 | 154 |
Cell cell = row.getCell(columnIndex); |
| 139 | 155 |
if (cell == null) {
|
--- src/main/java/kr/itn/itnhub/process/ProcessService.java
+++ src/main/java/kr/itn/itnhub/process/ProcessService.java
| Binary file is not shown |
--- src/main/resources/mapper/DashboardMapper.xml
+++ src/main/resources/mapper/DashboardMapper.xml
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-<?xml version="1.0" encoding="UTF-8"?> |
|
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | 2 |
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
| 3 | 3 |
"https://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| 4 | 4 |
<mapper namespace="kr.itn.itnhub.dashboard.DashboardMapper"> |
--- src/main/resources/mapper/ProcessItemMapper.xml
+++ src/main/resources/mapper/ProcessItemMapper.xml
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-<?xml version="1.0" encoding="UTF-8"?> |
|
| 1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | 2 |
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
| 3 | 3 |
"https://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| 4 | 4 |
<mapper namespace="kr.itn.itnhub.process.ProcessItemMapper"> |
+++ src/test/java/kr/itn/itnhub/process/ProcessExcelRoundTripTest.java
... | ... | @@ -0,0 +1,166 @@ |
| 1 | +package kr.itn.itnhub.process; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import kr.itn.itnhub.org.Organization; | |
| 5 | +import kr.itn.itnhub.org.OrganizationMapper; | |
| 6 | +import org.junit.jupiter.api.AfterEach; | |
| 7 | +import org.junit.jupiter.api.BeforeEach; | |
| 8 | +import org.junit.jupiter.api.Test; | |
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 10 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 11 | + | |
| 12 | +import java.io.ByteArrayInputStream; | |
| 13 | +import java.util.List; | |
| 14 | + | |
| 15 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * 권리처리 엑셀의 업로드↔다운로드 왕복. | |
| 19 | + * | |
| 20 | + * <p>업로드는 컬럼 번호({@code ProcessParser.COL_*})로, 다운로드는 헤더 이름 | |
| 21 | + * ({@code ProcessService.COLUMN_HEADERS})으로 각각 따로 정의돼 있다. 두 정의가 어긋나면 | |
| 22 | + * "내려받아 고쳐서 다시 올리는" 흐름에서 값이 조용히 사라진다. 그 어긋남을 여기서 잡는다.</p> | |
| 23 | + */ | |
| 24 | +class ProcessExcelRoundTripTest extends AbstractDbTest { | |
| 25 | + | |
| 26 | + @Autowired | |
| 27 | + ProcessService service; | |
| 28 | + | |
| 29 | + @Autowired | |
| 30 | + ProcessParser parser; | |
| 31 | + | |
| 32 | + @Autowired | |
| 33 | + OrganizationMapper orgMapper; | |
| 34 | + | |
| 35 | + @Autowired | |
| 36 | + JdbcTemplate jdbc; | |
| 37 | + | |
| 38 | + private Long orgId; | |
| 39 | + | |
| 40 | + @BeforeEach | |
| 41 | + void setUp() { | |
| 42 | + jdbc.update("delete from process_item"); | |
| 43 | + jdbc.update("delete from organization where org_no = '920'"); | |
| 44 | + | |
| 45 | + Organization org = new Organization(); | |
| 46 | + org.setOrgNo("920"); | |
| 47 | + org.setOrgName("왕복테스트기관"); | |
| 48 | + org.setChannelSlug("920"); | |
| 49 | + orgMapper.upsertBySeed(org); | |
| 50 | + orgId = orgMapper.findByOrgNoAndOrgName("920", "왕복테스트기관").getId(); | |
| 51 | + } | |
| 52 | + | |
| 53 | + @AfterEach | |
| 54 | + void cleanUp() { | |
| 55 | + jdbc.update("delete from process_item"); | |
| 56 | + jdbc.update("delete from organization where org_no = '920'"); | |
| 57 | + } | |
| 58 | + | |
| 59 | + /** 헤더 이름과 파서가 읽는 컬럼이 같은 자리를 가리키는지. 한쪽만 고치면 여기서 깨진다. */ | |
| 60 | + @Test | |
| 61 | + void 헤더_이름과_파서_컬럼이_같은_자리를_가리킨다() { | |
| 62 | + assertHeader(ProcessParser.COL_SEQ, "순번"); | |
| 63 | + assertHeader(ProcessParser.COL_SITE_NAME, "사이트명"); | |
| 64 | + assertHeader(ProcessParser.COL_CATEGORY, "범주"); | |
| 65 | + assertHeader(ProcessParser.COL_BOARD_NAME, "게시판명"); | |
| 66 | + assertHeader(ProcessParser.COL_POST_TITLE, "게시물제목"); | |
| 67 | + assertHeader(ProcessParser.COL_URL, "URL주소"); | |
| 68 | + assertHeader(ProcessParser.COL_DESCRIPTION, "설명"); | |
| 69 | + assertHeader(ProcessParser.COL_HAS_ATTACHMENT, "첨부파일여부"); | |
| 70 | + assertHeader(ProcessParser.COL_PRIOR_KOGL_TYPE, "기존 공공누리유형(+AI)"); | |
| 71 | + assertHeader(ProcessParser.COL_CONTRACT_DOCS, "계약서 유무"); | |
| 72 | + assertHeader(ProcessParser.COL_PRODUCED_DATE, "제작일"); | |
| 73 | + assertHeader(ProcessParser.COL_PUBLISHED_DATE, "공표일"); | |
| 74 | + assertHeader(ProcessParser.COL_REVIEW_MAJOR, "권리확인(대분류)"); | |
| 75 | + assertHeader(ProcessParser.COL_REVIEW_MINOR, "권리확인(세부)"); | |
| 76 | + assertHeader(ProcessParser.COL_REVIEW_RESULT, "처리 구분"); | |
| 77 | + assertHeader(ProcessParser.COL_REVIEW_KOGL_TYPE, "공공누리 유형"); | |
| 78 | + assertHeader(ProcessParser.COL_REVIEW_AI_TYPE, "AI유형"); | |
| 79 | + assertHeader(ProcessParser.COL_REVIEW_OPINION, "의견"); | |
| 80 | + assertHeader(ProcessParser.COL_REVIEW_NOTE, "비고"); | |
| 81 | + assertHeader(ProcessParser.COL_JUDGED_KOGL_TYPE, "공공누리유형(판정)"); | |
| 82 | + assertHeader(ProcessParser.COL_JUDGED_AI_TYPE, "AI유형(판정)"); | |
| 83 | + assertHeader(ProcessParser.COL_FINAL_OPINION, "최종의견"); | |
| 84 | + assertHeader(ProcessParser.COL_JUDGMENT_BASIS, "판단근거"); | |
| 85 | + assertHeader(ProcessParser.COL_PROCESS_STATUS, "처리상태"); | |
| 86 | + } | |
| 87 | + | |
| 88 | + private void assertHeader(int column, String expected) { | |
| 89 | + assertThat(ProcessService.COLUMN_HEADERS[column]) | |
| 90 | + .as("%d번 열의 헤더", column) | |
| 91 | + .isEqualTo(expected); | |
| 92 | + } | |
| 93 | + | |
| 94 | + /** | |
| 95 | + * 기존 증빙자료는 예외다. 기관이 보내오는 실제 파일은 헤더가 T열인데 데이터가 U열에 있어 | |
| 96 | + * 파서가 U를 먼저 읽고, 비어 있으면 헤더 자리인 T를 읽는다. 이 어긋남 자체를 명시적으로 | |
| 97 | + * 남겨 두어, 나중에 누가 "헤더랑 다르네" 하고 한쪽만 고치다 깨뜨리지 않게 한다. | |
| 98 | + */ | |
| 99 | + @Test | |
| 100 | + void 기존_증빙자료만_헤더와_데이터_열이_다르다() { | |
| 101 | + assertThat(ProcessService.COLUMN_HEADERS[19]).isEqualTo("기존 증빙자료"); | |
| 102 | + assertThat(ProcessParser.COL_PRIOR_EVIDENCE_HEADER).isEqualTo(19); | |
| 103 | + assertThat(ProcessParser.COL_PRIOR_EVIDENCE).isEqualTo(20); | |
| 104 | + } | |
| 105 | + | |
| 106 | + /** 기관 원본처럼 U열에 값이 있는 파일도 그대로 읽어야 한다(다운로드 파일만 고치면 안 된다). */ | |
| 107 | + @Test | |
| 108 | + void 원본처럼_U열에_값이_있어도_읽는다() throws Exception { | |
| 109 | + byte[] xlsx = ProcessParserFixtures.withPriorEvidenceAt(ProcessParser.COL_PRIOR_EVIDENCE, "원본증빙"); | |
| 110 | + | |
| 111 | + List<ProcessRow> rows = parser.parse(new ByteArrayInputStream(xlsx)); | |
| 112 | + | |
| 113 | + assertThat(rows).hasSize(1); | |
| 114 | + assertThat(rows.get(0).priorEvidence()).isEqualTo("원본증빙"); | |
| 115 | + } | |
| 116 | + | |
| 117 | + /** 내려받은 파일을 그대로 다시 올렸을 때 값이 살아남아야 한다. */ | |
| 118 | + @Test | |
| 119 | + void 내려받아_다시_올려도_값이_그대로다() { | |
| 120 | + jdbc.update("insert into process_item (org_id, seq, site_name, category, board_name, " | |
| 121 | + + "post_title, url, description, has_attachment, prior_kogl_type, contract_docs, " | |
| 122 | + + "produced_date, published_date, review_major, review_minor, review_result, " | |
| 123 | + + "review_kogl_type, review_ai_type, review_opinion, review_note, prior_evidence, " | |
| 124 | + + "judged_kogl_type, judged_ai_type, final_opinion, judgment_basis, process_status) " | |
| 125 | + + "values (?, 1, '사이트', '범주', '게시판', '제목', 'http://x', '설명', '있음', " | |
| 126 | + + "'4유형', '양도계약서', '2026-01-02', '2026-01-03', '제3자 권리', '초상권 포함', " | |
| 127 | + + "'권리처리 추진', '보류', 'Y', '의견', '비고', '증빙자료', '0유형', 'Y', " | |
| 128 | + + "'최종의견', '판단근거', '처리완료')", | |
| 129 | + orgId); | |
| 130 | + | |
| 131 | + byte[] xlsx = service.downloadWorkbook(orgId); | |
| 132 | + List<ProcessRow> rows = parser.parse(new ByteArrayInputStream(xlsx)); | |
| 133 | + | |
| 134 | + assertThat(rows).hasSize(1); | |
| 135 | + ProcessRow row = rows.get(0); | |
| 136 | + | |
| 137 | + assertThat(row.siteName()).isEqualTo("사이트"); | |
| 138 | + assertThat(row.category()).isEqualTo("범주"); | |
| 139 | + assertThat(row.boardName()).isEqualTo("게시판"); | |
| 140 | + assertThat(row.postTitle()).isEqualTo("제목"); | |
| 141 | + assertThat(row.url()).isEqualTo("http://x"); | |
| 142 | + assertThat(row.description()).isEqualTo("설명"); | |
| 143 | + assertThat(row.hasAttachment()).isEqualTo("있음"); | |
| 144 | + assertThat(row.priorKoglType()).isEqualTo("4유형"); | |
| 145 | + assertThat(row.contractDocs()).isEqualTo("양도계약서"); | |
| 146 | + assertThat(row.producedDate()).isEqualTo("2026-01-02"); | |
| 147 | + assertThat(row.publishedDate()).isEqualTo("2026-01-03"); | |
| 148 | + assertThat(row.reviewMajor()).isEqualTo("제3자 권리"); | |
| 149 | + assertThat(row.reviewMinor()).isEqualTo("초상권 포함"); | |
| 150 | + assertThat(row.reviewResult()).isEqualTo("권리처리 추진"); | |
| 151 | + assertThat(row.reviewKoglType()).isEqualTo("보류"); | |
| 152 | + assertThat(row.reviewAiType()).isEqualTo("Y"); | |
| 153 | + assertThat(row.reviewOpinion()).isEqualTo("의견"); | |
| 154 | + assertThat(row.reviewNote()).isEqualTo("비고"); | |
| 155 | + assertThat(row.judgedKoglType()).isEqualTo("0유형"); | |
| 156 | + assertThat(row.judgedAiType()).isEqualTo("Y"); | |
| 157 | + assertThat(row.finalOpinion()).isEqualTo("최종의견"); | |
| 158 | + assertThat(row.judgmentBasis()).isEqualTo("판단근거"); | |
| 159 | + assertThat(row.processStatus()).isEqualTo("처리완료"); | |
| 160 | + | |
| 161 | + // 기존 증빙자료는 다운로드가 T열에 쓰고 파서가 U열을 읽어 왕복에서 사라진다. | |
| 162 | + assertThat(row.priorEvidence()) | |
| 163 | + .as("내려받아 다시 올렸을 때 기존 증빙자료") | |
| 164 | + .isEqualTo("증빙자료"); | |
| 165 | + } | |
| 166 | +} |
+++ src/test/java/kr/itn/itnhub/process/ProcessParserFixtures.java
... | ... | @@ -0,0 +1,35 @@ |
| 1 | +package kr.itn.itnhub.process; | |
| 2 | + | |
| 3 | +import org.apache.poi.ss.usermodel.Row; | |
| 4 | +import org.apache.poi.ss.usermodel.Sheet; | |
| 5 | +import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |
| 6 | + | |
| 7 | +import java.io.ByteArrayOutputStream; | |
| 8 | + | |
| 9 | +/** 권리처리 엑셀 테스트용 최소 통합문서. 1행 그룹제목, 2행 헤더, 3행부터 데이터. */ | |
| 10 | +final class ProcessParserFixtures { | |
| 11 | + | |
| 12 | + private ProcessParserFixtures() { | |
| 13 | + } | |
| 14 | + | |
| 15 | + /** 순번 1행짜리 시트를 만들고, 기존 증빙자료 값을 지정한 열에만 넣는다. */ | |
| 16 | + static byte[] withPriorEvidenceAt(int column, String value) throws Exception { | |
| 17 | + try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { | |
| 18 | + Sheet sheet = wb.createSheet(ProcessParser.PRIMARY_SHEET_NAME); | |
| 19 | + sheet.createRow(0).createCell(0).setCellValue("게시물 정보(조사원)"); | |
| 20 | + | |
| 21 | + Row header = sheet.createRow(1); | |
| 22 | + for (int i = 0; i < ProcessService.COLUMN_HEADERS.length; i++) { | |
| 23 | + header.createCell(i).setCellValue(ProcessService.COLUMN_HEADERS[i]); | |
| 24 | + } | |
| 25 | + | |
| 26 | + Row data = sheet.createRow(2); | |
| 27 | + data.createCell(ProcessParser.COL_SEQ).setCellValue("1"); | |
| 28 | + data.createCell(ProcessParser.COL_SITE_NAME).setCellValue("사이트"); | |
| 29 | + data.createCell(column).setCellValue(value); | |
| 30 | + | |
| 31 | + wb.write(out); | |
| 32 | + return out.toByteArray(); | |
| 33 | + } | |
| 34 | + } | |
| 35 | +} |
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?