Files
document-haness/.agents/skills/publishing-tech-log-to-studio/references/studio-api.md
T
DongHyeonkaandClaude Opus 5 024362d096 fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과
keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지
밟았고 밟은 범위를 편마다 적었다.

명령이 못 도는 것을 고쳤다.

- kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다.
  라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다
- `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표
- busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다
- `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을
  못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다
- 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v`
- `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어
  그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다
- B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다
- D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는
  `kc-lab-edge` 의 `certbot.service` 다
- `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로
- `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다

결과가 조건부인 것을 갈랐다.

- readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다
- 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다
- A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다
- A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다

증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은
길이만 적었고 화면에 찍힌 토큰은 가렸다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:59:42 +09:00

7.9 KiB

Studio 가 실제로 주고받는 것

여기 적은 것은 2026-09-17 에 로그인한 브라우저에서 직접 불러 확인한 것이다. 문서를 보고 적은 것이 아니라 요청을 보내고 돌아온 것을 옮겼다. 확인한 판이 바뀌면 이 파일도 낡는다.

왜 이 파일이 필요해졌나

편집 화면의 본문이 블록 편집기로 바뀌었다. 전에는 본문이 본문 Markdown 이라는 textarea 하나였고 거기에 통째로 붙여넣으면 됐다. 지금은 블록 하나가 <textarea> 하나다 — 기록 한 편이 블록 171개인 것을 실제로 봤다. 화면으로 그것을 채우는 것은 사람이 한 편을 쓸 때의 방식이지 쓰인 기록을 옮기는 방식이 아니다.

그런데 저장이 서버로 보내는 것은 블록이 아니라 마크다운 문자열 하나다. 화면은 그 문자열 위의 편집기이고, 저장 요청은 bodyMarkdown 한 칸을 보낸다. 그래서 옮기는 일은 화면이 아니라 이 API 로 한다.

인증

자격증명을 이 저장소에 두지 않는다. 이미 로그인된 브라우저 세션을 쓰고, 요청은 그 페이지 안에서(fetch) 보낸다. 쿠키가 그대로 실린다.

CSRF 토큰은 XSRF-TOKEN 쿠키에 있고 x-csrf-token 헤더로 되돌려 준다.

document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/)?.[1]

로그인이 안 돼 있으면 /studio 가 「기록을 쓰려면 로그인이 필요합니다」를 낸다. 로그인 화면을 자동으로 통과하려 들지 않는다 — 사람에게 로그인해 달라고 말하고 멈춘다.

응답 껍데기

성공이든 실패든 같은 모양이다.

{ "success": true, "data": {  }, "error": null,
  "meta": { "requestId": "…", "traceId": "…", "correlationId": "…", "page": null } }

실패하면 datanull 이고 error{code, category, message, retryable, details} 가 온다. details.supportedMethods 처럼 고칠 방법을 담아 주는 경우가 있다.

경로

무엇 요청
목록 GET /api/v1/studio/documents?limit=100
한 편 GET /api/v1/studio/documents/{id}
새로 만들기 POST /api/v1/studio/documents → 201
저장 PUT /api/v1/studio/documents/{id} → 200
지우기 DELETE /api/v1/studio/{종류}/{id} → 204

limit 은 100 이 상한이다. 200 을 주면 422 REQUEST_VALIDATION_FAILED 다. 더 받으려면 data.nextCursorcursor 로 넘긴다.

/documents/{id}PUTGET 만 받는다. 거기에 DELETE 를 보내면 405 이고 details.supportedMethods["PUT","GET"] 을 준다. 삭제는 종류별 경로다 — cases · references · concepts · setups · questions, 그리고 Decision 은 projects/{projectId}/decisions/{decisionId}.

한 편 읽기

GET /api/v1/studio/documents/{id}
→ data: { document, currentValidation, latestPreview, currentPublication,
          dependencyRevision, nextAction }

nextAction 이 다음에 할 일을 말한다 — 저장만 된 기록은 VALIDATE 다. 검증 화면은 /studio/documents/{id}/validation 이고 편집 화면의 aside 에는 저장게시 둘뿐이라 검증은 그 화면에서 따로 한다.

저장

PUT /api/v1/studio/documents/{id}
headers: content-type: application/json
         accept: application/json
         x-csrf-token: <XSRF-TOKEN 쿠키>
         idempotency-key: <요청마다 새로>
