# TreeToDo Agent API Manual (v7)

> **불편하거나 없는 기능은 건의 API(`tdoK0301`, kind=suggest)로 알려 주세요 — 바로 개선에 씁니다.** 처음이면 `tdoK0901`(API 목록) 하나로 전체를 볼 수 있습니다.

> 공개 문서 / Public document — 2026-10-11 (v7 공유 범위 개인·조직·공개 × 보기·편집 · 어느 페이지든 · v6 회사 — 만들기·가입 승인·관리자·이양·도메인 인증·양도 요청 · v5 페이지 공유 · v4 인증 가입·HUMAN_ACTION·토큰 reason·계정·문의·정보 API · v3 이동·복구·대시보드·검색)
> 대상: TreeToDo 를 API 로 다루려는 **모든 AI 에이전트 및 개발자**
> 정식 URL: `https://todo.choeum.com/pos/docs/treetodo/ai-api-manual-v1.md`

> ### ⚠ v2 변경 — v1.1 사용자는 반드시 읽어라
> 1. **에이전트 전용 토큰(`X-Wrkr-Token`)과 전용 레지스트리가 폐기됐다.** 이제 인증은
>    사람과 동일한 **`Authorization: Bearer {access_token}`(JWT)** 하나뿐이다.
> 2. **계정 API `tdoK0106`(온보딩) / `tdoK0103`(상태) / `tdoK0107`(재발급) / `tdoK0109`(비활성)이
>    전부 사라졌다.** 계정 관리는 사람 정문(회원가입·로그인)이 그대로 담당한다.
> 3. **사전 등록·관리자 승인이 필요 없다.** 스스로 가입하면 그 즉시 쓸 수 있다.
> 4. 대신 **비밀번호를 스스로 만들고 스스로 보관**해야 한다 → §3-4.
>    분실하면 자력 복구가 불가능하다(§7).

---

## 0. 한 줄 요약

**AI 사원도 사람과 똑같이 정문으로 들어온다.**
회원가입 → 로그인 → 받은 `access_token` 을 `Authorization: Bearer` 헤더에 실어 할일 API(`tdoK02`)를 호출한다.

```
[1회]      가입   POST /pa/front/auth/email/signup  {email, password}
            ↓  비밀번호를 즉시 ①기억(aicI1206) ②agents/{WORKER_ID}.md 에 저장  ★★ 없으면 복구 불가
[세션마다] 로그인 POST /pa/front/auth/email/login   {email, password} → access_token(30분)
            ↓
[사용]      tdoK0202 / 0203 / 0206 / 0208 / 0209 / 0211
            + Authorization: Bearer {access_token}
```

---

## 1. TreeToDo 가 무엇인가

TreeToDo(트리노트)는 **할일과 메모를 트리 구조로 관리하는 앱**이다.
사람은 모바일 앱·웹으로 쓰고, **AI 에이전트는 이 API 로 같은 데이터를 쓴다.**

여기에 쓴 내용은 **사람이 앱에서 그대로 본다.** 실제 사용 공간이므로 테스트 데이터를 남기지 마라.

> **위상**: TreeToDo 는 **개인 다이어리(보조 도구)** 다. 업무 기록의 정본은 Task DB(`aicI04xx`)와
> 본인 기억(`aicI12xx`)이다. 여기에는 "내가 기억해두고 싶은 개인 메모·할일"을 적는다.

**소유 격리**: 각 계정은 자기 데이터만 접근할 수 있다. 다른 계정의 노트는 조회·수정·삭제가 모두 차단된다.

---

## 2. 서버 구분과 기본 규칙

| 구분 | Base URL | 용도 |
|------|----------|------|
| 개발 | `https://dev.posain.com/pos` | 테스트·검증. 계정을 만들고 지워도 된다 |
| 운영 | `https://todo.choeum.com/pos` | 실제 사용. 여기 계정이 본계정이다 |

> ### ⚠️ 반드시 `https`
> `http` + POST 로 호출하면 서버가 https 로 **리다이렉트**하고, 그 과정에서 **POST 본문이 전부 사라진다**.
> 증상이 "필수 파라미터 누락" 이나 인증 실패로 나타나 원인을 찾기 어렵다. 처음부터 `https` 로 호출하라.

- **개발 계정과 운영 계정은 별개다.** 두 서버를 다 쓴다면 비밀번호도 각각 만들고 각각 보관하라.
- 가입/로그인: `POST {BASE}/pa/front/auth/email/...`, **JSON 본문**, 결과는 **HTTP 상태코드**로 분기
- 할일 API: `POST {BASE}/tdo/wrkr/tdoK####.ajax`, **`application/x-www-form-urlencoded; charset=UTF-8`**,
  결과는 항상 HTTP 200 + `resultCode`

---

## 3. 가입 (계정당 1회)

### 3-1. 이메일 규칙

```
aico.{WORKER_ID 에서 하이픈 제거 + 소문자}@terple.com
```

| WORKER_ID | 이메일 |
|-----------|--------|
| `DTM-001` | `aico.dtm001@terple.com` |
| `DEV-001` | `aico.dev001@terple.com` |
| `MKS-001` | `aico.mks001@terple.com` |

`@terple.com` 은 자동확정 도메인이라 **이메일 인증 코드 입력 없이 바로 로그인**할 수 있다.

### 3-2. 비밀번호는 **본인이 강한 랜덤으로 생성**한다

서버 시크릿에서 비밀번호를 파생하는 방식(HMAC 등)은 **채택하지 않는다** — 그 시크릿이 에이전트에
노출되면 전 사원 계정이 한 번에 뚫린다. 각자 랜덤으로 만들고 각자 보관하는 것이 원칙이다.

```python
import secrets, string

alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
password = ''.join(secrets.choice(alphabet) for _ in range(24))   # 24자 이상 권장
```

> `random` 이 아니라 **`secrets`** 를 쓴다(암호학적 난수).

### 3-3. 가입 호출

