Skip to content

release: develop 변경사항 main 운영 반영#613

Merged
Hyeonjun0527 merged 20 commits into
mainfrom
develop
May 17, 2026
Merged

release: develop 변경사항 main 운영 반영#613
Hyeonjun0527 merged 20 commits into
mainfrom
develop

Conversation

@Hyeonjun0527

@Hyeonjun0527 Hyeonjun0527 commented May 17, 2026

Copy link
Copy Markdown
Member

요약

  • develop에 누적된 변경사항을 main 운영 배포 대상으로 올리는 PR입니다.
  • 클래스 관련 화면/플로우가 대폭 수정되어 운영 release intent는 release:major 라벨로 표시합니다.
  • QnA 이미지 key/blob URL 정리, 커리큘럼/e2e 보완, 에디터 클립보드 처리, 운영 릴리즈 기록 자동화가 포함됩니다.
  • ZERO-ONE 운영 버전관리 문서/스킬, release record 검증, backend dispatch 기록 workflow, rollback/release 체크리스트가 추가됩니다.

릴리즈/버전관리 참고

  • 이 PR은 main 대상이므로 운영 release/version-management 규칙이 적용됩니다.
  • PR 라벨 release:major를 기준으로 main 배포 자동화가 frontend version bump 의도를 읽습니다.
  • 운영 release record는 main 배포 자동화 이후 releases/prod-*.yaml에 기록됩니다.
  • backend가 독립 배포되는 경우 backend 정보는 backend-prod-deployed dispatch로 기록해야 합니다.

PR 생성 전 확인

  • origin/main..origin/develop 차이를 확인했습니다.
  • 기존 develop -> main 오픈 PR이 없는 것을 확인했습니다.
  • release/version-management SSOT를 확인했습니다.
    • AGENTS.md
    • ops/agent-skills/zeroone-version-management.md
    • ops/release-record-shared-contract.md
    • ops/version-management.md
    • ops/release-intent.md

머지 전 운영 체크리스트

  • 이번 운영 반영 의도가 major가 맞는지 확인하세요.
  • 현재 운영 backend image/API가 이번 frontend release와 호환되는지 확인하세요.
  • PR의 필수 CI가 모두 통과했는지 확인하세요.
  • main/develop 간 충돌은 해결되어 현재 PR은 mergeable 상태입니다.

운영 릴리즈 기록 순서

  • backend-prod Jenkins 배포는 이미 완료됐습니다.
  • 해당 backend dispatch는 당시 main에 record workflow/script가 없어서 run 25997420948에서 실패했습니다.
  • 그래서 이 PR은 releases/prod-20260517-1941.yaml로 기존 frontend/backend baseline을 수동 기록하고, releases/prod-20260518-0210.yaml로 backend-prod-56 결과를 수동 기록합니다.
  • release: develop 변경사항 main 운영 반영 #613 main 머지 후 frontend prod workflow는 최신 record prod-20260518-0210을 상속해 frontend-only release:major 배포 record를 추가합니다.
  • 이후 backend 독립 배포부터는 record-backend-prod-release.ymlmain에 존재하므로 backend-prod-deployed dispatch가 자동 release record를 남기는 경로로 동작합니다.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 프로덕션 릴리스 레코드 생성·검증·기록 자동화 및 배포 워크플로 확장
    • 백엔드 배포와 프론트엔드 기록 연동(디스패치) 및 릴리스 의도 해석 자동화
    • 관리자용 클립보드 검사 페이지 추가
    • 에디터의 표·리치 클립보드 변환 및 이미지 붙여넣기/업로드 개선
  • Bug Fixes

    • 첨부 이미지 로컬 미리보기 정리로 메모리 누수 개선
    • 커리큘럼 배지 렌더링 테스트 안정화
  • Documentation

    • 버전 관리·배포 체크리스트·롤백·릴리스 계약 문서 대폭 정비/추가
  • Tests

    • 릴리스 레코드 검증 CI 체크 및 관련 검증 도구 추가