body:    { "expectedVersion": <읽어 온 document.version>, "document": { … } }

expectedVersion 은 방금 읽은 document.version 이다. 저장이 끝나면 1 올라간다. 지어내지 말고 저장 직전에 GET 으로 읽는다.

document그 종류의 칸을 전부 담는다. 빠뜨린 칸은 비는 것으로 저장된다 — 부분 갱신이 아니다.

종류마다의 칸

여섯이 함께 갖는 것: kind · title · slug · summary · relations · projectId · topicId · variantIds. 서버가 주지만 보내지 않는 것: id · version · updatedAt.

kind 그 종류만의 칸
CASE problem · conclusion · environment · reproduction · bodyMarkdown · lastVerifiedOn
CONCEPT bodyMarkdown · basisVersion
SETUP bodyMarkdown · pinnedVersions
REFERENCE purpose · rules · applyWhen · exceptions · examples · verifiedOn
QUESTION facts · assumptions · unknowns · constraints · options · nextValidation · resolution · questionStatus

되풀이되는 칸의 모양이다. id 는 서버가 붙이므로 새로 넣을 때는 빼고 order 만 0 부터 센다.

항목
relations { targetId, reason, order }
facts · assumptions · unknowns · constraints · applyWhen · exceptions · examples { text, order }
options { title, description, order }
rules { title, body, order }제목과 본문이 따로다
pinnedVersions { name, version }order 가 없다

bodyMarkdown 은 바이트 그대로 돌아온다

넣은 것과 읽어 온 것이 같은지 확인했다. 코드 울타리의 정보 문자열이 그대로 살아남는다.

```bash label="[host] ① 친다"

넣고 다시 읽었을 때 label="…" 까지 한 글자도 안 바뀌었다. 그래서 studio-body.py 가 만든 본문을 그대로 실어 보내면 된다.

화면에서 블록이 어떻게 생겼나

옮기는 일은 API 로 하지만, 사람이 화면을 읽을 때와 무엇이 어긋났는지 볼 때 필요하다.

.studio-block-editor
├ textarea.studio-markdown-source     본문 전체의 거울. display:none · aria-hidden · readonly
├ .studio-block-editor__blocks
│  └ .studio-authoring-block[data-kind]   블록 하나
└ button  + 블록 추가

data-kindheading · paragraph · code · bullet · raw 다. raw(화면 이름 고급 블록)가 표처럼 블록으로 안 갈리는 것을 그대로 담는다.

코드 블록의 input.studio-code-language 는 언어 이름만 담지 않는다. 울타리 뒤의 정보 문자열 전체가 거기 들어간다 — bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다" 가 그 입력의 값이었다. 라벨을 넣을 별도 칸은 없다.

.studio-markdown-source 는 읽는 데만 쓴다. readonly 이고 display:none 이라 입력 자리가 아니다. 다만 화면이 지금 무엇을 직렬화할지를 그대로 보여 주므로, 저장 전에 눈으로 대조하기에 좋다.

합성한 ClipboardEvent 로 붙여넣는 것은 안 먹는다. 화면으로 본문을 채우려면 블록을 하나씩 만들어야 한다.

화면의 칸 셀렉터

칸은 이제 aria-label 로 찾는다. label.studio-field 는 제목과 요약 둘만 감싼다.

document.querySelector('input[aria-label="제목"]')
document.querySelector('textarea[aria-label="요약"]')
document.querySelector('input[aria-label="slug"]')
document.querySelector('input[aria-label="판단 기준 1 제목"]')
document.querySelector('textarea[aria-label="적용할 때 3"]')

되풀이 칸을 늘리는 버튼은 + 판단 기준 · + 적용할 때 · + 예외와 주의 · + 예시 · + 버전 추가 다.

상태 레일은 그대로다 — aside[class*="studio-document-status"](실제 class 는 studio-document-status-bar). 그 안에 저장게시 둘뿐이고, 저장된 상태에서는 저장disabled 이며 글자가 저장됨 이다.

하지 않는 것

  • 게시하지 않는다. 이 스킬은 저장까지다. 한 번이라도 게시한 문서는 게시를 취소해도 삭제가 409 로 거절된다
  • expectedVersion 을 지어내지 않는다. 저장 직전에 읽는다
  • 부분 갱신이라고 생각하지 않는다. 보내지 않은 칸은 비워진다
  • 로그인 화면을 자동으로 넘기지 않는다