가명 처리 버전 / Anonymized version — 코스 번호, 기관, 도구 이름, 작성자 모두 가명으로 치환됨 (공개 공유용).
Field Notes · Vol. I · § Practical Onboarding

Blackboard CXP — Modify & Re-Import Workflow

Blackboard에서 export한 코스 패키지(CXP)를 수정하고 다시 import할 때 발생하는 모든 import-failure 패턴과 fix recipe. 2026-05-04 [Summer Term] fix marathon에서 distill한 8-point recipe + 모든 성공한 Python 스크립트 + troubleshooting 절차.
Jewoong Moon University of Alabama · ELPTS jmoon19@ua.edu 2026-05-04 DOCT-α v1→v11 / UND-β v1→v6 / GRAD-γ v1→v5 Free use w/ attribution

0. 가이드북 소개 / About this guidebook

이 가이드는 Blackboard에서 export한 CXP 패키지(ArchiveExFile_*.zip)를 로컬에서 수정한 뒤 같은 또는 다른 코스로 re-import할 때 발생하는 import 실패와 runtime 에러를 진단하고 고치는 실전 workflow다. 2026-05-04 [Summer Term] fix marathon (DOCT-α v1→v11, UND-β v1→v6, GRAD-γ v1→v5)에서 distill한 8-point recipe와 모든 successful Python scripts + 절대 금지 사항을 한 곳에 모았다.

Audience
  • 코스를 학기마다 revise해서 import해야 하는 instructor / TA
  • Codex/Claude Code-style coding agent에게 fix를 시킬 때 참고할 recipe가 필요한 사람
  • 처음 Blackboard ULTRA로 코스를 옮겨야 하는 새 PM / instructional designer
Scope conservatism 이 가이드는 2026-05-04 시점의 Blackboard ULTRA 동작을 관찰한 결과를 기록한 practical onboarding guide다. Blackboard internals은 시간이 지나면 바뀐다. 모든 fix는 샌드박스 코스에서 먼저 검증한 뒤 production에 적용. 8가지 defect class는 본 세션에서 직접 재현·해결한 것만 포함; 다른 failure mode는 별도로 조사해야 한다.

이 가이드가 풀어주는 use case

  • 코스 export ZIP을 받았는데 import 시 "content item failed to convert" 에러가 뜬다.
  • Import는 됐는데 학생이 과제 / 토론을 클릭해도 안 열린다 (silent runtime failure).
  • Click 시마다 MjYwNTA0L* 같은 base64 access-code log reference가 쌓인다.
  • Due date가 옛 학기 그대로다.
  • Module에 새 콘텐츠 / 이미지 / URL을 자연스럽게 추가하고 싶다.
  • 한 부분만 selective import해서 빠르게 업데이트하고 싶다.

이 가이드의 한계

  • Blackboard의 UI-side import dialog는 다루지 않는다 (institution마다 다름).
  • Building Block / LTI 도구 등록은 admin이 해줘야 함.
  • 여기 기록된 8개 defect class는 본 세션의 incident에서 관찰한 것만; 다른 failure mode (LDAP, gradebook 변환, 외부 도구 OAuth 등)는 따로 조사 필요.

1. CXP 패키지 해부 / Anatomy of a CXP package

1.1 파일 구조 / Files in the ZIP

Blackboard에서 export한 archive ZIP을 풀면 다음과 같은 구조가 나온다:

ArchiveExFile_*.zip 풀면:
├─ imsmanifest.xml          ← 패키지 인덱스 (반드시 유효해야 함)
├─ .bb-package-info         ← Blackboard 시스템 메타데이터
├─ .bb-log-info             ← 패키지 빌드 로그 (정보성)
├─ .bb-package-sig          ← 서명 (수정 후 재계산 필요 X — Blackboard 무시)
├─ res00001.dat             ← 첫 번째 리소스 (XML)
├─ res00002.dat
├─ ...
├─ resNNNNNN.dat            ← N번째 리소스
└─ csfiles/
   └─ home_dir/
      └─ __xid-NNNNNN_1.png ← 임베드된 이미지 / PDF / Word / Excel 등
         __xid-NNNNNN_1.png.xml ← 사이드카 메타데이터
Anatomy of a Blackboard CXP archive ZIP file
Fig. 1Anatomy of a Blackboard CXP archive ZIP and how the parts fit together
핵심 원칙 패키지의 모든 변경은 (1) 파일 시스템 (.dat 파일들 + csfiles/)(2) imsmanifest.xml의 declaration이 동기화돼야 한다. 한쪽만 수정하면 import 시 silent failure 또는 explicit error.

1.2 imsmanifest.xml — 패키지의 인덱스

imsmanifest.xml은 IMS Common Cartridge 표준의 manifest 형식. Blackboard 확장 attribute (bb:file, bb:title, bb:type)이 추가됨. 두 개의 핵심 블록:

<manifest ...>
  <metadata>...</metadata>

  <organizations default="toc00001">
    <organization identifier="toc00001">
      <item identifier="itm00001" identifierref="res00007">
        <title>ROOT</title>
        <item identifier="itm00016" identifierref="res00047">
          <title>Module 6: Capstone</title>
          ...
        </item>
      </item>
    </organization>
  </organizations>

  <resources>
    <resource bb:file="res00001.dat" bb:title="Child Courses"
              identifier="res00001" type="course/x-bb-childcourses"
              xml:base="res00001"/>
    <resource bb:file="res00047.dat" bb:title="Module 6: Capstone"
              identifier="res00047" type="resource/x-bb-document"
              xml:base="res00047"/>
    ...
  </resources>
</manifest>
  • <organizations> = 학생이 보는 트리 (Module → Lesson → Item 구조)
  • <resources> = 모든 .dat 파일과 csfiles의 declaration
  • <item>identifierref<resource>identifier와 매칭돼야 함
  • <resource>bb:file은 실제 파일과 일치해야 함

Asmt-test-link / forumlink / courselink CONTENT 아이템은 본인의 데이터를 직접 갖고 있지 않다. 별도의 LINK 리소스가 wrapper와 underlying data를 연결한다:

res00071 (CONTENT, asmt-test-link, ULTRA marker)  ← 학생이 클릭하는 visible 아이템
   │
   ↓ (LINK가 walk됨)
   │
res00118 (LINK, ISAVAILABLE=true)                  ← 연결고리
   │
   ↓ (REFERREDTO)
   │
res00100 (COURSE_ASSESSMENT)                       ← assignment metadata
   │
   ↓ (ASMTID)
   │
res00015 (questestinterop)                         ← 실제 평가 데이터
LINK chain — normal flow vs broken flow
Fig. 2Normal vs broken LINK chain — silent runtime failure surfaces only on click
가장 흔한 함정 이 4-step chain의 어느 한 곳이 끊어지면 (LINK 누락, ISAVAILABLE=false, COURSE_ASSESSMENT 사라짐, ASMTID 잘못 가리킴) 학생이 클릭해도 아무 일도 일어나지 않는다. Convert-time 에러는 안 나고 runtime에만 깨짐.

2. 표준 워크플로 / Standard workflow

9-step Blackboard CXP modify-and-reimport workflow timeline
Fig. 5Nine-step modify-and-reimport workflow with the v(N) iteration loop

코스 revision은 다음 순서로 진행:

  1. Export — 원본 코스에서 archive (Blackboard UI: Packages and Utilities → Export Course).
  2. Backup — export한 ZIP을 03_Original-ZIPs-Backup/ 같은 디렉토리에 별도 보관. 이 파일은 절대 수정하지 않는다.
  3. Extract — ZIP을 working dir에 풀어서 작업.
  4. Audit (sec. 4) — 8-point recipe 돌려서 baseline defect 확인.
  5. Modify — 콘텐츠 수정 / 새 자료 추가 / 날짜 변경.
  6. Re-audit — 수정 후 8-point 다시. 수정 자체가 새 defect을 만들 수 있음.
  7. Repackage — ZIP으로 다시 압축 (sec. 9).
  8. Sandbox import — 학생이 보는 환경 (sandbox course)에서 실제로 import해보고 모든 클릭 가능한 항목을 click하며 검증. Convert-time 로그 + runtime 동작 둘 다 확인.
  9. Production import — sandbox에서 통과하면 production.
버전 넘버링 Production용으로 한 번에 fix하지 마라. 각 fix attempt마다 별도 ZIP (v1, v2, …)으로 저장하면 rollback과 paper trail이 보장된다. AIL-606은 v1→v11까지 갔다.

3. 8-Point Defect Catalog

CXP 8 — defect catalog at a glance
Fig. 3The eight defect classes — convert-time (4), runtime (3), tooling (1)

아래 8가지 defect class는 본 세션에서 직접 관찰·해결한 것. FAIL는 사용자 영향 있음, WARN는 informational, INFO는 메타. Tag:

  • CONVERT — Blackboard import 시점에 명시적 에러로 실패
  • RUNTIME — Import는 통과하지만 사용자가 클릭 / 사용 시 깨짐
  • TOOLING — Fix 작성할 때 빠지기 쉬운 함정
1 Orphan manifest declarations CONVERT

Manifest는 declare하는데 실제 .dat 파일이 ZIP에 없거나, 그 반대 케이스. Convert 시점에 "missing referenced file" 류 에러로 실패하거나 silent skip.

Diagnostic

comm -23 \
  <(grep -oE 'identifier="res[0-9]+"' imsmanifest.xml | sort -u) \
  <(ls *.dat | sed 's/\.dat$//' | sort -u)

Fix

  • 누락 파일이 있어야 할 경우: 원본 archive에서 복사
  • 없어도 될 경우: manifest의 <resource> declaration 삭제

실제 사례

DOCT-α v1: Codex가 res00104~126 (LINK 23개) + res00022~25 (announcements 4개)를 .dat 파일에서 삭제했지만 manifest에는 declaration이 그대로 남음. 27개 orphan declaration → 패키지 무결성 깨짐.

2 Retired-tool handlers (Journal / Blog / Wiki) CONVERT

Blackboard ULTRA는 Journal, Blog, Wiki 도구를 지원하지 않는다. <CONTENTHANDLER value="resource/x-bb-journallink"/> 같은 retired handler를 쓰는 visible content가 있으면 import 시 명시적 실패.

Symptom (import log)

[Journal Item] — Blogs aren't supported at this time and were removed.
[Journal Item] — The content item failed to convert.

Cascade chain

  1. Blackboard sees res00013 (x-bb-blog) → "Blogs aren't supported, removed" → drops res00013
  2. res00122 LINK points to res00013 (now gone) → orphan LINK silently dropped
  3. res00068 CONTENT has journallink handler expecting target → can't convert

Fix (3 things together)

  1. Visible CONTENT의 CONTENTHANDLERresource/x-bb-document로 + RENDERTYPEREGULAR
  2. Tool data resource 파일 삭제 (BLOG, JOURNAL, WIKI 페이로드)
  3. 이를 잇던 LINK 파일 삭제
  4. 두 deletion 모두 manifest에서 <resource> declaration 제거

📝 Script: fix_journal_blog_dependency.py

3 Wrong-type substitution CONVERT

Manifest의 bb:type이 선언한 종류와 실제 .dat 파일의 root XML element가 일치하지 않음.

Canonical example

res IDManifest bb:typeActual rootResult
res00128course/x-bb-coursemessagefolder<questestinterop>FAIL

How it happens

Coding agent가 "이 자리에 새 assessment를 넣자"라고 판단하고 기존 res ID에 다른 type의 페이로드를 덮어씀. Manifest는 안 건드림 → 충돌.

Fix

해당 .dat 파일을 원본에서 복원. 다른 resource ID를 절대 다른 type으로 덮어쓰지 마라; 새 resource를 추가하고 새 ID를 발급하라.

4 Empty leaf content items CONVERT

학생에게 보이는 <CONTENT> 아이템이:

  • <DESCRIPTION value=""/> (빈 description) AND
  • <TEXT/> (빈 body) AND
  • ISFOLDER value="false" (folder 아님) AND
  • <FILES><FILE 없음 (첨부 파일도 없음)

Blackboard converter가 거부.

False positives 주의

  • asmt-test-link / forumlink / courselink wrapper는 의도적으로 비어있음 (LINK 통해 target 가리킴) — skip
  • ULTRA_ASSESSMENT_MARKER=true인 ULTRA assignment surface — skip (defect 7 참고)
  • resource/x-bb-folder / x-bb-lesson — folder 자체 — skip

Fix

  • 원본 archive에서 description + body 복원
  • 또는 folder로 type 변경
  • 또는 minimum content 직접 추가 (예: TITLE만 message로 사용)
5 Disabled LINK resources RUNTIME
Disabled vs enabled LINK — before and after fix_disabled_links.py
Fig. 7Disabled vs enabled LINK — fix_disabled_links.py turns the silent failure back into a working click

Visible click-target (asmt-test-link / forumlink / courselink)을 underlying assessment / forum에 연결하는 <LINK><ISAVAILABLE value="false"/>로 비활성화돼있음. 학생이 클릭해도 안 열림.

Symptom

Convert-time 에러 없음. Gradebook 컬럼은 만들어짐. Click 시 아무것도 안 일어나거나 MjYwNTA0L* 같은 base64 access-code reference 표시 (decoded: 260504/_666818_1/2.log — 서버 로그 경로일 뿐, 실제 에러 메시지 아님).

Origin (Spring archive carryover)

이전 학기에 instructor가 과제를 미리 만들어두고 release 시점까지 비활성화했던 흔적. Export 시 disabled flag 그대로 carry over됨.

Fix

import re
c = re.sub(
    r'<ISAVAILABLE\s+value="false"\s*/>',
    '<ISAVAILABLE value="true"/>',
    c,
)

📝 Script: fix_disabled_links.py

실제 사례

DOCT-α v5: 6개 핵심 과제 ([User Testing Assignment], [Final Capstone Submission], [Storyboard Challenge], [Design Draft], [Usability Plan], [Case Study]) 모두 disabled LINK 때문에 안 열림.

6 Missing LINK resources RUNTIME

Defect 5보다 더 심각: LINK 파일과 manifest declaration이 둘 다 함께 stripped됐음. 패키지 내부 일관성은 유지됨 (orphan declaration 없음) → defect 1 audit으로 안 잡힘.

Diagnostic

Visible click-target CONTENT (handler가 asmt-test-link / forumlink / courselink) 중 available LINK가 REFERRER로 연결돼있지 않은 것을 찾는다.

Fix

원본 archive에서 LINK 파일을 복원하고, ISAVAILABLE=true로 flip하고, manifest에 declaration 추가.

📝 Script: restore_missing_links.py

실제 사례

  • UND-β: 원본 31개 LINK → cur 24개. 7개가 stripped (모두 disabled였던 것을 Codex가 "쓸모없는 거" 판단해서 삭제). 7개 과제가 안 열림.
  • GRAD-γ: 원본 17개 LINK → cur 0개. Option A restructure 때 모조리 삭제됨. 14개 과제 + 2개 토론/courselink 안 열림.