Review Change Stack

Hyeonjun0527 and others added 14 commits May 16, 2026 22:12
Why: 레슨 질문 모달에서 업로드 공개 URL을 imageKeys로 보내 백엔드가 잘못된 R2 key를 재서명하던 문제를 막기 위함.

Constraint: origin/develop 기반 클린브랜치에서 레슨 QnA 이미지 key 변환만 수정하고 API 계약은 변경하지 않음.

Tested: yarn eslint src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx --fix

Tested: yarn biome format --write src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx

Tested: yarn typecheck

Tested: git diff --check

Co-authored-by: OmX <omx@oh-my-codex.dev>
class E2E 테스트 추가 및 버그 수정
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: OmX <omx@oh-my-codex.dev>
…y-20260516

QnA 이미지 키 전송 오류 수정
Co-authored-by: OmX <omx@oh-my-codex.dev>
…-notion-paste

fix: 노션 복붙 본문 유실 방지
…teRequest 타입 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…화 범위 보완

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
프론트 레포를 운영 릴리즈 기록 SSOT로 두고 main prod 배포 및 backend-prod-deployed dispatch 기반 release record 자동 기록을 추가합니다.\n\n검증:\n- lint\n- prettier\n- typecheck\n- build\n- storybook\n- e2e\n- security\n- validate-release-records\n- Chromatic
Co-authored-by: OmX <omx@oh-my-codex.dev>
@vercel

vercel Bot commented May 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment May 17, 2026 5:47pm

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

프로덕션 릴리즈 의도 해석·검증·배포 워크플로우와 릴리즈 레코드 생성·검증을 추가하고, 에디터의 클립보드 이미지/표 처리·이미지 업로드 치환 및 preview 정리, 관리자용 클립보드 검사 페이지와 관련 문서를 도입합니다.

변경 사항

프로덕션 릴리즈 자동화 시스템

Layer / File(s) Summary
버전 관리 규칙 및 에이전트 인터페이스
.claude/rules/version-management-frontend.md, .claude/skills/zeroone-version-management/SKILL.md, .codex/skills/zeroone-version-management/SKILL.md, AGENTS.md, CLAUDE.md
프론트엔드 저장소 규칙은 ops/version-management.md를 SSOT로 명시하고 Claude/Codex 스킬 래퍼가 ops/agent-skills/zeroone-version-management.md를 우선 참조하도록 메타·지시를 추가합니다.
운영 정책 및 릴리즈 계약 문서
ops/version-management.md, ops/release-intent.md, ops/release-record-shared-contract.md, ops/backend-release-dispatch.md, ops/deploy-checklist.md, ops/rollback.md, ops/agent-skills/zeroone-version-management.md
릴리즈 ID/이미지 태그 형식, 불변 태그 정책, 배포 순서, 릴리즈 레코드 스키마, 백엔드→프론트엔드 dispatch 계약, 부트스트랩·롤백 규칙을 문서화합니다.
프로덕션 배포 워크플로우 및 릴리즈 레코드 생성
.github/workflows/deploy-prod.yml, scripts/release/resolve-prod-release-intent.mjs, scripts/release/generate-prod-release-record.mjs, package.json
메인 푸시/수동 트리거에서 릴리즈 의도를 해석하고 원격 백엔드 메타를 조회·검증한 뒤 프론트엔드 빌드/배포 및 릴리즈 레코드 생성·검증·main 커밋을 수행하도록 워크플로우와 스크립트를 확장합니다.
백엔드 릴리즈 레코드 자동화
.github/workflows/record-backend-prod-release.yml, scripts/release/generate-backend-prod-release-record.mjs
백엔드 배포 성공 이벤트를 받아 기존 FE 상태와 통합한 릴리즈 레코드를 생성하고 드라이런 아티팩트 업로드 또는 실제 커밋을 지원합니다.
릴리즈 레코드 검증 및 체크
.github/workflows/release-record-check.yml, scripts/release/validate-release-record.mjs
PR에서 릴리스 레코드 관련 경로 변경 시 prod-*.yaml 파일을 스캔·검증하고 스키마·교차필드·중복 deploy id를 검사하는 검증 파이프라인을 추가합니다.