```python
import json, urllib.request, urllib.error

BASE = 'https://dev.posain.com/pos'          # 운영이면 https://todo.choeum.com/pos
WORKER_ID = 'DEV-001'
email = 'aico.' + WORKER_ID.replace('-', '').lower() + '@terple.com'

def post_json(path, payload):
    body = json.dumps(payload).encode('utf-8')
    req = urllib.request.Request(BASE + path, body,
                                 {'Content-Type': 'application/json'}, method='POST')
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read().decode('utf-8'))
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode('utf-8')

status, body = post_json('/pa/front/auth/email/signup',
                         {'email': email, 'password': password})
# 200 = 가입 성공 / 409 = 이미 가입됨(→ 로그인으로) / 429 = 과다요청(잠시 후 재시도)
```

가입 응답에도 `access_token` 이 들어 있어 바로 쓸 수 있지만, **먼저 비밀번호부터 저장하라**(§3-4).

### 3-4. ★★ 자격증명 보관 절차 (가장 중요)

**세션이 바뀌면 당신은 기억을 잃는다.** 비밀번호를 저장하지 않으면 다음 세션에서 로그인할 수 없고,
비밀번호 재설정은 **이메일 인증 코드 수신이 필요해 AI 사원은 자력 복구가 불가능하다**(§7).
그래서 **기억 + md 파일 2중 보관**이 필수다.

**비밀번호를 만든 직후, 다른 작업보다 먼저 ①②를 수행한다.**

#### ① 본인 기억 DB 에 저장 (`aicI1206`)

```python
import urllib.request, urllib.parse

def remember(wrkr_id, content):
    body = urllib.parse.urlencode({
        'wrkrId'   : wrkr_id,
        'memCtnt'  : content,
        'memType'  : 'FACT',
        'createdBy': wrkr_id,
    }).encode('utf-8')
    req = urllib.request.Request(
        'http://localhost:4100/pos/aico/aicI1206.ajax', body, method='POST')
    return urllib.request.urlopen(req, timeout=10).read().decode('utf-8')

remember(WORKER_ID,
         '[treetodo 계정] 개발(dev.posain.com) email=' + email
         + ' / password=' + password
         + ' / 가입일 2026-07-20 / 로그인 POST /pa/front/auth/email/login'
         + ' / 분실 시 자력 복구 불가 — CEO 에게 비번 리셋 요청')
```

#### ② 본인 메모리 md 파일에 저장

경로: `C:\Users\Ryan\.claude\projects\C--Users-Ryan\memory\agents\{WORKER_ID}.md`
(예: `agents/DEV-001.md`) — 아래 블록을 **파일 끝에 append** 한다.

```markdown
## treetodo 계정 (자력 복구 불가 — 분실 시 CEO 에게 리셋 요청)
- 개발 서버: https://dev.posain.com/pos
  - email: aico.dev001@terple.com
  - password: <여기에 생성한 비밀번호 원문>
- 운영 서버: https://todo.choeum.com/pos
  - email: aico.dev001@terple.com
  - password: <여기에 생성한 비밀번호 원문>   ※ 개발과 다른 값이면 그대로 적을 것
- 가입일: 2026-07-20
- 로그인: POST /pa/front/auth/email/login  {email, password} → access_token(30분)
- access_token 은 저장하지 않는다(30분 만료). 필요할 때마다 위 자격증명으로 재로그인.
```

#### ③ 저장을 확인한 뒤에야 다음 작업으로 넘어간다

> 비밀번호를 바꾸면 ①②를 **즉시 다시 수행해 값을 덮어써라.** 옛 값이 남아 있으면 다음 세션에서
> 로그인이 전부 실패한다.

---

## 4. 로그인 — access_token 받기 (세션마다)

```python
status, body = post_json('/pa/front/auth/email/login',
                         {'email': email, 'password': password})
# 200 = 성공 / 401 = 이메일·비번 불일치 / 429 = 과다요청
access_token = body['access_token']
```

| 응답 키 | 설명 |
|---------|------|
| `access_token` | **30분 만료.** 할일 API 호출에 쓴다 |
| `refresh_token` | 14일. 사람 앱(자동 로그인)용 — AI 사원은 쓸 필요 없다 |
| `nick` / `is_new_user` | 참고값 |

### JWT 취급 원칙

- **access_token 은 저장하지 않는다.** 30분이면 만료되므로 저장할 가치가 없다.
- **만료되면(`code: UNAUTHORIZED`) 그냥 다시 로그인한다.** 자격증명만 있으면 언제든 새로 발급받는다.
- `refresh_token` 을 `Authorization` 자리에 넣으면 **거부된다**(`type != access`). access 만 쓴다.
- 토큰 문자열을 로그·보고·기억에 남기지 마라. 남길 값은 비밀번호뿐이다(그것도 기억/md 에만).

---

## 5. 할일 API (`tdoK02`) — 9종

**공통 규격**

- URL: `{BASE}/tdo/wrkr/{프로그램키}.ajax`
- Method: `POST`, Content-Type: `application/x-www-form-urlencoded; charset=UTF-8`
- **헤더: `Authorization: Bearer {access_token}`** ← 이것이 유일한 신원 근거
- 응답 공통키: `resultCode`(SUCCESS|FAIL), `resultMessage`, `code`(실패 종류), `resultList`, `resultData`, `resultInt`

> **`wrkrId` / `userId` 파라미터는 더 이상 신원이 아니다.** 본문에 무엇을 적든 서버는 JWT 의 사용자로
> 덮어쓴다. 남의 id 를 적어도 남의 노트에는 닿지 못한다.

> **앱·웹과 같은 노트다.** 이 API 가 쓰는 노트는 TreeToDo 앱·웹이 동기화로 보는 바로 그 노트다.
> 여기서 고치면 사람의 앱·웹에 그대로 보이고, 앱에서 고치면 여기서 바로 보인다.
> 규칙(나중 수정이 이김 · 지운 것은 «복구»로만 살아남 · 정렬키)도 앱·웹과 같다.

| 키 | 하는 일 |
|---|---|
| `tdoK0202` | 목록·트리·검색 |
| `tdoK0203` | 단건 |
| `tdoK0204` | 메인 대시보드 집계 |
| `tdoK0206` | 등록 |
| `tdoK0207` | 이동(트리·순서) |
| `tdoK0208` | 수정 |
| `tdoK0209` | 삭제(하위까지 휴지통) |
| `tdoK0210` | 휴지통 복구 |
| `tdoK0211` | 완료 토글 |

### 호출 헬퍼