왜 convert-time audit (defect 1)이 못 잡나 Manifest declaration과 .dat 파일이 같이 사라지면 패키지 내부 일관성은 유지되어 defect 1이 안 발견함. 오직 visible click-target ↔ LINK 매칭을 별도로 검증해야 잡힘.
7 ULTRA marker × classic-QTI conflict RUNTIME
ULTRA × classic-QTI conflict and the empty-section fix
Fig. 4ULTRA assignment surface vs classic QTI body — friction zone and the empty-section fix

가장 어려운 케이스. Asmt-test-link 아이템에 <EXTENDEDDATA><ENTRY key="ULTRA_ASSESSMENT_MARKER">true</ENTRY></EXTENDEDDATA>가 있고, 그 LINK chain의 끝 questestinterop 파일이 <section> 안에 <item> 콘텐츠를 가지면 발생.

Symptom

아이템은 정상적으로 열림. 하지만 render할 때마다 server log에 sequential entry가 쌓임: 260504/_666818_1/3.log, 4.log, 5.log, ...

Root cause

Marker는 ULTRA에게 "이건 ULTRA-native assignment surface (file upload + text submission)으로 render해라"라고 알려줌. 그러나 underlying QTI는 classic Blackboard test format (<questestinterop><assessment><section><item>). ULTRA가 두 model 사이를 매번 bridge하면서 conversion-trace log entry를 emit.

Fix — empty section preservation

QTI body의 <section><item>을 모두 제거. Marker는 그대로 유지. Empty section = "이건 ULTRA assignment, classic 질문 변환 시도하지 마"라는 신호.

<questestinterop>
  <assessment title="...">
    <assessmentmetadata>...</assessmentmetadata>
    <rubric>...</rubric>
    <presentation_material>...</presentation_material>
    <section>
      <sectionmetadata>...</sectionmetadata>
      <!-- 비어있어야 함; <item> 없음 -->
    </section>
  </assessment>
</questestinterop>
중요 — 진짜 quiz에는 적용하지 마라 이 fix는 asmt-test-link wrapper + ULTRA marker + 의도가 ULTRA assignment (file upload)일 때만 적용한다. CAT-531의 Module 1-6 Quiz처럼 multiple-choice 진짜 quiz가 들어있으면 절대 <item>을 지우면 안 됨 (quiz 깨짐).

📝 Script: fix_ultra_qti_conflict.py

v6 실패 사례 (DO NOT REPEAT)

이 log를 silence하려고 ULTRA marker 자체를 제거하면 → 아이템이 ULTRA에서 안 열림. Marker는 ULTRA rendering의 trigger다. QTI body를 비우는 것이 정답이지, marker를 제거하는 것이 아니다.

8 Whitespace-tolerant ISAVAILABLE replace TOOLING
Multi-line FLAGS regex trap
Fig. 8Single-line literal replace silently misses multi-line FLAGS — the whitespace-tolerant regex catches both

Defect 5/6 fix script를 짤 때 가장 빠지기 쉬운 함정.

The trap

Blackboard의 LINK XML은 두 가지 포맷으로 나옴:

Single-line:
<FLAGS><ISAVAILABLE value="false"/></FLAGS>

Multi-line:
<FLAGS><ISAVAILABLE
  value="false"/></FLAGS>

같은 archive 안에서도 두 포맷이 섞임. Naive single-line literal replace는 multi-line case를 silent하게 miss한다.

Wrong (single-line literal)

c = c.replace(
    '<ISAVAILABLE value="false"/>',
    '<ISAVAILABLE value="true"/>'
)
# Multi-line 케이스 7개 중 5개 놓침

Right (regex with whitespace tolerance)

import re
c = re.sub(
    r'<ISAVAILABLE\s+value="false"\s*/>',
    '<ISAVAILABLE value="true"/>',
    c
)
# 모든 케이스 매치

실제 사례

UND-β v2 fix: 7개 disabled LINK 중 2개만 fix됐고 5개가 그대로. v4의 audit에서 발견 → multi-line tolerant regex로 재적용.

4. 진단 도구 — 8-Point Audit Script

아래 스크립트는 위 8가지 defect를 한 번에 검사한다. 패키지를 폴더에 풀고 그 경로를 인자로 넘기면 PASS/FAIL 리포트를 출력. Exit code 0 = clean, 1 = needs work.

scripts/audit_8point.pyDownload
python audit_8point.py /path/to/extracted/package

예시 출력 (모두 통과):

======================================================================
8-Point Blackboard ULTRA Import Recipe Audit  --  /tmp/cat100_audit
======================================================================
  [PASS] 1. Orphan manifest decls
         decls=211 files=211 missing=0 orphan=0
  [PASS] 2. Retired-tool handlers in CONTENT
         hits=0 []
  [PASS] 3. Wrong-type substitution
         violations=0 []
  [PASS] 4. Empty leaf content items
         leaves=0 []
  [PASS] 5. Disabled LINKs blocking visible items
         disabled=0 []
  [PASS] 6. Click-targets without available LINK
         orphan_clicks=0 []
  [PASS] 7. ULTRA-marker x classic-QTI conflict
         conflicts=0
  [PASS] 8. Whitespace-tolerant ISAVAILABLE coverage
         multiline_misses=0