클립보드 이미지 및 첨부 파일 관리

Layer / File(s) Summary
클립보드 이미지 판별 및 표 처리
src/components/common/ui/editor/clipboard-utils.ts, src/components/common/ui/editor/markdown-editor.tsx, src/components/common/ui/editor/markdown-table-utils.ts
클립보드가 이미지 전용인지 판별하는 isClipboardImageOnly와 HTML 표 전용 판별 isHtmlTableOnlyPaste를 추가하고, 붙여넣기 분기 로직을 정교화합니다.
리치 클립보드 정규화 및 Notion 유틸
src/components/common/ui/editor/rich-clipboard-normalizer.ts, src/components/common/ui/editor/notion-clipboard-utils.ts
Rich clipboard HTML에서 table을 마크다운으로 변환하고 Notion 첨부 이미지를 placeholder로 치환하는 정규화 유틸과 Notion 페이로드 추출 유틸을 추가합니다.
에디터 이미지 업로드 및 소스 치환 훅
src/components/common/ui/editor/use-image-upload.ts
에디터 내 이미지 src를 업로드된 URL로 교체하는 ImageSourceFileReplacement 타입과 handleImageSourceFileReplacements를 추가합니다.
QnA 제출 모달 이미지 업로드 및 정리
src/app/(class-lesson)/.../lesson-qna-submission-modal.tsx
업로드 응답에서 key를 pathname 기준으로 추출하고, 로컬 preview Object URL을 생성·revoke하는 clearImagePreviews로 컴포넌트 언마운트 및 모달 닫힘 시 정리합니다.
관리자 클립보드 검사 페이지
src/app/(admin)/admin/clipboard-inspector/page.tsx
붙여넣기 스냅샷을 캡처·요약하고 공유용 JSON 복사 버튼을 제공하는 Admin Clipboard Inspector 페이지를 추가합니다.
관리자 마크다운 제한 안내
src/components/admin/courses/admin-course-markdown-editor.tsx, src/features/admin/course-management/model/admin-course-markdown.ts
관리자 편집기에서 콘텐츠 바이트/대략 한글 자수와 이미지 개수/파일 크기 한도를 UI에 표시하도록 변경합니다.
커리큘럼 드로어 배지 테스트 정확화
e2e/class/curriculum-drawer.spec.ts
잠금/잠금 해제 배지 테스트의 locator를 특정 링크 내부 이미지 접근 가능한 이름으로 변경해 검증을 명확히 합니다.

시퀀스 다이어그램

sequenceDiagram
  participant GH as GitHub Push
  participant Intent as resolve-prod-release-intent.mjs
  participant Deploy as deploy-prod.yml
  participant Backend as Remote Backend (docker inspect)
  participant Record as generate-prod-release-record.mjs
  participant Validate as validate-release-record.mjs
  participant Commit as Git Commit/Push

  GH->>Intent: PR 라벨/바디에서 의도 추출
  Intent->>Intent: 버전 계산 및 출력
  Deploy->>Backend: SSH docker inspect 메타 조회
  Deploy->>Deploy: 의도·백엔드 메타 검증
  Deploy->>Deploy: 프론트엔드 이미지 pull & compose up
  Deploy->>Record: 빌드 메타 + 백엔드 상태로 레코드 생성 호출
  Record->>Validate: 생성된 YAML 검증
  Deploy->>Commit: 검증된 레코드 main 커밋 & push