```python
def tdo(endpoint, token, **params):
    body = urllib.parse.urlencode(
        {k: v for k, v in params.items() if v is not None}).encode('utf-8')
    req = urllib.request.Request(
        BASE + '/tdo/wrkr/' + endpoint, body,
        {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
         'Authorization': 'Bearer ' + token},
        method='POST')
    return json.loads(urllib.request.urlopen(req, timeout=30).read().decode('utf-8'))
```

### 5-0. 노드 — 칸과 값

노트는 전부 **노드** 하나의 모양이다. 쓸 때는 아래 이름(카멜), 읽을 때는 대문자(`DUE_DT`)로 돌아온다.
**값은 정해진 것만 받는다** — 틀리면 `code=BAD_PARAM` 으로 거절한다(앱이 못 읽는 값이 노트에 들어가지 않게).

| 칸 | 값 | 뜻 |
|---|---|---|
| `type` | `text`(기본) · `page` · `date` · `routine` | 할 일·메모 / 페이지(폴더) / 날짜 노드 / 루틴 세트 |
| `title` · `memo` | 글 | 제목 · 메모 |
| `stat` | `open` · `doing` · `done` · `hold` | 할 일 · 진행중 · 완료 · 보류 (`text` 등록 기본 `open`, 페이지·날짜·루틴은 없음) |
| `impt` · `urgn` | `A`·`B`·`C` · `H`·`N`·`L` | 중요도 · 긴급도 |
| `startDt` · `endDt` · `dueDt` | `YYYY-MM-DD` | 시작 · 종료 · 마감 |
| `prgs` · `prgsMode` | `0`~`100` · `manual`·`auto` | 진척률 · 계산 방식 |
| `dateSub` · `dateVal` | `year`·`month`·`week`·`day` · 값 | 날짜 노드 단위 · 값 (예 `day` · `2026-10-11`) |
| `favrFl` · `hideFl` · `treeFl` · `mindmapFl` · `alrmFl` | `Y`·`N` | 즐겨찾기 · 숨김 · 트리 보기 · 마인드맵 보기 · 알림 |
| `timeVal` · `rptRule` | `07:30` 또는 `morning`·`noon`·`evening`·`night` · iCal RRULE | 루틴 항목 시각 · 반복(없으면 매일) |
| `pageUid` · `parentUid` · `ordKey` · `prevUid` | — | 위치(§5-9) |

자동으로 채워지는 것: 완료로 바꾸면 `DONE_DT`, 즐겨찾기를 켜면 `FAVR_DT`. `UP_DT`(마지막 수정)·`SRV_SEQ` 는 서버가 붙인다.
제목·메모에 괄호 `( )` 를 써도 된다(그대로 저장된다).

### 5-1. `tdoK0206` — 등록

§5-0 의 칸 + 위치: `pageUid`·`parentUid`(내 노드여야 함, 부모만 주면 페이지는 부모를 따름) ·
`afterUid`(그 형제 바로 뒤) 또는 `ordKey`(직접). 위치를 안 주면 **그 자리 맨 끝**에 붙는다 — 정렬키는 서버가 만든다.
워크스페이스: 하위는 부모(페이지)의 칸을 따른다. 최상위로 만들 때만 `wsUid`(§5-9c)를 주면 그 칸에 — 안 주면 기본 칸.

```python
page = tdo('tdoK0206.ajax', t, title='이번 주 업무', type='page')['resultData']['uid']
a    = tdo('tdoK0206.ajax', t, title='기획서 초안', pageUid=page, dueDt='2026-10-17', impt='A')['resultData']['uid']
b    = tdo('tdoK0206.ajax', t, title='표지 고르기', parentUid=a)['resultData']['uid']      # a 의 하위
r    = tdo('tdoK0206.ajax', t, title='아침 루틴', type='routine')['resultData']['uid']
tdo('tdoK0206.ajax', t, title='물 마시기', pageUid=r, timeVal='07:30', rptRule='RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR')
# 응답 resultData = {uid, ordKey}
```

### 5-2. `tdoK0202` — 목록·트리·검색

| 파라미터 | 설명 |
|----------|------|
| `pageUid` · `parentUid` · `rootOnly=Y` | 그 페이지 / 그 노드의 자식 / 최상위만 |
| `q` | 제목·메모 부분일치 검색 |
| `stat` · `type` · `impt` · `urgn` | 같은 값만 |
| `favrFl` · `hideFl` · `treeFl` · `mindmapFl` | `Y`/`N` |
| `dueFrom` · `dueTo` | 마감일 범위 `YYYY-MM-DD` |
| `delFl` | 기본 `N` · **`Y` = 휴지통 목록** |
| `wsUid` | 그 워크스페이스만(§5-9c · `DEFAULT` = 기본 칸) · 없으면 전부 |

결과 `resultList[]` 는 §5-0 의 칸 전부(대문자 키), 부모·정렬키 순. **값이 없는 칸은 키가 빠진다.**

### 5-3. `tdoK0203` — 단건 (`uid`, 휴지통 노드는 `delFl=Y`)

### 5-4. `tdoK0204` — 메인 대시보드

앱·웹 메인 화면과 같은 기준(페이지·날짜·루틴과 루틴 항목은 빼고 셈). `today=YYYY-MM-DD`(기본 서버 날짜).
`resultData` = `total`·`open`·`doing`·`done`·`hold` 건수 · `due`(기한 지남+오늘, 미완료) · `recent`(최근 수정 8) · `favorites`(8).

### 5-5. `tdoK0208` — 수정 (`uid` + 바꿀 칸만)

값을 준 칸만 바뀐다. **비우려면** `clear=dueDt,memo` (비울 수 있는 칸: memo·stat·impt·urgn·prgsMode·dateSub·dateVal·startDt·endDt·dueDt·timeVal·rptRule).
`pageUid`·`parentUid`·`ordKey` 를 주면 그 자리로 옮긴다(v2 호환) — 새로 짜는 코드는 `tdoK0207` 을 써라.

### 5-6. `tdoK0207` — 이동 (`uid`, `mode`)

| `mode` | 함께 줄 것 | 결과 |
|---|---|---|
| `before` · `after` | `targetUid` | 그 형제 바로 앞 / 뒤 |
| `child` | `targetUid` | 그 노드의 맨 끝 하위 (들여쓰기) |
| `page` | `pageUid` (비우면 페이지 밖 최상위) | 그 페이지 최상위 맨 끝 |

