Skip to content

20260515 fe 369 기능개선fe 글 쓰기 페이지 개선#370

Merged
discipline24 merged 6 commits into
mainfrom
20260515-FE-369-기능개선fe-글-쓰기-페이지-개선
May 19, 2026

Hidden character warning

The head ref may contain hidden characters: "20260515-FE-369-\uae30\ub2a5\uac1c\uc120fe-\uae00-\uc4f0\uae30-\ud398\uc774\uc9c0-\uac1c\uc120"
Merged

20260515 fe 369 기능개선fe 글 쓰기 페이지 개선#370
discipline24 merged 6 commits into
mainfrom
20260515-FE-369-기능개선fe-글-쓰기-페이지-개선

Conversation

@sangkyu39
Copy link
Copy Markdown
Contributor

@sangkyu39 sangkyu39 commented May 16, 2026

글 쓰기 페이지 개선
툴바 디자인 적용
게시글 보기 시 적용된 디자인을 볼 수 있도록 적용
글 수정시에도 툴바 적용
드래그앤드롭 적용 등

Summary by CodeRabbit

  • 새로운 기능

    • 리치 텍스트 에디터에 파일/비디오 업로드 및 첨부 위임 기능 추가
    • JSON 기반 리치 텍스트를 안전한 HTML로 변환하는 새 변환기 추가
  • 개선 사항

    • 에디터 툴바 및 색상/하이라이트 UI·스타일 전면 재설계
    • 본문 렌더링 우선순위: JSON 콘텐츠 우선 처리 및 하이라이트 동작 조정
    • 첨부 정규화 및 인라인 이미지 자동 필터링 로직 개선
  • 버그 수정

    • 첨부 업로드 실패 시 저장 중단 등 오류 처리 강화
    • 편집 화면에서 중복 테두리·레이아웃 문제 수정

Review Change Stack

Copilot AI review requested due to automatic review settings May 16, 2026 10:42
@sangkyu39 sangkyu39 requested a review from discipline24 as a code owner May 16, 2026 10:42
@sangkyu39 sangkyu39 linked an issue May 16, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@sangkyu39 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 503b6428-3fae-4abe-b31a-ccc0b0a6429e

📥 Commits

Reviewing files that changed from the base of the PR and between 800de10 and 756cb8a.

📒 Files selected for processing (1)
  • frontend/src/pages/BoardWrite.jsx

Walkthrough

RichTextEditor에 이미지/파일/비디오 업로드·드롭 처리와 미디어 툴바, 텍스트 배경색/선택 갱신을 추가하고, TipTap JSON→안전한 HTML 변환 유틸(jsonToHtml)을 도입했습니다. 게시글 첨부는 정규화·인라인 중복 필터링 후 업로드된 식별자를 합쳐 전송합니다.

Changes

RichTextEditor 미디어 업로드 및 도구모음 확장

Layer / File(s) Summary
RichTextEditor 아이콘 imports 및 props 확장
frontend/src/components/Board/RichTextEditor.jsx
미디어 SVG 임포트, onUploadFile/onUploadVideo/onAttachFiles props 및 파일·비디오 input refs와 selectionTick 상태 추가.
드래그드롭·업로드 처리 흐름
frontend/src/components/Board/RichTextEditor.jsx
handleDrop를 이미지/비이미지/비디오로 분류하여 이미지 삽입은 에디터 내부 처리, 비이미지는 onAttachFiles로 위임하거나 onUploadFile 후 링크 삽입하도록 구현.
파일/비디오 input 핸들러 및 유틸
frontend/src/components/Board/RichTextEditor.jsx
hidden input change 핸들러, HTML 이스케이프·insertFileLink 유틸 및 입력값 초기화 로직을 추가.
텍스트 배경색 및 selection 업데이트
frontend/src/components/Board/RichTextEditor.jsx
applyTextBackground로 Highlight 우선 적용·폴백 처리, TipTap selectionUpdate 이벤트로 selectionTick 갱신.
미디어·포맷 툴바 UI
frontend/src/components/Board/RichTextEditor.jsx
mediaToolbar(이미지/비디오/파일 버튼·숨김 input)과 텍스트 색상/배경색 입력, 정렬 아이콘 버튼을 추가·재구성.
도구모음 레이아웃 및 스타일 재설계
frontend/src/components/Board/RichTextEditor.module.css
toolbar를 column으로 전환, 버튼 36x36 고정, activeButton 스타일 및 아이콘/컬러 관련 CSS를 다수 추가/수정.

TipTap JSON → 안전한 HTML 변환 유틸

