이호영 이호영 07-28
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
+++ frontend/src/components/ContactsPage.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import ContactsPage from './ContactsPage'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/Dashboard.test.tsx
--- frontend/src/components/Dashboard.test.tsx
+++ frontend/src/components/Dashboard.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import Dashboard from './Dashboard'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/OrgOverview.test.tsx
--- frontend/src/components/OrgOverview.test.tsx
+++ frontend/src/components/OrgOverview.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import OrgOverview from './OrgOverview'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/ProcessBoard.test.tsx
--- frontend/src/components/ProcessBoard.test.tsx
+++ frontend/src/components/ProcessBoard.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import ProcessBoard from './ProcessBoard'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/ProcessBoard.tsx
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState, type ReactNode } from 'react'
+import { useEffect, useRef, useState, type ReactNode } from 'react'
 import {
   bulkDeleteProcess,
   bulkUpdateProcess,
frontend/src/components/ProjectsPage.test.tsx
--- frontend/src/components/ProjectsPage.test.tsx
+++ frontend/src/components/ProjectsPage.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import ProjectsPage from './ProjectsPage'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/ReviewBoard.test.tsx
--- frontend/src/components/ReviewBoard.test.tsx
+++ frontend/src/components/ReviewBoard.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import ReviewBoard from './ReviewBoard'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/ReviewBoard.tsx
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState, type ReactNode } from 'react'
+import { useEffect, useRef, useState, type ReactNode } from 'react'
 import {
   bulkDeleteReview,
   bulkUpdateReview,
frontend/src/components/SystemPage.test.tsx
--- frontend/src/components/SystemPage.test.tsx
+++ frontend/src/components/SystemPage.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import SystemPage from './SystemPage'
 import { withCodes } from '../codes/fixtures'
frontend/src/components/Timeline.test.tsx
--- frontend/src/components/Timeline.test.tsx
+++ frontend/src/components/Timeline.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import Timeline from './Timeline'
 import { withCodes } from '../codes/fixtures'
src/main/java/kr/itn/itnhub/process/ProcessParser.java
--- src/main/java/kr/itn/itnhub/process/ProcessParser.java
+++ src/main/java/kr/itn/itnhub/process/ProcessParser.java
@@ -43,35 +43,36 @@
 
     private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
-    private static final int COL_SEQ = 0;               // A
-    private static final int COL_SITE_NAME = 1;         // B
-    private static final int COL_CATEGORY = 2;          // C
-    private static final int COL_BOARD_NAME = 3;        // D
-    private static final int COL_POST_TITLE = 4;        // E
-    private static final int COL_URL = 5;                // F
-    private static final int COL_DESCRIPTION = 6;       // G
-    private static final int COL_HAS_ATTACHMENT = 7;    // H
-    private static final int COL_PRIOR_KOGL_TYPE = 8;   // I
-    private static final int COL_CONTRACT_DOCS = 9;     // J
-    private static final int COL_PRODUCED_DATE = 10;    // K
-    private static final int COL_PUBLISHED_DATE = 11;   // L
-    private static final int COL_REVIEW_MAJOR = 12;     // M
-    private static final int COL_REVIEW_MINOR = 13;     // N
-    private static final int COL_REVIEW_RESULT = 14;    // O
-    private static final int COL_REVIEW_KOGL_TYPE = 15; // P
-    private static final int COL_REVIEW_AI_TYPE = 16;   // Q
-    private static final int COL_REVIEW_OPINION = 17;   // R
-    private static final int COL_REVIEW_NOTE = 18;      // S
+    static final int COL_SEQ = 0;               // A
+    static final int COL_SITE_NAME = 1;         // B
+    static final int COL_CATEGORY = 2;          // C
+    static final int COL_BOARD_NAME = 3;        // D
+    static final int COL_POST_TITLE = 4;        // E
+    static final int COL_URL = 5;                // F
+    static final int COL_DESCRIPTION = 6;       // G
+    static final int COL_HAS_ATTACHMENT = 7;    // H
+    static final int COL_PRIOR_KOGL_TYPE = 8;   // I
+    static final int COL_CONTRACT_DOCS = 9;     // J
+    static final int COL_PRODUCED_DATE = 10;    // K
+    static final int COL_PUBLISHED_DATE = 11;   // L
+    static final int COL_REVIEW_MAJOR = 12;     // M
+    static final int COL_REVIEW_MINOR = 13;     // N
+    static final int COL_REVIEW_RESULT = 14;    // O
+    static final int COL_REVIEW_KOGL_TYPE = 15; // P
+    static final int COL_REVIEW_AI_TYPE = 16;   // Q
+    static final int COL_REVIEW_OPINION = 17;   // R
+    static final int COL_REVIEW_NOTE = 18;      // S
     // 실파일에서 헤더는 T열에 '기존 증빙자료'라고 적혀 있지만 실제 데이터는 U열에 있다
     // (T열에는 엑셀 화면용 '수정' 링크 텍스트가 들어 있음 - 데이터 기준으로 U를 읽는다)
-    private static final int COL_PRIOR_EVIDENCE = 20;   // U
-    // 20 = U, 수정 링크 열, 건너뜀
-    private static final int COL_JUDGED_KOGL_TYPE = 21; // V
-    private static final int COL_JUDGED_AI_TYPE = 22;   // W
-    private static final int COL_FINAL_OPINION = 23;    // X
+    static final int COL_PRIOR_EVIDENCE = 20;   // U
+    /** 헤더('기존 증빙자료')가 적힌 열. 이 시스템이 만드는 다운로드 파일은 여기에 값을 쓴다. */
+    static final int COL_PRIOR_EVIDENCE_HEADER = 19; // T
+    static final int COL_JUDGED_KOGL_TYPE = 21; // V
+    static final int COL_JUDGED_AI_TYPE = 22;   // W
+    static final int COL_FINAL_OPINION = 23;    // X
     // 24 = Y, 빈 열, 건너뜀
-    private static final int COL_JUDGMENT_BASIS = 25;   // Z
-    private static final int COL_PROCESS_STATUS = 26;   // AA
+    static final int COL_JUDGMENT_BASIS = 25;   // Z
+    static final int COL_PROCESS_STATUS = 26;   // AA
 
     public List<ProcessRow> parse(InputStream xlsx) {
         try (Workbook wb = WorkbookFactory.create(xlsx)) {
@@ -124,7 +125,7 @@
                     nullIfBlank(string(row, COL_REVIEW_AI_TYPE)),
                     nullIfBlank(string(row, COL_REVIEW_OPINION)),
                     nullIfBlank(string(row, COL_REVIEW_NOTE)),
-                    nullIfBlank(string(row, COL_PRIOR_EVIDENCE)),
+                    priorEvidence(row),
                     nullIfBlank(string(row, COL_JUDGED_KOGL_TYPE)),
                     nullIfBlank(string(row, COL_JUDGED_AI_TYPE)),
                     nullIfBlank(string(row, COL_FINAL_OPINION)),
@@ -134,6 +135,21 @@
         return result;
     }
 
+    /**
+     * 기존 증빙자료는 파일마다 값이 있는 열이 다르다.
+     *
+     * <p>기관이 보내오는 원본은 헤더가 T열인데 데이터가 U열에 있고(T에는 엑셀 화면용 '수정'
+     * 링크 텍스트가 들어 있다), 이 시스템이 만들어 주는 다운로드 파일은 헤더 자리인 T열에
+     * 값을 쓴다. 한쪽만 읽으면 "내려받아 고쳐서 다시 올리는" 흐름에서 값이 조용히 사라지므로
+     * U를 먼저 보고, 비어 있으면 T를 본다.</p>
+     */
+    private String priorEvidence(Row row) {
+        String fromDataColumn = nullIfBlank(string(row, COL_PRIOR_EVIDENCE));
+        return fromDataColumn != null
+                ? fromDataColumn
+                : nullIfBlank(string(row, COL_PRIOR_EVIDENCE_HEADER));
+    }
+
     private String string(Row row, int columnIndex) {
         Cell cell = row.getCell(columnIndex);
         if (cell == null) {
src/main/java/kr/itn/itnhub/process/ProcessService.java (Binary)
--- 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
+++ src/main/resources/mapper/DashboardMapper.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="kr.itn.itnhub.dashboard.DashboardMapper">
src/main/resources/mapper/ProcessItemMapper.xml
--- src/main/resources/mapper/ProcessItemMapper.xml
+++ src/main/resources/mapper/ProcessItemMapper.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="kr.itn.itnhub.process.ProcessItemMapper">
 
src/test/java/kr/itn/itnhub/process/ProcessExcelRoundTripTest.java (added)
+++ src/test/java/kr/itn/itnhub/process/ProcessExcelRoundTripTest.java
@@ -0,0 +1,166 @@
+package kr.itn.itnhub.process;
+
+import kr.itn.itnhub.AbstractDbTest;
+import kr.itn.itnhub.org.Organization;
+import kr.itn.itnhub.org.OrganizationMapper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 권리처리 엑셀의 업로드↔다운로드 왕복.
+ *
+ * <p>업로드는 컬럼 번호({@code ProcessParser.COL_*})로, 다운로드는 헤더 이름
+ * ({@code ProcessService.COLUMN_HEADERS})으로 각각 따로 정의돼 있다. 두 정의가 어긋나면
+ * "내려받아 고쳐서 다시 올리는" 흐름에서 값이 조용히 사라진다. 그 어긋남을 여기서 잡는다.</p>
+ */
+class ProcessExcelRoundTripTest extends AbstractDbTest {
+
+    @Autowired
+    ProcessService service;
+
+    @Autowired
+    ProcessParser parser;
+
+    @Autowired
+    OrganizationMapper orgMapper;
+
+    @Autowired
+    JdbcTemplate jdbc;
+
+    private Long orgId;
+
+    @BeforeEach
+    void setUp() {
+        jdbc.update("delete from process_item");
+        jdbc.update("delete from organization where org_no = '920'");
+
+        Organization org = new Organization();
+        org.setOrgNo("920");
+        org.setOrgName("왕복테스트기관");
+        org.setChannelSlug("920");
+        orgMapper.upsertBySeed(org);
+        orgId = orgMapper.findByOrgNoAndOrgName("920", "왕복테스트기관").getId();
+    }
+
+    @AfterEach
+    void cleanUp() {
+        jdbc.update("delete from process_item");
+        jdbc.update("delete from organization where org_no = '920'");
+    }
+
+    /** 헤더 이름과 파서가 읽는 컬럼이 같은 자리를 가리키는지. 한쪽만 고치면 여기서 깨진다. */
+    @Test
+    void 헤더_이름과_파서_컬럼이_같은_자리를_가리킨다() {
+        assertHeader(ProcessParser.COL_SEQ, "순번");
+        assertHeader(ProcessParser.COL_SITE_NAME, "사이트명");
+        assertHeader(ProcessParser.COL_CATEGORY, "범주");
+        assertHeader(ProcessParser.COL_BOARD_NAME, "게시판명");
+        assertHeader(ProcessParser.COL_POST_TITLE, "게시물제목");
+        assertHeader(ProcessParser.COL_URL, "URL주소");
+        assertHeader(ProcessParser.COL_DESCRIPTION, "설명");
+        assertHeader(ProcessParser.COL_HAS_ATTACHMENT, "첨부파일여부");
+        assertHeader(ProcessParser.COL_PRIOR_KOGL_TYPE, "기존 공공누리유형(+AI)");
+        assertHeader(ProcessParser.COL_CONTRACT_DOCS, "계약서 유무");
+        assertHeader(ProcessParser.COL_PRODUCED_DATE, "제작일");
+        assertHeader(ProcessParser.COL_PUBLISHED_DATE, "공표일");
+        assertHeader(ProcessParser.COL_REVIEW_MAJOR, "권리확인(대분류)");
+        assertHeader(ProcessParser.COL_REVIEW_MINOR, "권리확인(세부)");
+        assertHeader(ProcessParser.COL_REVIEW_RESULT, "처리 구분");
+        assertHeader(ProcessParser.COL_REVIEW_KOGL_TYPE, "공공누리 유형");
+        assertHeader(ProcessParser.COL_REVIEW_AI_TYPE, "AI유형");
+        assertHeader(ProcessParser.COL_REVIEW_OPINION, "의견");
+        assertHeader(ProcessParser.COL_REVIEW_NOTE, "비고");
+        assertHeader(ProcessParser.COL_JUDGED_KOGL_TYPE, "공공누리유형(판정)");
+        assertHeader(ProcessParser.COL_JUDGED_AI_TYPE, "AI유형(판정)");
+        assertHeader(ProcessParser.COL_FINAL_OPINION, "최종의견");
+        assertHeader(ProcessParser.COL_JUDGMENT_BASIS, "판단근거");
+        assertHeader(ProcessParser.COL_PROCESS_STATUS, "처리상태");
+    }
+
+    private void assertHeader(int column, String expected) {
+        assertThat(ProcessService.COLUMN_HEADERS[column])
+                .as("%d번 열의 헤더", column)
+                .isEqualTo(expected);
+    }
+
+    /**
+     * 기존 증빙자료는 예외다. 기관이 보내오는 실제 파일은 헤더가 T열인데 데이터가 U열에 있어
+     * 파서가 U를 먼저 읽고, 비어 있으면 헤더 자리인 T를 읽는다. 이 어긋남 자체를 명시적으로
+     * 남겨 두어, 나중에 누가 "헤더랑 다르네" 하고 한쪽만 고치다 깨뜨리지 않게 한다.
+     */
+    @Test
+    void 기존_증빙자료만_헤더와_데이터_열이_다르다() {
+        assertThat(ProcessService.COLUMN_HEADERS[19]).isEqualTo("기존 증빙자료");
+        assertThat(ProcessParser.COL_PRIOR_EVIDENCE_HEADER).isEqualTo(19);
+        assertThat(ProcessParser.COL_PRIOR_EVIDENCE).isEqualTo(20);
+    }
+
+    /** 기관 원본처럼 U열에 값이 있는 파일도 그대로 읽어야 한다(다운로드 파일만 고치면 안 된다). */
+    @Test
+    void 원본처럼_U열에_값이_있어도_읽는다() throws Exception {
+        byte[] xlsx = ProcessParserFixtures.withPriorEvidenceAt(ProcessParser.COL_PRIOR_EVIDENCE, "원본증빙");
+
+        List<ProcessRow> rows = parser.parse(new ByteArrayInputStream(xlsx));
+
+        assertThat(rows).hasSize(1);
+        assertThat(rows.get(0).priorEvidence()).isEqualTo("원본증빙");
+    }
+
+    /** 내려받은 파일을 그대로 다시 올렸을 때 값이 살아남아야 한다. */
+    @Test
+    void 내려받아_다시_올려도_값이_그대로다() {
+        jdbc.update("insert into process_item (org_id, seq, site_name, category, board_name, "
+                        + "post_title, url, description, has_attachment, prior_kogl_type, contract_docs, "
+                        + "produced_date, published_date, review_major, review_minor, review_result, "
+                        + "review_kogl_type, review_ai_type, review_opinion, review_note, prior_evidence, "
+                        + "judged_kogl_type, judged_ai_type, final_opinion, judgment_basis, process_status) "
+                        + "values (?, 1, '사이트', '범주', '게시판', '제목', 'http://x', '설명', '있음', "
+                        + "'4유형', '양도계약서', '2026-01-02', '2026-01-03', '제3자 권리', '초상권 포함', "
+                        + "'권리처리 추진', '보류', 'Y', '의견', '비고', '증빙자료', '0유형', 'Y', "
+                        + "'최종의견', '판단근거', '처리완료')",
+                orgId);
+
+        byte[] xlsx = service.downloadWorkbook(orgId);
+        List<ProcessRow> rows = parser.parse(new ByteArrayInputStream(xlsx));
+
+        assertThat(rows).hasSize(1);
+        ProcessRow row = rows.get(0);
+
+        assertThat(row.siteName()).isEqualTo("사이트");
+        assertThat(row.category()).isEqualTo("범주");
+        assertThat(row.boardName()).isEqualTo("게시판");
+        assertThat(row.postTitle()).isEqualTo("제목");
+        assertThat(row.url()).isEqualTo("http://x");
+        assertThat(row.description()).isEqualTo("설명");
+        assertThat(row.hasAttachment()).isEqualTo("있음");
+        assertThat(row.priorKoglType()).isEqualTo("4유형");
+        assertThat(row.contractDocs()).isEqualTo("양도계약서");
+        assertThat(row.producedDate()).isEqualTo("2026-01-02");
+        assertThat(row.publishedDate()).isEqualTo("2026-01-03");
+        assertThat(row.reviewMajor()).isEqualTo("제3자 권리");
+        assertThat(row.reviewMinor()).isEqualTo("초상권 포함");
+        assertThat(row.reviewResult()).isEqualTo("권리처리 추진");
+        assertThat(row.reviewKoglType()).isEqualTo("보류");
+        assertThat(row.reviewAiType()).isEqualTo("Y");
+        assertThat(row.reviewOpinion()).isEqualTo("의견");
+        assertThat(row.reviewNote()).isEqualTo("비고");
+        assertThat(row.judgedKoglType()).isEqualTo("0유형");
+        assertThat(row.judgedAiType()).isEqualTo("Y");
+        assertThat(row.finalOpinion()).isEqualTo("최종의견");
+        assertThat(row.judgmentBasis()).isEqualTo("판단근거");
+        assertThat(row.processStatus()).isEqualTo("처리완료");
+
+        // 기존 증빙자료는 다운로드가 T열에 쓰고 파서가 U열을 읽어 왕복에서 사라진다.
+        assertThat(row.priorEvidence())
+                .as("내려받아 다시 올렸을 때 기존 증빙자료")
+                .isEqualTo("증빙자료");
+    }
+}
 
src/test/java/kr/itn/itnhub/process/ProcessParserFixtures.java (added)
+++ src/test/java/kr/itn/itnhub/process/ProcessParserFixtures.java
@@ -0,0 +1,35 @@
+package kr.itn.itnhub.process;
+
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+import java.io.ByteArrayOutputStream;
+
+/** 권리처리 엑셀 테스트용 최소 통합문서. 1행 그룹제목, 2행 헤더, 3행부터 데이터. */
+final class ProcessParserFixtures {
+
+    private ProcessParserFixtures() {
+    }
+
+    /** 순번 1행짜리 시트를 만들고, 기존 증빙자료 값을 지정한 열에만 넣는다. */
+    static byte[] withPriorEvidenceAt(int column, String value) throws Exception {
+        try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            Sheet sheet = wb.createSheet(ProcessParser.PRIMARY_SHEET_NAME);
+            sheet.createRow(0).createCell(0).setCellValue("게시물 정보(조사원)");
+
+            Row header = sheet.createRow(1);
+            for (int i = 0; i < ProcessService.COLUMN_HEADERS.length; i++) {
+                header.createCell(i).setCellValue(ProcessService.COLUMN_HEADERS[i]);
+            }
+
+            Row data = sheet.createRow(2);
+            data.createCell(ProcessParser.COL_SEQ).setCellValue("1");
+            data.createCell(ProcessParser.COL_SITE_NAME).setCellValue("사이트");
+            data.createCell(column).setCellValue(value);
+
+            wb.write(out);
+            return out.toByteArray();
+        }
+    }
+}
Add a comment
List