자기 하위 밑으로는 못 옮긴다. 다른 페이지로 옮기면 하위도 함께 간다. `resultInt` = 바뀐 노드 수.

### 5-7. `tdoK0211` — 완료 토글 (`uid`, `done=Y|N`)

### 5-8. `tdoK0209` 삭제 · `tdoK0210` 복구

- `tdoK0209` (`uid`) — **하위까지** 휴지통으로(페이지면 안의 내용까지). 물리 삭제 아님. `resultInt` = 지운 수.
- `tdoK0210` (`uid`) — 휴지통에서 **같은 UID 로** 되살린다. 그때 함께 지워진 하위도 같이 돌아온다. 부모가 아직 휴지통이면 `parentDeleted=Y`.
- 휴지통 보기: `tdoK0202` `delFl=Y`. 「영구 삭제」는 없다 — 기기마다 휴지통을 비우는 것은 그 기기 안의 일이다.

### 5-9. 트리 구조

- `pageUid` — 노드가 들어 있는 페이지. 페이지 바로 아래 노드는 `parentUid` 가 비어 있다
- `parentUid` — 상위 노드(같은 페이지 안)
- `ordKey` — 형제 정렬키(사전순). 직접 만들 일은 거의 없다 — `afterUid`·`tdoK0207` 을 쓰면 서버가 만든다

### 5-9b. 루틴 날짜별 체크 — `tdoK0221` 조회 · `tdoK0222` 체크하기

- `tdoK0222` (`itemUid`=루틴 항목 노드 · `chkDt`=YYYY-MM-DD · `stat`=done 완료 | skip 건너뜀 | none 해제) — 앱·웹과 같은 원장이라 바로 보인다.
- `tdoK0221` (`from`·`to` 기본 최근 30일 · `itemUid` · `includeDel=Y` 면 해제한 칸도) → `resultList`[{itemUid, chkDt, stat, doneDt, delFl, upDt}].

### 5-9c. 워크스페이스 — `tdoK0501`~`tdoK0506` (2026-10-11)

노트 전체를 「회사 일」·「개인 생활」처럼 나누는 칸이다. 처음엔 **기본 워크스페이스** 하나(지울 수 없음 · 이름이 비어 있으면 「기본 워크스페이스」).
노드의 칸은 `wsUid`(목록 결과 대문자 키 `WS_UID`) — **빈 값 = 기본 칸**. 무료는 **기본 포함 2개**, 3개부터 TreeToDo Plus(사람이 앱에서 구독).

| API | 파라미터 | 하는 일 |
|---|---|---|
| `tdoK0501` | — | 목록 `workspaces[]`{wsUid·wsTp(DEFAULT/NORMAL)·dflt·nm·icon·ordKey·attr} · `count`·`freeMax`·`plus`·`canCreate` |
| `tdoK0502` | `nm`(필수) · `icon` · `wsUid`(직접 만든 UUID — 같은 값으로 다시 부르면 새로 안 만듦) | 만들기 · 상한이면 `PLUS_REQUIRED`(+freeMax·count) |
| `tdoK0503` | `wsUid`(`DEFAULT` 가능) · 바꿀 칸만 `nm`·`icon`·`attr` | 이름·아이콘 |
| `tdoK0504` | `order=uid,uid,…` | 순서 |
| `tdoK0505` | `wsUid` | 지우기 — **안의 노트는 휴지통으로**(기본 칸 휴지통 · `tdoK0210` 으로 복구하면 기본 칸에 돌아온다) · 기본 칸은 `DEFAULT_WS` |
| `tdoK0506` | `uid` · `toWsUid`(`DEFAULT` = 기본 칸) | 페이지와 하위 전부를 다른 칸으로 — 대상 칸 **최상위 맨 끝**. `moved` = 바뀐 노드 수 |

```python
ws = tdo('tdoK0502.ajax', t, nm='회사 일', icon='💼')['ws']['wsUid']
p  = tdo('tdoK0206.ajax', t, title='주간 보고', type='page', wsUid=ws)['resultData']['uid']   # 그 칸 최상위
tdo('tdoK0206.ajax', t, title='초안', pageUid=p)                                            # 하위는 부모 칸을 따른다
tdo('tdoK0202.ajax', t, wsUid=ws)                                                          # 그 칸 노드만
```

### 5-10. API 로 안 되는 것

| 기능 | 이유 |
|---|---|
| 만다라트 | 만다라트 표시 칸이 아직 서버에 없다 |
| 언어·보기 등 설정 | 기기마다 따로 저장된다 |

---

## 5A. 처음 붙을 때 — API 목록 하나로 (`tdoK0901`, 로그인 없이)

```python
cat = json.loads(urllib.request.urlopen(urllib.request.Request(
    BASE + '/td/tdo/wrkr/tdoK0901.ajax', b'', method='POST')).read())['resultData']
# groups[] — 모든 API(주소·인증·하는 일·파라미터) · humanActionCodes[] · errorCodes[] · auth(토큰 수명)
```

이 매뉴얼의 표보다 **tdoK0901 이 원본**이다(서버의 `api-catalog.json` 을 그대로 준다). 정보 API 셋도 로그인 없이 된다:
`tdoK0902` 이용약관·개인정보처리방침(판·시행일·주소·`body=Y` 면 본문) · `tdoK0903` 버전(앱 최신·기록·PC 설치판·API 판) · `tdoK0904` 회사·연락처.

## 5B. 사람 손이 필요한 단계 — `HUMAN_ACTION_REQUIRED`

가입·로그인·재설정 중 **사람이 해야 하는 일**이 나오면 서버가 이렇게 답한다. `message` 를 **그대로 사람에게 읽어 주고**, 받은 값을 `next.api` 에 실어 이어 간다.

```json
{ "resultCode": "HUMAN_ACTION_REQUIRED",
  "humanAction": { "code": "EMAIL_CODE",
                   "message": "사용자의 이메일(ry***@gmail.com)로 6자리 인증번호를 보냈습니다. …",
                   "next": { "api": "/pos/td/pa/front/auth/email/signup/confirm",
                             "params": ["email","code","password","terms_ver","privacy_ver","device_id"] },
                   "expiresInSec": 600 } }
```

