diff --git a/dap-was-cus/src/main/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadController.java b/dap-was-cus/src/main/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadController.java index 4ffe7657..7088729f 100644 --- a/dap-was-cus/src/main/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadController.java +++ b/dap-was-cus/src/main/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadController.java @@ -11,7 +11,9 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import java.util.regex.Pattern; import org.apache.poi.ss.usermodel.*; @@ -45,6 +47,7 @@ public class DtoExcelDownloadController { private static final String MCI_BASE_PACKAGE = "io.shinhanlife.dap.mcc.infra.itrf.mci"; private static final String INVALID_DTO_MESSAGE = "dto의 내용이 엑셀파일 양식에 맞지않습니다 파일을 확인해주세요"; + private static final Pattern VARIANT_SUFFIX = Pattern.compile("_[IO]$"); private final Map dtoClasses; @@ -54,8 +57,12 @@ public class DtoExcelDownloadController { @GetMapping("/dto-download/options") public List options() { - // 클래스패스에서 자동 검색된 DTO 목록을 화면에 제공한다. - return List.copyOf(dtoClasses.keySet()); + // DTO는 항상 _I/_O 한 쌍으로 존재하므로 접미사를 제거한 이름 단위로 묶어 화면에 제공한다. + Set baseNames = new TreeSet<>(); + for (String simpleName : dtoClasses.keySet()) { + baseNames.add(VARIANT_SUFFIX.matcher(simpleName).replaceFirst("")); + } + return List.copyOf(baseNames); } @GetMapping("/dto-download/{dtoName}") @@ -328,7 +335,9 @@ public class DtoExcelDownloadController { private Annotation telegramMetadata(Field field) { for (Annotation annotation : field.getDeclaredAnnotations()) { - if (annotation.annotationType().getSimpleName().equals("GlowTrgmField")) { + String annotationName = annotation.annotationType().getSimpleName(); + if (annotationName.equals("GlowTrgmField") + || annotationName.equals("GlowMciFieldInfo")) { return annotation; } } diff --git a/dap-was-lib/src/main/resources/static/tool-test-console.html b/dap-was-lib/src/main/resources/static/tool-test-console.html index a6ad6a6d..300410cd 100644 --- a/dap-was-lib/src/main/resources/static/tool-test-console.html +++ b/dap-was-lib/src/main/resources/static/tool-test-console.html @@ -94,7 +94,7 @@ const select = $('toolSelect'), args = $('arguments'), result = $('result'); async function loadDtoOptions() { - // 서버가 itrf/mci/**/io 패키지에서 찾은 DTO 목록으로 셀렉트박스를 구성한다. + // 서버가 itrf/mci/**/io 패키지에서 찾은 DTO를 _I/_O 접미사 없이 이름 단위로 묶어 셀렉트박스를 구성한다. const dtoSelect = $('dtoSelect'); try { const response = await fetch('/dto-download/options', { headers: { 'Cache-Control': 'no-cache' } }); @@ -104,7 +104,7 @@ for (const dtoName of dtoNames) { const option = document.createElement('option'); option.value = dtoName; - option.textContent = `${dtoName}.java`; + option.textContent = dtoName; dtoSelect.appendChild(option); } if (!dtoNames.length) dtoSelect.innerHTML = ''; @@ -115,34 +115,51 @@ function openDtoModal() { $('dtoDownloadModal').classList.remove('hidden'); loadDtoOptions(); $('dtoSelect').focus(); } function closeDtoModal() { $('dtoDownloadModal').classList.add('hidden'); $('dtoDownloadButton').focus(); } async function downloadSelectedDto() { - // 선택한 DTO의 엑셀 생성 API를 호출하고 응답 Blob을 브라우저 다운로드로 저장한다. - const dtoName = $('dtoSelect').value; - if (!dtoName) return alert('다운로드할 DTO를 선택해주세요.'); + // 선택한 DTO 이름의 _I/_O 엑셀 생성 API를 순차 호출해 존재하는 파일을 각각 브라우저 다운로드로 저장한다. + const baseName = $('dtoSelect').value; + if (!baseName) return alert('다운로드할 DTO를 선택해주세요.'); const confirmButton = $('dtoConfirmButton'); confirmButton.disabled = true; + const errors = []; + let successCount = 0; try { - const response = await fetch(`/dto-download/${encodeURIComponent(dtoName)}`); - if (!response.ok) { - // 422는 GlowTrgmField 메타데이터가 없어 설계서 양식으로 변환할 수 없는 경우다. - const message = (await response.text()).trim(); - throw new Error(response.status === 422 - ? 'dto의 내용이 엑셀파일 양식에 맞지않습니다 파일을 확인해주세요' - : message || (response.status === 404 - ? `${dtoName}.xlsx 파일을 찾을 수 없습니다.` - : `다운로드에 실패했습니다. (HTTP ${response.status})`)); + for (const variant of ['I', 'O']) { + const dtoName = `${baseName}_${variant}`; + try { + const response = await fetch(`/dto-download/${encodeURIComponent(dtoName)}`, { + cache: 'no-store', + headers: { 'Cache-Control': 'no-cache' } + }); + if (response.status === 404) continue; // _I/_O 중 한쪽만 존재하는 DTO는 정상 케이스이므로 건너뛴다. + if (!response.ok) { + // 422는 GlowTrgmField 메타데이터가 없어 설계서 양식으로 변환할 수 없는 경우다. + const message = (await response.text()).trim(); + throw new Error(response.status === 422 + ? `${dtoName}: dto의 내용이 엑셀파일 양식에 맞지않습니다 파일을 확인해주세요` + : message || `${dtoName} 다운로드에 실패했습니다. (HTTP ${response.status})`); + } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${dtoName}.xlsx`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + successCount += 1; + // 브라우저가 연속 다운로드를 한 번에 차단하지 않도록 다음 요청 전에 짧은 간격을 둔다. + await new Promise(resolve => setTimeout(resolve, 300)); + } catch (error) { + errors.push(error.message); + } } - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${dtoName}.xlsx`; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); + if (successCount === 0) { + alert(errors.length ? errors.join('\n') : `${baseName} 파일을 찾을 수 없습니다.`); + return; + } + if (errors.length) alert(errors.join('\n')); closeDtoModal(); - } catch (error) { - alert(error.message); } finally { confirmButton.disabled = false; }