From 59dd28c23b3f2e498626f3b032e0d19498c6035d Mon Sep 17 00:00:00 2001 From: Rinjae Date: Thu, 23 Jul 2026 19:13:16 +0900 Subject: [PATCH] =?UTF-8?q?-=20JSON=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?:=20ARRAY=20=ED=83=80=EC=9E=85=20=EC=8A=A4=EC=B9=BC=EB=9D=BC/?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EB=B0=B0=EC=97=B4=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20-=20JS:=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=20=EB=82=B4=EB=B6=80=20=EA=B0=9D=EC=B2=B4(children)?= =?UTF-8?q?=20=EB=B0=8F=20=ED=83=80=EC=9E=85(Render,=20Save)=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?-=20=EC=8A=A4=EC=B9=BC=EB=9D=BC=20=EB=B0=B0=EC=97=B4=20?= =?UTF-8?q?=EC=98=88=EC=A0=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Swagger?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/js/djb/apispec/app.js | 20 ++++-- .../transaction/apim/ApiSpecManService.java | 72 +++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) 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; } } }