From e364a35b66d46e5af53acbefec272fcf96709d25 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Thu, 13 Aug 2026 14:42:44 +0900 Subject: [PATCH] =?UTF-8?q?-=20=EC=86=8C=EC=88=98=20=EC=9E=90=EB=A6=BF?= =?UTF-8?q?=EC=88=98=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-=20DECIM?= =?UTF-8?q?AL/BIGDECIMAL=20=EC=A0=95=EC=88=98/=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=20-=20=EC=83=98=ED=94=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20-=20=EA=B8=B8=EC=9D=B4=20=EB=B0=8F=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=20=EC=9E=90=EB=A6=BF=EC=88=98=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=ED=99=94=20-=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B2=98=EB=A6=AC=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20-=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/js/djb/apispec/app.js | 48 +++++++---- .../transaction/apim/ApiSpecManService.java | 66 +++++++++------ .../apim/DjbApiSpecLayoutMergeService.java | 2 +- .../apim/DjbApiSpecSampleGenerator.java | 82 +++++++++++++------ 4 files changed, 133 insertions(+), 65 deletions(-) diff --git a/WebContent/js/djb/apispec/app.js b/WebContent/js/djb/apispec/app.js index 9367991..cdc4a13 100644 --- a/WebContent/js/djb/apispec/app.js +++ b/WebContent/js/djb/apispec/app.js @@ -224,6 +224,8 @@ const s = { type: row.type.value }; if (row.format && row.format.value) s.format = row.format.value; if (row.maxLength && row.maxLength.value) s.maxLength = Number(row.maxLength.value); + // 소수 자릿수(GW 레이아웃 유래) 보존 — 예제값 생성 시 '.' 포함 길이 계산에 사용 + if (row.decimals && row.decimals.value !== '' && row.decimals.value != null) s['x-djb-decimal'] = Number(row.decimals.value); if (row.pattern && row.pattern.value) s.pattern = row.pattern.value; if (row.description && row.description.value) s.description = row.description.value; if (row.example && row.example.value !== '') s.example = coerceExample(row.type.value, row.example.value); @@ -1466,6 +1468,7 @@ required: { value: false, locked: false }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, + decimals: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false }, @@ -1480,6 +1483,7 @@ required: { value: false, locked: false }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, + decimals: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false } @@ -1733,6 +1737,7 @@ var row = { name: wrap(name, gw), type: wrap(t, gw), required: wrap(!!required, false), format: wrap(p.format || ''), maxLength: wrap(p.maxLength == null ? '' : p.maxLength), + decimals: wrap(p['x-djb-decimal'] == null ? '' : p['x-djb-decimal']), pattern: wrap(p.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example), children: null }; @@ -1782,7 +1787,7 @@ }; d.parameters = (op.parameters || []).map(function (p) { var s = p.schema || {}; - return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) }; + return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), decimals: wrap(s['x-djb-decimal'] == null ? '' : s['x-djb-decimal']), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) }; }); var rb = op.requestBody || {}; var rbc = rb.content || {}; var mt = Object.keys(rbc)[0] || 'application/json'; d.requestBody = { mediaType: mt, required: wrap(!!rb.required), schema: schemaToRows((rbc[mt] || {}).schema) }; @@ -1827,19 +1832,34 @@ // 스키마 기반 샘플 JSON 생성 (자동생성 예제) function _coerce(t, v) { if (t === 'integer') return parseInt(v, 10) || 0; if (t === 'number') return parseFloat(v) || 0; if (t === 'boolean') return v === true || v === 'true'; return v; } - // 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123456.123, boolean→false - // - 숫자는 길이 지정 시 정수부를 그 길이에 맞춰 자름(실수는 소수 .123 유지) - function _defScalar(t, name, maxLen) { - var ml = parseInt(maxLen, 10); - if (t === 'integer') { - var iv = '123456'; - if (ml && ml > 0 && iv.length > ml) iv = iv.substring(0, ml); - return parseInt(iv, 10); + // 자릿수 문자열: start 위치부터 1~9 를 순환('123456789123...') + function _digits(start, count) { var s = ''; for (var i = 0; i < count; i++) s += String(((start + i) % 9) + 1); return s; } + // 숫자 예제값. 길이는 소수점(.)까지 포함한 문자열 전체 길이로 계산한다. + // 예) 길이 9 / 소수 5 → 123.45678 (정수부 3 + '.' 1 + 소수부 5 = 9자) + // - 길이가 소수부+2 보다 짧으면 정수부 1자리를 확보하고 소수부를 줄인다. + // - 길이 미지정이면 정수부 6자리(기존 기본값)를 사용한다. + function _numSample(maxLen, dec) { + var ml = parseInt(maxLen, 10) || 0, d = parseInt(dec, 10) || 0; + if (d <= 0) return _digits(0, ml > 0 ? Math.min(ml, 6) : 6); + var il; + if (ml > 0) { + il = ml - d - 1; + if (il < 1) { il = 1; d = Math.max(1, ml - 2); } + } else { + il = 6; } + return _digits(0, il) + '.' + _digits(il, d); + } + // 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123.45678, boolean→false + // - 숫자는 지정 길이를 넘지 않도록 자릿수를 맞춘다(소수는 '.' 포함 길이 기준). + // - number 인데 소수 자릿수 정보가 없으면 3자리로 가정(소수 0자리는 백엔드가 integer 로 매핑). + function _defScalar(t, name, maxLen, decimals) { + var ml = parseInt(maxLen, 10); + if (t === 'integer') return parseInt(_numSample(ml, 0), 10); if (t === 'number') { - var ip = '123456'; - if (ml && ml > 0 && ip.length > ml) ip = ip.substring(0, ml); - return parseFloat(ip + '.123'); + var d = parseInt(decimals, 10); + if (!(d > 0)) d = 3; + return parseFloat(_numSample(ml, d)); } if (t === 'boolean') return false; var v = 'sample_' + (name || ''); @@ -1851,7 +1871,7 @@ if (t === 'object') return _sampleObj(row.children || []); if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _defScalar(it, (row.name && row.name.value) || 'item', '')]; } if (ex !== undefined && ex !== '') return _coerce(t, ex); - return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value); + return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value, row.decimals && row.decimals.value); } function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { if (r && r.name && r.name.value) o[r.name.value] = _sampleVal(r); }); return o; } function ensureExamplesFromSchema(force) { @@ -1897,7 +1917,7 @@ if (r.children && r.children.length) fillSchemaExamples(r.children); } else { r.example = r.example || { value: '', locked: false }; - r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value)); + r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value, r.decimals && r.decimals.value)); } }); } diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiSpecManService.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiSpecManService.java index 1692cba..880fe53 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiSpecManService.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiSpecManService.java @@ -343,7 +343,7 @@ public class ApiSpecManService { String defaultValue = item.getLoutItemDefault(); String fieldName = item.getLoutItemName(); - boolean isInteger = isIntegerType(dataType); + boolean isInteger = isIntegerType(dataType, item.getLoutItemDecimal()); if (isNumericType(dataType)) { if (StringUtils.isNotEmpty(defaultValue)) { @@ -393,7 +393,7 @@ public class ApiSpecManService { private void addScalarToArray(ArrayNode arrayNode, LayoutItemUI item) { String dataType = item.getLoutItemDataType(); String defaultValue = item.getLoutItemDefault(); - boolean isInteger = isIntegerType(dataType); + boolean isInteger = isIntegerType(dataType, item.getLoutItemDecimal()); if (isNumericType(dataType)) { try { @@ -416,23 +416,29 @@ public class ApiSpecManService { } } - private boolean isIntegerType(String dataType) { + /** BIGDECIMAL/DECIMAL 은 소수 자릿수가 0 이면 정수로 취급한다. */ + private boolean isIntegerType(String dataType, int decimalLen) { if (dataType == null) return false; switch (dataType.toUpperCase()) { case "INTEGER": case "INT": case "LONG": return true; + case "DECIMAL": + case "BIGDECIMAL": + return decimalLen <= 0; default: return false; } } private boolean isNumericType(String dataType) { - if (isIntegerType(dataType)) return true; if (dataType == null) return false; switch (dataType.toUpperCase()) { + case "INTEGER": + case "INT": + case "LONG": case "DECIMAL": case "DOUBLE": case "BIGDECIMAL": @@ -621,11 +627,7 @@ public class ApiSpecManService { parameter.put("required", false); // 기본값으로 false 설정, 필요시 true로 변경 가능 ObjectNode schema = parameter.putObject("schema"); - schema.put("type", mapSwaggerType(item.getLoutItemDataType())); - int length = item.getLoutItemLength(); - if (length > 0) { - schema.put("maxLength", item.getLoutItemLength()); - } + putScalarSchema(schema, item); } } } @@ -651,11 +653,7 @@ public class ApiSpecManService { parameter.put("required", false); // 기본값 false, 필요시 사용자가 변경 ObjectNode schema = parameter.putObject("schema"); - schema.put("type", mapSwaggerType(item.getLoutItemDataType())); - int length = item.getLoutItemLength(); - if (length > 0) { - schema.put("maxLength", length); - } + putScalarSchema(schema, item); } } @@ -665,11 +663,7 @@ public class ApiSpecManService { ObjectNode header = headers.putObject(item.getLoutItemName()); header.put("description", item.getLoutItemDesc()); ObjectNode schema = header.putObject("schema"); - schema.put("type", mapSwaggerType(item.getLoutItemDataType())); - int length = item.getLoutItemLength(); - if (length > 0) { - schema.put("maxLength", item.getLoutItemLength()); - } + putScalarSchema(schema, item); } } } @@ -695,14 +689,10 @@ public class ApiSpecManService { switch (item.getLoutItemType().toUpperCase()) { case "FIELD": ObjectNode fieldSchema = parentNode.putObject(item.getLoutItemName()); - fieldSchema.put("type", mapSwaggerType(item.getLoutItemDataType())); + putScalarSchema(fieldSchema, item); if (StringUtils.isNotEmpty(item.getLoutItemDesc())) { fieldSchema.put("description", item.getLoutItemDesc()); } - int length = item.getLoutItemLength(); - if (length > 0) { - fieldSchema.put("maxLength", item.getLoutItemLength()); - } break; case "GROUP": @@ -730,14 +720,18 @@ public class ApiSpecManService { arrItems.put("type", "object"); depthMap.put(item.getLoutItemDepth(), arrItems.putObject("properties")); } else { - arrItems.put("type", mapSwaggerType(item.getLoutItemDataType())); + putScalarSchema(arrItems, item); } break; } } } - private String mapSwaggerType(String dataType) { + /** + * 레이아웃 데이터타입 → OpenAPI type. + *

