저장한 이메일 본문을 재조회하면 heading·bold 서식이 벗겨져 텍스트로 보이던 문제 — 로드 경로 전반 일원화
emailBodyHtml(SSOT)을 무시하고 plain emailBodyText를 markdownToHtml로 재구성 → <h2>·<strong> 소실. → resolveEditorBodyHtml로 HTML 우선 복원.deriveStepEditorBodyValue 누락(SequenceStepForm은 하던 걸 LaunchModal만 안 함) → markdown 에디터에 plain 주입돼 서식 소실. → 동일 정합 적용.htmlToStepMarkdown이 <h1~h6>를 plain으로 버리던 것을 #~######로 변환 — 양 경로 heading 보존.실데이터(beta)에서 같은 step에 서식 본문(794자, h2+strong)과 서식 벗겨진 본문(515자, p만)이 번갈아 저장돼 있었고, 최종 활성 row가 515였다.
// CreateCampaignStep2.tsx — 수정 전 (로드) let bodyText = step.emailBodyText || "" // DB body_text(plain 420) if (bodyText && !isHtmlContent(bodyText)) { bodyText = markdownToHtml(bodyText) // plain → 515 (h2→p, strong 소실) } // emailBodyHtml(794 SSOT) 무시 → 에디터에 515 주입 → 재저장 시 영구 손실
// 수정 후 — 순수 함수로 추출 (markdown.ts) export function resolveEditorBodyHtml(source) { const html = source.emailBodyHtml?.trim() ? source.emailBodyHtml : "" if (html) return html // SSOT 그대로 → 서식 보존 const legacy = source.emailBodyText ?? "" return legacy && !isHtmlContent(legacy) ? markdownToHtml(legacy) : legacy // 레거시만 폴백 }
sanitizeEmailHtml은 h1/h2/strong을 모두 허용하므로 sanitize와 무관.같은 emailBodyText 필드라도 에디터에 따라 담기는 포맷이 다르다 — 이게 핵심.
| 경로 | 에디터 | emailBodyText 의미 | 올바른 로드 변환 |
|---|---|---|---|
| Step2 / Step3 / EmailEditorPanel | Tiptap | HTML (getHTML 결과) | resolveEditorBodyHtml — emailBodyHtml 그대로 |
| SequenceStepForm / LaunchModal | MDEditor | markdown | deriveStepEditorBodyValue — emailBodyHtml→markdown |
<h2> 글자가 그대로 노출되는 회귀가 되므로, 경로별 포맷 구분이 결정적이었다.htmlToStepMarkdown이 bold·link·img·list는 markdown으로 살리면서 heading은 태그 제거로 plain화하고 있었다.
// step-edit-body.ts — 추가 .replace(/<h1[^>]*>/gi, "# ").replace(/<\/h1>/gi, "\n\n") .replace(/<h2[^>]*>/gi, "## ").replace(/<\/h2>/gi, "\n\n") // ... h3~h6
로드 시 <h2>안내</h2> → ## 안내 (markdown 보존), 저장 시 markdownToHtml로 <h2> 복원 — 라운드트립 완성. LaunchModal·SequenceStepForm 양쪽 혜택.
resolve-editor-body.test.ts (5케이스): emailBodyHtml 서식 보존 · 레거시 폴백 · 공백 · HTML 패스스루 · 빈값step-edit-body.scenario.test.ts: heading 로드 보존(## 안내) → 저장 HTML 복원(<h2>) 라운드트립| 실행 | 결과 | 해석 |
|---|---|---|
| test:all (전체 307, 8.5분) | 239 pass / 33 fail / 16 flaky | email-replies 12·platform·admin·leads·onboarding 등 우리 변경과 무관한 전 영역 진입/렌더 critical 실패 → 로컬 dev 환경 baseline 불안정 (안 건드린 email-replies 12건 실패가 결정적 증거) |
| step2 라운드트립 spec | 불검증 | DB 확인 결과 테스트의 입력→저장이 미반영(body_html 원본 그대로) → 로드 검증 자체가 불가한 테스트 단계 문제 |
| alpha RP 업로드 (alpha-full, CI) | 278 pass / 10 fail (93.3%) | alpha 서버 기준 full E2E → ReportPortal(rinda-alpha) 업로드. 로컬(33 fail)보다 안정 — 10 fail은 alpha baseline flaky(smoke도 F1), 우리 변경 무관 |
"alpha e2e link에 결과 올렸나?" → 올렸고 RP 대시보드에서 확인 가능.
/reportportal path에서 RP_API_KEY=ci-alpha-... 확보 (이전 .env.local의 local-dev 키는 401). RP API 인증 200 확인.gh workflow run "E2E on Alpha"(workflow_dispatch) → CI 토큰으로 alpha RP(rinda-alpha)에 정식 업로드.| 파일 | 변경 |
|---|---|
admin/src/lib/utils/markdown.ts | resolveEditorBodyHtml 순수 함수 추출 |
admin/src/pages/sequences/CreateCampaignStep2.tsx | 로드 시 HTML SSOT 우선 복원 |
admin/src/pages/leads/useSequenceLaunchModal.ts | 로드 시 deriveStepEditorBodyValue 정합 |
admin/src/pages/sequences/utils/step-edit-body.ts | heading(h1~h6)→markdown 변환 추가 |
admin/.../resolve-editor-body.test.ts · step-edit-body.scenario.test.ts | 라운드트립 회귀 테스트 |
e2e/.../step2-format-roundtrip.spec.ts | step2 서식 보존 E2E spec |