Overall: ALL PASS
운영 룰 이 스크립트는 패키징 직전마다 자동으로 돌리는 것이 안전. 수정 작업이 새 defect를 만들 수 있다 (예: 본문 교체 시 manifest 무결성 깨짐, 파일 삭제 시 orphan 발생). 매번 audit → 모두 PASS면 packaging.

5. Troubleshooting Decision Tree

샌드박스 import 결과를 보고 다음 순서로 추적:

Sandbox import 결과는? A. Convert error 에러 메시지 종류 확인 "X aren't supported, removed" → D2 B. Import OK, click 안 열림 LINK 체인 검증 D5 (disabled) → D6 (missing) C. 열리는데 access-code log D7 ULTRA × QTI 충돌 의심 QTI section 비우기 (assignment only) fix_journal_blog_dependency.py handler→document, RENDERTYPE→REGULAR D5: fix_disabled_links.py → 안 되면 D6: restore_missing_links.py orig archive 필요 fix_ultra_qti_conflict.py ⚠ 진짜 quiz면 skip marker 제거 절대 X audit_8point.py 다시 모두 PASS인가? FAIL → 다시 진단 PASS PowerShell Compress-Archive v(N+1).zip로 새 버전 저장 샌드박스 재import + 클릭 검증 모든 visible item 클릭 시도 통과 시 production

6. 절대 금지 사항 / What NOT to do

이 세션에서 직접 깨졌거나, 실수했을 때 복구 비용이 큰 것들:

1. 원본 archive ZIP 수정 03_Original-ZIPs-Backup/ArchiveExFile_*.zip 파일은 절대 수정하지 마라. 모든 fix는 working copy에서. 원본은 ground truth + rollback 자료.
2. 같은 res ID에 다른 type의 페이로드 덮어쓰기 "이 자리가 비어있으니까 새 assessment를 넣자" 같은 판단 금지. Manifest의 bb:type과 .dat 파일 root XML element가 일치해야 함. 새 콘텐츠가 필요하면 새 res ID 발급 + 새 declaration 추가.
3. ULTRA_ASSESSMENT_MARKER 제거 Access-code log를 silence하려고 marker를 빼면 → ULTRA에서 아이템이 안 열린다 (DOCT-α v6 실패 사례). Marker는 ULTRA rendering의 trigger다. 진짜 fix는 QTI body의 <item>을 비우는 것.
4. 진짜 Quiz QTI에서 <item> 제거 Defect 7 fix는 ULTRA assignment surface (file upload) 의도일 때만. 실제 multi-choice quiz (GRAD-γ Module 1-6 Quiz 같은 <response_lid> 포함)에서 item 제거하면 quiz가 통째로 깨진다.
5. Disabled LINK 통째로 삭제 "이 LINK는 disabled니까 쓸모없다"라고 판단해서 삭제하면 defect 6 (missing LINK) 만들어짐. Disabled여도 LINK 자체는 남겨둬야 visible click-target과 underlying data의 chain이 유지됨. Flip이 정답, delete가 정답이 아님.
6. Single-line literal로 ISAVAILABLE replace c.replace('<ISAVAILABLE value="false"/>', ...) 식의 literal replace는 multi-line FLAGS XML 케이스를 silent하게 miss. 항상 re.sub(r'<ISAVAILABLE\s+value="false"\s*/>', ...).
7. 새 FORUM 리소스 직접 만들기 모듈별 toggle Discussion forum 추가하려면 <FORUM> data + forumlink wrapper + LINK + manifest declaration + item-tree placement 모두 필요. 손으로 하면 ID 충돌 / chain 깨짐 위험 큼. 기존 generic Discussion board를 subject prefix ([M1 [Tension Mapper]] 등)로 재사용하는 게 훨씬 안전.
8. 한 번에 모든 fix 통합 패키지 v1에서 모든 defect를 한 번에 fix하려고 하면 어디서 깨졌는지 추적 불가. 한 defect = 한 version. DOCT-α v1→v11이 길어 보여도 각 step에서 정확히 무엇이 깨졌는지 진단·기록 가능.
9. CDT/CST timezone 무시 DUE date 형식: 2026-05-30 23:59:00 CDT. 5월 - 11월 첫째 일요일은 CDT (Central Daylight Time, UTC-5), 그 외는 CST (UTC-6). Summer term은 모두 CDT. Blackboard parser는 timezone string을 검증하지는 않지만 일관되게 쓰는 게 나음.
10. .bb-package-sig 재계산 시도 이 서명 파일은 무시해도 됨 (Blackboard import는 검증하지 않음). 재계산은 어차피 불가능 (Blackboard의 private key 필요). 그냥 두고 ZIP하면 됨.

7. 실제 사례 — [Summer Term] Fix Marathon

