Skip to content

20260509 fe 366 기능개선fe 글작성 페이지 개선#368

Merged
discipline24 merged 3 commits into
mainfrom
20260509-FE-366-기능개선fe-글작성-페이지-개선
May 11, 2026

Hidden character warning

The head ref may contain hidden characters: "20260509-FE-366-\uae30\ub2a5\uac1c\uc120fe-\uae00\uc791\uc131-\ud398\uc774\uc9c0-\uac1c\uc120"
Merged

20260509 fe 366 기능개선fe 글작성 페이지 개선#368
discipline24 merged 3 commits into
mainfrom
20260509-FE-366-기능개선fe-글작성-페이지-개선

Conversation

@sangkyu39
Copy link
Copy Markdown
Contributor

@sangkyu39 sangkyu39 commented May 11, 2026

글 작성시 서식을 지정할 수 있도록 추가
이미지도 간단히 복사 붙여넣기로 가능하도록 추가

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 게시물 작성 및 편집을 위한 풍부한 텍스트 편집기 추가
    • 편집기에서 이미지 업로드 및 크기 조정 기능 지원
    • 게시물 작성을 위한 새로운 전용 페이지 도입
    • 포맷된 콘텐츠 및 이미지 정규화로 향상된 게시물 표시

Review Change Stack

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

coderabbitai Bot commented May 11, 2026

작업 요약

이 PR은 보드 포스트 작성 및 편집 시스템에 TipTap 기반의 리치 텍스트 에디터를 도입합니다. 모달 기반의 인라인 작성 방식을 제거하고 라우팅 기반의 전용 페스트 작성 페이지로 전환하며, 이미지 업로드, 텍스트 포맷팅, 콘텐츠 정규화를 지원하는 완전한 에디터 시스템을 구현합니다.

변경 사항

리치 에디터 및 보드 포스트 기능

계층 / 파일 설명
의존성 및 기초 유틸리티
frontend/package.json, frontend/src/utils/imageUtils.js
TipTap 에디터 라이브러리(@tiptap/* 패키지) 의존성을 추가하고, 이미지 URL 정규화(toAbsoluteImageUrl), 이미지 파일 검증(isImageFile), 데이터 URL/Blob을 File로 변환하는 유틸리티를 구현합니다.
API 계층: 리치 포스트 관리
frontend/src/utils/boardApi.js
createRichPost(postData), updateRichPost(postId, postData) 포스트 생성/업데이트 API를 추가하고, uploadBoardImage(file), uploadBoardFile(file) 멀티파트 업로드 헬퍼를 구현합니다.
RichTextEditor 컴포넌트
frontend/src/components/Board/RichTextEditor.jsx, frontend/src/components/Board/RichTextEditor.module.css
TipTap 기반의 완전한 리치 텍스트 에디터를 구현합니다. 이미지 리사이징(마우스 드래그), 텍스트 포맷팅(굵게/기울임/밑줄 등), 도구 모음, 붙여넣기/드래그앤드롭 이미지 처리, 폰트 크기/패밀리 선택을 포함합니다.
PostHtmlView 컴포넌트
frontend/src/components/Board/PostDetail/PostHtmlView.jsx, frontend/src/components/Board/PostDetail/PostHtmlView.module.css
TipTap JSON 또는 HTML 콘텐츠를 정규화하고 렌더링하는 컴포넌트입니다. 이미지 URL을 절대 경로로 변환하고, 레이징 로딩을 처리하며, 크기 제약을 적용합니다.
PostView에서 리치 콘텐츠 표시
frontend/src/components/Board/PostDetail/PostView.jsx
포스트 표시 로직을 업데이트하여 contentHtmlcontentJson 중 선택하고, PostHtmlView 컴포넌트로 렌더링합니다.
PostEditForm에서 RichTextEditor 통합
frontend/src/components/Board/PostDetail/PostEditForm.jsx
포스트 편집 폼의 텍스트 영역을 RichTextEditor로 교체하고, boardApi.uploadBoardImage를 이미지 업로드 핸들러로 연결합니다.
Board 페이지: 모달에서 라우팅으로 전환
frontend/src/pages/Board.jsx
인라인 모달 기반 포스트 작성 UI를 제거하고, useNavigate를 통해 /board/:team/write 라우트로의 네비게이션으로 변경합니다. 권한 상태(canCreateSubBoard, canDeleteSubBoard)를 추가합니다.
BoardWrite 페이지: 리치 포스트 생성
frontend/src/pages/BoardWrite.jsx, frontend/src/pages/BoardWrite.module.css
리치 콘텐츠 포스트 작성을 위한 전용 페이지를 구현합니다. 보드 선택, TipTap JSON을 HTML/평문으로 변환, base64 이미지 자동 업로드 및 정규화, 첨부파일 관리, 익명 작성 옵션을 지원합니다.
PostDetail: 리치 콘텐츠 편집 지원
frontend/src/pages/PostDetail.jsx
포스트 편집 시 TipTap JSON 형식을 감지하고 updateRichPost API를 통해 HTML/평문 필드를 생성하여 전송합니다. 다중 콘텐츠 형식 호환성을 추가합니다.
라우팅 설정
frontend/src/App.jsx
보드 포스트 작성을 위한 새로운 라우트 /board/write, /board/:team/write를 등록하고 BoardWrite 컴포넌트를 연결합니다.

예상되는 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~60 minutes

관련 PR

  • SISC-IT/sisc-web#161: 보드/포스트 기능 수정 및 동일 파일(boardApi.js, PostDetail, PostEditForm, PostView 컴포넌트 등) 변경.
  • SISC-IT/sisc-web#21: 보드 쓰기 흐름 리팩토링(모달 기반에서 라우팅 기반으로) 및 Board.jsx 변경과 관련.
  • SISC-IT/sisc-web#285: 동일한 보드 관련 UI 및 API 코드(PostEditForm.jsx, boardApi.js) 수정.

권장 리뷰어

  • discipline24
  • gxuoo

축하 시

🐰 리치 에디터의 마술이 피어나고,
포스트는 이제 스타일을 자랑하며,
이미지들이 춤을 추고,
모달은 사라지고 라우트가 빛나네! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 주요 변경사항을 정확히 반영합니다. 글 작성 페이지 개선이라는 핵심 내용이 포함되어 있으며, 실제 변경사항(리치 텍스트 에디터 추가, 이미지 업로드 기능 등)과 일치합니다.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 20260509-FE-366-기능개선fe-글작성-페이지-개선

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@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: 19

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/components/Board/PostDetail/PostEditForm.jsx (1)

107-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

드래그앤드롭 핸들러가 RichTextEditor 내부 드롭과 중첩됩니다.

RichTextEditoreditorProps.handleDrop이 이미지 파일을 인라인 이미지로 업로드/삽입한 뒤에도 DOM 이벤트는 부모로 계속 버블링됩니다. 그 결과 사용자가 에디터 영역 위에 이미지 파일을 드롭하면:

  1. 본문에 인라인 이미지로 삽입되고,
  2. 동시에 부모 handleDrop이 동작해 같은 파일이 첨부파일 목록(newFiles)에도 추가됩니다.

이미지 본문 삽입 흐름과 첨부파일 흐름이 의도적으로 분리된 것이라면 명확히 구분되도록 처리가 필요합니다. 예시:

  • RichTextEditorhandleDrop에서 처리한 이벤트의 dataTransfer를 부모가 식별할 수 있도록 플래그(예: event.defaultPrevented 활용)를 활용하거나,
  • 부모 handleDrop에서 event.target이 에디터 내부면 무시하도록 가드.
🛡️ 제안 가드 예시
   const handleDrop = (e) => {
     e.preventDefault();
     e.stopPropagation();
     setIsDragOver(false);
+    // RichTextEditor가 이미 처리한 드롭(인라인 이미지 삽입)은 첨부에서 제외
+    if (e.defaultPrevented) return;

     const droppedFiles = Array.from(e.dataTransfer.files || []);
     if (droppedFiles.length > 0) {
       onAddNewFile({ target: { files: droppedFiles } });
     }
   };
🤖 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/PostDetail/PostEditForm.jsx` around lines 107 -
129, The parent onDrop handler in PostEditForm is receiving drops already
handled by RichTextEditor causing duplicate uploads; update the drop
coordination so only one path processes the file: either have RichTextEditor
call event.preventDefault()/event.stopPropagation() (or set a flag like
event.defaultPrevented) when it inserts images, and in the parent handleDrop
(the onDrop passed to the div around RichTextEditor) early-return if
event.defaultPrevented is true; alternatively, add a ref to the RichTextEditor
root and in the parent handleDrop check if
richEditorRef.current.contains(event.target) and skip adding to newFiles when
true. Ensure you update the RichTextEditor drop handling and the PostEditForm
onDrop (handleDrop/newFiles logic) consistently so images inserted inline by
RichTextEditor are not also appended to the attachment list.
🧹 Nitpick comments (15)
frontend/src/components/Board/RichTextEditor.jsx (4)

184-187: 💤 Low value

컴마 연산자 사용 — 가독성 저하.

else attrs.width = \${finalWidth}px`, attrs.height = `${finalHeight}px`;`는 컴마 연산자로 두 할당을 묶었지만, 블록을 명시하지 않으면 추후 리팩터링 시 한 줄만 추가/이동돼 미묘한 버그로 이어지기 쉽습니다. 명시적 블록과 별도 문장을 권장합니다.

♻️ 제안 수정
-          if (isHorizontalOnly) attrs.width = `${finalWidth}px`;
-          else if (isVerticalOnly) attrs.height = `${finalHeight}px`;
-          else attrs.width = `${finalWidth}px`, attrs.height = `${finalHeight}px`;
+          if (isHorizontalOnly) {
+            attrs.width = `${finalWidth}px`;
+          } else if (isVerticalOnly) {
+            attrs.height = `${finalHeight}px`;
+          } else {
+            attrs.width = `${finalWidth}px`;
+            attrs.height = `${finalHeight}px`;
+          }
🤖 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 184 - 187,
Replace the comma-expression assignment with explicit statement blocks: in the
RichTextEditor component where attrs is set (the conditional using
isHorizontalOnly, isVerticalOnly, finalWidth, finalHeight and attrs), change the
final else to use a proper block and two separate statements that assign
attrs.width and attrs.height instead of a single comma-separated expression to
avoid readability and future-refactor bugs.

199-202: 💤 Low value

catch 블록 — 최소한의 진단 로그를 권장합니다.

onUp 핸들러의 try/catch가 에러를 완전히 삼키고 있어 리사이즈 종료 시 발생한 예외(예: editor.chain 호출 실패)를 추적하기 어렵습니다. 사용자 경험을 망치지 않으면서도 진단 가능한 console.warn 정도는 남겨두는 것을 권장합니다.

♻️ 제안 수정
-      } catch (e) {
-        // ignore
-      }
+      } catch (error) {
+        if (import.meta.env.DEV) console.warn('image resize finalize failed:', error);
+      }
🤖 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 199 - 202, The
empty catch in the onUp handler swallows exceptions (e.g., failures from
editor.chain) making debugging impossible; replace the silent catch in the onUp
function inside RichTextEditor.jsx with a minimal diagnostic log (e.g.,
console.warn) that includes a short context message and the caught error object,
keeping the handler's behavior (no rethrow) otherwise unchanged so user flow
isn't affected.

244-264: 💤 Low value

['e','s']['se'] 두 map 호출은 단일 배열로 합칠 수 있습니다.

두 map의 렌더 내용이 동일하므로 ['e', 's', 'se'] 한 배열로 합치면 코드 중복을 줄일 수 있습니다.

♻️ 제안 수정
-            {['e', 's'].map((direction) => (
+            {['e', 's', 'se'].map((direction) => (
               <button
                 key={direction}
                 type="button"
                 className={`${styles.imageResizeHandle} ${styles[`imageResizeHandle${direction.toUpperCase()}`]}`}
                 data-direction={direction}
                 onMouseDown={startResizing}
                 aria-label={`이미지 크기 조절 ${direction}`}
               />
             ))}
-
-            {['se'].map((direction) => (
-              <button
-                key={direction}
-                type="button"
-                className={`${styles.imageResizeHandle} ${styles[`imageResizeHandle${direction.toUpperCase()}`]}`}
-                data-direction={direction}
-                onMouseDown={startResizing}
-                aria-label={`이미지 크기 조절 ${direction}`}
-              />
-            ))}
🤖 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 244 - 264, The
two consecutive map calls rendering identical resize handle buttons should be
merged into a single map over ['e','s','se'] to remove duplication; update the
JSX that uses startResizing, className template strings
(styles.imageResizeHandle and
styles[`imageResizeHandle${direction.toUpperCase()}`]), data-direction and
aria-label so they are created once for each direction, preserving the same key,
onMouseDown={startResizing} and attributes.

410-528: ⚖️ Poor tradeoff

handlePaste 핸들러의 복잡도가 높아 분리/축약을 권장합니다.

handlePaste 단일 함수가 약 120줄이고 분기(hasImageLikePayload, hasUnknownFilePayload, shouldTryNavigatorClipboardFallback, htmlImageSrcs)가 얽혀 있어 테스트와 유지보수가 어렵습니다. 또한 setTimeout(() => Promise.resolve().then(async)...)처럼 마이크로태스크/매크로태스크가 중첩되어 흐름 추적이 힘듭니다. 다음 분리를 검토해주세요:

  • extractPastedImages(event.clipboardData) 헬퍼로 source 정리
  • uploadPastedImages(files, htmlSrcs)로 업로드 루프 분리
  • 클립보드 폴백 분기는 별도 함수로 분리
🤖 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 410 - 528, The
handlePaste function is too large and mixes payload detection, navigator
fallback, and upload logic; refactor by extracting three helpers: implement
extractPastedImages(clipboardData) to encapsulate the detection logic currently
using clipboardFiles, clipboardItems, plainText, isImageFilenameText,
hasImageLikePayload, hasUnknownFilePayload, shouldTryNavigatorClipboardFallback
and htmlImageSrcs (reusing extractImageSrcsFromHtml), implement
uploadPastedImages(files, htmlSrcs) to perform the setIsUploadingImage /
insertImage loop and the dataUrlToFile/remoteUrlToFile/blobToFile conversion and
error handling, and implement handleNavigatorClipboardFallback() to replace the
nested setTimeout + Promise.resolve block that calls navigator.clipboard.read(),
hasClipboardImage, blobToFile and insertImage; then simplify handlePaste to call
these helpers and return early — remove nested micro/macro task chaining inside
handlePaste so the control flow is clear and testable.
frontend/src/components/Board/RichTextEditor.module.css (1)

97-101: 💤 Low value

!important 남용 — 활성 상태 표현을 보다 명확한 캐스케이드로 정리해보세요.

.activeButtonbackground/color/border-color에 모두 !important를 사용하는데, 이는 동일한 .toolbar button 셀렉터의 우선순위가 비슷해 단순히 클래스 결합 순서로 덮어쓰기 어려웠기 때문으로 보입니다. .toolbar button.activeButton처럼 specificity를 높여주면 !important 없이도 의도한 스타일을 적용할 수 있어 유지보수가 쉬워집니다.

♻️ 제안 수정
-.activeButton {
-  background: `#1d80f4` !important;
-  color: `#fff` !important;
-  border-color: `#1d80f4` !important;
+.toolbar button.activeButton {
+  background: `#1d80f4`;
+  color: `#fff`;
+  border-color: `#1d80f4`;
 }
🤖 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 97 -
101, The .activeButton rule is overusing !important for
background/color/border-color; replace it by increasing selector specificity so
the styles win without !important (e.g., change selectors to .toolbar
button.activeButton or combine selectors like .toolbar button.activeButton,
.toolbar .activeButton) and remove all !important declarations; update any
conflicting .toolbar button rules so the new, more specific selector sets
background, color and border-color as intended.
frontend/src/pages/PostDetail.jsx (1)

130-153: ⚡ Quick win

editContent 초기화 우선순위 — contentJson이 객체이지만 truthy로 항상 우선될 수 있는지 확인 필요.

updatedPost.contentJson || updatedPost.content || updatedPost.contentText 형태로 fallback 체인을 구성했는데, 일부 서버 응답에서 contentJson이 빈 객체({}) 또는 {"type":"doc","content":[]} 같은 "비어 있지만 truthy"한 값으로 반환되면 사용자에게는 빈 에디터가 표시되면서 동시에 contentText에 있던 실제 본문은 누락될 수 있습니다. contentJson이 의미 있는 doc 인지(예: content.length > 0) 확인 후 fallback 하는 것을 권장합니다.

🤖 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 130 - 153, The fallback for
initializing editContent uses contentJson || content || contentText which treats
empty/placeholder contentJson objects as truthy; update the logic where
setEditContent is called (in useEffect's fetchPostAndComments and the
updatedPost handling) to first validate contentJson actually contains meaningful
content (e.g., is an object with a non-empty content array or meaningful fields)
and only use it when valid, otherwise fall back to data.content or
data.contentText; ensure the same validation is applied wherever
setEditContent(updatedPost.contentJson || ...) is used so you don't lose actual
plain-text content.
frontend/src/utils/imageUtils.js (2)

71-76: ⚖️ Poor tradeoff

remoteUrlToFile: 크로스 오리진 이미지에 대한 CORS 실패에 주의하세요.

fetch(url)로 외부 도메인 이미지를 가져올 때, 해당 서버가 Access-Control-Allow-Origin을 제공하지 않으면 네트워크 에러로 실패합니다. 붙여넣기 흐름(extractImageSrcsFromHtmlremoteUrlToFile)에서 외부 이미지가 빈번하다면 서버측 프록시 엔드포인트를 두고 그 경유로 받아오는 방식을 검토해주세요. 최소한 호출부에서 실패를 부드럽게 처리하고(이미 RichTextEditor.jsx에 catch가 있음) 사용자에게 안내 메시지를 노출하는 것을 권장합니다.

🤖 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/imageUtils.js` around lines 71 - 76, The remoteUrlToFile
function can fail on cross-origin images due to missing CORS headers; update
remoteUrlToFile to catch fetch/blob errors and return a clear failure signal
(e.g., null or a thrown Error with a descriptive message) instead of letting a
raw network error bubble up, document that extractImageSrcsFromHtml callers
should handle nulls, and consider adding/pointing to a server-side proxy
endpoint fallback for external images; ensure RichTextEditor.jsx (where catch
already exists) displays a user-friendly message when remoteUrlToFile indicates
a CORS/fetch failure and keep blobToFile usage unchanged.

57-69: ⚡ Quick win

MIME 서브타입에 + 등이 포함되면 확장자 추출이 부정확합니다.

blob.typeimage/svg+xml인 경우 split('/')[1]'svg+xml'을 반환하여 .svg+xml 같은 잘못된 확장자가 붙습니다. 또한 image/jpegjpeg로 추출되지만 일반적으로 파일 확장자는 jpg로 정리하는 경우가 많아 서버측 검사와 어긋날 수 있습니다. MIME→확장자 매핑 테이블을 사용하거나 최소한 +xml 등 서브 토큰을 정리하는 것을 권장합니다.

♻️ 제안 수정
-export const dataUrlToFile = async (dataUrl, filename = 'pasted-image.png') => {
-  const response = await fetch(dataUrl);
-  const blob = await response.blob();
-  const extension = blob.type?.split('/')?.[1] || 'png';
-  const normalizedName = filename.includes('.') ? filename : `${filename}.${extension}`;
-  return new File([blob], normalizedName, { type: blob.type || 'image/png' });
-};
+const MIME_TO_EXT = {
+  'image/jpeg': 'jpg',
+  'image/png': 'png',
+  'image/gif': 'gif',
+  'image/webp': 'webp',
+  'image/bmp': 'bmp',
+  'image/heic': 'heic',
+  'image/heif': 'heif',
+  'image/svg+xml': 'svg',
+};
+
+const resolveExtension = (mime) => MIME_TO_EXT[mime] || mime?.split('/')?.[1]?.split('+')?.[0] || 'png';
+
+export const dataUrlToFile = async (dataUrl, filename = 'pasted-image.png') => {
+  const response = await fetch(dataUrl);
+  const blob = await response.blob();
+  const extension = resolveExtension(blob.type);
+  const normalizedName = filename.includes('.') ? filename : `${filename}.${extension}`;
+  return new File([blob], normalizedName, { type: blob.type || 'image/png' });
+};
🤖 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/imageUtils.js` around lines 57 - 69, The file extension
extraction in dataUrlToFile and blobToFile is fragile (e.g., "image/svg+xml" ->
"svg+xml", "image/jpeg" -> "jpeg"); update both functions to sanitize the MIME
subtype by splitting on '+' and taking the leftmost token, then map common
subtypes to canonical extensions (e.g., "jpeg" -> "jpg", "svg" -> "svg", "png"
-> "png", "webp" -> "webp") via a small lookup table; use that resolved
extension when building normalizedName and keep blob.type as the File type
fallback.
frontend/src/utils/boardApi.js (1)

181-209: ⚡ Quick win

파일 타입 검증과 수동 Content-Type 헤더 제거 권장

uploadBoardImageuploadBoardFile 함수의 두 가지 개선점을 제안합니다:

  1. 파일 타입 검증: uploadBoardImage에서는 이미지 파일 여부를 명시적으로 확인하여 불필요한 네트워크 요청을 줄이고 사용자 경험을 개선할 수 있습니다.

  2. 수동 Content-Type 헤더 제거: Axios 1.13.1에서는 FormData를 자동으로 감지하여 경계값(boundary)을 포함한 multipart/form-data 헤더를 자동 설정합니다. 수동으로 Content-Type 헤더를 지정하면 axios의 자동 설정이 무시되어 일부 백엔드에서 파싱 실패를 유발할 수 있으므로, 헤더를 제거하는 것이 안전합니다.

♻️ 제안 수정
 export const uploadBoardImage = async (file) => {
   if (!file) throw new Error('file is required');
+  if (!String(file.type || '').startsWith('image/')) {
+    throw new Error('이미지 파일만 업로드할 수 있습니다.');
+  }

   const formData = new FormData();
   formData.append('file', file);

-  const response = await api.post('/api/board/images', formData, {
-    headers: { 'Content-Type': 'multipart/form-data' },
-  });
+  const response = await api.post('/api/board/images', formData);

   return response.data;
 };
🤖 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/boardApi.js` around lines 181 - 209, Validate the incoming
file in uploadBoardImage by checking its MIME type (e.g.,
file.type.startsWith('image/')) and throw a descriptive error if it's not an
image to avoid unnecessary uploads; for both uploadBoardImage and
uploadBoardFile remove the manual headers: { 'Content-Type':
'multipart/form-data' } passed to api.post so axios can auto-set the multipart
boundary when sending the FormData appended with file.
frontend/src/pages/BoardWrite.jsx (4)

334-338: 💤 Low value

저장 성공 직후 setContentJson/setInlineMediaIds는 불필요합니다.

바로 다음 줄(navigate(currentBoardPath))에서 페이지를 떠나기 때문에 이 두 setState는 보일러플레이트일 뿐 아니라 unmount 직전 불필요한 리렌더를 유발합니다. 제거해도 동작에는 영향이 없습니다.

♻️ 제안 변경
       await boardApi.createRichPost(payload);
-      setContentJson(normalizedContentJson);
-      setInlineMediaIds(normalizedInlineMediaIds);
       alert('게시글이 작성되었습니다.');
       navigate(currentBoardPath);
🤖 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 334 - 338, Remove the
unnecessary state updates immediately after a successful create: inside the
block that calls boardApi.createRichPost (where you currently call
setContentJson(normalizedContentJson) and
setInlineMediaIds(normalizedInlineMediaIds)), delete those two setState calls
because the next line navigate(currentBoardPath) unmounts the component and
makes those updates redundant and causes an extra render; leave the alert('게시글이
작성되었습니다.') and navigate(currentBoardPath) intact.

94-138: 🏗️ Heavy lift

jsonToHtmlPostHtmlView의 변환 로직과 중복됩니다.

frontend/src/components/Board/PostDetail/PostHtmlView.jsx의 JSON→HTML 분기와 거의 동일한 코드입니다(text/paragraph/heading/image만 처리, 마크/리스트/링크/색상/정렬 등은 누락). 한쪽이라도 TipTap 스키마 확장이 생기면 다른 쪽과 동기화가 어긋납니다. 더 안전하게는 BoardWrite도 readonly TipTap Editor를 일회성으로 만들어 editor.getHTML()을 쓰거나, utils/로 단일 변환기를 추출해 양쪽에서 공유하는 것을 권합니다.

🤖 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 94 - 138, jsonToHtml
duplicates the JSON→HTML logic already implemented in PostHtmlView (missing
marks/lists/links and will diverge if TipTap schema changes); replace the
duplicate by either (A) creating a shared converter in utils (e.g.,
utils/jsonToHtml or utils/convertContentToHtml) and use that from both
BoardWrite (jsonToHtml) and PostHtmlView, or (B) in BoardWrite instantiate a
readonly TipTap Editor once with the same schema and use editor.getHTML() to
produce markup; update references to jsonToHtml in BoardWrite to call the shared
util or the editor.getHTML() method and remove the duplicate rendering logic to
keep a single source of truth.

268-280: ⚡ Quick win

저장되지 않은 작성 내용에 대한 이탈 확인이 없습니다.

handleBack(취소/뒤로) 와 handleBoardChange가 별도 확인 없이 동작합니다. 사용자가 제목/본문/첨부를 입력한 상태에서 실수로 누르면 모든 작업이 사라집니다. 최소한 title/contentJson/attachmentFiles 중 하나라도 비어있지 않으면 window.confirm으로 확인하도록 보강하시는 것을 추천합니다.

Also applies to: 442-456

🤖 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 268 - 280, The handlers
handleBack and handleBoardChange (and any similar handlers around lines 442-456)
currently navigate or change selection immediately; update them to first detect
unsaved input by checking if title, contentJson, or attachmentFiles are
non-empty and, if so, call window.confirm with a clear message and only proceed
with navigate(currentBoardPath) or setSelectedParentId/setSelectedSubBoardId
when the user confirms; if the user cancels, abort the action (do not clear
sub-board or navigate). Ensure you reference and modify handleBack and
handleBoardChange (and the analogous handler at 442-456) so the confirmation
guards both navigation and parent/sub-board changes.

1-23: ⚡ Quick win

import 문이 다른 코드 사이에 흩어져 있습니다.

L1-6의 import 블록과 L18-22의 import 사이에 createEmptyDoc 선언(L8-16)이 끼어 있습니다. ES 모듈 import는 호이스팅되어 동작상 문제는 없지만, 가독성/도구(린트, 자동 정렬) 호환성을 위해 모든 import를 상단에 모으는 게 좋습니다.

♻️ 제안 변경
 import { useEffect, useMemo, useState } from 'react';
 import { useLocation, useNavigate, useParams } from 'react-router-dom';
 import * as boardApi from '../utils/boardApi';
 import { normalizeBoardRouteSegment, toBoardRouteSegment } from '../utils/boardRoute';
 import RichTextEditor from '../components/Board/RichTextEditor';
 import styles from './BoardWrite.module.css';
+import {
+  isDataImageUrl,
+  toAbsoluteImageUrl,
+  dataUrlToFile,
+} from '../utils/imageUtils';

 const createEmptyDoc = () => ({ ... });

-import {
-  isDataImageUrl,
-  toAbsoluteImageUrl,
-  dataUrlToFile,
-} from '../utils/imageUtils';
-
-// dataUrlToFile imported from utils
🤖 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 1 - 23, The createEmptyDoc
helper is declared between two import groups which breaks import organization;
move all import statements to the top of the module and then place the
createEmptyDoc const (and any other local declarations) after them.
Specifically, combine the two import blocks (including imports for
useEffect/useState, boardApi, normalizeBoardRouteSegment/toBoardRouteSegment,
RichTextEditor, styles, and isDataImageUrl/toAbsoluteImageUrl/dataUrlToFile) at
the very top, then define createEmptyDoc below the imports.
frontend/src/components/Board/PostDetail/PostView.jsx (1)

38-51: ⚡ Quick win

htmlToRender 계산을 useMemo로 옮기고 불필요한 try/catch는 제거하세요.

매 렌더마다 새 IIFE가 실행되고 <PostHtmlView />html prop 참조가 매번 바뀝니다. transformHtmlImagesuseEffect가 다시 도는 원인이 됩니다. 또한 /<img\s+/i.test(html)는 throw하지 않으므로 try/catch가 필요하지 않습니다.

♻️ 제안 변경
-  const htmlToRender = (() => {
-    const html = post?.contentHtml || post?.content || '';
-    // prefer contentJson when contentHtml doesn't contain images but contentJson has image nodes
-    const json = post?.contentJson;
-
-    try {
-      if (html && /<img\s+/i.test(html)) return html;
-      if (json) return json;
-    } catch (e) {
-      return html;
-    }
-
-    return html;
-  })();
+  const htmlToRender = useMemo(() => {
+    const html = post?.contentHtml || post?.content || '';
+    const json = post?.contentJson;
+    if (html && /<img\s+/i.test(html)) return html;
+    if (json) return json;
+    return html;
+  }, [post?.contentHtml, post?.content, post?.contentJson]);

useMemo를 위해 import에 useMemo 추가가 필요합니다.

🤖 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/PostDetail/PostView.jsx` around lines 38 - 51,
Replace the IIFE that computes htmlToRender with a useMemo so the value only
changes when post.contentHtml, post.content, or post.contentJson change; remove
the unnecessary try/catch (/<img\s+/i.test(html) cannot throw) and ensure
useMemo imports useMemo from React, so PostHtmlView's html prop remains stable
and avoids retriggering transformHtmlImages and the related useEffect.
frontend/src/components/Board/PostDetail/PostHtmlView.jsx (1)

192-192: 💤 Low value

JSON 본문일 때 transformHtmlImages가 헛돌고 있습니다.

safeHtml은 렌더 시점에 항상 계산되지만, html이 TipTap JSON 객체이면 useEffect에서 TipTap Editor로 새로 렌더해 container.innerHTML에 덮어쓰므로 transformHtmlImages의 JSON 분기 결과는 버려집니다. 또한 JSON 경로에서 parser.parseFromString(html, ...)이 객체를 문자열화해 "[object Object]"로 처리되는 부수효과까지 있습니다. JSON일 때는 transformHtmlImages 호출을 건너뛰거나, useEffect 분기와 일관되도록 safeHtml 계산을 조건부로 두는 편이 좋겠습니다.

Also applies to: 194-252

🤖 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/PostDetail/PostHtmlView.jsx` at line 192,
safeHtml is always computed by calling transformHtmlImages(html) even when html
is a TipTap JSON object, causing that work to be wasted and
parser.parseFromString to stringify objects into "[object Object]"; change the
logic so transformHtmlImages is only called for actual HTML strings and skipped
for TipTap JSON. Concretely: in PostHtmlView.jsx, guard the safeHtml calculation
(the const safeHtml = transformHtmlImages(html)) with a check like typeof html
=== 'string' && !isTipTapJson(html) (or detect html being an object with .type
=== 'doc' / a stringified JSON starting with '{' containing "type":"doc"), and
ensure the useEffect branch that initializes the Editor/container.innerHTML
remains consistent with that condition (i.e., if html is TipTap JSON, do not
call transformHtmlImages or parser.parseFromString on it, and instead mount the
TipTap editor content path). Update transformHtmlImages invocations and any
parser.parseFromString usage to only execute when html is confirmed to be an
HTML string.
🤖 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/package.json`:
- Around line 13-22: The project is adding a duplicate Underline extension
alongside StarterKit which causes "Duplicate extension" warnings; locate where
StarterKit is imported/instantiated (StarterKit) and either disable its built-in
underline by calling StarterKit.configure({ underline: false }) and keep the
separate `@tiptap/extension-underline`, or remove the separate Underline import
(and instead configure underline via StarterKit.configure({ underline: { ... }
})); also review any separate Link usage and, if keeping a custom Link
extension, similarly configure StarterKit to avoid duplicating link
functionality.

In `@frontend/src/components/Board/PostDetail/PostHtmlView.jsx`:
- Around line 245-246: Remove or guard the debug console.log calls in the
PostHtmlView component (e.g., the "PostHtmlView - rendered from JSON via
TipTap", "raw json", "raw html", "transformed html" logs) so sensitive post
content is not emitted in production; either delete these console.log lines or
wrap them with a development-only guard (import.meta.env.DEV) around each log,
ensuring only dev builds print them (apply the same change to the other
occurrences around the render block mentioned).
- Line 224: The code in PostHtmlView.jsx sets container.innerHTML = rendered
(and safeHtml elsewhere), which injects un-sanitized user HTML; update
PostHtmlView.jsx to run the output of Editor.getHTML()/transformHtmlImages
through a sanitizer (e.g., DOMPurify) before injecting and switch to React
conventionally rendering with dangerouslySetInnerHTML using the sanitized
string; ensure transformHtmlImages is kept for image normalization but does not
replace the sanitizer, and remove/neutralize inline event handlers, script tags
and javascript: URLs during sanitization.
- Around line 269-277: The ensureVisible function currently strips stored
width/height (it calls img.removeAttribute('width') and sets
img.style.height='auto'), which overrides the preserved sizing applied in
transformHtmlImages; instead change ensureVisible to only modify sizing when the
image actually overflows its container: compute the container width (e.g.,
img.parentElement.clientWidth or img.getBoundingClientRect().width) and if
img.width > containerWidth then set img.style.maxWidth='100%' and optionally
remove the width attribute to force scaling, otherwise leave img.width/height
attributes untouched so the author's saved dimensions remain applied; update the
ensureVisible function accordingly to gate the width removal and height override
behind that overflow check.
- Around line 150-188: ResizableImage currently has width.renderHTML,
height.renderHTML and align.renderHTML each returning overlapping style rules;
change each attribute so width.renderHTML only returns the width attribute,
height.renderHTML only returns the height attribute, and align.renderHTML only
returns the data-align attribute (no style). Then implement a single renderHTML
for the ResizableImage node (within addAttributes or the node definition) that
reads attributes.width/attributes.height/attributes.align and composes a single
style string (width/height and display/margin rules) plus the data-align
attribute, ensuring style composition is centralized and no attribute returns
overlapping CSS.

In `@frontend/src/components/Board/PostDetail/PostHtmlView.module.css`:
- Around line 7-8: Replace the non-standard declaration in the
PostHtmlView.module.css by removing or changing "word-break: break-word"; set
"word-break: normal" and use a standards-compliant overflow-wrap value (e.g.,
"overflow-wrap: anywhere" or keep "overflow-wrap: break-word") so text-wrapping
behavior is preserved across browsers; update the styles where "word-break:
break-word" appears in this file to the recommended combination.

In `@frontend/src/components/Board/PostDetail/PostView.jsx`:
- Around line 28-36: Remove the debug console.log calls inside the useEffect
hooks in PostView.jsx (the useEffect that logs
post?.contentHtml/contentJson/contentText and the other useEffect at the later
block referencing htmlToRender) — either delete those useEffect blocks entirely
or wrap the logging in a development-only guard (e.g., process.env.NODE_ENV ===
'development') so that PostView.jsx no longer prints post content in production;
update the functions named useEffect in this file accordingly and ensure no
other console.log lines remain that leak post content.

In `@frontend/src/components/Board/RichTextEditor.jsx`:
- Around line 404-408: Remove the debug console.log from the onUpdate handler in
the RichTextEditor: inside the onUpdate: ({ editor: currentEditor }) => { ... }
callback delete the line console.log('editor content', nextJson); and keep only
the logic that computes nextJson (currentEditor.getJSON()) and calls
onChangeRef.current?.(nextJson); ensure no other debug logging remains in the
onUpdate function to avoid per-keystroke output and privacy issues.
- Around line 11-12: The editor is importing extensions (Underline, TextAlign,
Link, possibly TextStyle and Highlight) that StarterKit already provides,
causing duplicate extension warnings; fix by either removing the explicit
imports/registration for Underline, Link (and remove
TextAlign/TextStyle/Highlight if StarterKit already covers them) or by
configuring StarterKit to disable those built-ins (use StarterKit.configure({
underline: false, link: false }) and keep the separate imports for custom
behavior) and ensure only one copy of each extension (refer to StarterKit,
Underline, Link, TextAlign, TextStyle, Highlight, and where extensions are
registered in the editor configuration).
- Around line 687-697: The effect uses editor.commands.setContent(nextContent,
false) which is incompatible with TipTap 3.23 — change the call to pass an
options object (editor.commands.setContent(nextContent, { emitUpdate: false }))
and avoid brittle JSON.stringify comparisons by tracking the last applied editor
state: use a ref (e.g., lastAppliedContentRef) to store editor.getJSON() or the
normalized value and compare that ref to nextContent before calling setContent,
or switch to an explicit external revision prop (externalValue/revision) to only
update on external changes; update the useEffect (and references to
normalizeContent, editor.getJSON, value) accordingly to prevent unnecessary
setContent calls that move the cursor.

In `@frontend/src/components/Board/RichTextEditor.module.css`:
- Around line 267-295: 중복된 .imageResizeHandleSE 정의로 앞쪽 블록(right: -9px; bottom:
-9px;)이 무시되고 있으니, .imageResizeHandleSE를 다른 코너
핸들(.imageResizeHandleNW/.imageResizeHandleNE/.imageResizeHandleSW)과 동일한
기준(-7px)으로 통일하거나 의도대로 -9px를 적용하려면 두 정의 중 하나를 제거/병합하세요; 구체적으로는 첫 번째
.imageResizeHandleSE 블록(기존 right: -9px; bottom: -9px;)을 삭제 so that only the
intended .imageResizeHandleSE (right: -7px; bottom: -7px;) remains, or change
both occurrences to the same values to avoid duplicate selectors.

In `@frontend/src/pages/BoardWrite.jsx`:
- Around line 167-200: The effect using loadBoards is being retriggered because
currentBoardId (which is derived from state set inside loadBoards via
setBoardIdMap) is in the dependency array; remove currentBoardId from the
useEffect dependencies and instead derive initialBoardId inside loadBoards using
a safe coalescing expression (e.g., currentBoardId ?? location.state?.boardId)
passed in as a local const or use the functional updater when calling
setSelectedParentId so the effect only depends on location.state?.boardId;
update the useEffect dependency array to [location.state?.boardId] and keep the
functions/sets referenced (loadBoards, setBoardIdMap, setBoardNameMap,
setBoardOptions, setIdToSegment, setSelectedParentId) intact.
- Around line 176-200: The parent-board segment mapping must mirror Board.jsx by
mapping the "all board" name to the 'root' segment; update the loop that builds
idMap/nameMap/options/idSegmentMap (the block using
toBoardRouteSegment(boardName), idMap, nameMap, options, idSegmentMap) to check
for the special isAllBoardName and set segment = 'root' for that case (fall back
to toBoardRouteSegment(boardName) otherwise), then continue assigning
idMap[segment], nameMap[segment], options.push(...), and
idSegmentMap[board.boardId] = segment so setIdToSegment and boardIdMap contain
the 'root' mapping and currentBoardId lookup works.

In `@frontend/src/pages/BoardWrite.module.css`:
- Line 28: Remove the unnecessary quotes around the single-token font name
Pretendard in the font-family declarations to satisfy stylelint's
font-family-name-quotes rule; specifically update the font-family lines that
currently read "font-family: 'Pretendard', -apple-system, BlinkMacSystemFont,
'Segoe UI', sans-serif;" (and the two other identical occurrences) to use
Pretendard without quotes: font-family: Pretendard, -apple-system,
BlinkMacSystemFont, 'Segoe UI', sans-serif;.

In `@frontend/src/pages/PostDetail.jsx`:
- Around line 278-339: The inline TipTap JSON→HTML and text extraction logic
inside handleSaveEdit (the jsonToHtml function and the getText IIFE used to
build payload.contentHtml/contentText before calling boardApi.updateRichPost)
should be moved to a shared utility module (e.g., utils/tiptapJson.js) exporting
two named functions like tiptapJsonToHtml(json) and tiptapJsonToText(json);
replace the inline jsonToHtml and the IIFE with imports and calls to those
utilities in PostDetail.jsx so updateRichPost payload construction remains the
same but the handler is much shorter and reusable (ensure the utilities preserve
the escaping, image attr serialization, and newline behavior currently
implemented).
- Line 150: Remove the debug console.log left in PostDetail.jsx (the try {
console.log('fetched post data:', data); } catch (e) {}) to avoid exposing full
post payload in production; either delete that line or gate it so it only runs
in development (e.g. check NODE_ENV or an isDev flag) inside the PostDetail
component where the fetch/response handling occurs.
- Around line 293-307: The image serializer in renderNode produces invalid HTML
by emitting unit-suffixed width/height attributes and only escaping double
quotes; update renderNode to: parse node.attrs.width/height (from
ResizableImage) to separate numeric values and units, emit width/height
attributes only when the value is a unitless integer or a percentage (strip "px"
and other CSS units before creating the HTML attribute, keep style for px or
other units), and avoid duplicating sizing (prefer style when units are present
and attribute when unitless); additionally fully escape src and alt by replacing
& then < then > then " to HTML entities so URLs and text with & or < don't break
markup, and ensure you still handle Array.isArray(node.content) and the
contentJson.content.map(renderNode) return path.

In `@frontend/src/utils/boardApi.js`:
- Around line 168-175: The updateRichPost implementation currently sends JSON
and drops File objects; change the updateRichPost function to build a FormData
payload: create a new FormData, append scalar fields (e.g., boardId, title,
other metadata) and append contentJson as a string (e.g.,
form.append('contentJson', JSON.stringify(postData.contentJson) or
postData.contentJson if already a string)), loop over postData.files (or
newFiles passed from PostDetail.jsx) and append each File under the expected key
(e.g., form.append('files', file)), then call api.put('/api/board/post', form)
letting the browser/axios set multipart/form-data headers (do not manually set
Content-Type with a boundary); ensure updateRichPost returns response.data like
other API helpers and still handles the case of no files by sending FormData
with only metadata.

In `@frontend/src/utils/imageUtils.js`:
- Around line 16-44: toAbsoluteImageUrl currently rewrites any http(s) absolute
URL to use API_IMAGE_ORIGIN, breaking external-hosted images; update to detect
when src is an absolute URL whose origin differs from resolveImageOrigin() and
in that case return the original src unchanged. Keep existing handling for
data:/blob: URIs, for relative paths (normalize leading slashes and strip
/uploads/images/), and for same-origin absolute URLs (continue to construct
normalizedPath and prefix with API_IMAGE_ORIGIN). Use the URL constructor to
compare new URL(src).origin with API_IMAGE_ORIGIN (or new
URL(API_IMAGE_ORIGIN).origin) to decide whether to pass through.

---

Outside diff comments:
In `@frontend/src/components/Board/PostDetail/PostEditForm.jsx`:
- Around line 107-129: The parent onDrop handler in PostEditForm is receiving
drops already handled by RichTextEditor causing duplicate uploads; update the
drop coordination so only one path processes the file: either have
RichTextEditor call event.preventDefault()/event.stopPropagation() (or set a
flag like event.defaultPrevented) when it inserts images, and in the parent
handleDrop (the onDrop passed to the div around RichTextEditor) early-return if
event.defaultPrevented is true; alternatively, add a ref to the RichTextEditor
root and in the parent handleDrop check if
richEditorRef.current.contains(event.target) and skip adding to newFiles when
true. Ensure you update the RichTextEditor drop handling and the PostEditForm
onDrop (handleDrop/newFiles logic) consistently so images inserted inline by
RichTextEditor are not also appended to the attachment list.

---

Nitpick comments:
In `@frontend/src/components/Board/PostDetail/PostHtmlView.jsx`:
- Line 192: safeHtml is always computed by calling transformHtmlImages(html)
even when html is a TipTap JSON object, causing that work to be wasted and
parser.parseFromString to stringify objects into "[object Object]"; change the
logic so transformHtmlImages is only called for actual HTML strings and skipped
for TipTap JSON. Concretely: in PostHtmlView.jsx, guard the safeHtml calculation
(the const safeHtml = transformHtmlImages(html)) with a check like typeof html
=== 'string' && !isTipTapJson(html) (or detect html being an object with .type
=== 'doc' / a stringified JSON starting with '{' containing "type":"doc"), and
ensure the useEffect branch that initializes the Editor/container.innerHTML
remains consistent with that condition (i.e., if html is TipTap JSON, do not
call transformHtmlImages or parser.parseFromString on it, and instead mount the
TipTap editor content path). Update transformHtmlImages invocations and any
parser.parseFromString usage to only execute when html is confirmed to be an
HTML string.

In `@frontend/src/components/Board/PostDetail/PostView.jsx`:
- Around line 38-51: Replace the IIFE that computes htmlToRender with a useMemo
so the value only changes when post.contentHtml, post.content, or
post.contentJson change; remove the unnecessary try/catch (/<img\s+/i.test(html)
cannot throw) and ensure useMemo imports useMemo from React, so PostHtmlView's
html prop remains stable and avoids retriggering transformHtmlImages and the
related useEffect.

In `@frontend/src/components/Board/RichTextEditor.jsx`:
- Around line 184-187: Replace the comma-expression assignment with explicit
statement blocks: in the RichTextEditor component where attrs is set (the
conditional using isHorizontalOnly, isVerticalOnly, finalWidth, finalHeight and
attrs), change the final else to use a proper block and two separate statements
that assign attrs.width and attrs.height instead of a single comma-separated
expression to avoid readability and future-refactor bugs.
- Around line 199-202: The empty catch in the onUp handler swallows exceptions
(e.g., failures from editor.chain) making debugging impossible; replace the
silent catch in the onUp function inside RichTextEditor.jsx with a minimal
diagnostic log (e.g., console.warn) that includes a short context message and
the caught error object, keeping the handler's behavior (no rethrow) otherwise
unchanged so user flow isn't affected.
- Around line 244-264: The two consecutive map calls rendering identical resize
handle buttons should be merged into a single map over ['e','s','se'] to remove
duplication; update the JSX that uses startResizing, className template strings
(styles.imageResizeHandle and
styles[`imageResizeHandle${direction.toUpperCase()}`]), data-direction and
aria-label so they are created once for each direction, preserving the same key,
onMouseDown={startResizing} and attributes.
- Around line 410-528: The handlePaste function is too large and mixes payload
detection, navigator fallback, and upload logic; refactor by extracting three
helpers: implement extractPastedImages(clipboardData) to encapsulate the
detection logic currently using clipboardFiles, clipboardItems, plainText,
isImageFilenameText, hasImageLikePayload, hasUnknownFilePayload,
shouldTryNavigatorClipboardFallback and htmlImageSrcs (reusing
extractImageSrcsFromHtml), implement uploadPastedImages(files, htmlSrcs) to
perform the setIsUploadingImage / insertImage loop and the
dataUrlToFile/remoteUrlToFile/blobToFile conversion and error handling, and
implement handleNavigatorClipboardFallback() to replace the nested setTimeout +
Promise.resolve block that calls navigator.clipboard.read(), hasClipboardImage,
blobToFile and insertImage; then simplify handlePaste to call these helpers and
return early — remove nested micro/macro task chaining inside handlePaste so the
control flow is clear and testable.

In `@frontend/src/components/Board/RichTextEditor.module.css`:
- Around line 97-101: The .activeButton rule is overusing !important for
background/color/border-color; replace it by increasing selector specificity so
the styles win without !important (e.g., change selectors to .toolbar
button.activeButton or combine selectors like .toolbar button.activeButton,
.toolbar .activeButton) and remove all !important declarations; update any
conflicting .toolbar button rules so the new, more specific selector sets
background, color and border-color as intended.

In `@frontend/src/pages/BoardWrite.jsx`:
- Around line 334-338: Remove the unnecessary state updates immediately after a
successful create: inside the block that calls boardApi.createRichPost (where
you currently call setContentJson(normalizedContentJson) and
setInlineMediaIds(normalizedInlineMediaIds)), delete those two setState calls
because the next line navigate(currentBoardPath) unmounts the component and
makes those updates redundant and causes an extra render; leave the alert('게시글이
작성되었습니다.') and navigate(currentBoardPath) intact.
- Around line 94-138: jsonToHtml duplicates the JSON→HTML logic already
implemented in PostHtmlView (missing marks/lists/links and will diverge if
TipTap schema changes); replace the duplicate by either (A) creating a shared
converter in utils (e.g., utils/jsonToHtml or utils/convertContentToHtml) and
use that from both BoardWrite (jsonToHtml) and PostHtmlView, or (B) in
BoardWrite instantiate a readonly TipTap Editor once with the same schema and
use editor.getHTML() to produce markup; update references to jsonToHtml in
BoardWrite to call the shared util or the editor.getHTML() method and remove the
duplicate rendering logic to keep a single source of truth.
- Around line 268-280: The handlers handleBack and handleBoardChange (and any
similar handlers around lines 442-456) currently navigate or change selection
immediately; update them to first detect unsaved input by checking if title,
contentJson, or attachmentFiles are non-empty and, if so, call window.confirm
with a clear message and only proceed with navigate(currentBoardPath) or
setSelectedParentId/setSelectedSubBoardId when the user confirms; if the user
cancels, abort the action (do not clear sub-board or navigate). Ensure you
reference and modify handleBack and handleBoardChange (and the analogous handler
at 442-456) so the confirmation guards both navigation and parent/sub-board
changes.
- Around line 1-23: The createEmptyDoc helper is declared between two import
groups which breaks import organization; move all import statements to the top
of the module and then place the createEmptyDoc const (and any other local
declarations) after them. Specifically, combine the two import blocks (including
imports for useEffect/useState, boardApi,
normalizeBoardRouteSegment/toBoardRouteSegment, RichTextEditor, styles, and
isDataImageUrl/toAbsoluteImageUrl/dataUrlToFile) at the very top, then define
createEmptyDoc below the imports.

In `@frontend/src/pages/PostDetail.jsx`:
- Around line 130-153: The fallback for initializing editContent uses
contentJson || content || contentText which treats empty/placeholder contentJson
objects as truthy; update the logic where setEditContent is called (in
useEffect's fetchPostAndComments and the updatedPost handling) to first validate
contentJson actually contains meaningful content (e.g., is an object with a
non-empty content array or meaningful fields) and only use it when valid,
otherwise fall back to data.content or data.contentText; ensure the same
validation is applied wherever setEditContent(updatedPost.contentJson || ...) is
used so you don't lose actual plain-text content.

In `@frontend/src/utils/boardApi.js`:
- Around line 181-209: Validate the incoming file in uploadBoardImage by
checking its MIME type (e.g., file.type.startsWith('image/')) and throw a
descriptive error if it's not an image to avoid unnecessary uploads; for both
uploadBoardImage and uploadBoardFile remove the manual headers: {
'Content-Type': 'multipart/form-data' } passed to api.post so axios can auto-set
the multipart boundary when sending the FormData appended with file.

In `@frontend/src/utils/imageUtils.js`:
- Around line 71-76: The remoteUrlToFile function can fail on cross-origin
images due to missing CORS headers; update remoteUrlToFile to catch fetch/blob
errors and return a clear failure signal (e.g., null or a thrown Error with a
descriptive message) instead of letting a raw network error bubble up, document
that extractImageSrcsFromHtml callers should handle nulls, and consider
adding/pointing to a server-side proxy endpoint fallback for external images;
ensure RichTextEditor.jsx (where catch already exists) displays a user-friendly
message when remoteUrlToFile indicates a CORS/fetch failure and keep blobToFile
usage unchanged.
- Around line 57-69: The file extension extraction in dataUrlToFile and
blobToFile is fragile (e.g., "image/svg+xml" -> "svg+xml", "image/jpeg" ->
"jpeg"); update both functions to sanitize the MIME subtype by splitting on '+'
and taking the leftmost token, then map common subtypes to canonical extensions
(e.g., "jpeg" -> "jpg", "svg" -> "svg", "png" -> "png", "webp" -> "webp") via a
small lookup table; use that resolved extension when building normalizedName and
keep blob.type as the File type fallback.
🪄 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: 2b8cb6c6-2196-4486-9e78-4c84c8f98302

📥 Commits

Reviewing files that changed from the base of the PR and between 1bfbb49 and f5cc527.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • frontend/package.json
  • frontend/src/App.jsx
  • frontend/src/components/Board/PostDetail/PostEditForm.jsx
  • frontend/src/components/Board/PostDetail/PostHtmlView.jsx
  • frontend/src/components/Board/PostDetail/PostHtmlView.module.css
  • frontend/src/components/Board/PostDetail/PostView.jsx
  • frontend/src/components/Board/RichTextEditor.jsx
  • frontend/src/components/Board/RichTextEditor.module.css
  • frontend/src/pages/Board.jsx
  • frontend/src/pages/BoardWrite.jsx
  • frontend/src/pages/BoardWrite.module.css
  • frontend/src/pages/PostDetail.jsx
  • frontend/src/utils/boardApi.js
  • frontend/src/utils/imageUtils.js

Comment thread frontend/package.json
Comment thread frontend/src/components/Board/PostDetail/PostHtmlView.jsx
Comment thread frontend/src/components/Board/PostDetail/PostHtmlView.jsx
Comment thread frontend/src/components/Board/PostDetail/PostHtmlView.jsx
Comment thread frontend/src/components/Board/PostDetail/PostHtmlView.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/utils/boardApi.js
Comment thread frontend/src/utils/imageUtils.js
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.

Pull request overview

This PR introduces a TipTap-based rich text editor to the board post creation/editing flow, adding formatting controls and enabling image insertion via copy/paste, drag-and-drop, and upload.

Changes:

  • Add a new dedicated write page (/board/.../write) using RichTextEditor, and update the board list page to navigate there instead of opening a modal.
  • Add rich-post API helpers (createRichPost, updateRichPost) plus image/file upload endpoints, and utilities for normalizing image URLs and pasted images.
  • Update post detail view/edit to support rendering and editing rich content (HTML/TipTap JSON).

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
frontend/src/utils/imageUtils.js Adds helpers for detecting/normalizing image sources and converting pasted images to File.
frontend/src/utils/boardApi.js Adds rich-post create/update APIs and new upload endpoints for inline images and attachments.
frontend/src/pages/PostDetail.jsx Loads contentJson for editing and adds a rich update path.
frontend/src/pages/BoardWrite.module.css Styles the new write page layout.
frontend/src/pages/BoardWrite.jsx New write page using TipTap JSON, inline image normalization/upload, and attachment uploads.
frontend/src/pages/Board.jsx Replaces the write modal with navigation to the write page; minor pagination safety.
frontend/src/components/Board/RichTextEditor.module.css Adds styling for the TipTap toolbar/editor and image resizing UI.
frontend/src/components/Board/RichTextEditor.jsx New TipTap editor component with formatting controls and robust paste/drop image handling.
frontend/src/components/Board/PostDetail/PostView.jsx Switches post body rendering to PostHtmlView.
frontend/src/components/Board/PostDetail/PostHtmlView.module.css Styles rendered rich post content (paragraphs/images).
frontend/src/components/Board/PostDetail/PostHtmlView.jsx Renders post content as HTML or TipTap JSON and normalizes image URLs.
frontend/src/components/Board/PostDetail/PostEditForm.jsx Replaces textarea with RichTextEditor for editing posts.
frontend/src/App.jsx Adds routes for the new write page.
frontend/package.json Adds TipTap dependencies.
frontend/package-lock.json Locks new TipTap dependency tree.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/pages/BoardWrite.jsx
Comment thread frontend/src/pages/BoardWrite.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/components/Board/RichTextEditor.module.css
Comment thread frontend/src/pages/BoardWrite.jsx
Comment thread frontend/src/pages/PostDetail.jsx
Comment thread frontend/src/components/Board/PostDetail/PostEditForm.jsx
Comment thread frontend/src/components/Board/PostDetail/PostEditForm.jsx
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 f787258 into main May 11, 2026
5 checks 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