[Feat] WTH-136 : 마크다운 기반 게시글 작성 기능 구현#5
Conversation
…o WTH-136-마크다운-기반-게시글-작성-기능-구현
- useRef + scrollIntoView로 키보드 탐색 시 선택 항목 자동 스크롤 - data-index 속성으로 선택된 버튼 요소 탐색 - 하드코딩 gray-* 색상을 디자인 토큰으로 교체
- showSlashMenuRef로 handleKeyDown stale closure 방지 - onSelectionUpdate로 커서 이동 시 슬래시 메뉴 자동 닫기 - Backspace 시 listItem 처리 제거 (Tiptap 자체 처리) - 툴바 버튼 디자인 토큰 적용
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughTipTap 기반의 클라이언트 에디터 도입: 의존성 추가, 에디터 컴포넌트·확장·훅·UI(슬래시·버블 메뉴) 구현, Zustand 포스트 스토어 및 ProseMirror 전역 스타일 추가. (파일/공개 시그니처 변경 없음) Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BoardPage
participant Editor
participant TipTap
participant usePostStore
participant SlashMenu
User->>BoardPage: 페이지 접속
BoardPage->>Editor: dynamic 로드 (클라이언트)
Editor->>TipTap: 에디터 초기화 (extensions)
TipTap-->>Editor: 에디터 인스턴스 반환
Editor->>usePostStore: setContent(initial)
User->>Editor: 텍스트 입력
Editor->>TipTap: 입력 반영
TipTap-->>Editor: onUpdate 이벤트
Editor->>usePostStore: setContent(updated)
User->>Editor: '/' 입력
Editor->>SlashMenu: FloatingMenu 표시 (showSlashMenu=true)
User->>SlashMenu: 항목 선택
SlashMenu->>TipTap: 선택된 커맨드 실행 (삭제 후 명령)
TipTap-->>Editor: 콘텐츠 변경
Editor->>usePostStore: setContent(updated)
SlashMenu->>SlashMenu: 메뉴 닫기
sequenceDiagram
participant User
participant Editor
participant BubbleMenu
participant TipTap
participant usePostStore
User->>Editor: 텍스트 선택/포인터 이동
Editor->>BubbleMenu: 상태 업데이트
BubbleMenu-->>User: 포매팅 툴 표시
User->>BubbleMenu: 'Bold' 클릭
BubbleMenu->>TipTap: toggleBold()
TipTap-->>Editor: 스타일 변경
Editor->>usePostStore: setContent(updated)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
PR 검증 결과❌ TypeScript: 실패 |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`(private)/(main)/board/page.tsx:
- Around line 8-10: The main element in page.tsx is using a hardcoded utility
class p-8; replace it with the spacing design token class (e.g., p-200 or the
appropriate p-100~500 value) to comply with the spacing tokens, e.g., change the
<main className="mx-auto max-w-3xl p-8"> usage to use a token like p-200 (or
add/extend token styles if none match the required spacing), keeping other
classes (mx-auto, max-w-3xl) and leaving the <Editor /> component unchanged.
In `@src/app/globals.css`:
- Around line 2-4: Stylelint is flagging unknown at-rules (`@custom-variant`,
`@plugin`, `@import` 'tw-animate-css') in globals.css; fix by either adding a
per-file disable comment for at-rule-no-unknown or updating the stylelint config
to allow these at-rules (e.g., register customAtRules or plugins) so
`@custom-variant`, `@plugin` and the tw-animate-css import are accepted; locate the
occurrences of "@custom-variant", "@plugin" and the "@import 'tw-animate-css'"
line and apply the chosen fix consistently.
- Around line 494-542: Several ProseMirror rules (.ProseMirror blockquote,
.ProseMirror code:not(pre code), .ProseMirror pre, .ProseMirror pre code,
.ProseMirror ul[data-type='taskList'] li p) use hardcoded spacing and radius
values (rem/px); replace those with existing design token CSS variables (e.g.
spacing tokens like --space-2/--space-4 or radius tokens like
--radius-sm/--radius-md) or add new tokens in the globals.css token section, and
then reference the tokens (or `@utility` classes you define) instead of literal
values for padding, margin and border-radius to comply with tokenization rules.
In `@src/components/board/Editor/index.tsx`:
- Around line 156-248: The bubble menu button classes are using inline template
literals and raw spacing/typography utilities; refactor by extracting a
cva-based variant (e.g., BubbleMenuButton = cva(...)) for the common button
styles and tokenized variants (active/inactive) and replace each button's
className with cn(BubbleMenuButton({ state: editor.isActive(...) ? 'active' :
'default' }), additionalTokenIfNeeded). Update all buttons inside the BubbleMenu
(the Bold/Italic/Code buttons and the H1/H2/H3 heading toggles) to use cn from
'@/lib/cn' and the new BubbleMenuButton cva variants, and replace raw tokens
like p-1, px-2, gap-0.5, text-sm with the design token classes (p-100~500,
gap-100~400, typo-button*/typo-h1~h3 etc.) so styles are unified via cn() + cva.
- Around line 268-272: The EditorContent wrapper and its parent div use raw
Tailwind utilities (pl-14, prose-gray, text-gray-400, text-(--h1-size), etc.)
that violate design-token rules; update the parent div's padding (replace pl-14)
with the tokenized spacing utility (e.g., p-100/p-l-100 as per your token set)
and replace all typography and color utilities in the EditorContent className
with the design token classes (use typo-h1/typo-h2/typo-h3 for headings,
appropriate typo-body/typo-* for paragraphs, and
text-text-strong/text-text-normal/text-text-alternative for colors) and ensure
any placeholder/pseudo rules keep the tokenized text classes instead of
text-gray-400; locate the className on the EditorContent component and the
surrounding div to make these substitutions.
In `@src/components/board/Editor/SlashMenu.tsx`:
- Around line 106-141: In SlashMenu (rendering GROUPS.map and each item button)
replace the template-literal className concatenation on the item <button> and
the inner <span> and <p> tags with the cn() helper imported from '@/lib/cn', and
swap raw spacing/typography classes (px-3, py-2, gap-3, text-sm, text-xs, h-8,
w-8, etc.) for your design token classes (spacing p-100~500, gap-100~400 and
typography typo-* such as typo-body1/2 or typo-caption1/2) so e.g. the
conditional selected vs hover classes remain wrapped by cn(...) while using
tokens for padding, gap and text sizes; ensure handleSelect, selectedIndex and
runningIndex logic is unchanged and keep the data-index and onMouseDown behavior
intact.
In `@src/constants/editor.ts`:
- Around line 4-67: MenuItem currently types icon as string and
STYLE_ITEMS/INSERT_ITEMS use string glyphs; change MenuItem.icon to LucideIcon |
React.ReactNode, replace string icons in STYLE_ITEMS and INSERT_ITEMS with
corresponding Lucide React components (e.g., Type, TypeH1/Heading variants,
List, ListOrdered, CheckSquare, Quote, Code, Minus for separator) and update
SlashMenu.tsx render logic to render the icon component instead of printing a
string; ensure imports from 'lucide-react' are added where MenuItem,
STYLE_ITEMS, INSERT_ITEMS, and SlashMenu are defined.
| return ( | ||
| <main className="mx-auto max-w-3xl p-8"> | ||
| <Editor /> |
There was a problem hiding this comment.
메인 패딩은 토큰 간격 클래스로 맞춰주세요.
p-8 대신 토큰 간격(p-100~500)으로 치환하거나 필요한 경우 토큰을 확장해서 사용해주세요.
As per coding guidelines, Use spacing design token classes: p-100~500, gap-100~400.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(private)/(main)/board/page.tsx around lines 8 - 10, The main
element in page.tsx is using a hardcoded utility class p-8; replace it with the
spacing design token class (e.g., p-200 or the appropriate p-100~500 value) to
comply with the spacing tokens, e.g., change the <main className="mx-auto
max-w-3xl p-8"> usage to use a token like p-200 (or add/extend token styles if
none match the required spacing), keeping other classes (mx-auto, max-w-3xl) and
leaving the <Editor /> component unchanged.
| @import 'tw-animate-css'; | ||
| @custom-variant dark (&:is(.dark *)); | ||
| @plugin "@tailwindcss/typography"; |
There was a problem hiding this comment.
Stylelint 에러(@custom-variant/@plugin) 처리 필요.
현재 stylelint가 unknown at-rule로 에러를 내고 있어 린트 실패 가능성이 큽니다. 파일 단위 예외 처리 또는 stylelint 설정에서 해당 at-rule을 허용해주세요.
🔧 예시 수정
`@import` 'tailwindcss';
`@import` 'tw-animate-css';
+/* stylelint-disable-next-line scss/at-rule-no-unknown */
`@custom-variant` dark (&:is(.dark *));
+/* stylelint-disable-next-line scss/at-rule-no-unknown */
`@plugin` "@tailwindcss/typography";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @import 'tw-animate-css'; | |
| @custom-variant dark (&:is(.dark *)); | |
| @plugin "@tailwindcss/typography"; | |
| `@import` 'tw-animate-css'; | |
| /* stylelint-disable-next-line scss/at-rule-no-unknown */ | |
| `@custom-variant` dark (&:is(.dark *)); | |
| /* stylelint-disable-next-line scss/at-rule-no-unknown */ | |
| `@plugin` "@tailwindcss/typography"; |
🧰 Tools
🪛 Stylelint (17.3.0)
[error] 3-3: Unexpected unknown at-rule "@custom-variant" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
[error] 4-4: Unexpected unknown at-rule "@plugin" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/globals.css` around lines 2 - 4, Stylelint is flagging unknown
at-rules (`@custom-variant`, `@plugin`, `@import` 'tw-animate-css') in globals.css;
fix by either adding a per-file disable comment for at-rule-no-unknown or
updating the stylelint config to allow these at-rules (e.g., register
customAtRules or plugins) so `@custom-variant`, `@plugin` and the tw-animate-css
import are accepted; locate the occurrences of "@custom-variant", "@plugin" and
the "@import 'tw-animate-css'" line and apply the chosen fix consistently.
| <div className="relative pl-14"> | ||
| <EditorContent | ||
| editor={editor} | ||
| className="prose prose-gray max-w-none [&_.ProseMirror]:min-h-[400px] [&_.ProseMirror]:focus:outline-none [&_.ProseMirror_h1]:leading-(--h1-line-height) [&_.ProseMirror_h1]:text-(--h1-size) [&_.ProseMirror_h2]:leading-(--h2-line-height) [&_.ProseMirror_h2]:text-(--h2-size) [&_.ProseMirror_h3]:leading-(--h3-line-height) [&_.ProseMirror_h3]:text-(--h3-size) [&_.ProseMirror_p.is-empty::before]:pointer-events-none [&_.ProseMirror_p.is-empty::before]:float-left [&_.ProseMirror_p.is-empty::before]:h-0 [&_.ProseMirror_p.is-empty::before]:text-gray-400 [&_.ProseMirror_p.is-empty::before]:content-[attr(data-placeholder)] [&_.ProseMirror_ul[data-type=taskList]]:my-0 [&_.ProseMirror_ul[data-type=taskList]]:list-none [&_.ProseMirror_ul[data-type=taskList]_li]:my-0 [&_.ProseMirror_ul[data-type=taskList]_li]:flex [&_.ProseMirror_ul[data-type=taskList]_li]:items-center [&_.ProseMirror_ul[data-type=taskList]_li]:gap-3 [&_.ProseMirror>*]:my-3" | ||
| /> |
There was a problem hiding this comment.
EditorContent의 타이포/컬러 토큰 적용이 필요합니다.
prose-gray, text-gray-400, text-(--h1-size) 등 비토큰 스타일이 섞여 있어 디자인 토큰 규칙을 위반합니다. typo-h1~h3 유틸리티와 text-text-* 토큰으로 치환하고, pl-14도 토큰화된 간격 클래스(또는 토큰 기반 유틸리티)로 맞춰주세요.
🔧 예시 수정
- className="prose prose-gray max-w-none [&_.ProseMirror]:min-h-[400px] [&_.ProseMirror]:focus:outline-none [&_.ProseMirror_h1]:leading-(--h1-line-height) [&_.ProseMirror_h1]:text-(--h1-size) [&_.ProseMirror_h2]:leading-(--h2-line-height) [&_.ProseMirror_h2]:text-(--h2-size) [&_.ProseMirror_h3]:leading-(--h3-line-height) [&_.ProseMirror_h3]:text-(--h3-size) [&_.ProseMirror_p.is-empty::before]:pointer-events-none [&_.ProseMirror_p.is-empty::before]:float-left [&_.ProseMirror_p.is-empty::before]:h-0 [&_.ProseMirror_p.is-empty::before]:text-gray-400 [&_.ProseMirror_p.is-empty::before]:content-[attr(data-placeholder)] ..."
+ className="prose max-w-none [&_.ProseMirror]:min-h-[400px] [&_.ProseMirror]:focus:outline-none [&_.ProseMirror_h1]:typo-h1 [&_.ProseMirror_h2]:typo-h2 [&_.ProseMirror_h3]:typo-h3 [&_.ProseMirror_p.is-empty::before]:pointer-events-none [&_.ProseMirror_p.is-empty::before]:float-left [&_.ProseMirror_p.is-empty::before]:h-0 [&_.ProseMirror_p.is-empty::before]:text-text-disabled [&_.ProseMirror_p.is-empty::before]:content-[attr(data-placeholder)] ..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/index.tsx` around lines 268 - 272, The
EditorContent wrapper and its parent div use raw Tailwind utilities (pl-14,
prose-gray, text-gray-400, text-(--h1-size), etc.) that violate design-token
rules; update the parent div's padding (replace pl-14) with the tokenized
spacing utility (e.g., p-100/p-l-100 as per your token set) and replace all
typography and color utilities in the EditorContent className with the design
token classes (use typo-h1/typo-h2/typo-h3 for headings, appropriate
typo-body/typo-* for paragraphs, and
text-text-strong/text-text-normal/text-text-alternative for colors) and ensure
any placeholder/pseudo rules keep the tokenized text classes instead of
text-gray-400; locate the className on the EditorContent component and the
surrounding div to make these substitutions.
| return ( | ||
| <div className="border-line bg-container-neutral w-64 overflow-hidden rounded-lg border shadow-xl"> | ||
| <div ref={scrollContainerRef} className="max-h-80 overflow-x-hidden overflow-y-auto"> | ||
| {GROUPS.map((group, groupIdx) => ( | ||
| <div key={group.title}> | ||
| <div className={`px-3 pt-2 pb-1 ${groupIdx !== 0 ? 'border-line border-t' : ''}`}> | ||
| <p className="text-text-disabled text-xs font-semibold tracking-wider uppercase"> | ||
| {group.title} | ||
| </p> | ||
| </div> | ||
|
|
||
| {group.items.map((item) => { | ||
| const currentIndex = runningIndex++; | ||
| const isSelected = currentIndex === selectedIndex; | ||
|
|
||
| return ( | ||
| <button | ||
| key={item.label} | ||
| type="button" | ||
| data-index={currentIndex} | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| handleSelect(item); | ||
| }} | ||
| className={`flex w-full items-center gap-3 px-3 py-2 text-left transition-colors ${ | ||
| isSelected | ||
| ? 'bg-container-neutral-interaction' | ||
| : 'hover:bg-container-neutral-alternative' | ||
| }`} | ||
| > | ||
| <span className="border-line bg-container-neutral-alternative text-text-alternative flex h-8 w-8 shrink-0 items-center justify-center rounded border text-sm font-medium"> | ||
| {item.icon} | ||
| </span> | ||
| <div> | ||
| <p className="text-text-strong text-sm font-medium">{item.label}</p> | ||
| <p className="text-text-disabled text-xs">{item.description}</p> |
There was a problem hiding this comment.
className 결합 및 간격/타이포 토큰 적용이 필요합니다.
템플릿 리터럴로 클래스 결합 중이며, px-3, py-2, gap-3, text-sm/xs 등 비토큰 간격/타이포가 섞여 있습니다. cn()으로 결합을 통일하고, 간격·타이포는 p-100~500, gap-100~400, typo-* 토큰 클래스로 치환해주세요.
🔧 예시 수정
+import { cn } from '@/lib/cn';
...
-<div className={`px-3 pt-2 pb-1 ${groupIdx !== 0 ? 'border-line border-t' : ''}`}>
+<div className={cn('px-3 pt-2 pb-1', groupIdx !== 0 && 'border-line border-t')}>
...
- className={`flex w-full items-center gap-3 px-3 py-2 text-left transition-colors ${
- isSelected ? 'bg-container-neutral-interaction' : 'hover:bg-container-neutral-alternative'
- }`}
+ className={cn(
+ 'flex w-full items-center gap-3 px-3 py-2 text-left transition-colors',
+ isSelected ? 'bg-container-neutral-interaction' : 'hover:bg-container-neutral-alternative',
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/SlashMenu.tsx` around lines 106 - 141, In
SlashMenu (rendering GROUPS.map and each item button) replace the
template-literal className concatenation on the item <button> and the inner
<span> and <p> tags with the cn() helper imported from '@/lib/cn', and swap raw
spacing/typography classes (px-3, py-2, gap-3, text-sm, text-xs, h-8, w-8, etc.)
for your design token classes (spacing p-100~500, gap-100~400 and typography
typo-* such as typo-body1/2 or typo-caption1/2) so e.g. the conditional selected
vs hover classes remain wrapped by cn(...) while using tokens for padding, gap
and text sizes; ensure handleSelect, selectedIndex and runningIndex logic is
unchanged and keep the data-index and onMouseDown behavior intact.
| export const STYLE_ITEMS: MenuItem[] = [ | ||
| { | ||
| label: 'Text', | ||
| description: '일반 텍스트', | ||
| icon: 'T', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().clearNodes().setParagraph().run(), | ||
| }, | ||
| { | ||
| label: 'Heading 1', | ||
| description: '큰 제목', | ||
| icon: 'H1', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleHeading({ level: 1 }).run(), | ||
| }, | ||
| { | ||
| label: 'Heading 2', | ||
| description: '중간 제목', | ||
| icon: 'H2', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleHeading({ level: 2 }).run(), | ||
| }, | ||
| { | ||
| label: 'Heading 3', | ||
| description: '작은 제목', | ||
| icon: 'H3', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleHeading({ level: 3 }).run(), | ||
| }, | ||
| { | ||
| label: 'Bullet List', | ||
| description: '순서 없는 목록', | ||
| icon: '•', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleBulletList().run(), | ||
| }, | ||
| { | ||
| label: 'Numbered List', | ||
| description: '순서 있는 목록', | ||
| icon: '1.', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleOrderedList().run(), | ||
| }, | ||
| { | ||
| label: 'To-do List', | ||
| description: '체크리스트', | ||
| icon: '☑', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleTaskList().run(), | ||
| }, | ||
| { | ||
| label: 'Blockquote', | ||
| description: '인용', | ||
| icon: '"', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleBlockquote().run(), | ||
| }, | ||
| { | ||
| label: 'Code Block', | ||
| description: '코드 블록', | ||
| icon: '</>', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().toggleCodeBlock().run(), | ||
| }, | ||
| ]; | ||
|
|
||
| export const INSERT_ITEMS: MenuItem[] = [ | ||
| { | ||
| label: 'Separator', | ||
| description: '구분선', | ||
| icon: '—', | ||
| command: (editor: TiptapEditor) => editor.chain().focus().setHorizontalRule().run(), | ||
| }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
# Find MenuItem interface definition
rg -n "interface MenuItem|type MenuItem" srcRepository: Team-Weeth/weeth-client
Length of output: 116
🏁 Script executed:
# Check how STYLE_ITEMS and INSERT_ITEMS are used across the codebase
rg -n "STYLE_ITEMS|INSERT_ITEMS" src --type ts --type tsx -A 2Repository: Team-Weeth/weeth-client
Length of output: 94
🏁 Script executed:
# Search for icon usage in menu-related components
rg -n "\.icon" src --type ts --type tsx -B 1 -A 1Repository: Team-Weeth/weeth-client
Length of output: 94
🏁 Script executed:
# Read MenuItem interface definition
cat -n src/types/editor.tsRepository: Team-Weeth/weeth-client
Length of output: 307
🏁 Script executed:
# Check STYLE_ITEMS and INSERT_ITEMS usage without tsx flag
rg -n "STYLE_ITEMS|INSERT_ITEMS" src -A 2Repository: Team-Weeth/weeth-client
Length of output: 897
🏁 Script executed:
# Check icon property usage
rg -n "\.icon" src -B 1 -A 1Repository: Team-Weeth/weeth-client
Length of output: 451
🏁 Script executed:
# Check if Lucide React is already installed in the project
rg -n "lucide-react" src -i | head -20Repository: Team-Weeth/weeth-client
Length of output: 231
🏁 Script executed:
# Check package.json for lucide-react dependency
cat package.json | grep -A 5 -B 5 "lucide"Repository: Team-Weeth/weeth-client
Length of output: 394
MenuItem 아이콘을 Lucide React 컴포넌트로 변환해주세요.
현재 icon이 문자열로 정의되어 있어 코딩 가이드라인(Lucide React 사용)을 위반합니다. 다음 변경이 필요합니다:
MenuItem인터페이스의icon타입을string에서LucideIcon또는React.ReactNode로 변경STYLE_ITEMS와INSERT_ITEMS의 아이콘을 Lucide 컴포넌트로 매핑 (예:T→Type,H1→Heading1,•→List)SlashMenu.tsx의 렌더링 로직을 아이콘 컴포넌트 렌더링으로 수정
Lucide React는 이미 프로젝트에 설치되어 있으므로 즉시 적용 가능합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/constants/editor.ts` around lines 4 - 67, MenuItem currently types icon
as string and STYLE_ITEMS/INSERT_ITEMS use string glyphs; change MenuItem.icon
to LucideIcon | React.ReactNode, replace string icons in STYLE_ITEMS and
INSERT_ITEMS with corresponding Lucide React components (e.g., Type,
TypeH1/Heading variants, List, ListOrdered, CheckSquare, Quote, Code, Minus for
separator) and update SlashMenu.tsx render logic to render the icon component
instead of printing a string; ensure imports from 'lucide-react' are added where
MenuItem, STYLE_ITEMS, INSERT_ITEMS, and SlashMenu are defined.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/app/`(private)/(main)/board/page.tsx:
- Line 9: Replace the out-of-range spacing token "p-700" on the <main> element
(className="mx-auto max-w-3xl p-700") with an allowed spacing design token
(p-100 through p-500) or update the design token config if you intentionally
need a larger value; change the className to use an approved token (e.g., p-500)
or add a documented token expansion in the spacing tokens configuration and
reference that new token instead of "p-700".
In `@src/app/globals.css`:
- Around line 522-526: Replace the hardcoded border thickness in the
.ProseMirror blockquote rule (the border-left: 3px declaration) with a design
token CSS variable (e.g., var(--border-width-300) or a new token like
--blockquote-border) and add that token definition and an associated utility
class in src/app/globals.css; locate the .ProseMirror blockquote selector to
update border-left to use the token and then declare the new --blockquote-border
(or map it to an existing --border-width token) near the other design tokens and
add an `@utility` class for reuse.
- Around line 2-4: Stylelint flags unknown at-rules (`@custom-variant`, `@plugin`)
in globals.css; either add a file-level exception or register the custom
at-rules in stylelint config. Fix by updating stylelint config (rule
at-rule-no-unknown -> set ignoreAtRules to include "custom-variant" and "plugin"
and any other Tailwind-specific at-rules) or add a top-of-file stylelint disable
comment to globals.css to ignore these at-rules; ensure you reference the exact
tokens "@custom-variant" and "@plugin" when adding them to the ignore list so
the linter stops failing.
In `@src/components/board/Editor/index.tsx`:
- Around line 256-260: The EditorContent wrapper currently uses non-token
spacing classes (e.g., pl-800, gap-3, my-3) in the className; replace those with
approved spacing design tokens (p-100..p-500 for padding, gap-100..gap-400 for
gaps, and appropriate vertical margin tokens such as my-100..my-500) and update
the className string on the EditorContent element so all spacing classes conform
to the project's token ranges (refer to the EditorContent component and the
surrounding div that currently uses pl-800 to locate the change).
In `@src/components/board/Editor/SlashMenu.tsx`:
- Around line 106-142: The className usage mixes template literals and raw
utility classes and needs to use cn() and design tokens: import { cn } from
'@/lib/cn', then replace manual concatenation in GROUPS rendering (the outer
div, group header div, each item's button, the icon span and the two <p> labels
inside the item) to use cn() for merging conditional classes (e.g., the
isSelected background toggle) and swap spacing/typography utilities (px-3,
gap-3, py-2, text-xs, text-sm, etc.) with the project's spacing and typography
tokens (p-100~500, gap-100~400 and appropriate typo-* classes) while preserving
existing conditional logic that uses runningIndex, selectedIndex and the
onMouseDown -> handleSelect flow so behavior of handleSelect, Icon rendering and
data-index remain unchanged.
JIN921
left a comment
There was a problem hiding this comment.
useEffect의 클린업이나 useMemo를 사용한 리렌더 방지가 잘 되어 있어, zustand 관련해 말씀 드린 부분 빼면 고칠 부분은 없어 보입니다 수고하셨어요!!
슬래쉬로 마크다운 작성이 이런식으로 이루어 지는군요 신기해요 👀👀
(근데 슬래쉬 메뉴에 스크롤 없으면 더 예쁠 거 같습니다 ㅎ.ㅎ,, )
src/stores/usePostStore.ts
Outdated
| status: 'DRAFT' as const, | ||
| }; | ||
|
|
||
| export const usePostStore = create<PostState>((set, get) => ({ |
There was a problem hiding this comment.
요거 상태 추적이랑 타입 정의를 위해서 devtools랑 combine 사용하면 더 좋을 거 같아요!
There was a problem hiding this comment.
옹 devtools는 redux에서만 사용 가능한 줄 알앗는데 zustand에도 적용 가능하군요,,, 처음 알게 됐네용 말씀해주신 대로 combine도 같이 적용해서 상태/액션 구조 정리해두었습니다 !! 감사합니당 👍
src/stores/usePostStore.ts
Outdated
| // Actions | ||
| setBoard: (board: string) => void; | ||
| setTitle: (title: string) => void; | ||
| setCohort: (cohort: number) => void; |
There was a problem hiding this comment.
보다가 궁금해서 그러는데 이건 뭘 설정해 주는 걸까요..?? cohort..??? 집단 무리..??
There was a problem hiding this comment.
앗 기수 번호를 나타내는 값인데... cardinalNumber로 통일해서 적용해 놓겠습니다! 통일이 안 되어 잇엇네요 ... ...ㅜㅜ
| const GROUPS = useMemo( | ||
| () => [ | ||
| { title: 'Style', items: STYLE_ITEMS }, | ||
| { title: 'Insert', items: INSERT_ITEMS }, | ||
| ], | ||
| [], | ||
| ); | ||
|
|
||
| const flatItems = useMemo(() => GROUPS.flatMap((group) => group.items), [GROUPS]); |
There was a problem hiding this comment.
useMemo 사용된 부분 보면서 든 생각인데, React 19부터 자동으로 최적화해주는 ReactCompiler가 등장해서 수동 메모이제이션을 할 필요가 없어졌다고 합니다! useMemo, useCallback 이런거 안써도 된대요.. 완전히 필요 없어지는 건 아니지만 대부분은 ReactCompiler 가 잘 최적화해주는 것 같습니다
그래서 ReactCompiler를 사용해보는 게 어떤지 논의해보면 좋을 것 같습니당
babel-plugin-react-compiler 설치 후 next.config.ts파일에 설정 추가해주면 되는 것 같아요!
React 공식문서
https://ko.react.dev/learn/react-compiler/introduction
Next.js 한글번역 문서
https://nextjs-ko.org/docs/app/api-reference/next-config-js/reactCompiler
There was a problem hiding this comment.
Editor 컴포넌트가 현재 extension 설정, shortcut 처리, BubbleMenu UI까지 모두 포함하고 있어서 파일이 조금 길어지는 것 같습니닷
extensions 설정이나 handleKeyDown 로직, BubbleMenu는 분리하면 가독성과 유지보수성이 더 좋아질 것 같은데 어떻게 생각하시나욤??
There was a problem hiding this comment.
앗 좋습니다!! Editor 컴포넌트에서 아래처럼 각각 분리하는 방향으로 정리햇습니닷 b
- extensions.ts - extensions 설정 분리
- EditorBubbleMenu.tsx -
BubbleMenuUI - usePostEditor.ts -
handleKeyDown과 같은 에디터 설정 + 슬래시 메뉴 상태 관리
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
src/components/board/Editor/index.tsx (1)
44-44: [이전 리뷰 미해결]text-gray-400,prose-gray, 인라인 헤딩 스타일 등 비토큰 클래스가 여전히 남아 있습니다.이전 리뷰에서 지적된 내용(
text-gray-400→text-text-disabled,prose-gray제거,[&_.ProseMirror_h1]:text-(--h1-size)등 인라인 헤딩 오버라이드 제거)이 반영되지 않았습니다.추가로,
globals.css에.ProseMirror h1 {@applytypo-h1; }가 이미 선언되어 있으므로[&_.ProseMirror_h1]:text-(--h1-size)와[&_.ProseMirror_h1]:leading-(--h1-line-height)인라인 클래스는 중복입니다. 제거하면className문자열이 대폭 단축됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/board/Editor/index.tsx` at line 44, The className string on the Editor component still contains non-token classes and redundant ProseMirror overrides; remove prose-gray and text-gray-400 (replace with token class text-text-disabled where that placeholder is used), and delete the inline heading overrides [&_.ProseMirror_h1]:text-(--h1-size), [&_.ProseMirror_h1]:leading-(--h1-line-height) (and similar h2/h3 lines) since globals.css already applies .ProseMirror h1/h2/h3 via typo-h1/etc.; update the className in src/components/board/Editor/index.tsx to use tokenized colors and remove duplicate ProseMirror heading overrides to significantly shorten the string.src/app/globals.css (1)
2-4: [이전 리뷰 미해결] Stylelint@custom-variant/@plugin오류가 여전히 남아 있습니다.Stylelint가
scss/at-rule-no-unknown오류를 발생시키고 있으며, 이전 리뷰에서 지적된 내용이 아직 반영되지 않았습니다. Stylelint 설정에서 해당 at-rule을 허용하거나 파일 단위 disable 주석을 추가해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/globals.css` around lines 2 - 4, Stylelint is flagging the custom at-rules (`@custom-variant` and `@plugin`) in src/app/globals.css with scss/at-rule-no-unknown; either add these at-rules to the allowed list in the Stylelint config (e.g., permit "@custom-variant" and "@plugin" under the scss/at-rule-no-unknown allowlist) or add a file-level disable comment at the top of src/app/globals.css (disabling scss/at-rule-no-unknown) so the rules using `@custom-variant` and `@plugin` are not reported.src/components/board/Editor/SlashMenu.tsx (1)
102-134: [이전 리뷰 미해결] 템플릿 리터럴 className 결합 및 비토큰 스페이싱/타이포 클래스가 여전히 남아 있습니다.
px-3,py-2,gap-3,text-sm,text-xs등 비토큰 클래스와 템플릿 리터럴 className 결합이 이전 리뷰 지적 이후에도 수정되지 않았습니다.cn()으로 className 결합을 통일하고 스페이싱·타이포 토큰 클래스로 교체해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/board/Editor/SlashMenu.tsx` around lines 102 - 134, The button/item rendering in SlashMenu still uses template-literal className concatenation and raw spacing/typography utility classes (e.g., "px-3", "py-2", "gap-3", "text-sm", "text-xs", "border-line") inside the map over group.items (in the component that computes currentIndex, isSelected, Icon and calls handleSelect); replace those template-literal className concatenations with the shared cn(...) helper and swap the raw spacing/typography classes to the corresponding design-token class names (and keep conditional selection classes via cn) so all className composition for the button, wrapper span and text elements uses cn and tokenized classes consistently.
🧹 Nitpick comments (3)
package.json (2)
15-15:@tailwindcss/typography는devDependencies로 이동해야 합니다.
@plugin지시어를 통해 빌드 타임에만 사용되는 Tailwind CSS 플러그인입니다.tailwindcss자체가devDependencies에 있는 것과 일관성 있게devDependencies로 이동해주세요.♻️ 제안 수정
"dependencies": { - "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.62.11", ... "devDependencies": { "@tailwindcss/postcss": "^4", + "@tailwindcss/typography": "^0.5.19",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 15, Move the Tailwind CSS plugin entry "@tailwindcss/typography" out of "dependencies" into "devDependencies" in package.json (so it matches "tailwindcss" which is already a dev-only tool); update package.json by removing the "@tailwindcss/typography" line from dependencies and adding the same entry under devDependencies, then run your package manager (npm/yarn/pnpm) to update the lockfile so the change is applied.
17-43: Tiptap 2.4.0은 현재 최신 메이저 버전보다 크게 뒤처져 있습니다.현재
@tiptap/core의 최신 버전은 3.20.0입니다. Tiptap 3.0은 여러 개선사항과 breaking change를 포함하여 안정 버전으로 출시되었습니다.두 가지 문제가 있습니다:
- 메이저 버전 차이: 2.4.0은 약 1년 이상 뒤처진 버전으로, 2.x 라인의 버그 픽스나 보안 패치도 수신되지 않습니다.
- 정확한 버전으로 고정(
"2.4.0",^없음): 동일 메이저 내 패치 버전 업데이트도 불가합니다.신규 기능 도입 시점에 3.x 마이그레이션을 검토하거나, 최소한
^2.4.0으로 변경해 2.x 라인 패치를 자동으로 수신하도록 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 17 - 43, The package.json pins many Tiptap packages to "2.4.0" which prevents patch updates and is behind the current major (3.x); update the version strings for all Tiptap packages (e.g. "@tiptap/core", "@tiptap/react", "@tiptap/extension-heading", "@tiptap/extension-text", etc.) to use a caret range like ^2.4.0 to receive 2.x patch/security fixes, and separately open a task to evaluate migrating to Tiptap 3.x (3.20.0+) because that is the latest major and includes breaking changes that require code updates in your editor-related modules (search for imports/use of the tiptap packages to plan the migration).src/components/board/Editor/usePostEditor.ts (1)
29-39:onUpdate와onSelectionUpdate의 슬래시 감지 로직 중복
($from.nodeBefore?.textContent ?? '').endsWith('/')조건이 두 콜백에 동일하게 반복됩니다. 명명된 헬퍼로 추출하면 유지보수성이 향상됩니다.♻️ 제안 수정
+ const isAfterSlash = (editor: Parameters<NonNullable<Parameters<typeof useEditor>[0]['onUpdate']>>[0]['editor']) => { + const { $from } = editor.state.selection; + return ($from.nodeBefore?.textContent ?? '').endsWith('/'); + }; const editor = useEditor({ extensions: editorExtensions, content: '', onUpdate: ({ editor }) => { setContent(editor.getHTML()); - const { $from } = editor.state.selection; - updateSlashMenuState(($from.nodeBefore?.textContent ?? '').endsWith('/')); + updateSlashMenuState(isAfterSlash(editor)); }, onSelectionUpdate: ({ editor }) => { - const { $from } = editor.state.selection; - updateSlashMenuState(($from.nodeBefore?.textContent ?? '').endsWith('/')); + if (!isAfterSlash(editor)) { + closeSlashMenu(); + } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/board/Editor/usePostEditor.ts` around lines 29 - 39, Extract the repeated slash-detection expression into a small named helper (e.g., isCursorAfterSlash(editor) or detectCursorAfterSlash($from)) and call that helper from both onUpdate and onSelectionUpdate instead of duplicating ($from.nodeBefore?.textContent ?? '').endsWith('/'); update the callbacks to pass the editor (or its $from selection) to the helper and keep the existing updateSlashMenuState(...) call unchanged so both handlers share the same logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/board/Editor/EditorBubbleMenu.tsx`:
- Around line 34-94: The bubble menu buttons (the elements that call
editor.chain().focus().toggleBold().run(), toggleItalic().run(),
toggleCode().run(), and toggleHeading({ level: X }).run() and use
bubbleButtonVariants) lack aria-label attributes which reduces screen-reader
accessibility; add descriptive aria-labels to each button (e.g., "Bold",
"Italic", "Code", "Heading 1", "Heading 2", "Heading 3") on the corresponding
button elements so assistive tech can announce their purpose while keeping
existing onMouseDown handlers and className logic intact.
In `@src/components/board/Editor/IndentExtension.ts`:
- Line 25: The parseHTML handler in IndentExtension.ts uses parseInt without a
radix: update the parseHTML implementation (the arrow function referencing
el.dataset.indent) to call parseInt with a radix of 10 (e.g., parseInt(..., 10))
so the indent value is always parsed as base-10.
In `@src/components/board/Editor/index.tsx`:
- Line 44: In the Editor component's className string, replace the Tailwind
spacing utilities that use literal pixel sizes with the spacing design tokens:
change "gap-3" to "gap-300" and change the "[&_.ProseMirror>*]:my-3" occurrence
to "[&_.ProseMirror>*]:my-300" so the selectors use the p-*/gap-*/my-* token
convention; update only those two tokens inside the className attribute in
src/components/board/Editor/index.tsx (look for the long className string
containing [&_.ProseMirror]* selectors).
In `@src/components/board/Editor/SlashMenu.tsx`:
- Around line 54-63: The keyboard handler handleSelect (and the onClose prop
coming from usePostEditor / closeSlashMenu) is recreated every render, causing
the effect that registers DOM keyboard listeners to re-add/remove each render
and failing the hooks lint; wrap handleSelect in useCallback and ensure
onClose/closeSlashMenu from usePostEditor are memoized (also with useCallback)
so both have stable references, then update the useEffect dependency array to
include those stable callbacks only (preventing repeated
addEventListener/removeEventListener calls).
In `@src/components/board/Editor/usePostEditor.ts`:
- Around line 36-39: onSelectionUpdate currently calls updateSlashMenuState with
the boolean result of endsWith('/'), which can reopen the slash menu when the
cursor moves onto a '/'; change the handler so it only closes the menu when the
cursor leaves the slash: compute const isAfterSlash =
($from.nodeBefore?.textContent ?? '').endsWith('/'); and call
updateSlashMenuState(false) only when !isAfterSlash (do not call
updateSlashMenuState(true) here). Update the onSelectionUpdate function in
usePostEditor.ts to perform this one-way close-only behavior.
---
Duplicate comments:
In `@src/app/globals.css`:
- Around line 2-4: Stylelint is flagging the custom at-rules (`@custom-variant`
and `@plugin`) in src/app/globals.css with scss/at-rule-no-unknown; either add
these at-rules to the allowed list in the Stylelint config (e.g., permit
"@custom-variant" and "@plugin" under the scss/at-rule-no-unknown allowlist) or
add a file-level disable comment at the top of src/app/globals.css (disabling
scss/at-rule-no-unknown) so the rules using `@custom-variant` and `@plugin` are not
reported.
In `@src/components/board/Editor/index.tsx`:
- Line 44: The className string on the Editor component still contains non-token
classes and redundant ProseMirror overrides; remove prose-gray and text-gray-400
(replace with token class text-text-disabled where that placeholder is used),
and delete the inline heading overrides [&_.ProseMirror_h1]:text-(--h1-size),
[&_.ProseMirror_h1]:leading-(--h1-line-height) (and similar h2/h3 lines) since
globals.css already applies .ProseMirror h1/h2/h3 via typo-h1/etc.; update the
className in src/components/board/Editor/index.tsx to use tokenized colors and
remove duplicate ProseMirror heading overrides to significantly shorten the
string.
In `@src/components/board/Editor/SlashMenu.tsx`:
- Around line 102-134: The button/item rendering in SlashMenu still uses
template-literal className concatenation and raw spacing/typography utility
classes (e.g., "px-3", "py-2", "gap-3", "text-sm", "text-xs", "border-line")
inside the map over group.items (in the component that computes currentIndex,
isSelected, Icon and calls handleSelect); replace those template-literal
className concatenations with the shared cn(...) helper and swap the raw
spacing/typography classes to the corresponding design-token class names (and
keep conditional selection classes via cn) so all className composition for the
button, wrapper span and text elements uses cn and tokenized classes
consistently.
---
Nitpick comments:
In `@package.json`:
- Line 15: Move the Tailwind CSS plugin entry "@tailwindcss/typography" out of
"dependencies" into "devDependencies" in package.json (so it matches
"tailwindcss" which is already a dev-only tool); update package.json by removing
the "@tailwindcss/typography" line from dependencies and adding the same entry
under devDependencies, then run your package manager (npm/yarn/pnpm) to update
the lockfile so the change is applied.
- Around line 17-43: The package.json pins many Tiptap packages to "2.4.0" which
prevents patch updates and is behind the current major (3.x); update the version
strings for all Tiptap packages (e.g. "@tiptap/core", "@tiptap/react",
"@tiptap/extension-heading", "@tiptap/extension-text", etc.) to use a caret
range like ^2.4.0 to receive 2.x patch/security fixes, and separately open a
task to evaluate migrating to Tiptap 3.x (3.20.0+) because that is the latest
major and includes breaking changes that require code updates in your
editor-related modules (search for imports/use of the tiptap packages to plan
the migration).
In `@src/components/board/Editor/usePostEditor.ts`:
- Around line 29-39: Extract the repeated slash-detection expression into a
small named helper (e.g., isCursorAfterSlash(editor) or
detectCursorAfterSlash($from)) and call that helper from both onUpdate and
onSelectionUpdate instead of duplicating ($from.nodeBefore?.textContent ??
'').endsWith('/'); update the callbacks to pass the editor (or its $from
selection) to the helper and keep the existing updateSlashMenuState(...) call
unchanged so both handlers share the same logic.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
next.config.tspackage.jsonsrc/app/globals.csssrc/components/board/Editor/EditorBubbleMenu.tsxsrc/components/board/Editor/IndentExtension.tssrc/components/board/Editor/SlashMenu.tsxsrc/components/board/Editor/extensions.tssrc/components/board/Editor/index.tsxsrc/components/board/Editor/usePostEditor.tssrc/stores/usePostStore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/stores/usePostStore.ts
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleBold().run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('bold') }), 'font-bold')} | ||
| > | ||
| B | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleItalic().run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('italic') }), 'italic')} | ||
| > | ||
| I | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleCode().run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('code') }), 'font-mono')} | ||
| > | ||
| {'<>'} | ||
| </button> | ||
| <div className="bg-line mx-100 h-4 w-px" /> | ||
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleHeading({ level: 1 }).run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('heading', { level: 1 }) }))} | ||
| > | ||
| H1 | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleHeading({ level: 2 }).run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('heading', { level: 2 }) }))} | ||
| > | ||
| H2 | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| editor.chain().focus().toggleHeading({ level: 3 }).run(); | ||
| }} | ||
| className={cn(bubbleButtonVariants({ active: editor.isActive('heading', { level: 3 }) }))} | ||
| > | ||
| H3 | ||
| </button> |
There was a problem hiding this comment.
버블 메뉴 버튼에 aria-label이 없어 접근성이 떨어집니다.
"B", "I", "<>" 등의 텍스트 콘텐츠만으로는 스크린 리더 사용자가 버튼의 기능을 파악하기 어렵습니다. 각 버튼에 aria-label을 추가해주세요.
🛠️ 제안 수정 (예시: Bold 버튼)
<button
type="button"
+ aria-label="Bold"
onMouseDown={(e) => { ... }}
className={cn(bubbleButtonVariants({ active: editor.isActive('bold') }), 'font-bold')}
>
B
</button>
// Italic → aria-label="Italic"
// Code → aria-label="Code"
// H1 → aria-label="Heading 1"
// H2 → aria-label="Heading 2"
// H3 → aria-label="Heading 3"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/EditorBubbleMenu.tsx` around lines 34 - 94, The
bubble menu buttons (the elements that call
editor.chain().focus().toggleBold().run(), toggleItalic().run(),
toggleCode().run(), and toggleHeading({ level: X }).run() and use
bubbleButtonVariants) lack aria-label attributes which reduces screen-reader
accessibility; add descriptive aria-labels to each button (e.g., "Bold",
"Italic", "Code", "Heading 1", "Heading 2", "Heading 3") on the corresponding
button elements so assistive tech can announce their purpose while keeping
existing onMouseDown handlers and className logic intact.
| attributes: { | ||
| indent: { | ||
| default: 0, | ||
| parseHTML: (el) => parseInt(el.dataset.indent ?? '0'), |
There was a problem hiding this comment.
parseInt 기수(radix) 누락.
parseInt 호출 시 기수를 생략하면 일부 엔진에서 예기치 않은 진법 해석이 발생할 수 있습니다. 명시적으로 10을 지정해주세요.
🛠️ 제안 수정
- parseHTML: (el) => parseInt(el.dataset.indent ?? '0'),
+ parseHTML: (el) => parseInt(el.dataset.indent ?? '0', 10),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| parseHTML: (el) => parseInt(el.dataset.indent ?? '0'), | |
| parseHTML: (el) => parseInt(el.dataset.indent ?? '0', 10), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/IndentExtension.ts` at line 25, The parseHTML
handler in IndentExtension.ts uses parseInt without a radix: update the
parseHTML implementation (the arrow function referencing el.dataset.indent) to
call parseInt with a radix of 10 (e.g., parseInt(..., 10)) so the indent value
is always parsed as base-10.
| <div className="relative pl-800"> | ||
| <EditorContent | ||
| editor={editor} | ||
| className="prose prose-gray max-w-none [&_.ProseMirror]:min-h-[400px] [&_.ProseMirror]:focus:outline-none [&_.ProseMirror_h1]:leading-(--h1-line-height) [&_.ProseMirror_h1]:text-(--h1-size) [&_.ProseMirror_h2]:leading-(--h2-line-height) [&_.ProseMirror_h2]:text-(--h2-size) [&_.ProseMirror_h3]:leading-(--h3-line-height) [&_.ProseMirror_h3]:text-(--h3-size) [&_.ProseMirror_p.is-empty::before]:pointer-events-none [&_.ProseMirror_p.is-empty::before]:float-left [&_.ProseMirror_p.is-empty::before]:h-0 [&_.ProseMirror_p.is-empty::before]:text-gray-400 [&_.ProseMirror_p.is-empty::before]:content-[attr(data-placeholder)] [&_.ProseMirror_ul[data-type=taskList]]:my-0 [&_.ProseMirror_ul[data-type=taskList]]:list-none [&_.ProseMirror_ul[data-type=taskList]_li]:my-0 [&_.ProseMirror_ul[data-type=taskList]_li]:flex [&_.ProseMirror_ul[data-type=taskList]_li]:items-center [&_.ProseMirror_ul[data-type=taskList]_li]:gap-3 [&_.ProseMirror>*]:my-3" |
There was a problem hiding this comment.
gap-3, my-3를 스페이싱 토큰 클래스로 교체해주세요.
gap-3(= 12px = --spacing-300)은 gap-300으로, [&_.ProseMirror>*]:my-3의 my-3 역시 my-300으로 변경해야 합니다.
🛠️ 제안 수정 (해당 부분 발췌)
- [&_.ProseMirror_ul[data-type=taskList]_li]:gap-3
+ [&_.ProseMirror_ul[data-type=taskList]_li]:gap-300
...
- [&_.ProseMirror>*]:my-3
+ [&_.ProseMirror>*]:my-300As per coding guidelines, "Use spacing design token classes: p-100~500, gap-100~400".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/index.tsx` at line 44, In the Editor component's
className string, replace the Tailwind spacing utilities that use literal pixel
sizes with the spacing design tokens: change "gap-3" to "gap-300" and change the
"[&_.ProseMirror>*]:my-3" occurrence to "[&_.ProseMirror>*]:my-300" so the
selectors use the p-*/gap-*/my-* token convention; update only those two tokens
inside the className attribute in src/components/board/Editor/index.tsx (look
for the long className string containing [&_.ProseMirror]* selectors).
| const handleSelect = (item: MenuItem) => { | ||
| const { $anchor } = editor.state.selection; | ||
|
|
||
| const from = $anchor.pos - 1; | ||
| const to = $anchor.pos; | ||
|
|
||
| editor.chain().focus().deleteRange({ from, to }).run(); | ||
| item.command(editor); | ||
| onClose(); | ||
| }; |
There was a problem hiding this comment.
handleSelect를 useCallback으로 감싸지 않아 키보드 리스너가 매 렌더마다 재등록됩니다.
handleSelect는 useCallback 없이 정의되어 매 렌더마다 새 함수 참조가 생성됩니다. useEffect의 deps에 포함된 handleSelect가 매 렌더마다 변경되므로 dom.addEventListener / removeEventListener가 매 렌더마다 반복 호출됩니다. onClose(→ usePostEditor의 closeSlashMenu) 역시 useCallback 없이 정의되어 매 렌더마다 갱신되므로 연쇄적으로 동일 문제가 발생합니다.
CI "Lint & Build" 체크도 이 문제를 명시적으로 경고하고 있습니다.
React Compiler가 자동으로 최적화해줄 수 있지만,
eslint-plugin-react-hooks는 이를 인식하지 못하므로 lint 실패가 지속됩니다.useCallback을 명시적으로 사용하거나,react-compilerESLint 플러그인을 적용해 해당 규칙을 비활성화하는 방안을 검토해주세요.
🔧 제안 수정 (`useCallback` 적용)
+import { useEffect, useState, useRef, useCallback } from 'react';
...
- const handleSelect = (item: MenuItem) => {
+ const handleSelect = useCallback((item: MenuItem) => {
const { $anchor } = editor.state.selection;
const from = $anchor.pos - 1;
const to = $anchor.pos;
editor.chain().focus().deleteRange({ from, to }).run();
item.command(editor);
onClose();
- };
+ }, [editor, onClose]);Also applies to: 66-90
🧰 Tools
🪛 GitHub Check: Lint & Build
[warning] 54-54:
The 'handleSelect' function makes the dependencies of useEffect Hook (at line 90) change on every render. To fix this, wrap the definition of 'handleSelect' in its own useCallback() Hook
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/SlashMenu.tsx` around lines 54 - 63, The keyboard
handler handleSelect (and the onClose prop coming from usePostEditor /
closeSlashMenu) is recreated every render, causing the effect that registers DOM
keyboard listeners to re-add/remove each render and failing the hooks lint; wrap
handleSelect in useCallback and ensure onClose/closeSlashMenu from usePostEditor
are memoized (also with useCallback) so both have stable references, then update
the useEffect dependency array to include those stable callbacks only
(preventing repeated addEventListener/removeEventListener calls).
| onSelectionUpdate: ({ editor }) => { | ||
| const { $from } = editor.state.selection; | ||
| updateSlashMenuState(($from.nodeBefore?.textContent ?? '').endsWith('/')); | ||
| }, |
There was a problem hiding this comment.
onSelectionUpdate가 슬래시 메뉴를 의도치 않게 재오픈할 수 있음
주석(Line 35)은 "커서 이동으로 '/' 뒤를 벗어났을 때 메뉴를 닫기 위해" 추적한다고 명시하지만, updateSlashMenuState는 양방향으로 동작합니다. 사용자가 기존 문서 내 '/' 문자 바로 뒤로 커서를 이동하면 updateSlashMenuState(true)가 호출되어 슬래시 메뉴가 의도치 않게 열립니다. onSelectionUpdate에서는 닫는 방향만 처리해야 합니다.
🐛 제안 수정
- // 커서 이동만으로 '/' 뒤를 벗어났을 때도 메뉴를 닫기 위해 추적
- onSelectionUpdate: ({ editor }) => {
- const { $from } = editor.state.selection;
- updateSlashMenuState(($from.nodeBefore?.textContent ?? '').endsWith('/'));
- },
+ // 커서 이동만으로 '/' 뒤를 벗어났을 때도 메뉴를 닫기 위해 추적
+ onSelectionUpdate: ({ editor }) => {
+ const { $from } = editor.state.selection;
+ if (!($from.nodeBefore?.textContent ?? '').endsWith('/')) {
+ closeSlashMenu();
+ }
+ },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/board/Editor/usePostEditor.ts` around lines 36 - 39,
onSelectionUpdate currently calls updateSlashMenuState with the boolean result
of endsWith('/'), which can reopen the slash menu when the cursor moves onto a
'/'; change the handler so it only closes the menu when the cursor leaves the
slash: compute const isAfterSlash = ($from.nodeBefore?.textContent ??
'').endsWith('/'); and call updateSlashMenuState(false) only when !isAfterSlash
(do not call updateSlashMenuState(true) here). Update the onSelectionUpdate
function in usePostEditor.ts to perform this one-way close-only behavior.
- 버블 메뉴 버튼에 aria-label 추가 (B/I/<>/H1~H3) - parseInt radix 누락 수정 (IndentExtension) - gap-3, my-3 > 토큰 기반으로 교체 - handleSelect, closeSlashMenu에 useCallback 적용해 키보드 리스너 매 렌더 재등록 방지 - onSelectionUpdate를 닫기 전용으로 변경해 '/' 뒤 커서 이동 시 메뉴 의도치 않게 열리는 문제 수정
woneeeee
left a comment
There was a problem hiding this comment.
확인했습니다!! 마크다운 어려운데 고생하셨어요 ㅜㅜㅜ!!!! 늦은 리뷰 죄송합니당... ㅡㅜ🥺🥺
| // 선택된 아이템이 스크롤 영역 밖에 있을 때 자동 스크롤 | ||
| useEffect(() => { | ||
| const container = scrollContainerRef.current; | ||
| if (!container) return; | ||
|
|
||
| const selectedEl = container.querySelector<HTMLElement>(`[data-index="${selectedIndex}"]`); | ||
| selectedEl?.scrollIntoView({ block: 'nearest' }); | ||
| }, [selectedIndex]); |
There was a problem hiding this comment.
selectedIndex 변경 시 scrollIntoView 처리하는 로직은 추후 멘션 리스트나 커맨드 팔레트 등에서도 동일하게 사용할 가능성이 있어 보여서 custom hook으로 분리해도 좋을 것 같습니당!!! 예를 들면 useAutoScrollIntoView훅으로 분리하는건 어떨까욤??
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
Tiptap 기반 에디터 구현: 제목(H1~H3), 텍스트 스타일(굵게/기울임), 인라인 코드 및 코드 블록, 인용문, 목록(순서/비순서), 체크리스트, 구분선 등 마크다운 형식의 주요 블록 타입 지원
슬래시 메뉴 구현: / 입력 시 스타일(Style) 및 삽입(Insert) 그룹 커맨드 노출, 키보드 탐색 및 자동 스크롤 지원
버블 메뉴 구현 (임시): 텍스트 선택 시 인라인 포맷 버튼 표시
게시글 전역 상태 스토어 구성:
content필드 사전 연결을 위해 기존 API 기반으로 임시 구현ProseMirror 전역 스타일 적용: 인용문, 코드 블록, 체크리스트 등 주요 블록에 디자인 토큰 기반 스타일 적용 (요것도 임시...입니다 UI 확정 시 반영 예정입니당)
0224 기준 추가 사항
react compiler설치 및 설정📸 스크린샷 or 실행영상
에디터
슬래시 메뉴 (슬래시 입력 시 표시)
버블 메뉴 (드래그 시 표시)
🎸 기타 사항 or 추가 코멘트
Notion 에디터를 기반으로 구현했습니다!
tiptap라이브러리를 사용하긴 했지만 유료 기능을 제외하고 구현하다 보니 코드가 다소 길어졌네요...... ㅜ.^index.tsx(에디터),SlashMenu.tsx(슬래시 메뉴) 두 코드의 기능적인 부분만 확인해 주셔도 무방합니다. . .게시글 전역 상태 스토어(
usePostStore.ts)는content필드만 미리 연결하기 위해 기존 API를 기반으로 구현했습니다! 추후 변동 있을 경우 반영할 예정입니당Summary by CodeRabbit
릴리스 노트
신기능
스타일
문서/구성