목표
Confluence content tree의 folder를 정식 구조 노드이자 MDX landing page로 저장·변환합니다.
- folder metadata와 직계 자식 API 응답을
var/{folder_id}/에 저장합니다.
- folder와 하위 page/folder를 typed catalog에 포함합니다.
- folder 자체의 MDX에 직계 자식 문서 목록을 표시합니다.
- folder 이동·이름 변경·삭제 후 더 이상 유효하지 않은 생성 파일을 안전하게 정리합니다.
Related: #936
조사 기준
확정된 동작
- Folder MDX는 변환기가 전부 소유합니다.
convert_all.py 실행마다 folder MDX의 frontmatter, 제목, 직계 자식 목록을 완전히 재생성하며 수동 편집은 보존하지 않습니다.
- Folder MDX 목록에는 직계 자식
page와 folder만 표시합니다.
- Nested folder는 link 한 항목으로 표시하며, nested folder의 자식은 해당 folder MDX에서 조회합니다.
database, whiteboard, embed는 당분간 지원하지 않고 식별 가능한 경고를 남깁니다.
- 빈 folder도 MDX를 생성하고
하위 문서가 없습니다.를 표시합니다.
- 계층 구조 변경은 즉시 감지할 필요가 없습니다. Folder 생성·이동·이름 변경·삭제는
--remote가 갱신합니다.
--recent는 저장된 hierarchy 안의 기존 page 내용만 갱신하고 children.v2.yaml은 갱신하지 않습니다.
- 이전 conversion manifest가 소유권을 증명하는 stale MDX와
_meta.ts는 성공적인 변환 후 자동 삭제합니다.
재현 대상 구조
저장된 Confluence V1/V2 응답에서 확인한 구조는 다음과 같습니다.
관리자 매뉴얼 (544178405, page)
└─ MCP Server (2167636017, folder)
├─ MAC General Configurations (2167144528, page)
├─ MCP Server Connection Management (2167242794, page)
└─ MCP Access Control (2167799919, page)
하위 page의 page.v2.yaml에는 folder 부모가 정확히 기록되어 있습니다.
parentId: "2167636017"
parentType: "folder"
현재 결과
| 대상 |
현재 결과 |
var/2167636017/ |
생성되지 않습니다. |
| folder의 하위 page 디렉터리 |
--recent로 개별 page data가 저장될 수 있습니다. |
var/pages.qm.yaml |
folder와 하위 page 3개가 모두 없습니다. |
target/ko/administrator-manual/mcp-server.mdx |
생성되지 않습니다. |
target/ko/administrator-manual/mcp-server/ |
하위 page MDX가 생성되지 않습니다. |
관련 _meta.ts |
folder와 하위 page navigation이 없습니다. |
convert_all.py는 catalog에 있는 항목만 처리하므로, 하위 page의 page.xhtml이 이미 저장되어 있어도 orphan 상태에서는 변환하지 않습니다.
원인
1. Page-only child endpoint를 사용합니다
현재 ApiClient.get_child_pages()는 다음 endpoint를 사용합니다.
GET /api/v2/pages/{id}/children
이 endpoint는 page만 반환합니다. 모든 직계 content type을 발견하려면 다음 endpoint가 필요합니다.
GET /api/v2/pages/{id}/direct-children
2. Folder child endpoint가 올바르지 않습니다
현재 folder 분기는 /api/v2/folders/{id}/children을 구성하지만 공식 직계 자식 endpoint는 다음과 같습니다.
GET /api/v2/folders/{id}/direct-children
3. 재귀 순회가 content type을 버립니다
현재 재귀 입력은 children.v2.yaml에서 추출한 ID 문자열뿐입니다. direct-children이 type: folder를 반환해도 다음 요청의 API routing에 전달할 수 없습니다.
4. Cache가 없는 non-root child를 page로 간주합니다
최초 발견한 non-root folder에는 page.v2.yaml cache가 없으므로 /api/v2/pages/{folder_id}를 잘못 호출합니다. 현재 folder 지원은 사실상 folder가 sync root인 일부 경우로 제한되어 있습니다.
5. Catalog가 content type을 보존하지 않습니다
현재 Page와 pages.<code>.yaml에는 type이 없습니다. Fetch와 conversion 단계가 folder를 본문 없는 정식 노드로 구분할 수 없습니다.
6. Breadcrumb 생성이 V1 page ancestor에 의존합니다
Stage4Processor는 page.v1.yaml이 없으면 catalog entry를 만들지 않습니다. Folder에는 page V1 body/ancestor를 요구할 수 없으므로 parent traversal context에서 breadcrumb와 path를 계산해야 합니다.
7. Navigation 생성이 XHTML 변환에 결합되어 있습니다
_meta.ts 생성은 개별 page.xhtml 변환의 side effect입니다. Folder는 XHTML이 없으므로 folder 아래 navigation을 생성할 실행 지점이 없습니다.
8. 출력 소유권 기록이 없어 이전 경로를 안전하게 정리할 수 없습니다
Folder 이동·이름 변경 시 새 경로를 생성하는 것만으로는 이전 folder와 descendant MDX가 남습니다. 변환기가 만든 파일과 수동 파일을 구분할 manifest가 필요합니다.
구현 디자인
R1. Typed direct-children 순회
재귀 호출은 ID가 아니라 최소 다음 구조를 전달합니다.
ContentRef
id
type
title
childPosition
- page:
/api/v2/pages/{id}/direct-children
- folder:
/api/v2/folders/{id}/direct-children
- cursor pagination을 끝까지 수집합니다.
page, folder 외 type은 순회하지 않고 parent_id, id, type, title을 경고로 기록합니다.
- pagination 중간 실패 시 부분 결과로 이전
children.v2.yaml을 덮어쓰지 않습니다.
R2. Folder raw data 저장
var/{folder_id}/
├── folder.v2.yaml
└── children.v2.yaml
folder.v2.yaml: /api/v2/folders/{id} metadata 응답
children.v2.yaml: pagination을 합친 직계 자식 snapshot
- Folder에는
page.v1.yaml, page.v2.yaml, page.xhtml, attachments.v1.yaml을 만들지 않습니다.
R3. Typed catalog
기존 consumer 호환성을 위해 ID key는 page_id를 유지하고 type을 추가합니다.
- page_id: "2167636017"
type: folder
title: MCP Server
breadcrumbs:
- 관리자 매뉴얼
- MCP Server
path:
- administrator-manual
- mcp-server
Breadcrumb는 V1 ancestor에만 의존하지 않고 parent traversal context에서 계산합니다. Child 표시 title과 link path는 최신 catalog에서, 순서는 parent의 children.v2.yaml에서 해석합니다.
R4. 실행 mode별 hierarchy freshness
| Mode |
Page 내용 |
Hierarchy |
--remote |
갱신 |
page/folder 전체 갱신 |
--recent |
CQL로 발견한 기존 page만 갱신 |
기존 children.v2.yaml 유지 |
--local |
로컬 data 사용 |
기존 children.v2.yaml 유지 |
Folder 생성·이동·이름 변경·삭제는 다음 --remote 실행 때 반영되는 eventual consistency를 허용합니다.
R5. Folder MDX landing page
예상 파일:
target/ko/administrator-manual/mcp-server.mdx
예상 본문:
---
title: 'MCP Server'
confluenceUrl: 'https://querypie.atlassian.net/wiki/spaces/QM/folder/2167636017'
---
# MCP Server
## 하위 문서
- [MAC General Configurations](./mcp-server/mac-general-configurations)
- [MCP Server Connection Management](./mcp-server/mcp-server-connection-management)
- [MCP Access Control](./mcp-server/mcp-access-control)
childPosition 순서의 직계 자식 page/folder만 표시합니다.
- Nested folder의 descendant를 현재 목록에 펼치지 않습니다.
- Link는 현재 folder MDX에서 child MDX까지의 상대 경로로 계산하고
.mdx suffix를 제거합니다.
- 지원되는 직계 자식이 없으면
하위 문서가 없습니다.를 표시합니다.
- Folder MDX는 매 conversion에서 완전히 덮어씁니다.
- Folder에는 XHTML sidecar 또는 reverse-sync mapping을 만들지 않습니다.
R6. Catalog-level navigation pass
_meta.ts 생성을 XHTML converter에서 분리하여 convert_all.py의 전체 catalog pass로 수행합니다.
administrator-manual/_meta.ts
mcp-server: MCP Server
administrator-manual/mcp-server/_meta.ts
mac-general-configurations: MAC General Configurations
mcp-server-connection-management: MCP Server Connection Management
mcp-access-control: MCP Access Control
Page/folder를 같은 규칙으로 포함하고 childPosition 순서를 유지합니다.
R7. Manifest 기반 stale output 삭제
var/convert-manifest.<sync-code>.yaml에 변환기가 생성한 MDX와 _meta.ts를 기록합니다.
- 이전 manifest를 읽습니다.
- 현재 catalog의 전체 output을 생성하고 검증합니다.
- 하나라도 실패하면 삭제와 manifest 교체를 하지 않습니다.
- 모두 성공하면
previous_paths - current_paths만 삭제합니다.
- output root 내부의 허용된 소유 파일인지 검증합니다.
- 비어 있는 directory만 제거하고 비소유 파일은 보존합니다.
- 현재 manifest를 atomic replace합니다.
Manifest 도입 전 stale 파일은 소유권을 증명할 수 없으므로 최초 실행에서는 삭제하지 않습니다. Folder 이동·이름 변경으로 경로가 바뀐 descendant MDX와 generated _meta.ts도 manifest 기준으로 정리합니다. Attachment cleanup은 이 issue 범위에 포함하지 않습니다.
R8. 기존 profile 호환성
- QM의 page root와 QCP의 folder root를 같은 typed model로 처리합니다.
- Sync root는 catalog/path 기준으로 유지하되 기존과 같이 root 자체 MDX는 생성하지 않습니다.
- 기존 page XHTML, attachment, MDX path와 title translation 동작을 회귀시키지 않습니다.
테스트
API client
- page/folder
direct-children endpoint 선택
- cursor 2개 이상의 pagination merge
- 중간 실패 시 기존 snapshot 보존
Fetch/tree
root page
└─ page
└─ folder
├─ page A
├─ nested folder
│ └─ page B
└─ whiteboard (unsupported)
- type이 재귀 호출과 catalog까지 유지됩니다.
- folder와 하위 page/folder path가 올바릅니다.
- unsupported child가 제외되고 경고가 남습니다.
--remote만 hierarchy를 갱신합니다.
--recent, --local은 cached hierarchy를 재사용합니다.
Conversion
- folder frontmatter, H1, 직계 자식 목록, 상대 link, ordering
- nested folder가 한 항목으로만 표시됨
- 빈 folder 안내 문구
- page/folder 혼합
_meta.ts
- folder 이동·이름 변경·삭제 후 stale 소유 파일 삭제
- conversion 실패 시 이전 output/manifest 보존
- QM
2167636017과 QCP folder root smoke
완료 조건
검토 결론
Issue #1028을 endpoint 변경만으로 해결해서는 folder를 올바르게 처리할 수 없습니다. Typed tree, folder raw storage, catalog type, folder MDX generator, catalog-level navigation, mode별 hierarchy freshness, manifest cleanup까지 함께 구현하면 승인된 folder 처리 요구사항을 충족할 수 있습니다.
목표
Confluence content tree의
folder를 정식 구조 노드이자 MDX landing page로 저장·변환합니다.var/{folder_id}/에 저장합니다.Related: #936
조사 기준
main9eb535ff5d38b9ad99cb677dce8b71b6dd1bb906확정된 동작
convert_all.py실행마다 folder MDX의 frontmatter, 제목, 직계 자식 목록을 완전히 재생성하며 수동 편집은 보존하지 않습니다.page와folder만 표시합니다.database,whiteboard,embed는 당분간 지원하지 않고 식별 가능한 경고를 남깁니다.하위 문서가 없습니다.를 표시합니다.--remote가 갱신합니다.--recent는 저장된 hierarchy 안의 기존 page 내용만 갱신하고children.v2.yaml은 갱신하지 않습니다._meta.ts는 성공적인 변환 후 자동 삭제합니다.재현 대상 구조
저장된 Confluence V1/V2 응답에서 확인한 구조는 다음과 같습니다.
하위 page의
page.v2.yaml에는 folder 부모가 정확히 기록되어 있습니다.현재 결과
var/2167636017/--recent로 개별 page data가 저장될 수 있습니다.var/pages.qm.yamltarget/ko/administrator-manual/mcp-server.mdxtarget/ko/administrator-manual/mcp-server/_meta.tsconvert_all.py는 catalog에 있는 항목만 처리하므로, 하위 page의page.xhtml이 이미 저장되어 있어도 orphan 상태에서는 변환하지 않습니다.원인
1. Page-only child endpoint를 사용합니다
현재
ApiClient.get_child_pages()는 다음 endpoint를 사용합니다.이 endpoint는 page만 반환합니다. 모든 직계 content type을 발견하려면 다음 endpoint가 필요합니다.
2. Folder child endpoint가 올바르지 않습니다
현재 folder 분기는
/api/v2/folders/{id}/children을 구성하지만 공식 직계 자식 endpoint는 다음과 같습니다.3. 재귀 순회가 content type을 버립니다
현재 재귀 입력은
children.v2.yaml에서 추출한 ID 문자열뿐입니다.direct-children이type: folder를 반환해도 다음 요청의 API routing에 전달할 수 없습니다.4. Cache가 없는 non-root child를 page로 간주합니다
최초 발견한 non-root folder에는
page.v2.yamlcache가 없으므로/api/v2/pages/{folder_id}를 잘못 호출합니다. 현재 folder 지원은 사실상 folder가 sync root인 일부 경우로 제한되어 있습니다.5. Catalog가 content type을 보존하지 않습니다
현재
Page와pages.<code>.yaml에는type이 없습니다. Fetch와 conversion 단계가 folder를 본문 없는 정식 노드로 구분할 수 없습니다.6. Breadcrumb 생성이 V1 page ancestor에 의존합니다
Stage4Processor는page.v1.yaml이 없으면 catalog entry를 만들지 않습니다. Folder에는 page V1 body/ancestor를 요구할 수 없으므로 parent traversal context에서 breadcrumb와 path를 계산해야 합니다.7. Navigation 생성이 XHTML 변환에 결합되어 있습니다
_meta.ts생성은 개별page.xhtml변환의 side effect입니다. Folder는 XHTML이 없으므로 folder 아래 navigation을 생성할 실행 지점이 없습니다.8. 출력 소유권 기록이 없어 이전 경로를 안전하게 정리할 수 없습니다
Folder 이동·이름 변경 시 새 경로를 생성하는 것만으로는 이전 folder와 descendant MDX가 남습니다. 변환기가 만든 파일과 수동 파일을 구분할 manifest가 필요합니다.
구현 디자인
R1. Typed
direct-children순회재귀 호출은 ID가 아니라 최소 다음 구조를 전달합니다.
/api/v2/pages/{id}/direct-children/api/v2/folders/{id}/direct-childrenpage,folder외 type은 순회하지 않고parent_id,id,type,title을 경고로 기록합니다.children.v2.yaml을 덮어쓰지 않습니다.R2. Folder raw data 저장
folder.v2.yaml:/api/v2/folders/{id}metadata 응답children.v2.yaml: pagination을 합친 직계 자식 snapshotpage.v1.yaml,page.v2.yaml,page.xhtml,attachments.v1.yaml을 만들지 않습니다.R3. Typed catalog
기존 consumer 호환성을 위해 ID key는
page_id를 유지하고type을 추가합니다.Breadcrumb는 V1 ancestor에만 의존하지 않고 parent traversal context에서 계산합니다. Child 표시 title과 link path는 최신 catalog에서, 순서는 parent의
children.v2.yaml에서 해석합니다.R4. 실행 mode별 hierarchy freshness
--remote--recentchildren.v2.yaml유지--localchildren.v2.yaml유지Folder 생성·이동·이름 변경·삭제는 다음
--remote실행 때 반영되는 eventual consistency를 허용합니다.R5. Folder MDX landing page
예상 파일:
예상 본문:
childPosition순서의 직계 자식page/folder만 표시합니다..mdxsuffix를 제거합니다.하위 문서가 없습니다.를 표시합니다.R6. Catalog-level navigation pass
_meta.ts생성을 XHTML converter에서 분리하여convert_all.py의 전체 catalog pass로 수행합니다.Page/folder를 같은 규칙으로 포함하고
childPosition순서를 유지합니다.R7. Manifest 기반 stale output 삭제
var/convert-manifest.<sync-code>.yaml에 변환기가 생성한 MDX와_meta.ts를 기록합니다.previous_paths - current_paths만 삭제합니다.Manifest 도입 전 stale 파일은 소유권을 증명할 수 없으므로 최초 실행에서는 삭제하지 않습니다. Folder 이동·이름 변경으로 경로가 바뀐 descendant MDX와 generated
_meta.ts도 manifest 기준으로 정리합니다. Attachment cleanup은 이 issue 범위에 포함하지 않습니다.R8. 기존 profile 호환성
테스트
API client
direct-childrenendpoint 선택Fetch/tree
--remote만 hierarchy를 갱신합니다.--recent,--local은 cached hierarchy를 재사용합니다.Conversion
_meta.ts2167636017과 QCP folder root smoke완료 조건
var/2167636017/folder.v2.yaml과children.v2.yaml이 생성됩니다.pages.qm.yaml에 folder와 하위 page 3개가type과 함께 포함됩니다.administrator-manual/mcp-server.mdx가 생성됩니다._meta.ts가 XHTML 유무와 관계없이 생성됩니다.--remote가 hierarchy 변경을 반영합니다.--recent는 hierarchy를 갱신하지 않고 기존 page 내용만 갱신합니다._meta.ts만 성공적인 변환 후 삭제됩니다.database,whiteboard,embed는 제외되고 경고가 남습니다.검토 결론
Issue #1028을 endpoint 변경만으로 해결해서는 folder를 올바르게 처리할 수 없습니다. Typed tree, folder raw storage, catalog type, folder MDX generator, catalog-level navigation, mode별 hierarchy freshness, manifest cleanup까지 함께 구현하면 승인된 folder 처리 요구사항을 충족할 수 있습니다.