# 앱·프로젝트 관리
> **한 줄로**: 같은 API 키 하나로 여러 앱·고객사·환경을 운영하면서, 각각 다른 정책을 적용하고 사용량·로그를 분리해서 보는 기능이에요.
>
> 앱이 하나뿐이면 굳이 만들 필요 없어요. 대시보드의 [설정](/dashboard/settings) 정책이 모든 호출에 자동 적용됩니다.
## 언제 쓰면 좋아요?
- **앱이 여러 개**: 모바일 앱은 욕설만 차단 / 어린이용 앱은 모든 혐오 차단. 같은 키로 둘 다 운영하면서 정책만 다르게.
- **고객사가 여러 곳**: B2B SaaS 운영자가 고객사 A·B·C 별로 차단 강도와 로그 보관 정책을 따로 설정. 사용량·청구도 자동 분리.
- **환경(개발·스테이징·운영)**: 운영은 본문 미저장, 개발은 전체 보관. 같은 코드·같은 키로 운영하면서 환경만 헤더로 구분.
- **소속 사용자가 위탁사를 1,000개씩 보유**: 위탁사마다 자체 정책. 한 곳의 데이터가 다른 곳에 절대 노출되지 않음.
기술적으로는 한 사용자가 **여러 앱** 을 운영하거나, 자기 밑으로 **수많은 sub-project** 를 둘 때 — 각 앱·프로젝트 별로 차단 정책을 분리하고, 단일 API 키로 호출 시 헤더로 구분하는 패턴이에요.
## 개념
**앱·프로젝트 트리** — 조직(org) 아래에 여러 앱·프로젝트가 있을 수 있고, 각 앱·프로젝트가 다시 자식 앱·프로젝트를 가질 수 있어요. 정책은 **가까운 쪽이 우선**:
```
Org (default policy)
├── Tenant "mobile-app" ← 자체 정책 있음
├── Tenant "web-app" ← 자체 정책 있음
└── Tenant "customer-acme" ← 자체 정책
├── sub "acme-prod" ← 자체 정책 있으면 사용
└── sub "acme-staging" ← 정책 없음 → 부모 acme 정책 inherit
```
각 앱·프로젝트는:
- **slug** (URL/API 식별자, 영문 알파벳·숫자·하이픈)
- **name** (display 이름)
- **filter_policy_json** (PII / DLP / Moderation 별 차단·허용 카테고리)
- **audit_settings_json** (각 모델 audit 레벨: none / blocked / full)
## 호출 시 앱·프로젝트 지정
기본은 `Authorization` 헤더만 — 그러면 키의 org-level 정책이 적용돼요. 특정 앱·프로젝트로 호출하려면 헤더 추가:
```http
Authorization: Bearer sk_live_…
X-Corepin-Tenant: mobile-app
```
또는 자식 앱·프로젝트:
```http
X-Corepin-Tenant: customer-acme/acme-prod
```
응답의 `meta.tenant` 에 적용된 앱·프로젝트 slug가 반환돼요.
## 1. 앱·프로젝트 CRUD
### 1.1 목록
```bash
curl https://api.corepin.ai/v1/tenants \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
응답:
```json
{
"tenants": [
{
"id": 12,
"slug": "mobile-app",
"name": "Mobile App",
"parent_slug": null,
"created_at": 1777685000,
"policies": {"pii": true, "dlp": false, "moderation": true},
"audit": {"pii": "blocked", "dlp": "none", "moderation": "blocked"}
},
...
]
}
```
### 1.2 생성
```bash
curl -X POST https://api.corepin.ai/v1/tenants \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "mobile-app",
"name": "Mobile App",
"parent_slug": null
}'
```
`slug` 는 영문 소문자·숫자·하이픈만, 1 - 40자. 같은 부모 안에서 unique.
### 1.3 단건 조회
```bash
curl https://api.corepin.ai/v1/tenants/mobile-app \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
### 1.4 수정 / 삭제
```bash
# 이름·부모 변경
curl -X PATCH https://api.corepin.ai/v1/tenants/mobile-app \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "iOS + Android"}'
# 삭제 (자식 있으면 거부됨; 자식 먼저 삭제)
curl -X DELETE https://api.corepin.ai/v1/tenants/mobile-app \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
### 1.5 비활성화 / 재활성화
운영 정지 (계약 만료, 보안 사고, 법무 요청 등) 시 앱·프로젝트를 즉시 차단할 수 있어요. **자식 앱·프로젝트가 있어도 동작** — 삭제와 달리 비활성화는 흔적을 남깁니다.
```bash
# 앱·프로젝트 비활성화 — bound 된 모든 API 키 자동 폐기
curl -X POST https://api.corepin.ai/v1/tenants/mobile-app/disable \
-H "Authorization: Bearer $COREPIN_API_KEY"
# → {"disabled": true, "slug": "mobile-app", "revoked_keys": 3}
# 재활성화 — 비활성화로 폐기된 키는 복구 안 됨, 새로 발급해야 함
curl -X POST https://api.corepin.ai/v1/tenants/mobile-app/enable \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
비활성화 시:
- 앱·프로젝트 자체 행에 `disabled_at` 가 세팅돼요.
- 그 앱·프로젝트에 bound 된 모든 API 키 (`api_keys.tenant_id = <slug-id>`) 가 즉시 폐기 (`revoked_at`).
- 그 키 또는 `X-Corepin-Tenant: <slug>` 헤더로 호출 시도 → **401 `tenant_disabled`** (invalid_api_key와 구별되는 명시적 코드).
- org-scope 키는 정상 — 다른 테넌트는 영향 없음.
재활성화 후 새 키를 발급하려면 `POST /v1/tenants/{slug}/keys` (§5.1) 사용. 이전 키는 보안 감사 trail 보존을 위해 의도적으로 복구되지 않아요.
권한: org admin 또는 system key만 호출 가능. 테넌트 전용 키는 자기 앱·프로젝트도 비활성화 못 함 (자해 방지).
## 2. 정책 조회 / 수정
조직 default 정책과 앱·프로젝트별 정책을 따로 관리할 수 있어요. 앱·프로젝트에 자체 정책이 없으면 부모 → 조직 default로 fallback.
### 2.0 조직 default 정책
```bash
# 조회
curl https://api.corepin.ai/v1/policy/moderation \
-H "Authorization: Bearer $COREPIN_API_KEY"
# 저장
curl -X PUT https://api.corepin.ai/v1/policy/moderation \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"block_labels": ["M07","M08","M09","M10"], "audit_level": "blocked"}'
```
조직 전체 fallback 으로 적용돼요. 앱·프로젝트별 override는 §2.1-2.3 참조.
### 2.1 한 앱·프로젝트의 모델별 정책 조회
```bash
curl https://api.corepin.ai/v1/tenants/mobile-app/policy/moderation \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
응답:
```json
{
"tenant_slug": "mobile-app",
"model_id": "moderation",
"policy": {
"block_labels": ["M07", "M08", "M09", "M10", "M01"],
"audit_level": "blocked"
},
"inherited_from": null
}
```
`inherited_from` — 이 앱·프로젝트에 자체 정책이 없어 부모/조직의 정책을 사용 중일 때, 정책의 출처 앱·프로젝트 slug 또는 `"organization"`.
### 2.2 정책 저장
```bash
curl -X PUT https://api.corepin.ai/v1/tenants/mobile-app/policy/moderation \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"block_labels": ["M07", "M08", "M09", "M10", "M01", "M02"],
"audit_level": "blocked"
}'
```
PII / DLP 정책은 라벨 코드 대신 카테고리 / 등급 키를 사용해요:
```bash
# PII 정책 — 카테고리별 차단/허용
curl -X PUT https://api.corepin.ai/v1/tenants/mobile-app/policy/pii \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"categories": {
"kr_rrn": true,
"kr_passport": true,
"private_email": false,
"private_phone": false
},
"audit_level": "full"
}'
# DLP 정책 — 등급별 차단
curl -X PUT https://api.corepin.ai/v1/tenants/mobile-app/policy/dlp \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"block_grades": ["RESTRICTED", "TRADE_SECRET", "CLASSIFIED"],
"block_types": [],
"audit_level": "blocked"
}'
```
### 2.3 정책 삭제 (부모 정책으로 fallback)
```bash
curl -X DELETE https://api.corepin.ai/v1/tenants/mobile-app/policy/moderation \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
이 앱·프로젝트의 자체 정책이 제거되고, 다음 호출부터는 부모 / 조직 정책을 inherit.
## 3. 정책 resolve 순서 (호출 시점)
매 호출마다 **각 모델별로 독립적으로** 다음 우선순위를 walk 합니다 — 가장 위 레벨에서 정의된 값이 그 모델에 적용돼요. 한 모델만 자체 정책을 두면 그 모델만 덮어쓰이고 나머지 모델은 그대로 fallback.
```
┌─ 1. 요청별 override (가장 우선)
│ 호출 본문의 block_labels (유해발화) / apply_policy=true (PII·DLP)
│
├─ 2. 이 앱·프로젝트의 자체 정책 ← /dashboard/tenants?edit=… 에서 토글
│
├─ 3. 부모 앱·프로젝트의 정책 ← parent_tenant_id 체인 따라 위로
│ (예: prod 미설정 → acme 부모 → acme 정책 사용)
│
├─ 4. 조직 기본 정책 ← /dashboard/settings에서 토글
│
└─ 5. 시스템 기본값
· PII : 모든 카테고리 keep (개인정보 모두 통과 — 사용자가 기본 켜야 차단)
· DLP : 차단 등급 / 유형 모두 비어있음 (모든 등급 통과)
· Mod : 차단 라벨 비어있음 (모든 라벨 통과)
```
`X-Corepin-Tenant` 헤더 미지정 + 키가 tenant-bound도 아닌 경우 → 2~3단계 건너뛰고 4 → 5만 평가.
각 모델은 **독립적으로 walk** 하므로, 예를 들어 `acme-prod` 앱·프로젝트에서 PII만 자체 정책 두면:
| 모델 | 적용되는 정책 |
|---|---|
| PII | acme-prod 자체 (2단계에서 매치) |
| DLP | 부모 `acme` 또는 조직 기본 |
| Mod | 부모 `acme` 또는 조직 기본 |
| Chat | 부모 `acme` 또는 조직 기본 |
| MH | 부모 `acme` 또는 조직 기본 |
대시보드 편집 화면의 모델별 `출처: …` 배지가 **현재 어느 단계에서 매치됐는지** 알려줘요 — `tenant` (자체) / 부모 slug / `organization` (조직 기본) / `default` (시스템 기본).
## 4. 대시보드
[대시보드 → 설정 → 앱·프로젝트](/dashboard/tenants)에서 GUI로 관리 가능. API와 1:1 동기화 — 둘 중 어느 쪽으로 변경해도 즉시 반영.
## 5. 테넌트 전용 키 (B2B SaaS 통합)
한 사용자가 자기 고객사 여러 개를 운영하는 경우, 각 고객사마다 **테넌트 전용 키** 를 발급해서 고객사 관리자에게 부여해요. 이 키로 호출하면 자기 테넌트만 접근 가능 — 다른 고객사 데이터는 절대 못 봐요 (cross-tenant 격리는 코드 레벨에서 강제).
### 5.1 키 발급
```bash
# org admin 키로 호출 → A 고객사 (slug=acme) 전용 키 발급
curl -X POST https://api.corepin.ai/v1/tenants/acme/keys \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note": "A 고객사 운영 키", "tier": "business"}'
```
응답 (raw token은 **이때만 1회** 보여줘요):
```json
{
"token": "sk_live_xxxxxxxxxxxxxxxxx",
"key_id": "sk_live_xxxxxx",
"tenant_slug": "acme",
"tenant_id": 42,
"tier": "business",
"warning": "raw token은 이 응답에서만 보여드려요. 분실 시 재발급 필요."
}
```
### 5.2 테넌트 전용 키의 동작
- `Authorization: Bearer sk_live_xxx` 만 보내도 자동으로 자기 테넌트 scope.
- `X-Corepin-Tenant` 헤더로 **다른 테넌트 지정 시 401** (cross-tenant 우회 차단).
- `/v1/tenants` 리스트는 자기 테넌트만 반환.
- `/v1/tenants/{다른슬러그}` GET/PATCH/DELETE → **403 tenant_access_denied**.
- `/v1/tenants/{다른슬러그}/policy/{model_id}` GET/PUT/DELETE → **403 tenant_access_denied**.
- `/v1/policy/{model_id}` (org-default) → **403** (자기 테넌트 정책은 `/v1/tenants/{자기슬러그}/policy/{model_id}` 으로).
- `/v1/{model}/audit`, `/v1/{model}/history` → 자기 테넌트의 로그·이력만.
- 모델 호출 (`/v1/pii/detect` 등) → 자기 테넌트의 정책으로만 필터링.
### 5.3 키 관리
```bash
# 한 테넌트의 키 목록 (raw token 없음)
curl https://api.corepin.ai/v1/tenants/acme/keys \
-H "Authorization: Bearer $COREPIN_API_KEY"
# 키 폐기 (cross-tenant 시도 자동 차단)
curl -X DELETE https://api.corepin.ai/v1/tenants/acme/keys/sk_live_xxxxxx \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
### 5.4 통합 패턴 (B2B 콘솔 시나리오)
```
운영 콘솔 (단일 Corepin org)
├── A 고객사 가입 → Corepin tenant "acme" 자동 생성 + 전용 키 발급
│ └── A 고객사 관리자 = 그 키로 자기 정책 토글, 자기 로그만 조회
├── B 고객사 가입 → tenant "globex" + 전용 키 ...
└── ... (반복)
```
각 고객사 관리자가 운영 콘솔에서 정책 토글 → 그 토글이 Corepin API의 `PUT /v1/tenants/{자기슬러그}/policy/{model_id}` 호출로 매핑돼서 즉시 반영. **다른 고객사의 토글에는 절대 영향 없음** (코드 레벨 격리).
## 6. 앱·프로젝트별 예산 한도
조직 전체 예산 (`/dashboard/billing`) 외에 **앱·프로젝트마다 독립된 한도**를 둘 수 있어요. 한 앱·프로젝트가 한도를 다 써도 다른 앱·프로젝트는 영향 없음 — B2B 1,000 고객사 시나리오에서 한 고객사 폭주가 다른 고객사를 차단하지 못하게 하는 핵심 안전망.
평가 순서: 호출 시 **조직 한도** + **앱·프로젝트 한도** 둘 다 체크 → 어느 쪽이든 초과하면 차단.
### 6.1 앱·프로젝트 예산 조회
```bash
curl https://api.corepin.ai/v1/tenants/acme/budgets \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
응답:
```json
{
"tenant_slug": "acme",
"tenant_id": 42,
"budgets": [
{
"tenant_id": 42,
"model_id": "*",
"monthly_amount_krw": 200000,
"alert_at_percent": 80,
"hard_block": true,
"notify_emails": "ops@acme.com"
},
{
"tenant_id": 42,
"model_id": "pii",
"monthly_amount_krw": 50000,
"alert_at_percent": 90,
"hard_block": true,
"notify_emails": ""
}
],
"count": 2
}
```
### 6.2 앱·프로젝트 예산 설정 / 삭제
```bash
# 모델별 한도 설정 (또는 갱신)
curl -X PUT https://api.corepin.ai/v1/tenants/acme/budgets/pii \
-H "Authorization: Bearer $COREPIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"monthly_amount_krw": 50000,
"alert_at_percent": 80,
"hard_block": true,
"notify_emails": "ops@acme.com,finance@acme.com"
}'
# 한도 제거 (그 앱·프로젝트의 그 모델만 — 조직 한도는 그대로 적용)
curl -X DELETE https://api.corepin.ai/v1/tenants/acme/budgets/pii \
-H "Authorization: Bearer $COREPIN_API_KEY"
```
`model_id` 값:
- `*` — 그 앱·프로젝트의 모든 모델 합계 (PII + DLP + Mod + MH + Chat KRW 합산)
- `pii` / `dlp` / `moderation` / `chat` / `mental_health` — 모델별 개별 한도
### 6.3 평가 동작
- **조직 한도** + **앱·프로젝트 한도** 모두 항상 평가. 둘 중 하나라도 초과 시 호출 차단 (`429 budget_exceeded`).
- 응답의 `info.scope` = `"org"` 또는 `"tenant"` 로 어느 한도가 막았는지 알려줌.
- `hard_block: false` 면 알림만, 차단 안 함.
- 한도가 조직만·앱·프로젝트만·둘 다 — 어떤 조합이든 가능.
- 앱·프로젝트가 비활성화돼도 한도 행은 보존 (재활성화 시 그대로 유효).
권한: org admin / system key 또는 그 앱·프로젝트에 bound 된 admin 키만. 다른 앱·프로젝트의 한도 조회·수정 시도 → 403 `tenant_access_denied`.
## 7. 한도 / 제한
- 한 조직 당 앱·프로젝트 트리 최대 깊이: **5단계**
- 한 조직 당 앱·프로젝트 총 개수: **5,000** (그 이상 필요 시 영업팀 문의)
- 정책 변경 반영 지연: **5초 이내** (in-memory 캐시 TTL)
- 사용량·청구는 **앱·프로젝트별 분리 집계** (대시보드 → 사용량 에서 앱·프로젝트 필터)
## 8. 운영 팁
- 새 환경 (예: staging) 띄울 때 자식 앱·프로젝트 만들고 정책 미설정 → 운영 정책 자동 inherit. 검증 후 정책 분리.
- sub-project가 많을 때 (1,000+)는 정책을 부모 트리에 한 번만 설정하고, 자식들은 inherit만 사용 권장 — DB 부담 ↓.
- 앱·프로젝트별 사용량 alert도 설정 가능 (대시보드 → 사용량 → 알림).
- 비활성화한 앱·프로젝트는 `disable` 후 1주일 이내에 복구 결정 권장 — 그 이후는 관련 키·로그 archival 처리 가능.