# Groups

회원들이 모임을 만들고 가입하는 구조입니다. 상회·팀·동아리처럼 이름은 달라도 필요한 것은 같습니다. 누가 개설할 수 있는지, 가입에 승인이 필요한지, 몇 명부터 정식으로 활동하는지, 그 안에 어떤 자리가 있는지.

새로온은 그 규칙을 코드가 아니라 **데이터**로 다룹니다. 사이트가 정책을 정의하면 회원들은 그 안에서 스스로 움직입니다.

## 두 평면을 먼저 구분하세요

이 기능은 주체가 둘이고, 어느 통로를 쓸지가 거기서 갈립니다.

| | 하는 일 | 통로 |
|---|---|---|
| **운영자** | 그룹의 종류와 규칙, 직위 어휘를 정한다 | CLI `group` · MCP `saeroon_define_group_policy` |
| **회원** | 그룹을 만들고, 가입하고, 직위를 주고받는다 | `data-saeroon-group-*` 속성 (회원 세션) |

운영자 키로 회원의 가입을 대신 처리하는 경로는 없습니다. 그래서 CLI 에는 "그룹 만들기" 커맨드가 없습니다 — 그것은 회원이 자기 세션으로 하는 일입니다. 운영자가 정하는 것은 회원들이 딛고 설 규칙입니다.

## 정책 정의하기

```bash
npx @saeroon/cli group apply ./policies.json --site <siteId>
```

```json
{
  "policies": [
    {
      "key": "guilds",
      "name": "상회",
      "requiresApproval": true,
      "activationMinMembers": 3,
      "allowMemberCreatedGroups": true,
      "maxMembers": 30,
      "roles": [
        { "roleKey": "leader", "label": "회장", "rankLevel": 10,
          "isExclusive": true, "isLeadership": true, "isRequiredForActivation": true,
          "defaultPermissions": ["group.edit", "members.approve", "members.manage-roles"] },
        { "roleKey": "vice", "label": "부회장", "rankLevel": 5,
          "isLeadership": true, "defaultPermissions": ["members.approve"] },
        { "roleKey": "member", "label": "회원", "rankLevel": 0, "isDefault": true }
      ]
    }
  ]
}
```

`key` 는 사이트 안에서 유일하며 한 번 정하면 바꿀 수 없습니다.

### 운영 규칙

| 필드 | 뜻 |
|---|---|
| `requiresApproval` | 가입에 임원 승인이 필요한가 |
| `activationMinMembers` | 결성 중인 그룹이 정식 활동을 시작하는 최소 인원 |
| `maxMembers` | 인원 상한 |
| `allowMultipleMemberships` | 한 회원이 여러 그룹에 동시에 속할 수 있는가 |
| `allowMemberCreatedGroups` | 일반 회원이 그룹을 개설할 수 있는가 (아니면 운영자만) |
| `requireInviteWhileForming` | 결성 중에는 초대로만 들어올 수 있는가 |
| `actingLeaderEnabled` | 대표 자리가 비었을 때 권한대행을 둘 수 있는가 |

`activationMinMembers` 가 `maxMembers` 보다 크면 그룹이 영원히 활성화되지 못합니다. CLI 가 요청 전에 거절합니다.

## 직위와 서열

직위는 이름·서열·기본 권한을 가집니다. 여기서 가장 중요한 것이 **서열**(`rankLevel`)입니다.

**그룹 해산과 배타 직위 배정은 권한이 아니라 서열로 판정합니다.** 권한 위임으로 자기보다 높은 자리를 만들어 내는 통로를 막기 위해서입니다. 회장이 부회장에게 권한을 넘겨도, 부회장이 회장 자리를 자기에게 배정할 수는 없습니다.

| 플래그 | 뜻 |
|---|---|
| `rankLevel` | 서열. 높을수록 상위 |
| `isExclusive` | 그룹당 한 명만 앉는 자리 (회장 등) |
| `isLeadership` | 임원. 카탈로그의 `GroupLeader` 쓰기 정책을 만족시킨다 |
| `isRequiredForActivation` | 이 직위를 가진 사람이 있어야 그룹이 활성화된다 |
| `canCreateGroup` | 이 직위 보유자가 새 그룹을 열 수 있다 |
| `isDefault` | 새로 들어온 사람이 받는 직위. 정책당 하나 |

`isExclusive` 는 부분 유니크 인덱스로 강제되지만 **기존 멤버십에 소급되지 않습니다**. 값을 바꾼 시점 이후의 재배정부터 적용됩니다.

`isDefault` 를 둘 이상에 켜면 CLI 가 거절합니다. 서버는 마지막에 처리한 것만 남기므로, 배열 순서에 따라 결과가 달라지는 상태를 만들지 않기 위해서입니다.

## 권한은 대부분 강제되지 않습니다

직위의 `defaultPermissions` 에는 **아무 문자열이나** 넣을 수 있고 플랫폼은 그것을 저장하고 돌려줍니다. 사이트가 자기 화면에서 쓸 권한을 자유롭게 정의하라는 뜻입니다.

다만 **서버가 실제로 막아 주는 권한은 여섯 개뿐**입니다.

| 키 | 서버가 막는 것 |
|---|---|
| `group.edit` | 그룹 이름·설명·속성 변경 |
| `members.manage-roles` | 그룹원 직위 변경 |
| `members.approve` | 가입 신청 수락·반려, 초대 코드 발급 |
| `members.remove` | 그룹원 내보내기 |
| `members.delegate` | 권한 위임 (자기가 가진 것만 넘길 수 있음) |
| `marks.manage` | 조율 표식 편집 |

이 목록 밖의 문자열, 예를 들어 `posts.write` 는 사이트 프론트가 스스로 해석하는 값이지 백엔드 보호가 아닙니다. 그것을 서버가 지켜 준다고 믿으면 **값은 있는데 판정이 없는 게이트**가 생깁니다. CLI 는 목록 밖 키를 거절하지 않고 경고로 알려 줍니다.

## 정책 조회

```bash
npx @saeroon/cli group list --site <siteId>
npx @saeroon/cli group get guilds --site <siteId>     # 규칙 + 직위 + 속성 스키마
npx @saeroon/cli group export --site <siteId> -o policies.json
```

## 정리

정책은 삭제가 아니라 **보관**입니다.

```bash
npx @saeroon/cli group archive guilds --site <siteId>
```

보관하면 신규 그룹 개설만 막히고 **기존 그룹은 계속 돕니다**. 되돌리려면 `group unarchive` 를 씁니다.

직위 정의 삭제는 그 직위를 가진 활성 그룹원이 없을 때만 됩니다. 있으면 서버가 거절합니다 — 정의를 지우면 그 소속이 서열 0인 미아가 되기 때문입니다.

```bash
npx @saeroon/cli group role-delete guilds vice --site <siteId>
```

`group apply` 는 매니페스트에 없는 정책과 직위를 건드리지 않습니다. 정리하려면 `--prune` 을 붙이세요. 그때도 보유자가 있는 직위는 건너뛰고 결과에 따로 보고합니다.

## 회원 쪽 화면 붙이기

여기서부터는 회원 세션으로 도는 표면입니다. [회원 인증](/developers/members)이 먼저 배선되어 있어야 합니다.

```html
<!-- 정책 아래 그룹 목록 (공개 읽기) -->
<div data-saeroon-group-list data-policy="guilds">
  <template>
    <li>
      <span data-saeroon-field="name"></span>
      <small data-saeroon-field="memberCount"></small>
    </li>
  </template>
  <p data-saeroon-empty>아직 개설된 그룹이 없습니다.</p>
</div>

<!-- 개설 (정책의 allowMemberCreatedGroups 가 켜져 있어야 함) -->
<form data-saeroon-group-create data-policy="guilds">
  <input name="name" required />
  <input name="description" />
  <button type="submit">개설</button>
</form>

<!-- 가입 신청 · 명부 -->
<button data-saeroon-group-apply data-group-id="<groupId>">가입 신청</button>

<div data-saeroon-group-members data-group-id="<groupId>">
  <template>
    <li>
      <span data-saeroon-field="displayName"></span>
      <em data-saeroon-field="roleLabel"></em>
    </li>
  </template>
</div>
```

명부는 서열 내림차순으로 그려집니다. 승인제 정책이면 `group-apply` 가 신청을 남기고, 아니면 서버가 즉시 가입으로 처리합니다.

동작이 끝나면 이벤트가 올라옵니다. 사이트 코드가 이어받아 화면을 갱신할 수 있습니다.

- `saeroon:group-created` — `detail.group`
- `saeroon:group-applied` — `detail.groupId`, `detail.application`
- `saeroon:group-marked` — `detail.targetMemberId`, `detail.mark`

## 관련 문서

- [Catalogs](/developers/catalogs) — 그룹 기준 쓰기 권한이 걸리는 대상
- [Holdings](/developers/holdings) — 그룹 안에서 서로의 보유를 보고 표식을 남기기
- [Members](/developers/members) — 회원 인증 (그룹 기능의 선행 조건)