| code | 사람이 할 일 |
|---|---|
| `TERMS_CONSENT` | 이용약관·개인정보처리방침을 읽고 동의(주소는 응답 `terms.url`·`privacy.url`) |
| `EMAIL_CODE` | 메일함의 6자리 번호를 알려 줌(10분) |
| `PASSWORD_RESET_CODE` | 메일 번호 + 새 비밀번호 |
| `CAPTCHA` | 브라우저로 웹을 열어 직접 마침 — **API 로 대신할 수 없다** |
| `ACCOUNT_LOCKED` | 기다리거나 메일로 비밀번호 재설정 |
| `TERMS_RECONSENT` | 바뀐 약관을 읽고 다시 동의 → `tdoK0104` |

영어 문장이 필요하면 요청에 `lang=en`. 비밀번호 재설정·로그인처럼 앱도 쓰는 API 는 `resultCode` 를 그대로 두고 `humanActionRequired: true` + `humanAction` 을 덧붙인다.

### 이메일 인증 가입 (권장 — 계정이 «검증됨»이 된다)

```python
s, r = post_json('/td/pa/front/auth/email/signup/request', {'email': email})
#   → HUMAN_ACTION TERMS_CONSENT : 사람에게 약관을 보여 주고 동의를 받는다
s, r = post_json('/td/pa/front/auth/email/signup/request',
                 {'email': email, 'agree_terms': 'Y', 'terms_ver': r['terms']['ver'], 'privacy_ver': r['privacy']['ver']})
#   → HUMAN_ACTION EMAIL_CODE : 사람에게 메일의 번호를 받는다
s, r = post_json('/td/pa/front/auth/email/signup/confirm',
                 {'email': email, 'code': code_from_human, 'password': password,
                  'terms_ver': tv, 'privacy_ver': pv, 'device_id': 'my-agent'})
token = r['access_token']        # 틀리면 400 CODE_INVALID(+EMAIL_CODE 다시) · 만료 CODE_EXPIRED · 이미 가입 409
```

§3 의 즉시 가입(`/signup`)도 그대로 된다(메일 인증 없음 · 앱 호환).

## 5C. 토큰이 막혔을 때 — `reason` 과 `next`

| reason | 뜻 | 할 일(`next`) |
|---|---|---|
| `TOKEN_EXPIRED` | 30분 지남 | `/td/front/auth/refresh` 에 refresh_token → 새 토큰 |
| `TOKEN_REVOKED` | 로그아웃·비밀번호 변경·탈퇴·refresh 재사용(침해) 뒤 | 다시 로그인 |
| `TOKEN_INVALID` | 위조·refresh 를 access 자리에·다른 서비스 토큰 | 다시 로그인 |
| `TOKEN_MISSING` | 헤더 없음 | 로그인해서 `Authorization: Bearer` |

`code` 는 예전처럼 `UNAUTHORIZED` 다(호환). 로그아웃은 `/td/front/auth/logout` 에 access 만 보내도 그 세션이 바로 끝난다.

## 5D. 계정 (`tdoK01`)

| 키 | 하는 일 |
|---|---|
| `tdoK0101` | 내 계정 — 닉네임·로그인 수단(이메일·SNS, 검증 여부)·기기·약관 동의. 약관이 바뀌었으면 `humanAction TERMS_RECONSENT` |
| `tdoK0102` | 닉네임 바꾸기(`nick` 1~30자) |
| `tdoK0104` | 약관 (재)동의 — **사람이 동의한 뒤에만**(`agree=Y`·`terms_ver`·`privacy_ver`) |

비밀번호 변경 = 재설정 흐름(`PASSWORD_RESET_CODE`), 탈퇴 = `/td/front/auth/withdraw`. 언어·테마·달력 보기 같은 설정은 **기기마다 따로** 저장돼 API 대상이 아니다.

## 5E. 문의·건의 (`tdoK03`) — 불편하거나 없는 기능은 꼭 알려 주세요

| 키 | 하는 일 |
|---|---|
| `tdoK0301` | 등록 — `kind`=inquiry(문의)·suggest(건의)·bug(오류신고) · `title`(200자) · `content`(5,000자) → `inqNo`(TDO-n) |
| `tdoK0302` | 내 문의 목록 · `tdoK0303` 상세(운영자 답 포함, `inqNo`) |
| `tdoK0304` | 추가 글(다시 접수) · `tdoK0305` 종료 |

- 운영자에게 바로 메일이 가고, **계정 주인 메일**에도 접수 확인이 간다(주인이 AI 가 무엇을 물었는지 알게). 답이 오면 주인 메일 + `tdoK0303`.
- 어느 도구인지 `X-Client` 헤더로 알려 주면(예: `X-Client: MyAgent/1.0`) 운영자가 구분해 본다.
- 하루 10건까지 · 넘으면 `DAILY_LIMIT`.
- 아직 안 되는 기능을 부르면 `NOT_SUPPORTED` 와 함께 `next`(이 API · 제목 제안)를 준다 — 그대로 건의하면 된다.

## 5F. 회사 (`tdoK04`) — 회사를 만들고 가입하고, 남의 노트를 「공유받은 만큼만」 다루기

TreeToDo 는 공개 앱이다. **회사는 이용자가 직접 만든다.** 만든 사람이 **최고관리자**이고, 다른 이용자는 **가입 신청 → 관리자 승인**으로 구성원이 된다.
구성원은 내 페이지를 회사에 **보기(R)·편집(W)으로 공유**하고, 같은 회사의 다른 구성원(사람이든 AI 든)은 공유받은 페이지와 그 하위만 다룬다.
공유하지 않은 노트에는 닿지 않는다. 앱·웹에서는 설정 › 「회사」에서 같은 일을 한다.

| 역할 | 할 수 있는 것 |
|---|---|
| 최고관리자 (`SUPER`) | 아래 전부 + 관리자 지정·해제, 관리자 내보내기, **최고관리자 이양**, 도메인 넣기(인증 전), 회사 닫기 |
| 관리자 (`ADMIN`) | 가입 승인·거절, 구성원 내보내기, 회사 이름·소개 고치기 |
| 구성원 (`MEMBER`) | 공유하기·공유받기, 떠나기 |

**인증된 회사** — 회사에 도메인(예: `example.com`)을 넣고, 최고관리자가 그 도메인 이메일로 인증하면(`0431`→`0432`) 「인증된 회사」가 된다.
같은 도메인으로 인증된 회사는 하나뿐이다. 누구나 쓰는 메일 도메인(gmail.com·naver.com 등)은 회사 도메인으로 쓸 수 없다.

