Skip to content

Comments

[Feat] WTH-136 : 마크다운 기반 게시글 작성 기능 구현#5

Open
nabbang6 wants to merge 19 commits intomainfrom
WTH-136-마크다운-기반-게시글-작성-기능-구현

Hidden character warning

The head ref may contain hidden characters: "WTH-136-\ub9c8\ud06c\ub2e4\uc6b4-\uae30\ubc18-\uac8c\uc2dc\uae00-\uc791\uc131-\uae30\ub2a5-\uad6c\ud604"
Open

[Feat] WTH-136 : 마크다운 기반 게시글 작성 기능 구현#5
nabbang6 wants to merge 19 commits intomainfrom
WTH-136-마크다운-기반-게시글-작성-기능-구현

Conversation

@nabbang6
Copy link
Collaborator

@nabbang6 nabbang6 commented Feb 20, 2026

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호

  • Closed #136

✅ Key Changes

  • Tiptap 기반 에디터 구현: 제목(H1~H3), 텍스트 스타일(굵게/기울임), 인라인 코드 및 코드 블록, 인용문, 목록(순서/비순서), 체크리스트, 구분선 등 마크다운 형식의 주요 블록 타입 지원

  • 슬래시 메뉴 구현: / 입력 시 스타일(Style) 및 삽입(Insert) 그룹 커맨드 노출, 키보드 탐색 및 자동 스크롤 지원

  • 버블 메뉴 구현 (임시): 텍스트 선택 시 인라인 포맷 버튼 표시

  • 게시글 전역 상태 스토어 구성: content 필드 사전 연결을 위해 기존 API 기반으로 임시 구현

  • ProseMirror 전역 스타일 적용: 인용문, 코드 블록, 체크리스트 등 주요 블록에 디자인 토큰 기반 스타일 적용 (요것도 임시...입니다 UI 확정 시 반영 예정입니당)


📸 스크린샷 or 실행영상

에디터

image

슬래시 메뉴 (슬래시 입력 시 표시)

image

버블 메뉴 (드래그 시 표시)

image

🎸 기타 사항 or 추가 코멘트

  • Notion 에디터를 기반으로 구현했습니다! tiptap 라이브러리를 사용하긴 했지만 유료 기능을 제외하고 구현하다 보니 코드가 다소 길어졌네요...... ㅜ.^ index.tsx (에디터), SlashMenu.tsx (슬래시 메뉴) 두 코드의 기능적인 부분만 확인해 주셔도 무방합니다. . .

  • BubbleMenu(index.tsx의 156~249라인)의 경우 아직 임시로만 구현해 둔 기능이라... 예진님과 간단한 POC 진행 후 확정되면 컴포넌트 분리하여 정리할 예정입니다!

  • 게시글 전역 상태 스토어(usePostStore.ts)는 content 필드만 미리 연결하기 위해 기존 API를 기반으로 구현했습니다! 추후 변동 있을 경우 반영할 예정입니당

Summary by CodeRabbit

릴리스 노트

  • 신기능

    • 풍부한 TipTap 기반 텍스트 편집기 추가(슬래시 명령어, 버블/플로팅 메뉴 포함).
    • 작성용 상태 관리 추가(메타데이터, 내용, 첨부파일 및 페이로드 생성 지원).
  • 스타일

    • 편집기 전반 타이포그래피·프로스 스타일 및 코드/인용구 등 세부 스타일 추가.
  • 문서/구성

    • 에디터 관련 타입 정의와 명령 항목 목록 추가로 편집 경험 강화.

@nabbang6 nabbang6 self-assigned this Feb 20, 2026
@nabbang6 nabbang6 added the ✨ Feature 기능 개발 label Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

TipTap 기반의 클라이언트 사이드 에디터 도입: 의존성 추가, 에디터 컴포넌트 및 슬래시/버블 메뉴 구현, Zustand 스토어와 타입 정의, ProseMirror 관련 전역 스타일 추가(파일 출력/서명 미변경).

Changes

