Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/getting-started/tutorial.ko.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 튜토리얼: 0에서 실행까지
# 튜토리얼: 빈 프로젝트부터 실행까지

**devslab-kit을 한 번도 안 써본 사람**을 위한, 복붙으로 따라 하는 완전 가이드입니다. 끝까지
하면 로그인, 관리자 계정, 역할·권한, 직접 만든 권한 보호 엔드포인트, 테넌트 단위 데이터, ABAC
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/access.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class UserAdminService {
**그룹**은 여러 사용자를 위해 역할을 묶습니다 — 역할을 일일이 붙이는 대신 사용자를
`eng-team`에 한 번 넣으면 됩니다. 사용자의 유효 권한은 직접 역할과 그룹 역할의 합집합입니다.

## ABAC 정책
## ABAC 정책 { #abac-policies }

RBAC는 "이 사용자가 권한을 가졌는가?"에 답합니다. ABAC는 더 세밀한 "…*이 특정 리소스에
대해, 지금?*"에 답합니다. **`Policy`** 빈을 하나 이상 구현하면, kit의
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/admin-console.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,22 @@

## Platform

### Menus (메뉴)
### Menus (메뉴) { #menus }
제품 UI가 사용자별로 렌더할 수 있는 권한 필터링 네비게이션 트리.

- **트리**로 표시됩니다. 루트 항목을 **Create** 하거나, 노드의 **자식 추가** 동작을 사용.
- 각 항목은 라벨, 경로, 아이콘, **필요 권한**(기존 권한 코드 드롭다운 — 그 권한을 가진 사용자만 항목을 봄), 표시 순서를 가집니다. 노드별 **수정** / **삭제**.
- kit은 로그인 사용자에게 필터된 트리를 제공하고, 그리는 방식은 프런트엔드가 정합니다([Menus 가이드](menus.md)).

### Tenants (테넌트)
### Tenants (테넌트) { #tenants }
격리된 작업공간; 모든 플랫폼 데이터는 테넌트 단위.

- 테넌트 **생성**(`code` + 이름). **상태 변경**(`ACTIVE` / `SUSPENDED` / `ARCHIVED`). **삭제**.
- 단일 테넌트 모드면 보통 `default` 하나만 둡니다.

### Policies (ABAC)
역할 위에 얹는 속성 기반 규칙. **정책은 코드**입니다(당신이 `Policy` 빈을 구현 —
[Access 가이드](access.md#abac-정책) 참고). 이 화면은 등록된 정책을 **나열**하고 **테스트**합니다.
[Access 가이드](access.md#abac-policies) 참고). 이 화면은 등록된 정책을 **나열**하고 **테스트**합니다.

- 정책을 고르고 **주체**(사용자/테넌트), **자원**(타입·id·속성), **환경** 속성을 채운 뒤 **Test**.
- 결과는 **결정**(`PERMIT` / `DENY` / `NOT_APPLICABLE`)에 **이유**와 **매칭된 규칙**까지 — 부작용 없는 dry-run.
Expand Down Expand Up @@ -105,6 +105,6 @@ kit의 **적용된 설정**(`devslab.kit.*`)을 읽기 전용으로 보는 화

## 더 보기

- [튜토리얼: 0에서 실행까지](../getting-started/tutorial.md) — 위 작업 다수를 API + 콘솔로 수행.
- [튜토리얼: 빈 프로젝트부터 실행까지](../getting-started/tutorial.md) — 위 작업 다수를 API + 콘솔로 수행.
- [Access (RBAC + ABAC)](access.md) · [Menus](menus.md) · [Config Sync](config-sync.md)
- [관리자 REST API](../reference/admin-api.md) — 모든 화면 뒤의 엔드포인트.
137 changes: 121 additions & 16 deletions docs/guides/menus.ko.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,88 @@
# 동적 메뉴

kit은 **사용자별 메뉴 트리**를 만듭니다: 전체 메뉴를 한 번 정의하면(항목마다 필요 권한
지정), 각 사용자는 허용된 항목만 보게 됩니다.
**동적 메뉴**는 사용자마다 자기 권한으로 볼 수 있는 항목만 보이는 내비게이션 트리입니다. 전체
메뉴를 **한 번만** 정의하고 항목마다 필요 권한을 붙여 두면, kit이 사용자마다 걸러진 사본을
내려줍니다. 프론트엔드 곳곳에 `v-if="canSeeX"`를 흩뿌릴 필요가 없습니다.

## 동작 방식
처음이면 [튜토리얼](../getting-started/tutorial.md)부터 — 이 가이드는 권한 몇 개가 정의된
실행 중인 앱이 있다고 가정합니다.

1. 메뉴 항목은 선택적 `requiredPermission`과 순서와 함께 저장됩니다.
2. `MenuProvider`가 주어진 사용자에 대해 트리를 만들면서, 사용자가 갖지 못한 필요 권한의
항목을 **걸러냅니다**(그리고 비게 된 가지를 잘라냅니다).
3. 결과는 불변 `MenuTree(List<MenuItem> roots)`입니다.
## 개념 잡기

```
한 번 정의 (테넌트 단위) kit이 요청마다 필터링 화면 렌더링
───────────────────────── ──────────────────── ──────────
Dashboard (권한 없음) ┐
Users (user.read 필요) ├──► menusFor(현재 사용자) ──► MenuTree JSON
└ Invite (user.write 필요) │ 못 보는 항목 제거, → 사이드바
Billing (billing.read 필요)┘ 빈 가지 정리
```

세 부분으로 나뉩니다:

1. **메뉴 항목**은 kit에 저장됩니다(한 줄에 라벨, 경로, 선택적 `requiredPermission`, 표시
순서, 중첩용 선택적 부모).
2. **`MenuProvider`**가 특정 사용자 기준 트리를 만듭니다 — `requiredPermission`이 없는 사용자
에게서 항목을 **빼고**, 그 결과 비어 버린 가지를 **정리**합니다.
3. **프론트엔드**는 걸러진 트리를 받아 그대로 그립니다. 가시성을 스스로 판단하지 않습니다.

## 1단계 — 메뉴 항목 정의

항목마다: `code`(고정 id), `label`(사용자가 보는 글자), `path`(링크 위치), 선택적 `icon`,
선택적 `requiredPermission`, `displayOrder`, 선택적 `parentId`(최상위면 생략).

=== "관리자 콘솔"

1. [관리자 콘솔](admin-console.md) → **Menus** 열기.
2. **New** 클릭 후 label / path / icon 입력.
3. **Required permission**에서, 이 항목을 보려면 가져야 할 권한 선택(비우면 "모두에게").
4. 중첩하려면 새 항목의 **Parent**를 기존 항목으로 설정.
5. 드래그로 순서 변경 — `displayOrder`로 저장됩니다.

전체 화면은 [관리자 콘솔 가이드 → Menus](admin-console.md#menus) 참고.

=== "REST API"

```bash
# user.read 보유자에게만 보이는 최상위 "Users" 항목:
curl -X POST localhost:8080/admin/api/v1/menus \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"tenantId": "default",
"code": "users",
"label": "Users",
"path": "/users",
"icon": "pi pi-users",
"requiredPermission": "user.read",
"displayOrder": 20,
"parentId": null
}'
```

`requiredPermission`과 `parentId`는 선택 — 공개·최상위 항목이면 생략(또는 `null`).
`menus` 리소스 전체(목록, 트리, 수정, 삭제)는 [관리자 REST API](../reference/admin-api.md) 참고.

## 2단계 — 걸러진 트리를 프론트엔드에 내려주기

현재 사용자의 트리를 돌려주는 엔드포인트 하나를 두세요. 필터링은 kit이 하니, `menusFor`가
주는 걸 그대로 반환하면 됩니다:

```java
// src/main/java/com/example/myapp/NavController.java
import kr.devslab.kit.menu.MenuProvider;
import kr.devslab.kit.menu.MenuTree;
import kr.devslab.kit.identity.CurrentUserProvider;

@RestController
class NavController {

private final MenuProvider menus;
private final CurrentUserProvider users;

NavController(MenuProvider menus, CurrentUserProvider users) {
this.menus = menus; this.users = users;
this.menus = menus;
this.users = users;
}

@GetMapping("/api/nav")
Expand All @@ -30,17 +92,60 @@ class NavController {
}
```

## 항목 관리
응답은 `MenuTree` — 루트 `MenuItem` 목록이고, 각 항목의 허용된 자식이 `children` 아래에
중첩됩니다:

```json
{
"roots": [
{ "code": "dashboard", "label": "Dashboard", "path": "/", "icon": "pi pi-home",
"requiredPermission": null, "children": [] },
{ "code": "users", "label": "Users", "path": "/users", "icon": "pi pi-users",
"requiredPermission": "user.read", "children": [
{ "code": "users.invite", "label": "Invite", "path": "/users/invite",
"icon": "pi pi-user-plus", "requiredPermission": "user.write", "children": [] }
] }
]
}
```

`user.read`가 **없는** 사용자에게는 `users` 노드 자체가 안 보이고 — 유일한 자식도 같이
사라지므로 — 덩그러니 남는 게 없습니다.

메뉴 항목의 생성·수정·순서변경·삭제는 관리자 API(`menus`)로 합니다 —
[관리자 REST API](../reference/admin-api.md) 참고.
[관리자 콘솔](https://github.com/devslab-kr/devslab-kit-admin-ui)이 트리 편집기를 제공합니다.
## 3단계 — 화면에 그리기

프론트엔드는 트리를 그대로 렌더링합니다. 최소 예:

```vue
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const roots = ref([])
onMounted(async () => { roots.value = (await axios.get('/api/nav')).data.roots })
</script>

<template>
<nav>
<RouterLink v-for="item in roots" :key="item.code" :to="item.path">
<i :class="item.icon" /> {{ item.label }}
<!-- 중첩 메뉴는 item.children 재귀 -->
</RouterLink>
</nav>
</template>
```

## 캐싱

사용자별 트리는 공유 [캐시](cache.md)에 (사용자 id를 키로) 캐시되어, 반복되는 내비게이션
요청이 매번 재계산하지 않습니다. 사용자의 노출 메뉴를 수정하면 해당 항목이 evict됩니다.
사용자별 트리는 공유 [캐시](cache.md)에 (user id 키로) 캐시되어, 반복되는 내비게이션 요청
마다 다시 계산하지 않습니다. 사용자가 볼 수 있는 메뉴를 수정하면 해당 항목이 자동으로
무효화됩니다 — 직접 관리할 일이 없습니다.

!!! note "의존 방향"
메뉴는 권한을 참조할 수 있지만, 권한은 메뉴를 전혀 모릅니다 — 그 반대 방향 의존은
없습니다(핵심 [설계 원칙](../index.md#why-a-starter)).
메뉴는 권한을 참조하지만, 권한은 메뉴를 전혀 모릅니다 — 의존이 거꾸로 뒤집히지 않습니다
(핵심 [설계 원칙](../index.md#why-a-starter)).

## 더 보기

- [Access (RBAC + ABAC)](access.md) — 항목에 붙일 권한을 정의.
- [관리자 콘솔 → Menus](admin-console.md#menus) — 트리 편집기.
- [관리자 REST API](../reference/admin-api.md) — `menus` 리소스.
136 changes: 122 additions & 14 deletions docs/guides/menus.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,93 @@
# Dynamic Menus

The kit builds a **per-user menu tree**: you define the full menu once (with a
required permission per item), and each user sees only the items they're allowed.
A **dynamic menu** is a navigation tree where each user sees only the items their
permissions allow. You define the full menu **once**, tag each item with a required
permission, and the kit hands every user their own filtered copy. No `v-if="canSeeX"`
scattered across your frontend.

## How it works
New here? Do the [Tutorial](../getting-started/tutorial.md) first, then come back —
this guide assumes you have a running app with a few permissions defined.

1. Menu items are stored with an optional `requiredPermission` and an order.
2. `MenuProvider` builds the tree for a given user, **filtering out** items whose
required permission the user lacks (and pruning now-empty branches).
3. The result is an immutable `MenuTree(List<MenuItem> roots)`.
## The mental model

```
You define (once, per tenant) Kit filters (per request) You render
───────────────────────────── ───────────────────────── ──────────
Dashboard (no permission) ┐
Users (needs user.read) ├──► menusFor(currentUser) ──► MenuTree JSON
└ Invite (needs user.write) │ drops items the user → your sidebar
Billing (needs billing.read)┘ can't see, prunes empties
```

Three moving parts:

1. **Menu items** live in the kit (one row each: a label, a path, an optional
`requiredPermission`, a display order, an optional parent for nesting).
2. **`MenuProvider`** builds the tree for a given user — **dropping** items whose
`requiredPermission` the user lacks and **pruning** branches that end up empty.
3. **Your frontend** fetches the filtered tree and renders it. It never decides
visibility itself.

## Step 1 — Define your menu items

Each item has: a `code` (stable id), a `label` (what users see), a `path` (where it
links), an optional `icon`, an optional `requiredPermission`, a `displayOrder`, and an
optional `parentId` (omit for a top-level item).

=== "Admin console"

1. Open the [admin console](admin-console.md) → **Menus**.
2. Click **New** and fill in label / path / icon.
3. In **Required permission**, pick the permission a user must hold to see this
item (leave blank for "everyone").
4. To nest, set the new item's **Parent** to an existing item.
5. Drag to reorder — the order is saved as `displayOrder`.

See the [Admin Console guide → Menus](admin-console.md#menus) for the full screen.

=== "REST API"

```bash
# Top-level "Users" item, visible only to holders of user.read:
curl -X POST localhost:8080/admin/api/v1/menus \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"tenantId": "default",
"code": "users",
"label": "Users",
"path": "/users",
"icon": "pi pi-users",
"requiredPermission": "user.read",
"displayOrder": 20,
"parentId": null
}'
```

`requiredPermission` and `parentId` are optional — omit (or `null`) for a
public, top-level item. See the [Admin REST API](../reference/admin-api.md) for
the full `menus` resource (list, tree, update, delete).

## Step 2 — Serve the filtered tree to your frontend

Expose one endpoint that returns the current user's tree. The kit does the filtering;
you just return what `menusFor` gives you:

```java
// src/main/java/com/example/myapp/NavController.java
import kr.devslab.kit.menu.MenuProvider;
import kr.devslab.kit.menu.MenuTree;
import kr.devslab.kit.identity.CurrentUserProvider;

@RestController
class NavController {

private final MenuProvider menus;
private final CurrentUserProvider users;

NavController(MenuProvider menus, CurrentUserProvider users) {
this.menus = menus; this.users = users;
this.menus = menus;
this.users = users;
}

@GetMapping("/api/nav")
Expand All @@ -30,19 +97,60 @@ class NavController {
}
```

## Managing items
The response is a `MenuTree` — a list of root `MenuItem`s, each with its allowed
children nested under `children`:

```json
{
"roots": [
{ "code": "dashboard", "label": "Dashboard", "path": "/", "icon": "pi pi-home",
"requiredPermission": null, "children": [] },
{ "code": "users", "label": "Users", "path": "/users", "icon": "pi pi-users",
"requiredPermission": "user.read", "children": [
{ "code": "users.invite", "label": "Invite", "path": "/users/invite",
"icon": "pi pi-user-plus", "requiredPermission": "user.write", "children": [] }
] }
]
}
```

A user **without** `user.read` simply won't see the `users` node at all — and because
its only child is then gone too, nothing dangling is left behind.

Create, edit, reorder and delete menu items through the admin API (`menus`) — see
[Admin REST API](../reference/admin-api.md). The
[admin console](https://github.com/devslab-kr/devslab-kit-admin-ui) provides a tree
editor for them.
## Step 3 — Render it

Your frontend renders the tree verbatim. A minimal example:

```vue
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const roots = ref([])
onMounted(async () => { roots.value = (await axios.get('/api/nav')).data.roots })
</script>

<template>
<nav>
<RouterLink v-for="item in roots" :key="item.code" :to="item.path">
<i :class="item.icon" /> {{ item.label }}
<!-- recurse into item.children for nested menus -->
</RouterLink>
</nav>
</template>
```

## Caching

The per-user tree is cached on the shared [cache](cache.md) (keyed by user id), so
repeated navigation requests don't recompute it. Editing a user's visible menus
evicts their entry.
evicts their entry automatically — you don't manage this.

!!! note "Direction of dependency"
Menus may reference permissions, but permissions know nothing about menus — the
dependency never reverses (a core [design principle](../index.md#why-a-starter)).

## See also

- [Access (RBAC + ABAC)](access.md) — define the permissions you tag items with.
- [Admin Console → Menus](admin-console.md#menus) — the tree editor.
- [Admin REST API](../reference/admin-api.md) — the `menus` resource.
Loading
Loading