Layer / File(s) Summary
sanitize/escape 헬퍼 및 상수
frontend/src/utils/richTextHtml.js
허용 프로토콜/색상/폰트 화이트리스트, CSS 길이 패턴, escapeHtmlText/Attribute, sanitizeHref 및 검증 헬퍼들을 추가.
jsonToHtml 재귀 렌더러
frontend/src/utils/richTextHtml.js
text/marks, paragraph/heading/image/file/video 노드를 안전하게 HTML로 변환하는 jsonToHtml 구현과 폴백 처리.
콘텐츠 렌더링 우선순위 및 Highlight 설정
frontend/src/components/Board/PostDetail/PostView.jsx, frontend/src/components/Board/PostDetail/PostHtmlView.jsx
PostView에서 contentJson이 존재하면 TipTap JSON 우선 사용으로 단순화하고, PostHtmlView에서 Highlight.configure({ multicolor: true }) 적용.

첨부 정규화·인라인 필터링·업로드 흐름

Layer / File(s) Summary
첨부 정규화 및 인라인 URL 추출
frontend/src/pages/PostDetail.jsx
normalizeAttachments로 다양한 응답 키를 공통 포맷으로 정규화하고 extractInlineUrlsFromContent로 본문 이미지/링크 URL을 수집.
게시글 로드·수정 시 첨부 필터링
frontend/src/pages/PostDetail.jsx
fetch/refresh 및 handleEditStart에서 정규화·인라인 필터링 결과로 post와 편집 상태(editFiles 등)를 구성.
편집 저장: 업로드 선행 및 attachmentIds 전송
frontend/src/pages/PostDetail.jsx
편집 저장 전 newFiles 업로드로 식별자 수집(실패 시 저장 중단), 기존 식별자와 신규 식별자를 병합한 attachmentIds 전송.
BoardWrite 첨부 업로드/관리
frontend/src/pages/BoardWrite.jsx
handleAttachFiles로 업로드 수행·attachmentFiles 누적, 업로드 응답 검증 및 getAttachmentIdentifier/handleRemoveAttachment로 삭제 처리. RichTextEditor에 onUploadFile/onUploadVideo/onAttachFiles 연결.

PostEditForm 및 페이지 스타일 조정

Layer / File(s) Summary
PostEditForm 파일 입력 제거 및 드롭 위임
frontend/src/components/Board/PostDetail/PostEditForm.jsx
useRef/FolderIcon/숨김 file input 및 파일 버튼 핸들러 제거, handleDrop은 isDragOver만 해제해 파일 처리를 에디터로 위임; 제목 입력을 editTitleBox로 래핑.
BoardWrite 스타일 업데이트
frontend/src/pages/BoardWrite.module.css
페이지 배경을 단색으로 변경하고 헤더/타이틀/첨부 리스트 관련 CSS 클래스 추가 및 레이아웃 조정.
PostDetail 편집 영역 스타일 오버라이드
frontend/src/pages/PostDetail.module.css
editTitleBox 추가, editContentContainer의 border 제거 및 RichTextEditor 중첩 셸/바디 오버라이드로 테두리/배경 이중 표시 제거.

Sequence Diagram

sequenceDiagram
  participant User
  participant Editor as RichTextEditor
  participant onUploadFile as onUploadFile 콜백
  participant onAttachFiles as onAttachFiles 콜백
  User->>Editor: 파일 드롭/파일 선택
  Editor->>Editor: 파일 타입 분류 (이미지 / 비이미지 / 비디오)
  alt 이미지
    Editor->>Editor: 이미지 삽입
  else 비이미지 & onAttachFiles 있음
    Editor->>onAttachFiles: onAttachFiles(files)
    onAttachFiles-->>Editor: 메타데이터 반환
    Editor->>Editor: 에디터에 메타데이터 반영(링크 등)
  else 비이미지 & onAttachFiles 없음
    Editor->>onUploadFile: 업로드 요청
    onUploadFile-->>Editor: 업로드 결과(URL/식별자)
    Editor->>Editor: 링크(escaped) 삽입
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • SISC-IT/sisc-web#368: PostView/PostHtmlView의 TipTap JSON 렌더링 및 RichTextEditor의 미디어 처리 경로 변경과 중첩된 관련성이 있습니다.
  • SISC-IT/sisc-web#161: PostDetail/PostEditForm 관련 구조 변경 및 에디터 통합 작업과 코드 레벨 연관이 있습니다.

Suggested reviewers

  • discipline24

Poem

🐰 퐁당 파일이 드롭되면 툴바가 춤추네
색과 하이라이트는 반짝이며 빛나고
링크는 안전히 감싸이고 첨부는 정돈돼
에디터에선 글과 미디어가 손을 잡아
축하해, 이제 작성이 더 부드러워졌네 ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 글쓰기 페이지 개선이라는 실제 변경 내용과 부분적으로 관련이 있으나, 툴바 디자인 적용, 드래그앤드롭 기능, 게시글 조회 시 디자인 일관성 등 주요 변경 사항을 구체적으로 담지 못하고 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 20260515-FE-369-기능개선fe-글-쓰기-페이지-개선

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
frontend/src/pages/BoardWrite.module.css (2)

