package kr.itn.itnhub.contact; import kr.itn.itnhub.AbstractDbTest; import kr.itn.itnhub.org.Organization; import kr.itn.itnhub.org.OrganizationMapper; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import java.io.ByteArrayOutputStream; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @AutoConfigureMockMvc @WithMockUser(roles = "ADMIN") class MemberImportControllerTest extends AbstractDbTest { @Autowired MockMvc mvc; @Autowired ContactMapper contactMapper; @Autowired OrganizationMapper orgMapper; @Autowired JdbcTemplate jdbc; @BeforeEach void clean() { jdbc.update("delete from organization"); jdbc.update("delete from contact"); } /** 회원정보 시트: 1행 헤더, 2행부터 데이터. A=순번 B=구분 C=기관명 D=부서명 E=담당자명 F=직급 G=전화 H=이메일 */ private MockMultipartFile workbook(String[]... dataRows) throws Exception { try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { Sheet sheet = wb.createSheet(MemberDirectoryParser.SHEET_NAME); Row header = sheet.createRow(0); String[] headers = {"순번", "구분", "기관명", "부서명", "담당자명", "직급/직함", "유선전화", "이메일"}; for (int i = 0; i < headers.length; i++) { header.createCell(i).setCellValue(headers[i]); } int rowNum = 1; for (String[] data : dataRows) { Row row = sheet.createRow(rowNum++); for (int i = 0; i < data.length; i++) { if (data[i] == null) { continue; } row.createCell(i).setCellValue(data[i]); } } wb.write(out); return new MockMultipartFile("file", "members.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", out.toByteArray()); } } @Test void 구분을_주관기관_변호사_신청기관_수행기관으로_각각_매핑한다() throws Exception { MockMultipartFile file = workbook( new String[]{"1", "주관기관", null, "저작권정책과", "김문정", "사무관", "02-1", "mj@mcst.go.kr"}, new String[]{"2", "변호사", null, null, "이변호", null, "02-2", "law@firm.kr"}, new String[]{"3", "신청기관", "국제방송교류재단", "데이터정보화팀", "송민지", "과장", "02-3", "ming@arirang.com"}, new String[]{"4", "수행기관", "수행사", "운영팀", "홍수행", "대리", "02-4", "op@vendor.kr"}); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").value(4)) .andExpect(jsonPath("$.skipped").value(0)); List all = contactMapper.findAll(null); assertThat(all).hasSize(4); assertThat(all).extracting(Contact::getCategory) .containsExactlyInAnyOrder("MJ", "LAWYER", "APPLICANT", "OPERATOR"); Contact operator = all.stream().filter(c -> "OPERATOR".equals(c.getCategory())).findFirst().orElseThrow(); assertThat(operator.getName()).isEqualTo("홍수행"); assertThat(operator.getAffiliation()).isEqualTo("수행사"); assertThat(operator.getDeptName()).isEqualTo("운영팀"); } @Test void 같은_사람이_여러_행에_있으면_한_명만_생성되고_두번째부터는_갱신으로_집계된다() throws Exception { MockMultipartFile file = workbook( new String[]{"1", "신청기관", "경찰청", "데이터정책계", "박진우", "경위", "02-3150-3205", "hi@police.go.kr"}, new String[]{"2", "신청기관", "서울청", "데이터정책계", "박진우", "경위", "02-3150-3205", "hi@police.go.kr"}, new String[]{"3", "신청기관", "부산청", "데이터정책계", "박진우", "경위", "02-3150-3205", "hi@police.go.kr"}); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").value(1)) .andExpect(jsonPath("$.updated").value(2)); assertThat(jdbc.queryForObject("select count(*) from contact", Integer.class)).isEqualTo(1); } @Test void 신청기관_담당자는_기관명이_같은_기관에_자동_배정된다() throws Exception { Organization org = new Organization(); org.setOrgNo("001_00"); org.setOrgName("국제방송교류재단"); org.setChannelSlug("001_00"); orgMapper.upsertBySeed(org); Long orgId = orgMapper.findAll().get(0).getId(); assertThat(orgMapper.findById(orgId).getApplicantContactId()).isNull(); MockMultipartFile file = workbook(new String[]{ "1", "신청기관", "국제방송교류재단", "데이터정보화팀", "송민지", "과장", "02-3475-5434", "ming@arirang.com"}); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.assigned").value(1)); Organization after = orgMapper.findById(orgId); assertThat(after.getApplicantContactId()).isNotNull(); assertThat(after.getApplicant().getName()).isEqualTo("송민지"); } @Test void 이미_배정된_기관은_재업로드해도_배정이_바뀌지_않는다() throws Exception { Organization org = new Organization(); org.setOrgNo("001_00"); org.setOrgName("국제방송교류재단"); org.setChannelSlug("001_00"); orgMapper.upsertBySeed(org); Long orgId = orgMapper.findAll().get(0).getId(); MockMultipartFile file = workbook(new String[]{ "1", "신청기관", "국제방송교류재단", "데이터정보화팀", "송민지", "과장", "02-3475-5434", "ming@arirang.com"}); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()); Long firstContactId = orgMapper.findById(orgId).getApplicantContactId(); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.assigned").value(0)); assertThat(orgMapper.findById(orgId).getApplicantContactId()).isEqualTo(firstContactId); } @Test void 알수없는_구분은_건너뛰고_skipped로_집계된다() throws Exception { MockMultipartFile file = workbook( new String[]{"1", "신청기관", "국제방송교류재단", "팀", "송민지", "과장", "02-1", "a@b.kr"}, new String[]{"2", "알수없음", "어떤기관", "팀", "홍길동", "대리", "02-2", "b@c.kr"}); mvc.perform(multipart("/api/contacts/import").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").value(1)) .andExpect(jsonPath("$.skipped").value(1)); assertThat(jdbc.queryForObject("select count(*) from contact", Integer.class)).isEqualTo(1); } @Test void 확장자가_xlsx_xlsm이_아니면_400이다() throws Exception { MockMultipartFile txt = new MockMultipartFile("file", "members.txt", "text/plain", "아무 내용".getBytes()); mvc.perform(multipart("/api/contacts/import").file(txt).with(csrf())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(containsString("xlsx"))); } }