Cohort / File(s) Summary
Dependencies
package.json
TipTap 2.4.0 관련 패키지 및 tippy.js 등 여러 에디터 관련 의존성 추가. 변경은 선언부(의존성)만 해당.
Page / Client Loading
src/app/(private)/(main)/board/page.tsx
BoardPage를 클라이언트 컴포넌트로 전환하고 Editor를 dynamic으로 SSR 비활성화하여 클라이언트에서 로드하도록 변경.
Global Styles
src/app/globals.css
Tailwind Typography 플러그터 추가 및 ProseMirror/Prose(텍스트, 헤더, 목록, 인용, 코드블록 등) 상세 스타일 규칙 추가.
Editor Component
src/components/board/Editor/index.tsx
TipTap 기반 Editor 컴포넌트 추가: 다양한 확장, FloatingMenu(슬래시 메뉴), BubbleMenu, 키보드 처리(백틱 인라인 코드, 헤딩->문단 변환 등), 상태 동기화 및 usePostStore 연동.
Slash Menu UI
src/components/board/Editor/SlashMenu.tsx
슬래시 커맨드 메뉴 컴포넌트 추가: 항목 그룹화, 키보드/마우스 내비게이션, 항목 실행 시 에디터 커맨드 실행 및 삭제 동작.
Editor Constants & Types
src/constants/editor.ts, src/types/editor.ts
MenuItem 타입(LucideIcon 포함) 추가 및 STYLE_ITEMS/INSERT_ITEMS 명세(에디터 명령 함수 포함) 추가.
State Store
src/stores/usePostStore.ts
Zustand 기반 포스트 작성 스토어 추가: 메타데이터, 에디터 컨텐츠, 파일 관리, 상태 설정과 API 페이로드 생성 유틸 포함.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant BoardPage
    participant Editor
    participant TipTap
    participant usePostStore
    participant SlashMenu

    User->>BoardPage: 페이지 접속
    BoardPage->>Editor: dynamic 로드 (클라이언트)
    Editor->>TipTap: 에디터 초기화 (확장 설정)
    TipTap-->>Editor: 에디터 인스턴스 반환
    Editor->>usePostStore: setContent 초기 동기화

    User->>Editor: 텍스트 입력
    Editor->>TipTap: 콘텐츠 업데이트
    TipTap-->>Editor: onUpdate 이벤트
    Editor->>usePostStore: setContent(content)

    User->>Editor: '/' 입력
    Editor->>SlashMenu: FloatingMenu 표시
    User->>SlashMenu: 항목 선택
    SlashMenu->>TipTap: 선택된 커맨드 실행 (삭제 후 명령)
    TipTap-->>Editor: 콘텐츠 변경
    Editor->>usePostStore: setContent(content)
    SlashMenu->>SlashMenu: 메뉴 닫기
Loading
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(content)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • dalzzy
  • woneeeee

Poem

🐰 새 글이 깃들었네, 깡총깡총
슬래시로 부르면 메뉴가 춤추고,
버블로 빛나는 꾸밈을 얹네.
TipTap 꿈결 속에 상태는 안전히,
보드 위에 토끼가 박수를 보낸다. 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 마크다운 기반 게시글 작성 기능 구현이라는 핵심 변경사항을 명확하게 설명하고 있으며, 변경 내용과 직접적으로 관련되어 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿 구조를 따르고 있으며, PR 유형, 관련 이슈번호, 핵심 변경사항, 스크린샷, 추가 코멘트를 모두 포함하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch WTH-136-마크다운-기반-게시글-작성-기능-구현

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

@github-actions
Copy link

github-actions bot commented Feb 20, 2026

PR 검증 결과

TypeScript: 실패
ESLint: 통과
Prettier: 실패
Build: 통과

⚠️ 일부 검증에 실패했습니다. 확인 후 수정해주세요.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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.

Comment on lines 8 to 10
return (
<main className="mx-auto max-w-3xl p-8">
<Editor />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

메인 패딩은 토큰 간격 클래스로 맞춰주세요.

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.

Comment on lines +2 to +4
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
@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.

Comment on lines 268 to 272
<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"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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)] ..."
As per coding guidelines, Use design token classes for text styling: `text-text-strong`, `text-text-normal`, `text-text-alternative`, `text-text-disabled`, `text-text-inverse`; Use typography design token classes: `typo-h1~h3`, `typo-sub1~2`, `typo-body1~2`, `typo-caption1~2`, `typo-button1~2`; 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` 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.

Comment on lines 106 to 141
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>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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',
+  )}
As per coding guidelines, Use `cn()` from `@/lib/cn` for className merging instead of manual concatenation; Use typography design token classes: `typo-h1~h3`, `typo-sub1~2`, `typo-body1~2`, `typo-caption1~2`, `typo-button1~2`; 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/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.

Comment on lines 4 to 67
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(),
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find MenuItem interface definition
rg -n "interface MenuItem|type MenuItem" src

Repository: 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 2

Repository: 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 1

Repository: Team-Weeth/weeth-client

Length of output: 94


🏁 Script executed:

# Read MenuItem interface definition
cat -n src/types/editor.ts

Repository: 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 2

Repository: Team-Weeth/weeth-client

Length of output: 897


🏁 Script executed:

# Check icon property usage
rg -n "\.icon" src -B 1 -A 1

Repository: 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 -20

Repository: 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_ITEMSINSERT_ITEMS의 아이콘을 Lucide 컴포넌트로 매핑 (예: TType, H1Heading1, 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 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.

Copy link
Collaborator

@JIN921 JIN921 left a comment

Choose a reason for hiding this comment

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

useEffect의 클린업이나 useMemo를 사용한 리렌더 방지가 잘 되어 있어, zustand 관련해 말씀 드린 부분 빼면 고칠 부분은 없어 보입니다 수고하셨어요!!

슬래쉬로 마크다운 작성이 이런식으로 이루어 지는군요 신기해요 👀👀
(근데 슬래쉬 메뉴에 스크롤 없으면 더 예쁠 거 같습니다 ㅎ.ㅎ,, )

status: 'DRAFT' as const,
};

export const usePostStore = create<PostState>((set, get) => ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거 상태 추적이랑 타입 정의를 위해서 devtools랑 combine 사용하면 더 좋을 거 같아요!

// Actions
setBoard: (board: string) => void;
setTitle: (title: string) => void;
setCohort: (cohort: number) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

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

보다가 궁금해서 그러는데 이건 뭘 설정해 주는 걸까요..?? cohort..??? 집단 무리..??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants