diff --git a/WebContent/js/djb/apispec/app.js b/WebContent/js/djb/apispec/app.js index ca60197..5789493 100644 --- a/WebContent/js/djb/apispec/app.js +++ b/WebContent/js/djb/apispec/app.js @@ -248,9 +248,11 @@ if (row.description && row.description.value) obj.properties[name].description = row.description.value; if (row.name && row.name.locked) obj.properties[name]['x-djb-gw'] = true; } else if (row.type.value === 'array') { + const itemType = (row.itemsType && row.itemsType.value) || 'string'; obj.properties[name] = { type: 'array', - items: { type: (row.itemsType && row.itemsType.value) || 'string' } + // array(GW GRID): 자식 행을 items.properties 로 직렬화. (누락 시 저장 왕복에서 자식 유실) + items: itemType === 'object' ? schemaArrayToObject(row.children || []) : { type: itemType } }; if (row.description && row.description.value) obj.properties[name].description = row.description.value; if (row.name && row.name.locked) obj.properties[name]['x-djb-gw'] = true; @@ -672,6 +674,7 @@ const isLocked = row.name.locked; const isObject = row.type.value === 'object'; const isArray = row.type.value === 'array'; + const isArrayOfObject = isArray && (row.itemsType && row.itemsType.value) === 'object'; const expandedKey = `${path}.__expanded`; const isExpanded = state[expandedKey] !== false; @@ -681,7 +684,7 @@ ${isLocked ? '🔒' : ''}
- ${isObject || isArray + ${isObject || isArrayOfObject ? `` : ``} ${isObject ? '{}' : isArray ? '[]' : '·'} @@ -712,7 +715,7 @@ isArray ? `
items: - ${selectInput((row.itemsType && row.itemsType.value) || 'string', ['string', 'integer', 'number', 'boolean'], `data-path="${path}.itemsType.value"`, { extra: 'text-xs' })} + ${selectInput((row.itemsType && row.itemsType.value) || 'string', ['string', 'integer', 'number', 'boolean', 'object'], `data-path="${path}.itemsType.value" data-rerender="step3"`, { extra: 'text-xs' })}
` : input(row.example.value, `data-path="${path}.example.value"`, { placeholder: '예제값' })} @@ -724,8 +727,8 @@ `; - // children - if (isObject && isExpanded) { + // children (object + array(GRID) 공통) + if ((isObject || isArrayOfObject) && isExpanded) { if (row.children && row.children.length) { html += renderSchemaRows(row.children, `${path}.children`, depth + 1, seq); } @@ -1683,7 +1686,12 @@ example: wrap(p.example == null ? '' : p.example), children: null }; if (t === 'object') row.children = schemaToRows(p); - else if (t === 'array') row.itemsType = wrap((p.items && p.items.type) || 'string'); + else if (t === 'array') { + var itemType = (p.items && p.items.type) || 'string'; + row.itemsType = wrap(itemType); + // array(GW GRID): items.properties 를 자식 행으로 복원. (누락 시 자식 필드 유실) + if (itemType === 'object') row.children = schemaToRows(p.items); + } return row; } function schemaToRows(schema) { 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 c84e066..1692cba 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 @@ -280,6 +280,17 @@ public class ApiSpecManService { ObjectNode arrayItem = arrayNode.addObject(); nodeMap.put(item.getLoutitempath(), arrayItem); break; + + // JSON 레이아웃의 반복 항목(itemType="Array"). 자식이 있으면 객체배열, 없으면 스칼라배열. + case "ARRAY": + ArrayNode arrNode = parentNode.putArray(item.getLoutItemName()); + if (hasChildItems(items, item)) { + ObjectNode arrObj = arrNode.addObject(); + nodeMap.put(item.getLoutitempath(), arrObj); + } else { + addScalarToArray(arrNode, item); + } + break; } } @@ -357,6 +368,54 @@ public class ApiSpecManService { } } + /** + * 지정 항목이 하위(자식) 항목을 가지는지 여부. + *

JSON 레이아웃에서 배열/객체는 FIELD 노드(반복='*' → itemType="Array")로 저장되고 + * 자식 항목은 loutitempath 접두어 + depth+1 로 표현된다. 자식 유무로 객체배열/스칼라배열을 구분한다. + */ + private boolean hasChildItems(List items, LayoutItemUI parent) { + String parentPath = parent.getLoutitempath(); + if (parentPath == null) { + return false; + } + String prefix = parentPath + "."; + int childDepth = parent.getLoutItemDepth() + 1; + for (LayoutItemUI it : items) { + String p = it.getLoutitempath(); + if (p != null && p.startsWith(prefix) && it.getLoutItemDepth() == childDepth) { + return true; + } + } + return false; + } + + /** 스칼라 배열(자식 없는 Array)의 예시 요소 1개를 배열에 추가한다. addFieldValue 의 배열 버전. */ + private void addScalarToArray(ArrayNode arrayNode, LayoutItemUI item) { + String dataType = item.getLoutItemDataType(); + String defaultValue = item.getLoutItemDefault(); + boolean isInteger = isIntegerType(dataType); + + if (isNumericType(dataType)) { + try { + if (StringUtils.isNotEmpty(defaultValue)) { + if (isInteger) { + arrayNode.add(Integer.parseInt(defaultValue)); + } else { + arrayNode.add(Double.parseDouble(defaultValue)); + } + } else { + arrayNode.add(isInteger ? 0 : 0.0); + } + } catch (NumberFormatException e) { + arrayNode.add(isInteger ? 0 : 0.0); + } + } else if ("BOOLEAN".equalsIgnoreCase(dataType)) { + arrayNode.add(StringUtils.isNotEmpty(defaultValue) ? Boolean.parseBoolean(defaultValue) : false); + } else { + arrayNode.add(StringUtils.isNotEmpty(defaultValue) ? defaultValue : "sample_" + item.getLoutItemName()); + } + } + private boolean isIntegerType(String dataType) { if (dataType == null) return false; switch (dataType.toUpperCase()) { @@ -661,6 +720,19 @@ public class ApiSpecManService { ObjectNode arrayItemProps = arrayItemSchema.putObject("properties"); depthMap.put(item.getLoutItemDepth(), arrayItemProps); break; + + // JSON 레이아웃의 반복 항목(itemType="Array"). 자식이 있으면 객체배열(items.properties), 없으면 스칼라배열. + case "ARRAY": + ObjectNode arrSchema = parentNode.putObject(item.getLoutItemName()); + arrSchema.put("type", "array"); + ObjectNode arrItems = arrSchema.putObject("items"); + if (hasChildItems(layoutItems, item)) { + arrItems.put("type", "object"); + depthMap.put(item.getLoutItemDepth(), arrItems.putObject("properties")); + } else { + arrItems.put("type", mapSwaggerType(item.getLoutItemDataType())); + } + break; } } }