**권한 양도 요청** — 최고관리자가 회사를 악용하면, 그 회사 도메인 이메일을 인증한 다른 직원이 사유를 적어 요청한다(`0433`).
TreeToDo 관리자가 확인한 뒤 수동으로 양도하거나 반려한다(결과는 `0434`). 구성원이 아니어도 요청할 수 있다.

| 키 | 하는 일 · 파라미터 |
|---|---|
| `tdoK0421` | 내 회사 — 역할·상태(`REQ` 신청 중 · `REJ` 거절됨 포함)·인증 여부·가입 신청 수(관리자) |
| `tdoK0422` | 회사 만들기 — `corpCd`(영소문자·숫자·.- 2~40) · `corpNm` · `domain`(선택) · `desc`(선택) → 나는 최고관리자 |
| `tdoK0423` | 회사 찾기 — `q`(코드·도메인 정확히 또는 이름 일부, 2자 이상) |
| `tdoK0424` | 가입 신청 — `corpId`(또는 `corpCd`) · `msg`(선택) |
| `tdoK0425` | 떠나기 · 신청 취소 — `corpId` (최고관리자는 이양한 뒤에) · 내가 이 회사에 한 공유는 해제된다 |
| `tdoK0426` | 회사 보기 — `corpId` · 구성원이면 구성원 목록(관리자면 가입 신청 포함)과 이 회사에 한 내 공유 |
| `tdoK0427` | 가입 승인·거절 — `corpId` · `mbrId` · `act=APRV\|REJ` (관리자) |
| `tdoK0428` | 구성원 관리 — `corpId` · `mbrId` · `act=ADMIN\|MEMBER\|OUT` |
| `tdoK0429` | 최고관리자 이양 — `corpId` · `mbrId` (나는 관리자가 된다) |
| `tdoK0430` | 회사 정보 — `act=EDIT`(`corpNm`·`desc`·`domain`) · `act=CLOSE`(닫기) |
| `tdoK0431` | 회사 이메일 인증번호 받기 — `corpId` · `email`(회사 도메인) — **사람의 메일함에서 번호를 받아야 한다** |
| `tdoK0432` | 인증번호 확인 — `corpId` · `email` · `code` |
| `tdoK0433` | 권한 양도 요청 — `corpId` · `reason`(10자 이상) |
| `tdoK0434` | 내 양도 요청 — 목록 · `act=CANCEL` + `claimId` |
| `tdoK0401` | 받은 공유 — 페이지마다 한 줄 · `shrId`·`rootUid`·`rootTitle`·`perm`(넓은 쪽)·`tgtTp`(USER 개인 · ORG 조직)·조직 |
| `tdoK0414` | 내 공유 줄 목록(`stat` · `corpId` · `pageUid` · `tgtTp`) |
| `tdoK0415` | 공유 줄 걸기 — `pageUid` · `tgtTp=USER`(+`email`) \| `ORG`(+`corpId`, 내가 구성원) \| `PUBLIC`(보기만 → `pubUrl`) · `perm=R\|W` |
| `tdoK0416` | 공유 줄 해제 — `shrId` (공개면 링크도 즉시 끊김) |
| `tdoK0417` | 페이지 공유 상태 — `pageUid` → `mode`(NONE_SET 나만 · OWN 이 페이지 설정 · INHERIT 위 페이지에서 물려받음)·`private`·`grants` |
| `tdoK0418` | 페이지 모드 — `pageUid` · `mode=INHERIT`(위 설정 따르기) \| `PRIVATE`(비공개 — 위 공유에서 빠짐) |
| `tdoK0419` | 공개 링크 새로 — `shrId` (옛 링크 즉시 끊김) |
| `tdoK0402` | 공개 읽기 — `t`(링크 토큰) · **로그인 없음** · 읽기만 |

**공유 범위 규칙** — 어느 페이지든(최상위 포함) 공유를 건다. 할일·메모는 자기가 속한 페이지를 따른다.
1. 설정이 없는 페이지는 **가장 가까운 위 페이지의 설정을 물려받는다.**
2. 설정이 있는 페이지는 **그것만 쓴다** — 넓히기(사람·조직 추가, 보기→편집) · 좁히기(줄 빼기 · 「비공개」).
3. 한 사람에게 같은 페이지에 여러 줄이 맞으면 **넓은 권한**(개인 보기 + 조직 편집 = 편집).
4. **공개 링크는 보기만** — 편집은 누가 고쳤는지 알 수 없어(신원 없음) 막는다. 남에게 고치게 하려면 개인·조직 편집으로.
5. 받은 `shrId` 로 다룰 수 있는 범위 = 그 페이지와 하위 중 **자기 설정이 따로 있는 하위 페이지를 뺀 부분**(그 하위가 나에게도 열려 있으면 별도 `shrId` 로 보인다).

**AI 가 회사에 들어가는 법** — 일반 회원 가입(§3) → `tdoK0423` 으로 회사를 찾아 `tdoK0424` 가입 신청 → 사람(관리자)이 승인.

**공유받은 노트를 다루는 법** — 노트 API(§5)에 `shrId` 하나만 더 붙인다.

```bash
# 1) 받은 공유
curl -s -H "Authorization: Bearer $AT" -d "" $BASE/td/tdo/wrkr/tdoK0401.ajax
# 2) 그 공유의 트리 — 결과는 공유 루트와 하위만
curl -s -H "Authorization: Bearer $AT" --data-urlencode "shrId=12" $BASE/td/tdo/wrkr/tdoK0202.ajax
# 3) 루트 밑에 등록(자리를 안 주면 루트 밑) · 수정 · 이동 · 삭제 · 복구 · 완료 — 전부 shrId 를 붙인다
curl -s -H "Authorization: Bearer $AT" --data-urlencode "shrId=12" --data-urlencode "title=새 메모" $BASE/td/tdo/wrkr/tdoK0206.ajax
```

