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 deleted file mode 100644 index 7088729f7..000000000 --- a/dap-was-cus/src/main/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadController.java +++ /dev/null @@ -1,433 +0,0 @@ -package io.shinhanlife.dap.mcc.presentation; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -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.*; -import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.XSSFColor; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; -import org.springframework.core.type.filter.RegexPatternTypeFilter; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -/** - * MCI DTO 클래스를 조회하고 인터페이스 설계서 형식의 엑셀 파일을 생성한다. - * 외부 템플릿 파일에 의존하지 않고 Apache POI로 양식과 데이터를 모두 만든다. - */ -@RestController -public class DtoExcelDownloadController { - - private static final int FIRST_FIELD_ROW = 12; - private static final int TEMPLATE_LAST_ROW = 35; - private static final int COLUMN_COUNT = 20; - private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - 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; - - public DtoExcelDownloadController() { - this.dtoClasses = scanDtoClasses(); - } - - @GetMapping("/dto-download/options") - public List options() { - // 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}") - public ResponseEntity download(@PathVariable String dtoName) throws IOException { - // 스캔되지 않은 이름을 받아 임의 클래스를 조회하지 못하도록 제한한다. - String className = dtoClasses.get(dtoName); - if (className == null) { - return ResponseEntity.notFound().build(); - } - byte[] workbook = createWorkbook(dtoName, className); - String fileName = dtoName + ".xlsx"; - return ResponseEntity.ok() - .contentType(XLSX_MEDIA_TYPE) - .contentLength(workbook.length) - .header(HttpHeaders.CONTENT_DISPOSITION, - ContentDisposition.attachment().filename(fileName).build().toString()) - .body(workbook); - } - - @ExceptionHandler(DtoFormatException.class) - public ResponseEntity handleInvalidDto() { - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .contentType(MediaType.parseMediaType("text/plain;charset=UTF-8")) - .body(INVALID_DTO_MESSAGE); - } - - private byte[] createWorkbook(String dtoName, String className) throws IOException { - // 요청마다 새 워크북을 생성하므로 여러 사용자의 다운로드가 서로 영향을 주지 않는다. - try (XSSFWorkbook workbook = createTemplateWorkbook(); - ByteArrayOutputStream output = new ByteArrayOutputStream()) { - Sheet sheet = workbook.getSheetAt(0); - setText(sheet, 2, 2, dtoName); - setText(sheet, 3, 2, dtoName); - - List fields; - try { - fields = describeFields(Class.forName(className)); - } catch (ReflectiveOperationException error) { - throw new IOException("DTO class could not be inspected: " + className, error); - } - writeFields(sheet, fields); - workbook.write(output); - return output.toByteArray(); - } - } - - private XSSFWorkbook createTemplateWorkbook() { - // 기준 문서의 시트명, 열 너비, 병합, 색상과 테두리를 코드로 재현한다. - XSSFWorkbook workbook = new XSSFWorkbook(); - Sheet sheet = workbook.createSheet("대내"); - sheet.setDisplayGridlines(false); - sheet.createFreezePane(0, 12); - sheet.getPrintSetup().setLandscape(true); - sheet.setRepeatingRows(new CellRangeAddress(11, 11, -1, -1)); - - double[] widths = {4.44, 8, 23.22, 23.22, 10, 14, 18, 11, 9, 7.44, - 8, 9.55, 9, 10.55, 11.44, 9, 13, 10.55, 14, 30}; - for (int column = 0; column < widths.length; column++) { - sheet.setColumnWidth(column, (int) (widths[column] * 256)); - } - - CellStyle titleStyle = style(workbook, "000000", "FFFFFF", true, (short) 14, - HorizontalAlignment.CENTER, false); - CellStyle sectionStyle = style(workbook, "F2F2F2", "000000", true, (short) 10, - HorizontalAlignment.CENTER, false); - CellStyle labelStyle = borderedStyle(workbook, "F2F2F2", true, HorizontalAlignment.CENTER); - CellStyle inputStyle = borderedStyle(workbook, "C6D9F1", false, HorizontalAlignment.LEFT); - CellStyle requiredStyle = borderedStyle(workbook, "C6D9F1", false, HorizontalAlignment.LEFT); - CellStyle autoStyle = borderedStyle(workbook, "F2DCDB", false, HorizontalAlignment.LEFT); - CellStyle userStyle = borderedStyle(workbook, "FFFFFF", false, HorizontalAlignment.LEFT); - CellStyle headerStyle = borderedStyle(workbook, "D9D9D9", true, HorizontalAlignment.CENTER); - headerStyle.setWrapText(true); - CellStyle whiteDataStyle = borderedStyle(workbook, "FFFFFF", false, HorizontalAlignment.CENTER); - CellStyle blueDataStyle = borderedStyle(workbook, "C6D9F1", false, HorizontalAlignment.CENTER); - CellStyle pinkDataStyle = borderedStyle(workbook, "F2DCDB", false, HorizontalAlignment.CENTER); - - createStyledRow(sheet, 0, 25.5f, titleStyle); - merge(sheet, "A1:T1"); - setText(sheet, 0, 0, "인터페이스 설계서(대내)"); - - createStyledRow(sheet, 1, 18f, sectionStyle); - merge(sheet, "A2:T2"); - setText(sheet, 1, 0, "기본정보"); - - String[] labels = {"코드", "한글명", "영문명", "암호화", "유형", "레코드구분자", "필드구분자"}; - for (int index = 0; index < labels.length; index++) { - int rowIndex = index + 2; - Row row = sheet.createRow(rowIndex); - cell(row, 1, labelStyle).setCellValue(labels[index]); - cell(row, 2, inputStyle); - cell(row, 3, inputStyle); - merge(sheet, "C" + (rowIndex + 1) + ":D" + (rowIndex + 1)); - } - setText(sheet, 6, 2, "json"); - sheet.getRow(7).setHeightInPoints(24); - sheet.getRow(8).setHeightInPoints(24); - - for (int rowIndex = 3; rowIndex <= 5; rowIndex++) { - Row row = sheet.getRow(rowIndex); - CellStyle legendStyle = rowIndex == 3 ? requiredStyle : rowIndex == 4 ? autoStyle : userStyle; - cell(row, 5, legendStyle); - cell(row, 6, legendStyle); - merge(sheet, "F" + (rowIndex + 1) + ":G" + (rowIndex + 1)); - } - setText(sheet, 3, 7, "필수입력"); - setText(sheet, 4, 7, "필드자동채우기(메타시스템 연동시)"); - setText(sheet, 5, 7, "사용자입력(필요시)"); - - createStyledRow(sheet, 10, 18f, sectionStyle); - merge(sheet, "A11:T11"); - setText(sheet, 10, 0, "필드정보"); - - String[] headers = {"NO", "Level", "한글명", "부모식별자(한글명)", "끝수여부", "영문명", - "부모식별자(영문명)", "데이터유형", "필드길이", "SCALE", "기본값", "정렬기준", - "채움문자", "암호화방식", "메타체크여부", "한글여부", "소수점포함여부", - "마스킹여부", "마스킹패턴코드", "비고"}; - Row header = sheet.createRow(11); - header.setHeightInPoints(30); - for (int column = 0; column < headers.length; column++) { - cell(header, column, headerStyle).setCellValue(headers[column]); - } - - for (int rowIndex = FIRST_FIELD_ROW; rowIndex <= TEMPLATE_LAST_ROW; rowIndex++) { - Row row = sheet.createRow(rowIndex); - row.setHeightInPoints(15.75f); - for (int column = 0; column < COLUMN_COUNT; column++) { - CellStyle dataStyle; - if (column == 0 || column == 4 || (column >= 14 && column <= 16) || column == 19) { - dataStyle = whiteDataStyle; - } else if (column >= 1 && column <= 3) { - dataStyle = blueDataStyle; - } else { - dataStyle = pinkDataStyle; - } - cell(row, column, dataStyle); - } - } - return workbook; - } - - private CellStyle style(XSSFWorkbook workbook, String fillColor, String fontColor, - boolean bold, short fontSize, HorizontalAlignment alignment, - boolean bordered) { - CellStyle style = workbook.createCellStyle(); - style.setAlignment(alignment); - style.setVerticalAlignment(VerticalAlignment.CENTER); - style.setFillForegroundColor(new XSSFColor(java.awt.Color.decode("#" + fillColor), null)); - style.setFillPattern(FillPatternType.SOLID_FOREGROUND); - Font font = workbook.createFont(); - font.setFontName("맑은 고딕"); - font.setFontHeightInPoints(fontSize); - font.setBold(bold); - font.setColor("FFFFFF".equals(fontColor) ? IndexedColors.WHITE.getIndex() : IndexedColors.BLACK.getIndex()); - style.setFont(font); - if (bordered) setBorders(style); - return style; - } - - private CellStyle borderedStyle(XSSFWorkbook workbook, String fillColor, - boolean bold, HorizontalAlignment alignment) { - return style(workbook, fillColor, "000000", bold, (short) 9, alignment, true); - } - - private void setBorders(CellStyle style) { - style.setBorderTop(BorderStyle.THIN); - style.setBorderBottom(BorderStyle.THIN); - style.setBorderLeft(BorderStyle.THIN); - style.setBorderRight(BorderStyle.THIN); - style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); - style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); - style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); - style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); - } - - private void createStyledRow(Sheet sheet, int rowIndex, float height, CellStyle style) { - Row row = sheet.createRow(rowIndex); - row.setHeightInPoints(height); - for (int column = 0; column < COLUMN_COUNT; column++) cell(row, column, style); - } - - private Cell cell(Row row, int column, CellStyle style) { - Cell cell = row.createCell(column); - cell.setCellStyle(style); - return cell; - } - - private void merge(Sheet sheet, String range) { - sheet.addMergedRegion(CellRangeAddress.valueOf(range)); - } - - private void writeFields(Sheet sheet, List fields) { - // 기본 24행을 유지하고 필드가 더 많으면 마지막 행의 서식을 복제해 확장한다. - int requiredRows = Math.max(fields.size(), TEMPLATE_LAST_ROW - FIRST_FIELD_ROW + 1); - for (int offset = 0; offset < requiredRows; offset++) { - int rowIndex = FIRST_FIELD_ROW + offset; - Row row = sheet.getRow(rowIndex); - if (row == null) { - row = cloneTemplateRow(sheet, rowIndex); - } - clearRowValues(row); - setNumber(row, 0, offset + 1); - if (offset < fields.size()) { - FieldRow field = fields.get(offset); - setNumber(row, 1, field.level()); - setText(row, 2, field.description()); - setText(row, 3, field.parentDescription()); - setText(row, 5, field.name()); - setText(row, 6, field.parentName()); - setText(row, 7, field.dataType()); - if (field.length() > 0) { - setNumber(row, 8, field.length()); - } - } - } - } - - private Row cloneTemplateRow(Sheet sheet, int rowIndex) { - Row source = sheet.getRow(TEMPLATE_LAST_ROW); - Row target = sheet.createRow(rowIndex); - target.setHeight(source.getHeight()); - for (int column = 0; column < COLUMN_COUNT; column++) { - Cell sourceCell = source.getCell(column, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); - Cell targetCell = target.createCell(column); - CellStyle style = sourceCell.getCellStyle(); - targetCell.setCellStyle(style); - } - return target; - } - - private void clearRowValues(Row row) { - for (int column = 0; column < COLUMN_COUNT; column++) { - Cell cell = row.getCell(column, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); - cell.setBlank(); - } - } - - private List describeFields(Class rootClass) { - List output = new ArrayList<>(); - appendFields(rootClass, 1, "", "", output); - return output; - } - - private void appendFields(Class type, int level, String parentName, - String parentDescription, List output) { - // 중첩 DTO와 List 요소 타입을 재귀적으로 펼쳐 Level 및 부모 식별자를 계산한다. - List fields = new ArrayList<>(List.of(type.getDeclaredFields())); - fields.removeIf(field -> field.isSynthetic()); - fields.sort(Comparator.comparingInt(this::fieldOrder)); - for (Field field : fields) { - Annotation metadata = telegramMetadata(field); - // 한글명, 순서, 길이를 알 수 없는 DTO는 설계서 양식으로 변환할 수 없다. - if (metadata == null) { - throw new DtoFormatException(); - } - String description = annotationString(metadata, "description", field.getName()); - int length = annotationInt(metadata, "length", 0); - Class nestedType = nestedType(field); - String dataType = annotationString(metadata, "type", simpleDataType(field)); - output.add(new FieldRow(level, description, parentDescription, field.getName(), - parentName, dataType, length)); - if (nestedType != null && nestedType != type) { - appendFields(nestedType, level + 1, field.getName(), description, output); - } - } - } - - private int fieldOrder(Field field) { - return annotationInt(telegramMetadata(field), "order", Integer.MAX_VALUE); - } - - private Annotation telegramMetadata(Field field) { - for (Annotation annotation : field.getDeclaredAnnotations()) { - String annotationName = annotation.annotationType().getSimpleName(); - if (annotationName.equals("GlowTrgmField") - || annotationName.equals("GlowMciFieldInfo")) { - return annotation; - } - } - return null; - } - - private String annotationString(Annotation annotation, String methodName, String fallback) { - Object value = annotationValue(annotation, methodName); - return value instanceof String text && !text.isBlank() ? text : fallback; - } - - private int annotationInt(Annotation annotation, String methodName, int fallback) { - Object value = annotationValue(annotation, methodName); - return value instanceof Number number ? number.intValue() : fallback; - } - - private Object annotationValue(Annotation annotation, String methodName) { - if (annotation == null) { - return null; - } - try { - Method method = annotation.annotationType().getMethod(methodName); - return method.invoke(annotation); - } catch (ReflectiveOperationException ignored) { - return null; - } - } - - private Class nestedType(Field field) { - Class type = field.getType(); - if (List.class.isAssignableFrom(type) && field.getGenericType() instanceof ParameterizedType generic) { - Type argument = generic.getActualTypeArguments()[0]; - if (argument instanceof Class itemType && isDtoType(itemType)) { - return itemType; - } - } - return isDtoType(type) ? type : null; - } - - private boolean isDtoType(Class type) { - return !type.isPrimitive() - && !type.getName().startsWith("java.") - && !type.isEnum(); - } - - private String simpleDataType(Field field) { - if (List.class.isAssignableFrom(field.getType())) { - return "List"; - } - return field.getType().getSimpleName(); - } - - private void setText(Sheet sheet, int row, int column, String value) { - setText(sheet.getRow(row), column, value); - } - - private void setText(Row row, int column, String value) { - row.getCell(column, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue(value == null ? "" : value); - } - - private void setNumber(Row row, int column, int value) { - row.getCell(column, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue(value); - } - - private Map scanDtoClasses() { - // itrf.mci 하위의 모든 io 패키지를 검색하므로 신규 DTO 추가 시 하드코딩이 필요 없다. - ClassPathScanningCandidateComponentProvider scanner = - new ClassPathScanningCandidateComponentProvider(false); - scanner.addIncludeFilter(new RegexPatternTypeFilter( - Pattern.compile(".*\\.itrf\\.mci\\..*\\.io\\.[^.]+$"))); - - Map classes = new TreeMap<>(); - scanner.findCandidateComponents(MCI_BASE_PACKAGE).forEach(candidate -> { - String className = candidate.getBeanClassName(); - if (className == null || className.contains("$")) { - return; - } - String simpleName = className.substring(className.lastIndexOf('.') + 1); - String previous = classes.putIfAbsent(simpleName, className); - if (previous != null) { - throw new IllegalStateException("Duplicate DTO class name: " + simpleName); - } - }); - return Map.copyOf(classes); - } - - private record FieldRow(int level, String description, String parentDescription, - String name, String parentName, String dataType, int length) { - } - - private static final class DtoFormatException extends RuntimeException { - } -} diff --git a/settings.gradle b/settings.gradle index 5c2197505..b9801daab 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,7 @@ rootProject.name = 'dap-was-dapmt' include 'dap-was-lib' -include 'dap-was-sms' -include 'dap-was-oth' +include 'dap-was-cus' +include 'dap-was-sal' +include 'dap-was-pro' +include 'dap-was-sys' \ No newline at end of file