88-90: ⚡ Quick win

.boardSelectCol 중복 선언을 하나로 정리해주세요.

현재 같은 선택자가 두 번 선언되어(300px260px) 실제 적용값이 캐스케이드 순서에 의존합니다. 의도값 하나만 남기면 레이아웃 회귀를 줄일 수 있습니다.

제안 diff
 .boardSelectCol {
-  flex: 0 0 300px;
+  flex: 0 0 260px;
 }
@@
-.boardSelectCol {
-  flex: 0 0 260px;
-}

Also applies to: 304-306

🤖 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 `@frontend/src/pages/BoardWrite.module.css` around lines 88 - 90, Duplicate
.boardSelectCol rules exist with conflicting widths (300px vs 260px); remove the
duplicate and consolidate into a single .boardSelectCol declaration (use the
intended value — keep 260px if that is the desired layout) so the style no
longer depends on cascade order; update the one in BoardWrite.module.css (and
remove the other occurrence around the second reported region) to a single
definition.

203-215: ⚡ Quick win

삭제 버튼에 :focus-visible 스타일을 추가해주세요.

키보드 탐색 시 포커스 위치가 명확하지 않아 접근성이 떨어집니다. hover와 별도로 focus-visible 상태를 정의하는 것이 좋습니다.

제안 diff
 .attachmentRemove:hover {
   color: `#ef4444`;
 }
+
+.attachmentRemove:focus-visible {
+  outline: 2px solid `#2b6ef3`;
+  outline-offset: 2px;
+  color: `#ef4444`;
+}
🤖 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 `@frontend/src/pages/BoardWrite.module.css` around lines 203 - 215, Add a
visible keyboard-focus style for the .attachmentRemove button by defining a
.attachmentRemove:focus-visible rule (separate from :hover) that sets an
accessible focus indicator (for example a clear outline, box-shadow ring, or
border with sufficient contrast and preserved border-radius) and ensures
outline-offset or padding so the indicator doesn't overlap content; update
.attachmentRemove class if needed to avoid conflicting outlines and ensure the
focus style matches the visual design and hover color semantics.
frontend/src/components/Board/RichTextEditor.jsx (1)

630-643: 💤 Low value

도달 불가능한 코드 (Dead code)

else if (onAttachFiles) 분기는 실행되지 않습니다. onAttachFiles가 truthy이면 위의 554번 라인 조건에서 처리 후 589번 라인에서 반환되므로, 이 코드는 onAttachFiles가 falsy일 때만 도달합니다.

이 블록을 제거하여 코드 가독성을 개선하세요.

♻️ 제안된 수정
                  if (onUploadFile) {
                    for (const file of otherFiles) {
                      try {
                        const uploaded = await onUploadFile(file);
                        const normalizedUrl = toAbsoluteImageUrl(uploaded?.url || uploaded?.fileUrl || uploaded?.downloadUrl || uploaded?.savedUrl || file.name);
                        insertFileLink(normalizedUrl || '', uploaded?.originalFilename || file.name);
                      } catch (err) {
                        console.error('파일 드롭 업로드 실패 (onUploadFile):', err);
                      }
                    }
-                  } else if (onAttachFiles) {
-                    // fallback: onAttachFiles may upload and return uploaded metadata array
-                    try {
-                      const uploadedArr = await onAttachFiles(otherFiles);
-                      if (Array.isArray(uploadedArr)) {
-                        for (const uploaded of uploadedArr) {
-                          const normalizedUrl = toAbsoluteImageUrl(uploaded?.url || uploaded?.fileUrl || uploaded?.downloadUrl || uploaded?.savedUrl || uploaded?.url);
-                          insertFileLink(normalizedUrl || '', uploaded?.originalFilename || uploaded?.name || '첨부파일');
-                        }
-                      }
-                    } catch (err) {
-                      console.error('파일 드롭 업로드 실패 (onAttachFiles):', err);
-                    }
                  }
🤖 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 `@frontend/src/components/Board/RichTextEditor.jsx` around lines 630 - 643, The
else-if branch checking onAttachFiles inside the file-drop handler is dead code
because onAttachFiles is already handled earlier (and the function returns), so
remove the entire "else if (onAttachFiles) { ... }" block that attempts to call
onAttachFiles, normalize URLs with toAbsoluteImageUrl, and call insertFileLink
for uploadedArr; ensure any necessary error handling or variables (otherFiles,
uploadedArr) are not referenced elsewhere after removal.
frontend/src/components/Board/RichTextEditor.module.css (1)

282-286: 💤 Low value

.imageResizeHandleSE 클래스 중복 선언

동일한 클래스 .imageResizeHandleSE가 두 번 선언되어 있습니다 (라인 282-286과 306-310). 두 번째 선언(right: -7px; bottom: -7px)이 첫 번째 선언(right: -9px; bottom: -9px)을 덮어씁니다.

의도된 값만 남기고 중복 선언을 제거하세요.

Also applies to: 306-310

🤖 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 `@frontend/src/components/Board/RichTextEditor.module.css` around lines 282 -
286, The .imageResizeHandleSE class is declared twice (duplicate declarations
where the second overrides the first); remove the redundant declaration and
consolidate into a single .imageResizeHandleSE rule, preserving the intended
right/bottom values (pick and keep either right: -7px; bottom: -7px or right:
-9px; bottom: -9px so there is only one authoritative declaration) and keep
cursor: nwse-resize in that single rule.
🤖 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 `@frontend/src/components/Board/PostDetail/PostEditForm.jsx`:
- Around line 43-48: The onDrop handler handleDrop currently only calls
setIsDragOver(false) which allows the browser to navigate/open dropped files
when users drop outside the RichTextEditor; update handleDrop to call
e.preventDefault() (and optionally e.stopPropagation()) before clearing drag
state so the browser default file navigation is suppressed and the
RichTextEditor can still handle drops inside its own onDrop; locate handleDrop
in PostEditForm.jsx and add the preventDefault call while preserving the
setIsDragOver(false) behavior.

In `@frontend/src/components/Board/RichTextEditor.jsx`:
- Around line 991-1000: The color picker is reading
editor.getAttributes('textStyle')?.backgroundColor while applyTextBackground
calls toggleHighlight({ color }) (the Highlight mark stores color on the
'highlight' mark), so the picker value isn't synced to the actual highlight
color; update the input value to read the highlight mark attribute (e.g.,
editor.getAttributes('highlight')?.color) and ensure any code that reads/writes
colors uses the same mark/attribute (align applyTextBackground/toggleHighlight
and any getters to use the 'highlight' mark's color attribute instead of
textStyle.backgroundColor).

In `@frontend/src/pages/BoardWrite.jsx`:
- Around line 431-433: The current handleRemoveAttachment removes attachments by
comparing only mediaId, which causes items with undefined mediaId to all be
removed; change it to compute and compare the same fallback key used in
rendering (mediaId || url || originalFilename || name) so the removal only
removes the exact item—i.e., accept an identifier param (or keep mediaId but
treat it as the fallback id), compute const idToRemove = mediaId || url ||
originalFilename || name for the target and in the filter compute each file's id
the same way and compare equality; apply the same change to the other removal
handlers in the same area (the functions around the 500–505 range) so all
deletes use the identical fallback identifier logic.
- Around line 112-129: The link and inline-style insertion in node.marks.forEach
is vulnerable to XSS because mark.attrs.href and style tokens (mark.attrs.color,
mark.attrs.backgroundColor, mark.attrs.fontFamily, mark.attrs.fontSize) are
inserted raw into HTML; fix by validating and normalizing href with a whitelist
of safe URL schemes (e.g., http, https, mailto, tel) using URL parsing and
rejecting/omitting any value with disallowed schemes or suspicious characters
(e.g., "javascript:" or protocol-relative values), and escape/encode the final
href value before interpolation; for inline styles, validate each token against
allowed patterns/whitelists (e.g., safe color regex, allowed font-family list,
numeric fontSize bounds) and only include style declarations that pass
validation, otherwise omit them; ensure all attribute values are safely escaped
(replace quotes/angle brackets) before composing the `<a>` or `<span>` strings
so no attribute-escaping or script injection is possible.

In `@frontend/src/pages/PostDetail.jsx`:
- Around line 422-440: The jsonToHtml path currently interpolates unvalidated
href and style values into HTML (see jsonToHtml logic in PostDetail.jsx that
builds <a href="..."> and <span style="...">), creating XSS risk; extract a
shared sanitizer utility (e.g., sanitizeHref and sanitizeStyle or a single
sanitizeMarkAttrs used by jsonToHtml and the edit/save path) that enforces
allowed URL schemes (http/https/mailto), strips/escapes dangerous characters,
and validates/normalizes CSS values (color, backgroundColor, fontFamily,
fontSize) before constructing the HTML string, then replace direct usage of
mark.attrs.href and style assembly in jsonToHtml and the edit-save conversion to
call that sanitizer so both paths use the same safe logic.

---

Nitpick comments:
In `@frontend/src/components/Board/RichTextEditor.jsx`:
- Around line 630-643: The else-if branch checking onAttachFiles inside the
file-drop handler is dead code because onAttachFiles is already handled earlier
(and the function returns), so remove the entire "else if (onAttachFiles) { ...
}" block that attempts to call onAttachFiles, normalize URLs with
toAbsoluteImageUrl, and call insertFileLink for uploadedArr; ensure any
necessary error handling or variables (otherFiles, uploadedArr) are not
referenced elsewhere after removal.

In `@frontend/src/components/Board/RichTextEditor.module.css`:
- Around line 282-286: The .imageResizeHandleSE class is declared twice
(duplicate declarations where the second overrides the first); remove the
redundant declaration and consolidate into a single .imageResizeHandleSE rule,
preserving the intended right/bottom values (pick and keep either right: -7px;
bottom: -7px or right: -9px; bottom: -9px so there is only one authoritative
declaration) and keep cursor: nwse-resize in that single rule.

In `@frontend/src/pages/BoardWrite.module.css`:
- Around line 88-90: Duplicate .boardSelectCol rules exist with conflicting
widths (300px vs 260px); remove the duplicate and consolidate into a single
.boardSelectCol declaration (use the intended value — keep 260px if that is the
desired layout) so the style no longer depends on cascade order; update the one
in BoardWrite.module.css (and remove the other occurrence around the second
reported region) to a single definition.
- Around line 203-215: Add a visible keyboard-focus style for the
.attachmentRemove button by defining a .attachmentRemove:focus-visible rule
(separate from :hover) that sets an accessible focus indicator (for example a
clear outline, box-shadow ring, or border with sufficient contrast and preserved
border-radius) and ensures outline-offset or padding so the indicator doesn't
overlap content; update .attachmentRemove class if needed to avoid conflicting
outlines and ensure the focus style matches the visual design and hover color
semantics.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29047c3a-3bdd-4604-867b-c3a8535e1c75

📥 Commits

Reviewing files that changed from the base of the PR and between f787258 and 11d2b07.

⛔ Files ignored due to path filters (8)
  • frontend/src/assets/toolbox-file-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-img-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-left-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-mid-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-right-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-textBackgroundColor-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-textcolor-icon.svg is excluded by !**/*.svg
  • frontend/src/assets/toolbox-video-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • frontend/src/components/Board/PostDetail/PostEditForm.jsx
  • frontend/src/components/Board/PostDetail/PostHtmlView.jsx
  • frontend/src/components/Board/PostDetail/PostView.jsx
  • frontend/src/components/Board/RichTextEditor.jsx
  • frontend/src/components/Board/RichTextEditor.module.css
  • frontend/src/pages/BoardWrite.jsx
  • frontend/src/pages/BoardWrite.module.css
  • frontend/src/pages/PostDetail.jsx
  • frontend/src/pages/PostDetail.module.css

Comment thread frontend/src/components/Board/PostDetail/PostEditForm.jsx
Comment thread frontend/src/components/Board/RichTextEditor.jsx
Comment thread frontend/src/pages/BoardWrite.jsx Outdated
Comment thread frontend/src/pages/BoardWrite.jsx Outdated
Comment thread frontend/src/pages/PostDetail.jsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
frontend/src/pages/PostDetail.jsx (1)

228-239: ⚡ Quick win

인라인 첨부 제외 로직은 헬퍼로 한 번만 두는 편이 안전합니다.

같은 필터가 세 군데에 복사돼 있어서, 조건이 한 번만 바뀌어도 조회/새로고침/수정 진입 결과가 쉽게 어긋납니다. getNonInlineAttachments(post) 같은 helper로 묶어두면 유지보수가 훨씬 쉬워집니다.

Also applies to: 265-274, 378-387

🤖 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 `@frontend/src/pages/PostDetail.jsx` around lines 228 - 239, The
inline-attachment filtering logic is duplicated in three places (around the
blocks using normalizeAttachments, extractInlineUrlsFromContent and the
filteredAttachments logic); extract this into a single helper function named
getNonInlineAttachments(post) that accepts a post, calls
normalizeAttachments(post) and extractInlineUrlsFromContent(post) (normalizing
URLs by stripping query strings), and returns the filtered attachments array
using the same rules (check att.url, att.publicPath, att.savedFilename,
att.originalFilename and compare against inline URLs). Replace the three
duplicated filter blocks with calls to getNonInlineAttachments(post) (e.g.,
where filteredAttachments is currently computed) so updates to the rule are made
in one place.
🤖 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 `@frontend/src/pages/PostDetail.jsx`:
- Around line 422-423: The current extraction for newly uploaded file IDs only
reads u?.mediaId, which is inconsistent with other code paths that normalize IDs
using f.postAttachmentId || f.mediaId || f.id; update the upload handling that
populates uploadedNewMediaIds (the Promise.all result of
boardApi.uploadBoardFile) to normalize each upload response the same way (check
postAttachmentId, mediaId, id, etc.) and only include truthy IDs, or
throw/handle an error if an upload response contains no identifiable ID; make
the same normalization change where attachmentFiles are mapped (the
attachmentFiles.map(...).filter(Boolean) usage) so both existing and newly
uploaded attachments use the identical ID-selection logic.

In `@frontend/src/utils/richTextHtml.js`:
- Around line 164-166: The fallback in contentHtml currently only joins nodes
with node.content, causing leaf media nodes (e.g., file/video) to be dropped;
update the renderNode logic (used by contentHtml) to explicitly handle supported
media leaf node types (e.g., "file", "video") by returning appropriate HTML for
those nodes when Array.isArray(node.content) is false, while keeping the
existing branch that maps and joins node.content; ensure renderNode and any
helpers produce the expected HTML for media so contentHtml and contentJson
remain consistent.
- Around line 141-143: The heading rendering can produce `<hNaN>` when
node.attrs.level is a non-numeric string (e.g. "h2"); update the heading branch
(node.type === 'heading') so you parse/coerce the level safely (e.g. use
parseInt(node.attrs?.level, 10) or Number and then if isNaN(level) set level =
1) before applying Math.min/Math.max, ensuring level is an integer between 1 and
6; keep the rest of the return using renderNode unchanged.
- Around line 149-159: The width/height values from node.attrs are only escaped
but not validated, allowing malicious CSS like "10px; position: fixed; inset:
0;"; add a CSS length sanitizer (e.g., isValidCssLength) that permits only
numeric values and safe units (px, em, rem, %, vh, vw, etc., or "auto"), use it
to validate width and height before building
alignStyle/style/widthAttr/heightAttr, and if a value fails validation fall back
to omitting the dimension or using "auto"; update where width/height are read
(the width/height variables and the construction of style, widthAttr,
heightAttr) to call the sanitizer and only include the escaped value when it
passes.

---

Nitpick comments:
In `@frontend/src/pages/PostDetail.jsx`:
- Around line 228-239: The inline-attachment filtering logic is duplicated in
three places (around the blocks using normalizeAttachments,
extractInlineUrlsFromContent and the filteredAttachments logic); extract this
into a single helper function named getNonInlineAttachments(post) that accepts a
post, calls normalizeAttachments(post) and extractInlineUrlsFromContent(post)
(normalizing URLs by stripping query strings), and returns the filtered
attachments array using the same rules (check att.url, att.publicPath,
att.savedFilename, att.originalFilename and compare against inline URLs).
Replace the three duplicated filter blocks with calls to
getNonInlineAttachments(post) (e.g., where filteredAttachments is currently
computed) so updates to the rule are made in one place.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 263afdd3-4750-47c5-b47c-270a54a906b8

📥 Commits

Reviewing files that changed from the base of the PR and between 11d2b07 and 05a787b.

📒 Files selected for processing (5)
  • frontend/src/components/Board/PostDetail/PostEditForm.jsx
  • frontend/src/components/Board/RichTextEditor.jsx
  • frontend/src/pages/BoardWrite.jsx
  • frontend/src/pages/PostDetail.jsx
  • frontend/src/utils/richTextHtml.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/Board/PostDetail/PostEditForm.jsx
  • frontend/src/pages/BoardWrite.jsx
  • frontend/src/components/Board/RichTextEditor.jsx

Comment thread frontend/src/pages/PostDetail.jsx Outdated
Comment thread frontend/src/utils/richTextHtml.js
Comment thread frontend/src/utils/richTextHtml.js Outdated
Comment thread frontend/src/utils/richTextHtml.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/pages/BoardWrite.jsx (2)

197-199: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

heading 레벨에서 NaN이 발생할 수 있습니다.

Number(node.attrs?.level)"h2" 같은 문자열이면 NaN이 되어 <hNaN> 태그가 생성됩니다. richTextHtml.js에는 이미 수정되어 있으나 이 복사본에는 적용되지 않았습니다.

수정 예시 (중복 제거 전 임시 수정)
     if (node.type === 'heading') {
-      const level = Math.min(Math.max(Number(node.attrs?.level || 1), 1), 6);
+      let level = parseInt(node.attrs?.level, 10);
+      if (Number.isNaN(level)) level = 1;
+      level = Math.min(Math.max(level, 1), 6);
       return `<h${level}>${(node.content || []).map(renderNode).join('')}</h${level}>`;
     }
🤖 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 `@frontend/src/pages/BoardWrite.jsx` around lines 197 - 199, The heading
renderer can produce NaN when node.attrs?.level is a non-numeric string (e.g.,
"h2"); update the heading handling in BoardWrite.jsx (the block where node.type
=== 'heading' that uses node.attrs?.level and renderNode) to coerce the level
safely by parsing an integer and falling back to 1, then clamp it between 1 and
6 before emitting the tag; in short, replace Number(node.attrs?.level || 1) with
a safe parse+fallback (e.g., parseInt with isNaN check) and keep the existing
clamp and renderNode usage.

201-217: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

이미지 width/height에 CSS 인젝션 취약점이 있습니다.

검증 없이 width/height 값이 style 속성에 직접 삽입됩니다. 100px; position: fixed; inset: 0; 같은 값으로 추가 CSS 선언이 주입될 수 있습니다. richTextHtml.js에는 isValidCssLength 검증이 있지만 이 복사본에는 누락되었습니다.

수정 예시 (중복 제거 전 임시 수정)
+    const SAFE_CSS_LENGTH_PATTERN = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|%|vh|vw)?$/i;
+    const isValidCssLength = (v) => {
+      const raw = String(v || '').trim();
+      return raw === 'auto' || SAFE_CSS_LENGTH_PATTERN.test(raw);
+    };
+
     if (node.type === 'image') {
       const src = String(node.attrs?.src || '').replace(/"/g, '&quot;');
       const alt = String(node.attrs?.alt || '').replace(/"/g, '&quot;');
-      const width = String(node.attrs?.width || '').trim();
-      const height = String(node.attrs?.height || '').trim();
+      const width = isValidCssLength(node.attrs?.width) ? String(node.attrs?.width).trim() : '';
+      const height = isValidCssLength(node.attrs?.height) ? String(node.attrs?.height).trim() : 'auto';
🤖 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 `@frontend/src/pages/BoardWrite.jsx` around lines 201 - 217, The image renderer
in BoardWrite.jsx embeds unsanitized width/height into the style attribute (the
node.type === 'image' block using variables width, height, style, widthAttr,
heightAttr), allowing CSS injection; fix by validating and normalizing
width/height before use—import or reimplement the existing isValidCssLength from
richTextHtml.js and only include width/height in the style string and
widthAttr/heightAttr when isValidCssLength(value) returns true (otherwise omit
and fall back to 'auto' or no attribute), and ensure values are stripped of
trailing semicolons or unsafe characters (e.g., remove ';' and disallow
additional CSS declarations) so only a single CSS length/token can be injected
into style.
🧹 Nitpick comments (3)
frontend/src/utils/richTextHtml.js (3)

192-192: 💤 Low value

poster 속성도 URL 프로토콜 검증을 고려해 주세요.

poster는 URL 속성이므로 sanitizeHref를 통한 검증이 권장됩니다.

수정 예시
-      const poster = node.attrs?.poster ? ` poster="${escapeHtmlAttribute(String(node.attrs.poster).trim())}"` : '';
+      const posterUrl = node.attrs?.poster ? sanitizeHref(String(node.attrs.poster).trim()) : null;
+      const poster = posterUrl ? ` poster="${posterUrl}"` : '';
🤖 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 `@frontend/src/utils/richTextHtml.js` at line 192, The poster attribute is
currently only escaped via escapeHtmlAttribute but needs URL protocol
validation; update the logic around node.attrs?.poster (where poster is
constructed) to pass the raw value through sanitizeHref (or your URL sanitizer)
first, use the sanitized result only if non-empty/allowed, then escape it with
escapeHtmlAttribute before emitting `poster="..."`; ensure you reference
node.attrs?.poster and the poster variable construction so the poster is omitted
when sanitizeHref rejects the URL.

99-102: 💤 Low value

공백이 포함된 폰트명은 CSS에서 따옴표로 감싸야 합니다.

Noto Sans KR 같은 폰트명이 따옴표 없이 출력되면 CSS 표준에 맞지 않습니다. 대부분 브라우저에서 동작하지만 엣지 케이스에서 문제가 될 수 있습니다.

수정 예시
-    if (fontFamily) styles.push(`font-family: ${fontFamily}`);
+    if (fontFamily) styles.push(`font-family: "${fontFamily}"`);
🤖 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 `@frontend/src/utils/richTextHtml.js` around lines 99 - 102, The fontFamily
value is inserted into CSS without quotes which breaks CSS for names with spaces
(e.g., "Noto Sans KR"); update the logic around the fontFamily usage (the
styles.push call that currently does styles.push(`font-family: ${fontFamily}`))
to detect if fontFamily contains whitespace or special characters and wrap it in
appropriate quotes (and escape any embedded quotes) before appending, so the
generated CSS is always valid.

163-179: ⚡ Quick win

이미지 src도 다른 미디어 노드처럼 프로토콜 검증을 적용하는 것이 좋습니다.

file, video 노드는 getMediaNodeUrlsanitizeHref를 통해 프로토콜 검증을 하지만, imageescapeHtmlAttribute만 적용됩니다. 일관성 있는 보안 레이어를 위해 이미지도 동일하게 처리하는 것을 권장합니다.

수정 예시
     if (node.type === 'image') {
-      const src = escapeHtmlAttribute(node.attrs?.src || '');
+      const rawSrc = node.attrs?.src || '';
+      // data: URL은 이미지에서 유효하므로 별도 허용
+      const isDataUrl = rawSrc.startsWith('data:image/');
+      const src = isDataUrl
+        ? escapeHtmlAttribute(rawSrc)
+        : (sanitizeHref(rawSrc) || '');
+      if (!src) return '';
       const alt = escapeHtmlAttribute(node.attrs?.alt || '');
🤖 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 `@frontend/src/utils/richTextHtml.js` around lines 163 - 179, The image node
handling in the image branch should apply the same protocol sanitization as
file/video nodes: instead of only using escapeHtmlAttribute for src, pass the
node (or node.attrs.src) through getMediaNodeUrl and/or sanitizeHref before
escaping; keep escapeHtmlAttribute for alt and other attributes and preserve
existing width/height/align logic (functions referenced: getMediaNodeUrl,
sanitizeHref, escapeHtmlAttribute, isValidCssLength, and the image branch
handling for node.type === 'image'). Update the src assignment to use the
sanitized URL result so image URLs receive the same protocol validation as other
media nodes.
🤖 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 `@frontend/src/pages/BoardWrite.jsx`:
- Around line 26-73: Remove the duplicated sanitization and HTML conversion
implementations in BoardWrite.jsx and instead import and use the canonical
utilities from richTextHtml.js (e.g., import escapeHtmlAttribute, sanitizeUrl,
sanitizeColor, sanitizeFontFamily, sanitizeFontSize, jsonToHtml, and any helpers
like isValidCssLength/heading normalization); replace local definitions of
escapeHtmlAttribute, sanitizeUrl, sanitizeColor, sanitizeFontFamily,
sanitizeFontSize and any jsonToHtml usage with the imported functions so the
file inherits the security fixes (isValidCssLength checks and heading NaN
handling) present in richTextHtml.js and delete the redundant code blocks in
BoardWrite.jsx.

---

Outside diff comments:
In `@frontend/src/pages/BoardWrite.jsx`:
- Around line 197-199: The heading renderer can produce NaN when
node.attrs?.level is a non-numeric string (e.g., "h2"); update the heading
handling in BoardWrite.jsx (the block where node.type === 'heading' that uses
node.attrs?.level and renderNode) to coerce the level safely by parsing an
integer and falling back to 1, then clamp it between 1 and 6 before emitting the
tag; in short, replace Number(node.attrs?.level || 1) with a safe parse+fallback
(e.g., parseInt with isNaN check) and keep the existing clamp and renderNode
usage.
- Around line 201-217: The image renderer in BoardWrite.jsx embeds unsanitized
width/height into the style attribute (the node.type === 'image' block using
variables width, height, style, widthAttr, heightAttr), allowing CSS injection;
fix by validating and normalizing width/height before use—import or reimplement
the existing isValidCssLength from richTextHtml.js and only include width/height
in the style string and widthAttr/heightAttr when isValidCssLength(value)
returns true (otherwise omit and fall back to 'auto' or no attribute), and
ensure values are stripped of trailing semicolons or unsafe characters (e.g.,
remove ';' and disallow additional CSS declarations) so only a single CSS
length/token can be injected into style.

---

Nitpick comments:
In `@frontend/src/utils/richTextHtml.js`:
- Line 192: The poster attribute is currently only escaped via
escapeHtmlAttribute but needs URL protocol validation; update the logic around
node.attrs?.poster (where poster is constructed) to pass the raw value through
sanitizeHref (or your URL sanitizer) first, use the sanitized result only if
non-empty/allowed, then escape it with escapeHtmlAttribute before emitting
`poster="..."`; ensure you reference node.attrs?.poster and the poster variable
construction so the poster is omitted when sanitizeHref rejects the URL.
- Around line 99-102: The fontFamily value is inserted into CSS without quotes
which breaks CSS for names with spaces (e.g., "Noto Sans KR"); update the logic
around the fontFamily usage (the styles.push call that currently does
styles.push(`font-family: ${fontFamily}`)) to detect if fontFamily contains
whitespace or special characters and wrap it in appropriate quotes (and escape
any embedded quotes) before appending, so the generated CSS is always valid.
- Around line 163-179: The image node handling in the image branch should apply
the same protocol sanitization as file/video nodes: instead of only using
escapeHtmlAttribute for src, pass the node (or node.attrs.src) through
getMediaNodeUrl and/or sanitizeHref before escaping; keep escapeHtmlAttribute
for alt and other attributes and preserve existing width/height/align logic
(functions referenced: getMediaNodeUrl, sanitizeHref, escapeHtmlAttribute,
isValidCssLength, and the image branch handling for node.type === 'image').
Update the src assignment to use the sanitized URL result so image URLs receive
the same protocol validation as other media nodes.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a5e1b61-fb01-44a0-a2e2-2e790dd90f13

📥 Commits

Reviewing files that changed from the base of the PR and between 05a787b and 800de10.

📒 Files selected for processing (3)
  • frontend/src/pages/BoardWrite.jsx
  • frontend/src/pages/PostDetail.jsx
  • frontend/src/utils/richTextHtml.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/PostDetail.jsx

Comment thread frontend/src/pages/BoardWrite.jsx Outdated
Copy link
Copy Markdown
Contributor

@discipline24 discipline24 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

@discipline24 discipline24 merged commit d7cbfc3 into main May 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚀 [기능개선][FE] 글 쓰기 페이지 개선

3 participants