본 가이드의 distillation source. 단일 일자 (2026-05-04) 안에 세 코스의 11회+의 iteration을 기록.

DOCT-α (LXD doctoral capstone) — 11 iterations

BuildDefect 적용결과
v1baseline (Codex 5/3 build)D6 + D5 latent
v2[Journal Item] payload 복원D2 발현
v3D2 fix (Journal/Blog dependency 제거)discussion 작동, 과제 안 열림
v4콘텐츠 reorganization (AI Coding Assistants 섹션 Module 6 안으로)여전히 안 열림
v5D5 fix (6 disabled LINK flip)열리지만 access-code log 발생
v6실패 — ULTRA marker 제거 시도아이템 안 열림 (revert)
v7/v8v5 byte-exact 복원v5 상태로 복귀
v9D7 fix 2개 아이템 검증access-code log 사라짐
v10D7 fix 6개 ULTRA 아이템 모두모두 통과
v113개 generated diagram 임베드 (design.md, 7-step, guardrails)최종 / 25.2 MB

UND-β (Communication, undergrad 1-2) — 6 iterations

BuildDefect 적용
baseline5/2 build
v2D6 fix (7개 missing LINK 복원 + COURSE_ASSESSMENT 검증)
v317 graded item 모두 [Summer Term] dates로 갱신 (no Sundays, no Juneteenth, ENFORCE_DUE_DATE matched to baseline)
v4Module 6 Lesson 6.2 — AI + GitHub Pages 콘텐츠 + Playwright screenshots; D8 fix (multi-line ISAVAILABLE 5개 추가 발견)
v5학부 1-2학년 톤 재작성 + 큐레이션된 YouTube/Medium/GitHub Blog 링크
v67개 ChatGPT-generated diagram 임베드, M5 dual-path AI avatar (SadTalker + ElevenLabs vs PowerPoint Recording + Gemini Live), [Teaching Sim]→[Teaching Sim] 통일, M1 [Tension Mapper]+[Ethics Tutor] URL+screenshot, M2 GitHub Codex 표현 명확화. 최종 / 18.3 MB

GRAD-γ (Critical/Cultural Aspects of Tech, master's) — 5 iterations

BuildDefect 적용
baseline5/2 Option A restructure
v2D6 fix — 17개 LINK 모두 stripped됐던 것 복원 (가장 심각한 케이스)
v3M1 [Tension Mapper] / M2 [Teaching Sim] / M4 [Ethics Tutor] 가이드 + Playwright 스크린샷 + [Discussion Board] prefix-tagged discussion 패턴
v417 graded item [Summer Term] dates 갱신
v52개 generated concept diagram 추가 ([Tension Mapper] + [Ethics Tutor] anchors). 최종 / 38.9 MB
D7 알려진 한계 in GRAD-γ 6개 module quiz는 ULTRA marker × classic-QTI MC content 충돌 패턴이지만, 진짜 multi-choice quiz라 <item>을 지울 수 없다. Functional impact zero (학생들 quiz 정상 응시), runtime telemetry log 누적은 무시. Documented caveat.

8. 부가 기술 / Beyond defect fixes

8.1 이미지 / 미디어 임베드

슬라이드, infographic, 스크린샷을 콘텐츠 페이지에 inline rendering하려면 세 가지가 동시에 필요:

  1. csfiles/home_dir/__xid-NNNNNN_1.<ext> 형식으로 파일 복사 (xid는 충돌 방지 위해 990000 이상 권장)
  2. 같은 이름의 sidecar XML (__xid-NNNNNN_1.<ext>.xml) 생성
  3. imsmanifest.xml<resources> 블록에 <resource> declaration 추가

그 다음 콘텐츠 페이지 본문 HTML에서 Blackboard의 embed syntax로 참조:

<a data-bbid="img-NNNNNN_1"
   data-bbfile="{&quot;linkName&quot;:&quot;diagram.png&quot;,
     &quot;mimeType&quot;:&quot;image/png&quot;,
     &quot;alternativeText&quot;:&quot;Description&quot;,
     &quot;isDecorative&quot;:false,
     &quot;render&quot;:&quot;inline&quot;}"
   href="@X@EmbeddedFile.requestUrlStub@X@bbcswebdav/xid-NNNNNN_1">
</a>

📝 Script: embed_image.py — 위 세 단계를 한 번에.

8.2 콘텐츠 페이지 본문 교체

Blackboard의 콘텐츠 본문은 <CONTENT>...<BODY><TEXT>HTML</TEXT></BODY> 안에 entity-encoded HTML로 저장됨. <&lt;, >&gt; 등.

📝 Script: replace_body.py — 본문만 교체, 나머지 (DATES, FLAGS, CONTENTHANDLER, PARENTID...)는 보존.

python replace_body.py res00157.dat new_body.html \
    --updated "2026-05-04 04:00:00 CDT"

8.3 Due date 일괄 갱신

Gradebook 리소스의 <OUTCOMEDEFINITION> 블록 안 <DUE> 필드. JSON으로 schedule을 정의하면 일괄 적용:

{
  "Module 1: Quiz": "2026-05-27 23:59:00 CDT",
  "Module 1: Assess - [Tension Rationale]": "2026-05-30 23:59:00 CDT",
  ...
}

📝 Script: update_due_dates.py

제약: no Sundays, no Juneteenth (2026-06-19), 학기 window 안. ENFORCE_DUE_DATE는 baseline 그대로 유지 (instructor가 UI에서 항목별 결정).

8.4 Slim 패키지 — selective import

전체 코스를 다시 import하지 않고 한 모듈만 업데이트하고 싶을 때.

전략: 해당 Module의 item subtree + 시스템 리소스 (res00001 Child Courses + res00010 Content Handler Data + .bb-package-info / .bb-log-info / .bb-package-sig)만 포함한 slim ZIP 빌드. DOCT-α 예: 20MB → 294KB.

또는 더 간단히: Blackboard UI의 selective import 다이얼로그에서 full package를 import하되 원하는 module만 체크.

8.5 Discussion board 연결 — 새 FORUM 만들지 마라

모듈별 토론을 추가하려고 새 <FORUM> 리소스를 만드는 건 high-risk (ID 충돌, item-tree 깨짐). 안전한 패턴:

  • 기존 코스-wide Discussion board (예: "[Course Discussion Board]") 활용
  • 각 모듈 Explore 페이지에서 subject prefix 약속 ([M1 [Tension Mapper]], [M2 [Teaching Sim]], [M4 [Ethics Tutor]])
  • Post format 명시 (예: 100-150 단어, peer-reply 1개 이상)

구조 변경 없이 토론 connection 달성. CAT-531에서 사용한 패턴.

9. 패키징 & 검증 / Packaging & verification

9.1 PowerShell Compress-Archive

Windows에서 가장 안전한 방법:

$src = "C:\path\to\extracted_package_dir"
$dst = "C:\path\to\output\package_v6.zip"
if (Test-Path $dst) { Remove-Item $dst }
Compress-Archive -Path "$src\*" -DestinationPath $dst -CompressionLevel Optimal
$hash = (Get-FileHash $dst -Algorithm SHA256).Hash
$size = (Get-Item $dst).Length
Write-Output "Packaged: $size bytes / SHA256 $hash"
경로 separator 주의 PowerShell의 Compress-Archive는 ZIP에 backslash separator를 쓸 수 있다. unzip이 경고 ("appears to use backslashes as path separators") 하지만 Blackboard import는 정상 작동. 무시 가능.

9.2 Bash zip (Linux/Git Bash)

cd /path/to/extracted_package_dir
zip -r ../package_v6.zip . -x "*.DS_Store"

9.3 Verification checklist

  1. 패키지를 다시 풀어서 8-point audit 돌려보기 (압축 시 손상 없는지 확인)
  2. Manifest declared count == .dat file count
  3. SHA-256 기록 (rollback과 reproducibility)
  4. Sandbox 코스에 import → convert log 확인
  5. 모든 visible item을 직접 click하여 정상 open되는지 확인
  6. Gradebook 컬럼 생성 + due date 정확한지 확인
  7. 학생 view (또는 instructor preview)로 한 번 더 확인

10. FAQ

access-code log MjYwNTA0L*이 뭔가요?
Base64 encoded path. 디코드하면 260504/_666818_1/2.log 같은 서버측 로그 파일 경로. 실제 에러 메시지가 아님 — 에러 내용은 그 path의 로그 파일에 들어있음. 로그 내용을 확인하려면 Blackboard admin에게 그 경로를 알려주고 파일을 pull해달라고 요청. 학생이 보는 view에는 표시되지 않으므로 functional impact 없을 수 있음.
왜 ULTRA assignment surface인데 underlying QTI에 <item>이 있는 패키지가 만들어지나요?
두 가지 경로:
  1. 원본 코스가 classic Blackboard에서 시작했고 ULTRA로 마이그레이션될 때 marker만 추가됨 — body의 questestinterop 구조는 옛 그대로
  2. "Empty assessments converted to Question Banks" 같은 convert-time 경고를 silence하려고 누군가 (예: Codex) <item>을 hand-add한 경우