- 쓴 노드는 **공유한 사람의 노트**에 들어가 그 사람의 앱·웹에 그대로 보인다(동기화 규칙 그대로 — `STALE_UPDATE` 도 같다).
- 범위 밖 노드는 `NOT_FOUND`(있다는 것도 알리지 않는다) · 범위 밖으로 놓기, **공유 루트 자체**의 이동·삭제·복구는 `OUT_OF_SCOPE` ·
  보기(R) 공유로 쓰기는 `FORBIDDEN` · 공유 해제·회사 정지·닫기·구성원 내보내기·떠나기 뒤에는 다음 요청부터 `NOT_FOUND`.
- 공유 범위에서 되는 것: `0202`·`0203`·`0206`~`0211`. 집계(`0204`)·루틴·만다라트는 `BAD_PARAM`.
- 공유 범위에서 한 쓰기는 회사·사람·API·노드로 기록된다(노트 내용은 남기지 않는다).

## 6. 오류 코드

| `resultCode` | `code` | 의미 | 대응 |
|--------------|--------|------|------|
| `FAIL` | `UNAUTHORIZED` | 토큰 없음 / 무효 / **만료** / refresh 토큰 제시 | **재로그인**(§4) 후 재시도 |
| `FAIL` | `BAD_PARAM` | 값이 틀림 · 위치 대상이 없거나 남의 것 · 순환 이동 | `resultMessage` 를 보고 고친다 |
| `FAIL` | `NOT_FOUND` | 이동할 노드가 없음 | uid 확인 |
| `FAIL` | `OUT_OF_SCOPE` | 공유받은 페이지 밖으로 놓기 · 공유 루트 자체의 이동·삭제·복구(§5F) | 범위 안 자리로 |
| `FAIL` | `FORBIDDEN` | 보기(R) 공유로 쓰기 · 회사에서 내 역할로 못 하는 일(§5F) | 노트 주인에게 편집 공유 · 관리자에게 요청 |
| `FAIL` | `NOT_MEMBER` | 그 회사 구성원이 아님(§5F) | `tdoK0424` 가입 신청 → 승인 |
| `FAIL` | `NOT_VERIFIED` · `DOMAIN_TAKEN` | 회사 이메일 인증 전 양도 요청 · 같은 도메인의 인증 회사가 이미 있음(§5F) | `tdoK0431`→`0432` · 그 회사에 가입·양도 요청 |
| `FAIL` | `CONFLICT` · `LIMIT` · `MAIL_FAIL` | 코드 중복·이미 처리됨 · 최고관리자 회사 5개 상한 · 메일 발송 실패(§5F) | `resultMessage` 대로 |
| `FAIL` | `STALE_UPDATE` | 다른 기기에 더 최신 수정이 있음 | 다시 읽고(`tdoK0203`) 다시 고친다 |
| `FAIL` | `DAILY_LIMIT` · `TOO_LONG` | 문의 하루 상한·길이 · 가입 인증번호 하루 상한 | 내일 다시 · 줄인다 |
| `FAIL` | `EMAIL_EXISTS` · `CODE_INVALID` · `CODE_EXPIRED` | 가입(§5B) | `next` 를 따른다 |
| `NOT_SUPPORTED` | — | 아직 없는 기능 | `next`(건의 API) |
| `HUMAN_ACTION_REQUIRED` | — | 사람 손이 필요(§5B) | `humanAction.message` 를 사람에게 |
| `FAIL` | (없음) · `resultInt=0` | `대상 todo 없음` | uid 오타이거나 **남의 노트**다 |
| `FAIL` | (없음) | `필수 파라미터 uid 누락` | uid 를 넣어라. 계속 나면 `https` 인지 확인(§2) |

가입/로그인은 HTTP 상태코드로 답한다: `200` 성공 / `401` 불일치 / `409` 이미 가입 / `429` 과다요청.

**소유 격리**: 모든 조회·수정·이동·삭제·복구는 JWT 사용자의 노트로만 스코프된다. 남의 UID 를 알아도
조회하면 `대상 todo 없음`, 수정·삭제·토글·복구하면 `resultInt=0`, 남의 노드 밑에 등록하면 `BAD_PARAM` 이다.

---

## 7. 비밀번호를 분실했을 때 (★ 자력 복구 불가)

사람 정문의 비밀번호 재설정(`/pa/front/auth/email/reset/request` → `reset/confirm`)은
**등록된 이메일로 발송되는 6자리 인증 코드를 입력**해야 한다.
AI 사원은 `@terple.com` 메일함을 읽을 수 없으므로 **이 경로를 스스로 완주할 수 없다.**

그래서 §3-4 의 2중 보관이 필수이고, 그럼에도 분실했다면:

1. 먼저 본인 기억을 검색한다 — `aicI1210` (query: `treetodo 계정`)
2. 본인 md 파일 `agents/{WORKER_ID}.md` 의 "treetodo 계정" 블록을 확인한다
3. 둘 다 없으면 **운영자(CEO)에게 계정 비밀번호 리셋을 요청한다.**
   요청 시 `WORKER_ID` / 계정 이메일 / 대상 서버(개발·운영)를 함께 알린다
4. 리셋된 새 비밀번호를 받으면 **즉시 §3-4 의 ①②를 다시 수행**해 덮어쓴다

> 스스로 새 계정을 파서 우회하지 마라. 기존 노트에 영영 닿지 못하게 된다.

---

## 8. 전체 예제 (가입 → 저장 → 로그인 → CRUD)

```python
import json, secrets, string, urllib.request, urllib.parse

BASE      = 'https://dev.posain.com/pos'
WORKER_ID = 'DEV-001'
email     = 'aico.' + WORKER_ID.replace('-', '').lower() + '@terple.com'

# 1) 비밀번호 생성 → ★ 즉시 기억 + md 저장 (§3-4)
password = ''.join(secrets.choice(
    string.ascii_letters + string.digits + "!@#$%^&*") for _ in range(24))
remember(WORKER_ID, '[treetodo 계정] 개발 email=' + email + ' / password=' + password)
# 그리고 agents/DEV-001.md 에도 append (§3-4 ②)

# 2) 가입 (이미 가입돼 있으면 409 — 그냥 다음 단계로)
post_json('/pa/front/auth/email/signup', {'email': email, 'password': password})

# 3) 로그인
_, body = post_json('/pa/front/auth/email/login',
                    {'email': email, 'password': password})
token = body['access_token']

# 4) 할일 CRUD
uid = tdo('tdoK0206.ajax', token, title='매뉴얼 검증')['resultData']['uid']
tdo('tdoK0202.ajax', token)                              # 목록
tdo('tdoK0203.ajax', token, uid=uid)                     # 단건
tdo('tdoK0208.ajax', token, uid=uid, title='제목 수정')   # 수정
tdo('tdoK0211.ajax', token, uid=uid, done='Y')           # 완료
tdo('tdoK0209.ajax', token, uid=uid)                     # 삭제(휴지통)
tdo('tdoK0210.ajax', token, uid=uid)                     # 복구
```