BIGDECIMAL/DECIMAL 은 소수 자릿수(decimalLen)가 0 이면 정수로 취급한다. + */ + private String mapSwaggerType(String dataType, int decimalLen) { if (dataType == null) return "string"; switch (dataType.toUpperCase()) { @@ -747,8 +741,9 @@ public class ApiSpecManService { case "LONG": return "integer"; case "DECIMAL": - case "DOUBLE": case "BIGDECIMAL": + return decimalLen > 0 ? "number" : "integer"; + case "DOUBLE": return "number"; case "BOOLEAN": return "boolean"; @@ -757,6 +752,23 @@ public class ApiSpecManService { } } + /** + * 스칼라 FIELD 의 스키마(type/maxLength/소수 자릿수)를 채운다. + *

길이(maxLength)는 레이아웃 길이 그대로이며, 소수가 있는 숫자의 예제값은 + * 소수점(.)을 포함해 이 길이를 넘지 않도록 프론트에서 생성한다(예: 길이 9/소수 5 → 123.45678). + */ + private void putScalarSchema(ObjectNode schema, LayoutItemUI item) { + String type = mapSwaggerType(item.getLoutItemDataType(), item.getLoutItemDecimal()); + schema.put("type", type); + int length = item.getLoutItemLength(); + if (length > 0) { + schema.put("maxLength", length); + } + if ("number".equals(type) && item.getLoutItemDecimal() > 0) { + schema.put("x-djb-decimal", item.getLoutItemDecimal()); + } + } + private boolean isRequestBodyRequired(String method) { if (method == null) return false; return "POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method); diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecLayoutMergeService.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecLayoutMergeService.java index 49ad499..ff3f0cf 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecLayoutMergeService.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecLayoutMergeService.java @@ -45,7 +45,7 @@ public class DjbApiSpecLayoutMergeService { r.put("desc", it.getLoutItemDesc()); r.put("itemType", it.getLoutItemType()); // FIELD / GROUP / GRID r.put("dataType", dataType); - r.put("category", sampleGenerator.categoryOf(dataType)); + r.put("category", sampleGenerator.categoryOf(dataType, dec)); r.put("length", len); r.put("decimal", dec); r.put("depth", it.getLoutItemDepth()); diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecSampleGenerator.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecSampleGenerator.java index 67af17f..baad482 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecSampleGenerator.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/DjbApiSpecSampleGenerator.java @@ -4,19 +4,25 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; /** - * D10 예제값 자동 생성 규칙. + * D10 예제값 자동 생성 규칙. (프론트 app.js 의 _defScalar/_numSample 와 동일 규칙) *

+ *

BIGDECIMAL/DECIMAL 은 소수 자릿수가 0 이면 정수(integer)로 분류한다. */ @Service public class DjbApiSpecSampleGenerator { - /** 원시 데이터타입(레이아웃 값)을 OpenAPI 계열 카테고리로 분류. */ - public String categoryOf(String rawDataType) { + /** + * 원시 데이터타입(레이아웃 값)을 OpenAPI 계열 카테고리로 분류. + * + * @param rawDataType 레이아웃 데이터타입(원시) + * @param decimalLen 소수 자릿수. decimal 계열에서 0 이면 정수로 본다. + */ + public String categoryOf(String rawDataType, int decimalLen) { if (rawDataType == null) { return "string"; } @@ -24,18 +30,22 @@ public class DjbApiSpecSampleGenerator { if (t.contains("bool")) { return "boolean"; } - if (t.contains("decimal") || t.contains("double") || t.contains("float") || t.contains("number")) { - return "number"; - } - if (t.contains("int") || t.contains("long")) { - return "integer"; - } if (t.contains("object") || t.contains("group")) { return "object"; } if (t.contains("array") || t.contains("grid") || t.contains("list")) { return "array"; } + if (t.contains("decimal")) { + // BigDecimal/Decimal: 소수 자릿수 0 = 정수 + return decimalLen > 0 ? "number" : "integer"; + } + if (t.contains("double") || t.contains("float") || t.contains("number")) { + return "number"; + } + if (t.contains("int") || t.contains("long")) { + return "integer"; + } return "string"; } @@ -43,22 +53,15 @@ public class DjbApiSpecSampleGenerator { * 예제값 생성. * @param rawDataType 레이아웃 데이터타입(원시) * @param fieldName 항목명 - * @param maxLen 최대 길이(0/음수면 무제한) - * @param decimalLen 소수 자릿수(0이면 미적용) + * @param maxLen 최대 길이(0/음수면 무제한). 숫자는 소수점(.)을 포함한 문자열 길이 기준. + * @param decimalLen 소수 자릿수(0이면 정수) */ public String exampleFor(String rawDataType, String fieldName, int maxLen, int decimalLen) { - switch (categoryOf(rawDataType)) { + switch (categoryOf(rawDataType, decimalLen)) { case "integer": - return "1"; + return numberSample(maxLen, 0); case "number": - if (decimalLen > 0) { - StringBuilder sb = new StringBuilder("1."); - for (int i = 0; i < decimalLen; i++) { - sb.append(i == 0 ? '1' : '0'); - } - return sb.toString(); - } - return "1.1"; + return numberSample(maxLen, decimalLen > 0 ? decimalLen : 3); case "boolean": return "false"; case "object": @@ -72,4 +75,37 @@ public class DjbApiSpecSampleGenerator { return v; } } + + /** + * 숫자 예제값. 길이는 소수점(.)까지 포함한 문자열 전체 길이로 계산한다. + *

예) 길이 9 / 소수 5 → {@code 123.45678} (정수부 3 + '.' 1 + 소수부 5 = 9자). + * 길이가 소수부+2 보다 짧으면 정수부 1자리를 확보하고 소수부를 줄인다. + * 길이 미지정이면 정수부 6자리를 사용한다. + */ + private String numberSample(int maxLen, int decimalLen) { + if (decimalLen <= 0) { + return digits(0, maxLen > 0 ? Math.min(maxLen, 6) : 6); + } + int dec = decimalLen; + int intLen; + if (maxLen > 0) { + intLen = maxLen - dec - 1; // '.' 1자리 포함 + if (intLen < 1) { + intLen = 1; + dec = Math.max(1, maxLen - 2); + } + } else { + intLen = 6; + } + return digits(0, intLen) + "." + digits(intLen, dec); + } + + /** start 위치부터 1~9 를 순환하는 자릿수 문자열('123456789123...'). */ + private String digits(int start, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append((char) ('1' + ((start + i) % 9))); + } + return sb.toString(); + } }