Confluence 공간 2개 · Jira 프로젝트 7개를 명세서에서 실제 Atlassian 사이트까지 옮긴 과정. AI가 만든 명세와 실제 API 사이의 간극을 어디서, 어떻게 발견하고 흡수했는가가 이 챕터의 주제입니다.
이 챕터를 마치면 수강자는 다음을 할 수 있습니다.
---
Product 1: 'PM/PO AI 기획 웹' (KEY: IDEA_WEB)
Product 3: 'PM/PO AI 강의 웹' (KEY: LECTURE_WEB)
Product 2: TOOL_A / TOOL_B / TOOL_C / TOOL_D
Confluence 공간: PMPO_WIKI, PMPO_TOOLS💡 이 챕터의 핵심 교훈: AI가 쓴 기획서는 *읽기에* 완벽할 수 있습니다.
그것이 *실행 가능한지*는 완전히 다른 문제이고, 실행해 보기 전에는 알 수 없습니다.
---
실제로 사용한 프롬프트의 구조입니다.
| 요소 | 실제 입력값 |
|---|---|
| Role | Full-Stack Software Engineer & AI Automation Architect |
| Context | IT PM/PO 업무 자동화 4-Product 시스템. Confluence·Jira를 단일 원본으로 관리. 계정 비밀번호는 제공하지 않고 API 토큰만 사용 |
| Task | Confluence 공간/페이지 트리 세팅 가이드 + Jira 프로젝트·백로그 세팅 자동화 스크립트/명세서 작성 |
| Constraints | 멱등 실행, DRY-RUN 기본값, 비밀값 커밋 금지, 명세와 API 규칙이 충돌하면 먼저 알리고 정규화한 값으로 진행 |
| Format | Markdown 가이드 + Python CLI + 스펙 JSON |
Constraints 한 줄이 결과를 갈랐습니다.
"명세와 API 규칙이 충돌하면 먼저 알려주고 정규화한 값으로 진행한다"
이 문장이 없었다면 AI는 두 가지 중 하나를 했을 겁니다 — ①명세대로 IDEA_WEB을 써서 실행 시점에 400 에러로 죽거나, ②말없이 IDEAWEB으로 바꿔서 나중에 명세서와 실제 시스템이 어긋난 걸 아무도 모르거나. 둘 다 나쁩니다.
📌 실습 포인트: 프롬프트에 "충돌하면 먼저 알려라"를 넣는 것과 넣지 않는 것의
차이를 직접 비교해 보세요. 이것이 Module 1에서 배운 Constraints의 실전 효과입니다.
┌──────────────────────────────────┐
│ config/specs/*.json │ ← 단일 원본
│ confluence_spaces.json │ (구조를 바꾸려면 여기만)
│ jira_projects.json │
└───────────────┬──────────────────┘
│ 읽음
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
setup validate setup confluence setup jira
KEY 규칙 사전 검증 공간→페이지 트리 프로젝트→Epic→Story
(실행 전 차단) (멱등 생성) (+ 리모트 링크 바인딩)
│ │ │
└─────────────────────┼─────────────────────┘
▼
setup verify
실제 생성물 ↔ 스펙 대조 (읽기 전용)
docs/4-product-system/*.md ← JSON을 '설명'만 함. 여기를 고쳐도 아무것도 안 바뀜왜 이 구조인가: 문서와 코드가 각자 구조를 알고 있으면 반드시 어긋납니다. JSON 하나만 진실로 두고, 문서는 그것을 설명하고, 코드는 그것을 실행합니다. 실제로 이 프로젝트에서 구조가 3번 바뀌었는데(KEY 명명 규칙이 두 번, 공간 키가 한 번) 매번 JSON 한 파일만 고치고 스크립트를 재실행해 끝났습니다.
---
| 단계 | 작업 | 소요 | 산출물 |
|---|---|---|---|
| 1 | 명세서·대시보드 원문 읽고 실제 Atlassian 상태 조회 | 10분 | 현황 파악 |
| 2 | KEY 규칙 위반 발견 및 정규화 설계 | 15분 | 매핑 표 |
| 3 | 스펙 JSON 2종 작성 (공간·페이지 / 프로젝트·백로그) | 40분 | config/specs/ |
| 4 | 멱등 클라이언트 + 부트스트랩 구현 | 90분 | common/, setup_ops/ |
| 5 | 표준 템플릿 4종 + 세팅 가이드 3종 작성 | 60분 | docs/4-product-system/ |
| 6 | 테스트 작성 및 DRY-RUN 검증 | 40분 | pytest 86건 |
Atlassian의 실제 규칙:
| 대상 | 규칙 | 명세서 값 | 결과 |
|---|---|---|---|
| Jira 프로젝트 KEY | ^[A-Z][A-Z0-9]{1,9}$ | IDEA_WEB | ❌ 언더스코어 불가 |
| Confluence 공간 KEY | ^[A-Za-z0-9]+$ | PMPO_WIKI | ❌ 언더스코어 불가 |
정규화하면서 추적성을 잃지 않는 것이 설계의 핵심이었습니다.
| 명세 표기 | 실제 KEY | 보존 방법 |
|---|---|---|
IDEA_WEB | IDEAWEB | JSON requestedKey + Jira 라벨 spec:IDEA_WEB |
LECTURE_WEB | LECTWEB | 〃 |
TOOL_A~TOOL_D | TOOLA~TOOLD | 〃 |
PMPO_WIKI | PMPOWIKI | JSON requestedKey |
PMPO_TOOLS | PMPOTOOLS | 〃 |
def normalize_key(raw: str) -> str:
"""'DATA_PIPE' → 'DATAPIPE'. 규칙 위반이면 ValueError."""
key = re.sub(r"[^A-Za-z0-9]", "", raw).upper()[:10]
if not KEY_PATTERN.match(key):
raise ValueError(
f"Jira 프로젝트 KEY 로 쓸 수 없습니다: {raw!r} → {key!r}. "
"규칙: 대문자로 시작, 대문자·숫자만, 2~10자."
)
return key그리고 실행 전에 차단하는 명령을 따로 만들었습니다. 400 에러를 20분 뒤에 만나는 것보다 0초에 표로 보는 게 낫습니다.
$ python main.py setup validate
KEY 검증
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ 종류 ┃ 명세 표기 ┃ 실제 생성 KEY ┃ 결과 ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Confluence 공간 │ PMPO_WIKI │ PMPOWIKI │ 정규화됨 │
│ Jira 프로젝트 │ IDEA_WEB │ IDEAWEB │ 정규화됨 │
│ Jira 프로젝트 │ TOOL_A │ TOOLA │ 정규화됨 │
│ Jira 프로젝트 │ PORTFOLIO │ PORTFOLIO │ OK │
└─────────────────┴─────────────┴───────────────┴──────────┘부트스트랩은 몇 번을 돌려도 같은 결과여야 합니다. 그래야 "일단 돌려보자"가 가능합니다.
def ensure_project(self, *, key, name, description="", ...):
"""프로젝트가 없으면 생성하고, 있으면 그대로 반환합니다."""
key = normalize_key(key)
if existing := self.find_project(key):
logger.info("Jira 프로젝트 이미 존재: %s (%s)", key, existing.get("name"))
return existing
...이슈는 project + summary 완전일치로 조회한 뒤 없을 때만 만듭니다. Confluence 페이지는 제목 기준 upsert이되 기존 본문은 덮어쓰지 않습니다 — 사람이 채운 위키 내용을 스크립트 재실행이 날리는 사고를 막기 위한 기본값입니다.
write = method.upper() in {"POST", "PUT", "DELETE"} and not read_only
if write and self.dry_run:
logger.info("[DRY-RUN] %s %s %s", method, url, _preview(json))
return dry_run_result if dry_run_result is not None else {}읽기는 dry-run에서도 실제로 보냅니다. 그래야 "이미 있는 것 / 새로 만들 것"을 정확히 구분해 보여줄 수 있습니다. 읽기까지 막으면 DRY-RUN 출력이 거짓말을 하게 됩니다.
전부 자동화하려다 실패하는 것보다, 경계를 명확히 긋고 수동 절차를 체크리스트로 주는 것이 낫습니다. API 지원 범위를 조사해 아래를 문서에 명시했습니다.
| 항목 | 왜 수동인가 | 대응 |
|---|---|---|
| 보드 컬럼 개명 | 팀 관리형 프로젝트 상태를 다루는 공개 API 없음 | UI 1회 작업 + 체크리스트 |
| Confluence 공간 템플릿 등록 | REST 지원 제한적 | 99. 표준 템플릿 보관함 페이지로 대체 |
| Google 서비스 계정·OAuth | 콘솔 동의 필요 | 절차 문서화 |
| Apps Script 설치 | 계정 권한 동의 필요 | setupTriggers 원클릭 함수 제공 |
그리고 자동화 실패가 전체를 죽이지 않게 했습니다. 상태 전이가 불가능하면 경고만 남기고 이슈 생성은 계속합니다.
---
| 지표 | Before | After | 측정 근거 |
|---|---|---|---|
| 초기 세팅 소요 | 반나절(수기) | 약 3분(CLI) + 수동 2건 | 실행 로그 |
| 구조 변경 반영 | 전 항목 수기 재작업 | JSON 1파일 수정 + 재실행 | 실제 3회 변경 수행 |
| 잘못된 KEY로 인한 실패 | 발견 시점 = 실행 중 | 실행 전 setup validate에서 차단 | exit 1 |
| 명세 ↔ 시스템 추적 | 정규화 시 단절 | requestedKey + spec: 라벨로 유지 | JQL 조회 가능 |
실제 생성물 (2026-08-08 실측)
Confluence PMPOWIKI 9 페이지 (1단계 5 + Module 1~4)
PMPOTOOLS 13 페이지 (Tool A~D 4 + 각 하위 2 + 템플릿 1)
Jira 프로젝트 7개 + Epic 20여 개 + Story/Task 40여 건setup validate를 만든 것. 실패를 실행 전으로 당겼습니다.💡 위 두 개의 ❌ 는 우연이 아닙니다. 둘 다 "실행해 보지 않고 문서에 쓴 것" 입니다.
Chapter 2가 정확히 이 지점에서 출발합니다.
---
| 구분 | URL |
|---|---|
| 교안 PDF/슬라이드 | /downloads/module2-ch1-slides.pdf |
| 슬라이드 (HTML) | /downloads/module2-ch1-slides.html |
| 실습용 데이터 세트 | pm-po-automation-tools/config/specs/*.json |
| 시뮬레이터 실행 | (해당 없음) |
| GitHub 저장소 | https://github.com/ursulax/action |
| 세팅 가이드 | docs/4-product-system/01-confluence-space-guide.md |
| Jira 명세서 | docs/4-product-system/02-jira-project-spec.md |
| 실제 Confluence | PM/PO 업무 위키 (비공개) |
| 관련 PR | https://github.com/ursulax/action/pull/20 |
---
명세와 실제 API/시스템 규칙이 충돌하면 먼저 알려주고, 정규화한 값으로 진행하되 원 표기를 추적 가능하게 보존하라. ``validate 성격의 사전 검증 단계를 먼저 만드세요. 본 작업보다 먼저.제출물: 스펙 JSON 1개 + 검증 함수 1개 + 정규화 매핑 표 자가 점검 기준:
---
| 증상 | 원인 | 해결 |
|---|---|---|
| Jira 프로젝트 생성 시 400 | KEY에 언더스코어/소문자/11자 이상 | normalize_key() 통과 후 사용. setup validate로 사전 차단 |
| Confluence 공간 생성 시 거부 | 공간 KEY에 특수문자 | 영숫자만 사용. 표시명에는 슬래시 등 가능 |
| MCP 연동으로 자동화가 안 됨 | 세션 스코프에 공간 생성·Jira 권한 없음 | API 토큰 기반 REST 사용. 스코프를 먼저 확인할 것 |
| 재실행하니 티켓이 중복 생성됨 | 존재 확인 없이 create만 호출 | ensure_* 패턴 — 조회 후 없을 때만 생성 |
| 사람이 채운 위키가 날아감 | upsert가 항상 덮어씀 | 기본값을 본문 보존으로. 덮어쓰기는 명시적 옵션으로 분리 |
| DRY-RUN 출력이 실제와 다름 | 읽기 호출까지 막아서 상태를 모름 | 쓰기만 막고 읽기는 통과시킬 것 |