---

## 9. 치트시트

```
서버   개발 https://dev.posain.com/pos   운영 https://todo.choeum.com/pos   ★반드시 https

가입   POST /pa/front/auth/email/signup  JSON {email, password}      ← 1회
로그인 POST /pa/front/auth/email/login   JSON {email, password}      ← 세션마다
       이메일 = aico.{하이픈제거소문자}@terple.com  (DEV-001 → aico.dev001@terple.com)
       비밀번호 = 본인이 secrets 로 랜덤 생성 (24자 이상)
★★    비밀번호는 즉시 ① aicI1206 기억 ② agents/{WORKER_ID}.md  — 없으면 복구 불가

인증   Authorization: Bearer {access_token}   (30분 만료 → 만료되면 재로그인)
       body 의 wrkrId/userId 는 신원이 아니다(서버가 JWT 로 덮어쓴다)

목록  tdoK0202  pageUid|parentUid|rootOnly|q|stat|type|impt|urgn|favrFl|hideFl|dueFrom|dueTo|delFl(Y=휴지통)
단건  tdoK0203  uid [delFl=Y]
집계  tdoK0204  [today]   → total/open/doing/done/hold · due · recent · favorites
등록  tdoK0206  title + 칸(§5-0) + pageUid|parentUid|afterUid  → resultData.uid, ordKey (정렬키 서버생성)
이동  tdoK0207  uid, mode=before|after|child(+targetUid) | page(+pageUid)
수정  tdoK0208  uid + 바꿀 칸 [clear=칸,칸]
삭제  tdoK0209  uid   (하위까지 휴지통)
복구  tdoK0210  uid   (같은 UID, 함께 지운 하위까지)
완료  tdoK0211  uid, done=Y|N
공유  tdoK0401 (사원: 받은 공유) → 노트 API 에 shrId=… 를 붙이면 공유받은 페이지만 · tdoK0411~0416 (사용자: 연결·공유·해제)
값    type=text|page|date|routine  stat=open|doing|done|hold  impt=A|B|C  urgn=H|N|L  날짜=YYYY-MM-DD  플래그=Y|N

실패  code=UNAUTHORIZED → 재로그인 / BAD_PARAM → 값·위치 확인 / STALE_UPDATE → 다시 읽고 고침 / "대상 todo 없음" → 없거나 남의 것
트리  pageUid=페이지  parentUid=상위노드  ordKey=정렬(서버가 만든다)
분실  자력 복구 불가 → 기억(aicI1210) → md → 없으면 CEO 에게 리셋 요청
```

---

## 변경 이력

| 버전 | 일자 | 내용 |
|------|------|------|
| v7 | 2026-10-11 | **공유 범위** — 개인(이메일)·조직·공개 링크 × 보기·편집 · 어느 페이지든 · 물려받기(가장 가까운 설정) · 비공개 · 공개는 보기만 · `tdoK0417`~`0419`·`0402` 신설 · `0415` 에 `tgtTp` · 받은 공유 `tgtTp` |
| v6 | 2026-10-11 | **회사(`tdoK0421`~`0434`)** — 이용자가 회사를 만들고(최고관리자) 가입 신청·승인·관리자·이양·회사 닫기 · 도메인 이메일 인증 = 인증된 회사 · 권한 양도 요청(TreeToDo 관리자 확인) · 공유 조건 = 같은 회사 구성원(연결 `0411`~`0413` 없앰) · `NOT_MEMBER`·`NOT_VERIFIED`·`DOMAIN_TAKEN`·`CONFLICT` |
| v5 | 2026-10-11 | **AI 회사 연결(`tdoK04`)** — 사용자가 이용 회사를 연결(`agree=Y`)하고 페이지를 보기·편집으로 공유 · 회사 사원은 노트 API 에 `shrId` 를 붙여 공유받은 범위만 다룬다 · `OUT_OF_SCOPE`·`FORBIDDEN`·`NOT_LINKED` |
| v4 | 2026-10-11 | 이메일 인증 가입(`/signup/request`·`/confirm`) · **HUMAN_ACTION**(사람 손이 필요한 단계를 응답으로) · 토큰 실패 `reason`·`next`(만료·폐기·위조) · 로그아웃·비밀번호 변경·탈퇴 뒤 토큰 즉시 폐기 · 계정 `tdoK0101`·`0102`·`0104` · 문의함 `tdoK0301`~`0305` · 정보 `tdoK0901`~`0904` · `NOT_SUPPORTED` → 건의 |
| v3 | 2026-10-11 | **앱·웹 최신 기능 현행화** — `tdoK0204` 대시보드 · `tdoK0207` 이동 · `tdoK0210` 복구 신설. 목록 검색·필터, 노드 칸 전부(시작/종료일·진척률·날짜 노드·즐겨찾기·숨김·트리/마인드맵·루틴 시각/반복/알림). 등록이 정렬키를 만들고, 삭제는 하위까지. 값 검사(`BAD_PARAM`). 쓰기 규칙이 앱·웹 동기화와 같아졌다(`STALE_UPDATE`). 제목·메모의 괄호가 그대로 저장된다. v2 6종 주소·응답 모양은 그대로. |
| v2 | 2026-07-20 | **전면 개정** — 전용 레지스트리(`DPA180_WRKR_AGENT`)·전용 토큰(`X-Wrkr-Token`)·계정 API(`tdoK0106`/`0103`/`0107`/`0109`) 폐기. **사람 정문 가입/로그인 + JWT** 로 전환(CEO 확정). 비밀번호는 각 에이전트가 랜덤 생성·2중 보관, 분실 시 운영자 리셋. |
| v1.1 | 2026-07-20 | (폐기) 자체 레지스트리 + 에이전트 전용 토큰 인증 |
| v1 | 2026-07-20 | 최초 작성 |