Loading
sequenceDiagram
  participant BE as Backend (배포 완료)
  participant Dispatch as repository_dispatch
  participant FEWorkflow as record-backend-prod-release.yml
  participant GenBE as generate-backend-prod-release-record.mjs
  participant ValidateRec as validate-release-record.mjs
  participant Git as Git Commit/Push

  BE->>Dispatch: backend-prod-deployed 이벤트 (client_payload)
  Dispatch->>FEWorkflow: 워크플로우 트리거
  FEWorkflow->>GenBE: payload 전달
  GenBE->>GenBE: 최신 FE 상태 조회 & 중복 backend_deploy_id 검사
  GenBE->>ValidateRec: 생성 레코드 검증
  alt dry_run=true
    FEWorkflow->>FEWorkflow: 아티팩트 업로드
  else
    FEWorkflow->>Git: main 브랜치에 커밋 & push
  end
Loading

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs:

"🐰 릴리즈의 노래"
릴리즈는 기록되어 진실로 남아,
태그는 불변, 레코드는 안전히 쌓여,
자동으로 배포되어 로그는 반짝이고,
클립보드의 사진도 깨끗이 정리하며,
토끼는 점프하며 축하 노래를 부른다.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 개발 분기의 변경사항을 운영 분기(main)로 반영하는 것을 명확히 나타내며, 주요 목적과 일치합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hyeonjun0527 Hyeonjun0527 self-assigned this May 17, 2026
@Hyeonjun0527 Hyeonjun0527 changed the title release: develop to main production promotion release: develop 변경사항 main 운영 반영 May 17, 2026
@Hyeonjun0527 Hyeonjun0527 added the release:major Major production release label May 17, 2026
PR #613의 develop -> main 병합 전 충돌을 해소하기 위해 origin/main 변경사항을 develop에 반영합니다.
커리큘럼 드로어 배지 E2E는 develop의 scoped locator 방식을 유지해 Playwright strict mode 중복 매칭을 방지합니다.

Constraint: PR #613은 develop에서 main으로 올리는 운영 반영 PR이므로 develop에 main 최신 변경사항을 먼저 흡수한다
Rejected: page-global or locator 복원 | strict mode에서 배지 컨테이너와 아이콘이 동시에 매칭될 수 있음
Confidence: high
Scope-risk: moderate
Tested: yarn eslint e2e/class/curriculum-drawer.spec.ts
Tested: git diff --check
Related: PR #613
Co-authored-by: OmX <omx@oh-my-codex.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy-prod.yml:
- Around line 218-224: The workflow allows BACKEND_COMMIT and BACKEND_VERSION to
silently fall back to "unknown" which lets bad release intents proceed; update
the validation after computing BACKEND_COMMIT and BACKEND_VERSION to treat empty
or "unknown" as fatal: check the variables BACKEND_COMMIT and BACKEND_VERSION
(and similarly the second block's
INSPECTED_BACKEND_COMMIT/INSPECTED_BACKEND_VERSION fallback logic) and if either
is empty or equals "unknown" emit a clear "::error::" message and exit 1
(similar to the existing BACKEND_IMAGE check) so releases fail fast when
commit/version metadata is missing.

In `@ops/release-record-shared-contract.md`:
- Line 292: This file uses the token `bootstrap:approved` which conflicts with
the other docs that use `bootstrap: approved`; update every occurrence of
`bootstrap:approved` in this document to `bootstrap: approved` (with the space)
and scan for the alternate form elsewhere (e.g., the version-management and
release-intent docs) to ensure all release/approval token strings are normalized
to `bootstrap: approved`.

In `@scripts/release/generate-prod-release-record.mjs`:
- Around line 113-115: The code reads metadata.release_intent from
BACKEND_RELEASE_INTENT (via the backendReleaseIntent variable) which can be
empty for FE-only releases; change it to use the RELEASE_TYPE env var instead:
add or use a variable from getEnv('RELEASE_TYPE') and assign that value to
metadata.release_intent wherever backendReleaseIntent is currently used
(including the other occurrence around the block referenced by lines 185-186).
Ensure you keep getEnv usage consistent and replace references to
backendReleaseIntent with the RELEASE_TYPE-sourced variable so FE-only releases
record the correct release intent.

