diff --git a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/diagnostics/DiagnosticsController.java b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/diagnostics/DiagnosticsController.java
index 6e3649d..27ab20c 100644
--- a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/diagnostics/DiagnosticsController.java
+++ b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/diagnostics/DiagnosticsController.java
@@ -92,7 +92,7 @@ private kr.devslab.kit.menu.MenuItem filterByCodes(kr.devslab.kit.menu.MenuItem
.filter(java.util.Objects::nonNull)
.toList();
return new kr.devslab.kit.menu.MenuItem(
- item.id(), item.code(), item.label(), item.path(),
+ item.id(), item.code(), item.label(), item.path(), item.icon(),
item.requiredPermission(), visibleChildren);
}
diff --git a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/CreateMenuRequest.java b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/CreateMenuRequest.java
index 945131a..8542f4a 100644
--- a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/CreateMenuRequest.java
+++ b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/CreateMenuRequest.java
@@ -4,13 +4,23 @@
import jakarta.validation.constraints.Size;
import java.util.UUID;
+/**
+ * Wire shape for {@code POST /admin/api/v1/menus}.
+ *
+ *
Field names mirror what {@code devslab-kit-admin-ui} sends:
+ * {@code displayOrder} (not {@code sortOrder}),
+ * {@code requiredPermission} (not {@code requiredPermissionCode}),
+ * plus the new {@code icon} token (e.g. {@code "pi-users"}). The
+ * entity column names underneath are unchanged.
+ */
public record CreateMenuRequest(
@NotBlank @Size(max = 64) String tenantId,
@NotBlank @Size(max = 64) String code,
@NotBlank @Size(max = 255) String label,
@Size(max = 255) String path,
UUID parentId,
- int sortOrder,
- @Size(max = 128) String requiredPermissionCode
+ int displayOrder,
+ @Size(max = 128) String requiredPermission,
+ @Size(max = 64) String icon
) {
}
diff --git a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuAdminController.java b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuAdminController.java
index 2a24d4c..bd8ccee 100644
--- a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuAdminController.java
+++ b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuAdminController.java
@@ -1,11 +1,15 @@
package kr.devslab.kit.admin.menu;
import jakarta.validation.Valid;
+import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
+import java.util.stream.Collectors;
import kr.devslab.kit.admin.AdminApiPaths;
import kr.devslab.kit.core.id.MenuId;
import kr.devslab.kit.core.id.TenantId;
+import kr.devslab.kit.menu.core.entity.PlatformMenuEntity;
import kr.devslab.kit.menu.core.service.MenuAdminService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -36,20 +40,62 @@ public ResponseEntity create(@Valid @RequestBody CreateMenuRequest
req.label(),
req.path(),
req.parentId() == null ? null : MenuId.of(req.parentId()),
- req.sortOrder(),
- req.requiredPermissionCode()
+ req.displayOrder(),
+ req.requiredPermission(),
+ req.icon()
);
- return ResponseEntity.status(201).body(MenuResponse.from(entity));
+ return ResponseEntity.status(201).body(MenuResponse.flat(entity));
}
@GetMapping
public List list(@RequestParam String tenantId) {
- return service.listByTenant(TenantId.of(tenantId)).stream().map(MenuResponse::from).toList();
+ return service.listByTenant(TenantId.of(tenantId)).stream().map(MenuResponse::flat).toList();
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity get(@PathVariable UUID id) {
+ return service.findById(MenuId.of(id))
+ .map(MenuResponse::flat)
+ .map(ResponseEntity::ok)
+ .orElseGet(() -> ResponseEntity.notFound().build());
+ }
+
+ /**
+ * Tree view for the admin UI's TreeTable component. Returns root nodes only;
+ * each root carries its descendants nested under {@code children}, sorted by
+ * {@code displayOrder}.
+ */
+ @GetMapping("/tree")
+ public List tree(@RequestParam String tenantId) {
+ List entities = service.listByTenant(TenantId.of(tenantId));
+ Map> byParent = entities.stream()
+ .filter(e -> e.getParentId() != null)
+ .collect(Collectors.groupingBy(PlatformMenuEntity::getParentId));
+ return entities.stream()
+ .filter(e -> e.getParentId() == null)
+ .sorted(Comparator.comparingInt(PlatformMenuEntity::getSortOrder))
+ .map(root -> toNode(root, byParent))
+ .toList();
+ }
+
+ private MenuResponse toNode(PlatformMenuEntity entity, Map> byParent) {
+ List children = byParent.getOrDefault(entity.getId(), List.of()).stream()
+ .sorted(Comparator.comparingInt(PlatformMenuEntity::getSortOrder))
+ .map(child -> toNode(child, byParent))
+ .toList();
+ return MenuResponse.withChildren(entity, children);
}
@PutMapping("/{id}")
public ResponseEntity update(@PathVariable UUID id, @Valid @RequestBody UpdateMenuRequest req) {
- service.update(MenuId.of(id), req.label(), req.path(), req.sortOrder(), req.requiredPermissionCode());
+ service.update(
+ MenuId.of(id),
+ req.label(),
+ req.path(),
+ req.displayOrder(),
+ req.requiredPermission(),
+ req.icon()
+ );
return ResponseEntity.noContent().build();
}
diff --git a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuResponse.java b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuResponse.java
index a61b5b2..005c883 100644
--- a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuResponse.java
+++ b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/MenuResponse.java
@@ -1,8 +1,25 @@
package kr.devslab.kit.admin.menu;
+import java.util.List;
import java.util.UUID;
import kr.devslab.kit.menu.core.entity.PlatformMenuEntity;
+/**
+ * Wire shape for menu items returned by {@code /admin/api/v1/menus*}.
+ *
+ * Two representations live here:
+ *
+ *
+ * - {@link #flat(PlatformMenuEntity)} — single entity, {@code children == null}.
+ * Used by the flat list endpoint and by {@code GET /menus/{id}}.
+ * - {@link #withChildren(PlatformMenuEntity, List)} — entity plus its
+ * already-built child list. Used by {@code GET /menus/tree}.
+ *
+ *
+ * Field names mirror the admin UI's {@code MenuItem} interface:
+ * {@code displayOrder} (not the entity's {@code sortOrder}) and
+ * {@code requiredPermission} (not {@code requiredPermissionCode}).
+ */
public record MenuResponse(
UUID id,
String tenantId,
@@ -10,11 +27,13 @@ public record MenuResponse(
String label,
String path,
UUID parentId,
- int sortOrder,
- String requiredPermissionCode
+ int displayOrder,
+ String requiredPermission,
+ String icon,
+ List children
) {
- public static MenuResponse from(PlatformMenuEntity entity) {
+ public static MenuResponse flat(PlatformMenuEntity entity) {
return new MenuResponse(
entity.getId(),
entity.getTenantId(),
@@ -23,7 +42,24 @@ public static MenuResponse from(PlatformMenuEntity entity) {
entity.getPath(),
entity.getParentId(),
entity.getSortOrder(),
- entity.getRequiredPermissionCode()
+ entity.getRequiredPermissionCode(),
+ entity.getIcon(),
+ null
+ );
+ }
+
+ public static MenuResponse withChildren(PlatformMenuEntity entity, List children) {
+ return new MenuResponse(
+ entity.getId(),
+ entity.getTenantId(),
+ entity.getCode(),
+ entity.getLabel(),
+ entity.getPath(),
+ entity.getParentId(),
+ entity.getSortOrder(),
+ entity.getRequiredPermissionCode(),
+ entity.getIcon(),
+ children == null ? List.of() : List.copyOf(children)
);
}
}
diff --git a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/UpdateMenuRequest.java b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/UpdateMenuRequest.java
index e2b3fd3..2bad226 100644
--- a/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/UpdateMenuRequest.java
+++ b/devslab-kit-admin-api/src/main/java/kr/devslab/kit/admin/menu/UpdateMenuRequest.java
@@ -2,10 +2,17 @@
import jakarta.validation.constraints.Size;
+/**
+ * Wire shape for {@code PUT /admin/api/v1/menus/{id}}.
+ *
+ * Every field is optional — only those passed in are applied.
+ * Field names match {@link CreateMenuRequest} (and the admin UI).
+ */
public record UpdateMenuRequest(
@Size(max = 255) String label,
@Size(max = 255) String path,
- Integer sortOrder,
- @Size(max = 128) String requiredPermissionCode
+ Integer displayOrder,
+ @Size(max = 128) String requiredPermission,
+ @Size(max = 64) String icon
) {
}
diff --git a/devslab-kit-menu-api/src/main/java/kr/devslab/kit/menu/MenuItem.java b/devslab-kit-menu-api/src/main/java/kr/devslab/kit/menu/MenuItem.java
index 431e881..40b1650 100644
--- a/devslab-kit-menu-api/src/main/java/kr/devslab/kit/menu/MenuItem.java
+++ b/devslab-kit-menu-api/src/main/java/kr/devslab/kit/menu/MenuItem.java
@@ -10,6 +10,7 @@ public record MenuItem(
String code,
String label,
String path,
+ String icon,
Optional requiredPermission,
List