두 경우 모두 ULTRA marker + non-empty section 충돌 → defect 7. 정답은 <item>을 비우는 것 (assignment 의도일 때).
fix script 돌리니 manifest의 <organizations>가 두 개가 됐어요
Slim 패키지 빌드할 때 흔히 나오는 함정. find('<organizations>') 같은 single-string find가 multi-line attribute 케이스를 못 잡음 (<organizations\n default="..."> 형식). Regex로 r'<organizations\b.*?</organizations>' 매칭하면 해결.
한 modular 만 update하고 싶은데 selective import vs slim 패키지 어느 쪽이 좋아요?
Selective import이 더 안전 — Blackboard가 dependency를 알아서 처리. Slim 패키지는 res00001 + res00010 같은 시스템 리소스 의존성 + 새 manifest 빌드가 필요해서 손이 많이 감. 단, 패키지가 매우 크고 (수십 MB) 또는 institutional Blackboard가 selective import를 disable했다면 slim이 유일한 옵션.
일관된 xid number는 어떻게 정하나요?
기존 archive의 xid는 보통 8자리 (__xid-190865580_1). 새 미디어를 추가할 때는 훨씬 더 큰 prefix를 쓰는 게 충돌 방지에 안전. 본 세션에서는 990000~999999 범위 사용 (course별로 992xxx, 993xxx, 994xxx). 기존 코스의 xid 분포를 먼저 확인하고 그보다 명확히 큰 범위 선택.
왜 .bb-package-sig를 다시 만들 필요가 없나요?
Blackboard import는 이 서명 파일을 검증하지 않는다 (Blackboard의 private key가 필요하기 때문에 어차피 외부에서 재계산 불가). ZIP에 그대로 포함시키면 충분. 검증한 결과 (DOCT-α v1→v11 모두 같은 .bb-package-sig 사용) 문제 없음.
학생이 file을 submit해야 하는데 ULTRA assignment surface로 나오게 하고 싶어요
3가지 조건이 모두 만족돼야 함:
  1. Wrapper의 CONTENTHANDLERresource/x-bb-asmt-test-link
  2. EXTENDEDDATA에 <ENTRY key="ULTRA_ASSESSMENT_MARKER">true</ENTRY>
  3. LINK chain이 끝나는 questestinterop 파일의 <section>비어있음 (<sectionmetadata>만, <item> 없음)
세 조건이 모두 맞으면 ULTRA가 file upload + text submission UI로 render. defect 7 fix는 정확히 (3) 조건을 보장하기 위한 것.
패키지 안에 학생 데이터 (제출물, 성적)이 섞여 있어요
위험. Export 옵션에서 "Include student records" 같은 체크박스가 있으면 반드시 해제하고 다시 export. 만약 이미 그런 데이터가 있다면 import 전에 다음을 검색·삭제:
  • res*ATTEMPT*.dat
  • res*SUBMISSION*.dat
  • users.xml
  • csfiles/home_dir/users/
  • Gradebook의 <ATTEMPT> 블록
FERPA 컴플라이언스 — 학생 PII 노출 위험이 있다.
이 가이드의 fix script를 다른 LMS (Canvas, Moodle)에도 적용 가능한가요?
아니다. 이 스크립트들은 Blackboard의 IMS Common Cartridge + Blackboard 확장 attribute (bb:file, bb:title, bb:type)에 specifically 의존. Canvas와 Moodle은 자체 export format을 쓰며 manifest 구조가 다름. 8-point recipe의 concept은 transfer 가능 (orphan declarations, retired tools, disabled flags…)이지만 code는 그대로 안 됨.

11. Downloads

▶ Reusable Skill Bundle

Blackboard CXP Fix Skill — Claude Code / Codex CLI 등록용

이 가이드의 8-point recipe + 모든 fix script + reference 노트를 자동 invocation용 skill 번들로 묶었음. 개인정보 없이 순수 스킬만 등록 가능. 다른 instructor / TA / coding agent가 그대로 받아서 쓸 수 있음.

unzip blackboard-cxp-fix-skill.zip -d ~/.claude/skills/

모든 스크립트 — MIT-style ("use freely with attribution"). Python 3.8+ + 표준 라이브러리만 사용.

Recommended workflow

  1. audit_8point.py 베이스라인
  2. Defect 발견 → 해당 fix script 적용
  3. audit_8point.py 재검증
  4. 모두 PASS → packaging

12. References

Primary session record

  • Desktop\24_Teaching\Summer2026_CourseRevisions_2026-05-01\AIL606_IMPORT_FIX_RECORD_2026-05-04.md — full v1→v11 incident record
  • ~\.claude\projects\C--Users-jewoo\memory\reference_blackboard_ultra_import_fixes.md — auto-memory recipe
  • ObsidianVault\wiki\sources\[Summer Term] Course Prep Import Fix Marathon 2026-05-04.md — vault session record

Standards

Related entity pages (vault)

  • wiki/entities/[Summer Term] UA Course Prep.md
  • wiki/entities/[Tension Mapping App].md
  • wiki/entities/[Ethics Tutor].md

Auto-memory entries

  • project_summer2026_courses.md
  • reference_blackboard_ultra_import_fixes.md이 가이드의 distillation 원본
  • feedback_html_guides.md — HTML guidebook 패턴 (이 페이지가 따르는 형식)

Author

Jewoong Moon, Ph.D.
Assistant Professor of Instructional Technology
Educational Leadership, Policy, and Technology Studies (ELPTS)
University of Alabama
jmoon19@ua.edu

Disclosure

이 가이드는 2026-05-04 일자 [Summer Term] fix marathon 결과를 정리한 practical onboarding guide다. Blackboard internals은 시간이 지나면 변할 수 있으며, 이 가이드는 본 세션에서 직접 재현한 8가지 defect class만 다룬다. 다른 failure mode (LDAP integration, gradebook calculation columns, 외부 도구 OAuth 등)는 별도 조사가 필요하다. 모든 fix는 production 적용 전 sandbox에서 검증할 것.