In `@scripts/release/resolve-prod-release-intent.mjs`:
- Around line 19-36: The parseField function currently only extracts scalar
values, so YAML array entries like database.migration_files are skipped; update
parseField (in resolve-prod-release-intent.mjs) to detect when the matched key
is followed by indented list items (lines matching /^\s*-\s*(.*)$/ at a greater
indent) and collect those list values until the indent drops, then return them
(for compatibility either as a comma-separated string or as an array depending
on the caller) instead of an empty string; ensure you reference parseField and
the path parameter when locating the change and preserve existing scalar
behavior for non-list fields.

In
`@src/app/`(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx:
- Around line 90-99: The upload flow (setIsUploadingImage → await
uploadCommunityMarkdownImage → setImages) lacks a guard that the modal session
is still active, so a finished upload can resurrect images after the modal was
cleared; to fix, introduce and check a local session/active flag (or compare a
modalSessionId captured before awaiting) right after the await returned from
uploadCommunityMarkdownImage and before calling setImages or creating/keeping
the previewUrl, and if the modal is no longer active skip updating state (and
revoke the created preview URL if you created one but won’t use it); update
functions referenced: setIsUploadingImage, uploadCommunityMarkdownImage,
setImages, and the modal clear logic around where state is emptied to use the
same session/active marker.

In `@src/components/common/ui/editor/clipboard-utils.ts`:
- Around line 131-158: The isClipboardImageOnly function currently checks
text/plain first which can mask a HTML-only <img> clipboard entry; reorder the
logic so pastedHtml is inspected before the text/plain branch: call
removeHtmlWrappers(pastedHtml) and if it yields empty (or if pastedHtml contains
only image markup) return true or defer to
extractClipboardImageFiles(clipboardData) as appropriate, and only then proceed
to evaluate pastedText using the existing IMAGE_URL_PART, DATA_IMAGE_TEXT_PART
replacements and isAllowedUrl checks; this preserves HTML image paste while
keeping the existing text-based image URL heuristics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 318d1248-77d9-4f45-ab54-5e0e621a93be

📥 Commits

Reviewing files that changed from the base of the PR and between 8c2ce22 and f3a8189.

📒 Files selected for processing (25)
  • .claude/rules/version-management-frontend.md
  • .claude/skills/zeroone-version-management/SKILL.md
  • .codex/skills/zeroone-version-management/SKILL.md
  • .github/workflows/deploy-prod.yml
  • .github/workflows/record-backend-prod-release.yml
  • .github/workflows/release-record-check.yml
  • AGENTS.md
  • CLAUDE.md
  • e2e/class/curriculum-drawer.spec.ts
  • ops/agent-skills/zeroone-version-management.md
  • ops/backend-release-dispatch.md
  • ops/deploy-checklist.md
  • ops/release-intent.md
  • ops/release-record-shared-contract.md
  • ops/rollback.md
  • ops/version-management.md
  • package.json
  • releases/.gitkeep
  • scripts/release/generate-backend-prod-release-record.mjs
  • scripts/release/generate-prod-release-record.mjs
  • scripts/release/resolve-prod-release-intent.mjs
  • scripts/release/validate-release-record.mjs
  • src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx
  • src/components/common/ui/editor/clipboard-utils.ts
  • src/components/common/ui/editor/markdown-editor.tsx

Comment on lines +218 to +224
BACKEND_COMMIT="${RELEASE_BACKEND_COMMIT:-${INSPECTED_BACKEND_COMMIT:-unknown}}"
BACKEND_VERSION="${RELEASE_BACKEND_VERSION:-${INSPECTED_BACKEND_VERSION:-unknown}}"

if [ -z "$BACKEND_IMAGE" ]; then
echo "::error::Backend image metadata is required. Add backend_image/backend_commit/backend_version to the release intent body for the first recorded production release."
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

백엔드 커밋/버전이 unknown으로 통과될 수 있습니다

Line 218-219에서 unknown fallback을 허용하고, Line 221-224는 이미지만 검증합니다. 이 상태로 진행되면 릴리즈 레코드 품질이 깨지거나, 배포 이후 검증 단계에서 늦게 실패할 수 있습니다. BACKEND_COMMIT/BACKEND_VERSION도 여기서 즉시 실패 처리하는 게 안전합니다.

제안 수정안
           BACKEND_IMAGE="${RELEASE_BACKEND_IMAGE:-${INSPECTED_BACKEND_IMAGE:-}}"
           BACKEND_COMMIT="${RELEASE_BACKEND_COMMIT:-${INSPECTED_BACKEND_COMMIT:-unknown}}"
           BACKEND_VERSION="${RELEASE_BACKEND_VERSION:-${INSPECTED_BACKEND_VERSION:-unknown}}"

           if [ -z "$BACKEND_IMAGE" ]; then
             echo "::error::Backend image metadata is required. Add backend_image/backend_commit/backend_version to the release intent body for the first recorded production release."
             exit 1
           fi
+          if [ -z "$BACKEND_COMMIT" ] || [ "$BACKEND_COMMIT" = "unknown" ]; then
+            echo "::error::Backend commit metadata is required and must not be unknown."
+            exit 1
+          fi
+          if [ -z "$BACKEND_VERSION" ] || [ "$BACKEND_VERSION" = "unknown" ]; then
+            echo "::error::Backend version metadata is required and must not be unknown."
+            exit 1
+          fi

Also applies to: 230-245

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-prod.yml around lines 218 - 224, The workflow
allows BACKEND_COMMIT and BACKEND_VERSION to silently fall back to "unknown"
which lets bad release intents proceed; update the validation after computing
BACKEND_COMMIT and BACKEND_VERSION to treat empty or "unknown" as fatal: check
the variables BACKEND_COMMIT and BACKEND_VERSION (and similarly the second
block's INSPECTED_BACKEND_COMMIT/INSPECTED_BACKEND_VERSION fallback logic) and
if either is empty or equals "unknown" emit a clear "::error::" message and exit
1 (similar to the existing BACKEND_IMAGE check) so releases fail fast when
commit/version metadata is missing.


## 8. Bootstrap

If current production is not yet on canonical image tags, backend may use bootstrap mode only with explicit `bootstrap:approved` approval on the backend side.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

bootstrap 승인 토큰 표기를 다른 운영 문서와 일치시켜 주세요.

여기서는 bootstrap:approved를 사용하지만, ops/version-management.mdops/release-intent.mdbootstrap: approved를 사용합니다. 배포 승인 절차 문서가 이중 표기를 가지면 운영/자동화 해석이 갈려 릴리즈 기록 생성이 실패할 수 있습니다. 한 가지 표기로 통일해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ops/release-record-shared-contract.md` at line 292, This file uses the token
`bootstrap:approved` which conflicts with the other docs that use `bootstrap:
approved`; update every occurrence of `bootstrap:approved` in this document to
`bootstrap: approved` (with the space) and scan for the alternate form elsewhere
(e.g., the version-management and release-intent docs) to ensure all
release/approval token strings are normalized to `bootstrap: approved`.

Comment thread scripts/release/generate-prod-release-record.mjs Outdated
Comment thread scripts/release/resolve-prod-release-intent.mjs
Comment on lines 90 to 99
setIsUploadingImage(true);
try {
const publicUrl = await uploadCommunityMarkdownImage(file);
const key = new URL(publicUrl).pathname.slice(1);
const previewUrl = URL.createObjectURL(file);
setImages((prev) => [...prev, { previewUrl, key: publicUrl }]);
setImages((prev) => [...prev, { previewUrl, key }]);
} catch {
showToast('이미지 업로드에 실패했습니다.', 'error');
} finally {
setIsUploadingImage(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

모달을 닫은 뒤 완료된 업로드가 첨부 이미지를 다시 살릴 수 있습니다.

Line 92-95의 await 뒤에는 현재 모달 세션을 확인하는 가드가 없습니다. 사용자가 업로드 중에 닫으면 Line 77-80에서 상태를 비운 뒤에도, 나중에 완료된 업로드가 setImages로 새 blob URL을 다시 넣어 버립니다. 그러면 다음에 모달을 열었을 때 이전 세션의 첨부가 되살아나고, 그 preview URL도 다음 정리 시점까지 해제되지 않습니다.

🔧 수정 예시
   const fileInputRef = useRef<HTMLInputElement>(null);
   const imageListRef = useRef<AttachedImage[]>([]);
+  const openRef = useRef(open);
+  const modalSessionRef = useRef(0);

@@
   useEffect(() => {
+    openRef.current = open;
     if (!open) {
+      modalSessionRef.current += 1;
       clearImagePreviews();
     }
   }, [open]);

   async function handleImageAdd(file: File) {
+    const modalSession = modalSessionRef.current;
     if (images.length >= 10) {
       showToast('최대 10장까지 첨부 가능합니다.', 'error');
       return;
     }
     setIsUploadingImage(true);
     try {
       const publicUrl = await uploadCommunityMarkdownImage(file);
+      if (!openRef.current || modalSession !== modalSessionRef.current) {
+        return;
+      }
       const key = new URL(publicUrl).pathname.slice(1);
       const previewUrl = URL.createObjectURL(file);
       setImages((prev) => [...prev, { previewUrl, key }]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/`(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx
around lines 90 - 99, The upload flow (setIsUploadingImage → await
uploadCommunityMarkdownImage → setImages) lacks a guard that the modal session
is still active, so a finished upload can resurrect images after the modal was
cleared; to fix, introduce and check a local session/active flag (or compare a
modalSessionId captured before awaiting) right after the await returned from
uploadCommunityMarkdownImage and before calling setImages or creating/keeping
the previewUrl, and if the modal is no longer active skip updating state (and
revoke the created preview URL if you created one but won’t use it); update
functions referenced: setIsUploadingImage, uploadCommunityMarkdownImage,
setImages, and the modal clear logic around where state is emptied to use the
same session/active marker.

Comment on lines +131 to +158
export const isClipboardImageOnly = (clipboardData: DataTransfer) => {
const pastedText = clipboardData.getData('text/plain').trim();
const pastedHtml = clipboardData.getData('text/html').trim();

if (pastedText) {
const textWithoutImageRefs = pastedText
.replace(IMAGE_URL_PART, '')
.replace(DATA_IMAGE_TEXT_PART, '')
.replace(/\s+/g, '')
.trim();

if (textWithoutImageRefs) {
return false;
}

return !!isAllowedUrl(pastedText, ['image', 'data-image']);
}

if (!pastedHtml) {
return extractClipboardImageFiles(clipboardData).length > 0;
}

const htmlText = removeHtmlWrappers(pastedHtml);
if (htmlText) {
return false;
}

return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

text/plain 우선 분기 때문에 HTML 이미지 붙여넣기가 막힐 수 있습니다.

Line 135-147이 먼저 false를 반환하면, 클립보드에 <img>만 들어 있는 text/html이 있어도 HTML 분기를 보지 못합니다. 그래서 브라우저가 text/plain으로 페이지 URL이나 확장자 없는 CDN URL을 같이 넣는 경우, 이제 이미지 붙여넣기가 일반 텍스트 붙여넣기로 퇴행할 수 있습니다. HTML에 실제 이미지가 있고 래퍼 제거 후 잔여 텍스트가 없으면 그쪽을 먼저 우선해야 합니다.

🔧 수정 예시
 export const isClipboardImageOnly = (clipboardData: DataTransfer) => {
-  const pastedText = clipboardData.getData('text/plain').trim();
   const pastedHtml = clipboardData.getData('text/html').trim();
 
-  if (pastedText) {
+  if (pastedHtml) {
+    const htmlText = removeHtmlWrappers(pastedHtml);
+    if (htmlText) {
+      return false;
+    }
+
+    if (extractHtmlImageUrls(pastedHtml).length > 0) {
+      return true;
+    }
+  }
+
+  const pastedText = clipboardData.getData('text/plain').trim();
+  if (pastedText) {
     const textWithoutImageRefs = pastedText
       .replace(IMAGE_URL_PART, '')
       .replace(DATA_IMAGE_TEXT_PART, '')
       .replace(/\s+/g, '')
       .trim();
@@
-  if (!pastedHtml) {
-    return extractClipboardImageFiles(clipboardData).length > 0;
-  }
-
-  const htmlText = removeHtmlWrappers(pastedHtml);
-  if (htmlText) {
-    return false;
-  }
-
-  return true;
+  return extractClipboardImageFiles(clipboardData).length > 0;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/ui/editor/clipboard-utils.ts` around lines 131 - 158,
The isClipboardImageOnly function currently checks text/plain first which can
mask a HTML-only <img> clipboard entry; reorder the logic so pastedHtml is
inspected before the text/plain branch: call removeHtmlWrappers(pastedHtml) and
if it yields empty (or if pastedHtml contains only image markup) return true or
defer to extractClipboardImageFiles(clipboardData) as appropriate, and only then
proceed to evaluate pastedText using the existing IMAGE_URL_PART,
DATA_IMAGE_TEXT_PART replacements and isAllowedUrl checks; this preserves HTML
image paste while keeping the existing text-based image URL heuristics.

Notion 등 복합 HTML 붙여넣기에서 텍스트/표/이미지 순서를 보존하고, 실제 클립보드 payload 확인을 위한 어드민 진단 페이지를 추가합니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/common/ui/editor/markdown-editor.tsx`:
- Around line 397-414: The current paste flow lets the browser insert mixed HTML
(including <img>) before replaceMixedClipboardImagesAfterDefaultPaste runs,
allowing disallowed or too-many images to remain; before calling
replaceMixedClipboardImagesAfterDefaultPaste (and before allowing default paste
when markdownTable is undefined), pre-scan pastedHtml for image nodes and
validate against the editor's imageConfig (e.g., in AdminCourseMarkdownEditor)
and the uploader limits: if any image is disallowed or would exceed limits, call
event.preventDefault() and either block the paste entirely or sanitize the HTML
by removing invalid/overflowing <img> nodes then insert the cleaned content
programmatically via insertMarkdownTable/Editor API; otherwise allow the default
paste and continue to call replaceMixedClipboardImagesAfterDefaultPaste for
upload/replace handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28f1cd7d-f862-4d0d-91b2-e30313ea37ab

📥 Commits

Reviewing files that changed from the base of the PR and between 8329b31 and 0f72ddd.

📒 Files selected for processing (6)
  • src/app/(admin)/admin/clipboard-inspector/page.tsx
  • src/components/admin/courses/admin-course-markdown-editor.tsx
  • src/components/common/ui/editor/markdown-editor.tsx
  • src/components/common/ui/editor/markdown-table-utils.ts
  • src/components/common/ui/editor/use-image-upload.ts
  • src/features/admin/course-management/model/admin-course-markdown.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/common/ui/editor/markdown-table-utils.ts

Comment thread src/components/common/ui/editor/markdown-editor.tsx
공용 MarkdownEditor의 복합 붙여넣기에서 표를 마크다운 표로 보존하고 Notion attachment 이미지는 위치 안내 문구로 남깁니다.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: OmX <omx@oh-my-codex.dev>
@Hyeonjun0527 Hyeonjun0527 merged commit d2efde0 into main May 17, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:major Major production release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants