20260509 fe 366 기능개선fe 글작성 페이지 개선#368
Hidden character warning
Conversation
Co-authored-by: Copilot <copilot@github.com>
작업 요약이 PR은 보드 포스트 작성 및 편집 시스템에 TipTap 기반의 리치 텍스트 에디터를 도입합니다. 모달 기반의 인라인 작성 방식을 제거하고 라우팅 기반의 전용 페스트 작성 페이지로 전환하며, 이미지 업로드, 텍스트 포맷팅, 콘텐츠 정규화를 지원하는 완전한 에디터 시스템을 구현합니다. 변경 사항리치 에디터 및 보드 포스트 기능
예상되는 코드 리뷰 노력🎯 4 (Complex) | ⏱️ ~60 minutes 관련 PR
권장 리뷰어
축하 시
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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내부 드롭과 중첩됩니다.
RichTextEditor의editorProps.handleDrop이 이미지 파일을 인라인 이미지로 업로드/삽입한 뒤에도 DOM 이벤트는 부모로 계속 버블링됩니다. 그 결과 사용자가 에디터 영역 위에 이미지 파일을 드롭하면:
- 본문에 인라인 이미지로 삽입되고,
- 동시에 부모
handleDrop이 동작해 같은 파일이 첨부파일 목록(newFiles)에도 추가됩니다.이미지 본문 삽입 흐름과 첨부파일 흐름이 의도적으로 분리된 것이라면 명확히 구분되도록 처리가 필요합니다. 예시:
RichTextEditor의handleDrop에서 처리한 이벤트의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남용 — 활성 상태 표현을 보다 명확한 캐스케이드로 정리해보세요.
.activeButton이background/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을 제공하지 않으면 네트워크 에러로 실패합니다. 붙여넣기 흐름(extractImageSrcsFromHtml→remoteUrlToFile)에서 외부 이미지가 빈번하다면 서버측 프록시 엔드포인트를 두고 그 경유로 받아오는 방식을 검토해주세요. 최소한 호출부에서 실패를 부드럽게 처리하고(이미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 winMIME 서브타입에
+등이 포함되면 확장자 추출이 부정확합니다.
blob.type이image/svg+xml인 경우split('/')[1]은'svg+xml'을 반환하여.svg+xml같은 잘못된 확장자가 붙습니다. 또한image/jpeg는jpeg로 추출되지만 일반적으로 파일 확장자는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 헤더 제거 권장
uploadBoardImage와uploadBoardFile함수의 두 가지 개선점을 제안합니다:
파일 타입 검증:
uploadBoardImage에서는 이미지 파일 여부를 명시적으로 확인하여 불필요한 네트워크 요청을 줄이고 사용자 경험을 개선할 수 있습니다.수동 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
jsonToHtml이PostHtmlView의 변환 로직과 중복됩니다.
frontend/src/components/Board/PostDetail/PostHtmlView.jsx의 JSON→HTML 분기와 거의 동일한 코드입니다(text/paragraph/heading/image만 처리, 마크/리스트/링크/색상/정렬 등은 누락). 한쪽이라도 TipTap 스키마 확장이 생기면 다른 쪽과 동기화가 어긋납니다. 더 안전하게는BoardWrite도 readonly TipTapEditor를 일회성으로 만들어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 winimport 문이 다른 코드 사이에 흩어져 있습니다.
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 />의htmlprop 참조가 매번 바뀝니다.transformHtmlImages와useEffect가 다시 도는 원인이 됩니다. 또한/<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 valueJSON 본문일 때
transformHtmlImages가 헛돌고 있습니다.
safeHtml은 렌더 시점에 항상 계산되지만,html이 TipTap JSON 객체이면useEffect에서 TipTapEditor로 새로 렌더해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
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (14)
frontend/package.jsonfrontend/src/App.jsxfrontend/src/components/Board/PostDetail/PostEditForm.jsxfrontend/src/components/Board/PostDetail/PostHtmlView.jsxfrontend/src/components/Board/PostDetail/PostHtmlView.module.cssfrontend/src/components/Board/PostDetail/PostView.jsxfrontend/src/components/Board/RichTextEditor.jsxfrontend/src/components/Board/RichTextEditor.module.cssfrontend/src/pages/Board.jsxfrontend/src/pages/BoardWrite.jsxfrontend/src/pages/BoardWrite.module.cssfrontend/src/pages/PostDetail.jsxfrontend/src/utils/boardApi.jsfrontend/src/utils/imageUtils.js
There was a problem hiding this comment.
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) usingRichTextEditor, 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.
글 작성시 서식을 지정할 수 있도록 추가
이미지도 간단히 복사 붙여넣기로 가능하도록 추가
Summary by CodeRabbit
릴리스 노트