Skip to content

Fix(#264): 모바일/태블릿 Divider 터치 지원 및 캔버스 필기감 개선#265

Open
stringnine wants to merge 4 commits into
devfrom
fix/264-mobile-viewer-bug
Open

Fix(#264): 모바일/태블릿 Divider 터치 지원 및 캔버스 필기감 개선#265
stringnine wants to merge 4 commits into
devfrom
fix/264-mobile-viewer-bug

Conversation

@stringnine
Copy link
Copy Markdown
Collaborator

@stringnine stringnine commented May 14, 2026

📌 관련 이슈

🏷️ PR 타입

  • 🐛 버그 수정 (Bug Fix)

📝 작업 내용

Divider 터치 드래그 지원

  • useResizable.ts: touchstart / touchmove / touchend 이벤트 핸들러 추가
  • touchmove 중 스크롤 방지 (passive: false)
  • requestAnimationFrame으로 드래그 업데이트 throttle → 태블릿 버벅임 해소
  • Divider.tsx: onTouchStart prop 추가
  • useChatPanel.ts / ChatPage.tsx: handleTouchStart 전달 연결

캔버스 필기감 개선 (perfect-freehand 옵션 튜닝)

  • MIN_POINT_DISTANCE: 0.35 → 2.0 (포인트 수 감소, 계산 부하 개선)
  • streamline: 0.32 → 0.38 (떨림 보정, 커밋 시 수축 현상 없는 범위)
  • smoothing: 0.68 → 0.72
  • thinning: 0.32 → 0.15 (굵기 변화 줄여 일관된 선 느낌)

✅ 체크리스트

  • 코드 리뷰를 받을 준비가 완료되었습니다
  • 셀프 리뷰를 완료했습니다
  • 코드 스타일 가이드를 준수했습니다

📎 기타 참고사항

  • 캔버스 디자인 개선 및 컴포넌트 분리는 별도 이슈로 진행 예정
  • 필기감 파라미터는 실사용 피드백에 따라 추가 조정 가능

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 터치 스크린에서 채팅 패널 크기 조절 지원 추가
    • 펜/지우개 도구의 크기 조절 슬라이더 추가로 세밀한 그리기 제어 가능
  • Refactor

    • 드로잉 스트로크 생성 및 포인터 입력 처리 로직 개선
    • 채팅 패널 상태 관리 체계 최적화

Review Change Stack

@stringnine stringnine self-assigned this May 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Warning

Rate limit exceeded

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

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79cdd9a3-f5b6-4aa6-966c-b60a52e04891

📥 Commits

Reviewing files that changed from the base of the PR and between 04c86c9 and 90e4845.

📒 Files selected for processing (3)
  • src/features/chat/hooks/useChatPanel.ts
  • src/features/chat/hooks/useResizable.ts
  • src/features/editor/components/canvas/CanvasBoard.tsx
📝 Walkthrough

워크스루

이 PR은 두 가지 주요 입력 처리 개선을 포함합니다: 채팅 패널 divider의 터치 이벤트 지원 추가와 캔버스 드로잉 엔진의 포인터 압력 및 캡처 메커니즘 재구성입니다. Divider 터치 지원은 모바일/태블릿 환경에서 패널 리사이즈를 활성화하며, 캔버스 개선은 펜/지우개 도구의 선 품질과 입력 안정성을 강화합니다.

변경사항

채팅 패널 Divider 터치 이벤트 지원

레이어 / 파일 요약
useResizable 훅 - 터치 이벤트
src/features/chat/hooks/useResizable.ts
UseResizableReturn 타입에 handleTouchStart 필드 추가, RAF 기반 쓰로틀링을 위해 rafRef/pendingClientXRef 도입, handleTouchStart 핸들러 구현, touchmove 리스너를 passive: false로 등록하여 스크롤 방지 유지, 종료 시 RAF 취소 및 isDragging 상태 관리.
Divider 컴포넌트 - 터치 이벤트 Props
src/features/chat/components/divider/Divider.tsx
DividerProps 인터페이스에 onTouchStart: (e: React.TouchEvent) => void 추가, 컴포넌트 디스트럭처링 업데이트, 드래그 핸들에 onTouchStart 바인딩 및 touch-none CSS 클래스 추가.
useChatPanel 훅 - 상태 파생화 및 터치 통합
src/features/chat/hooks/useChatPanel.ts
activeTab/isViewerOpen 직접 상태 + useEffect 동기화에서 fallbackTab/viewerOpenOverride 파생 계산 방식으로 전환, handleTabChangehandleToggleViewer를 URL 파라미터 갱신 중심으로 변경, 반환값에 handleTouchStart 추가.
ChatPage - 터치 핸들러 연결
src/pages/ChatPage.tsx
useChatPanel에서 handleTouchStart 디스트럭처링, Divider 컴포넌트에 onTouchStart={handleTouchStart} 전달.

캔버스 드로잉 개선

레이어 / 파일 요약
선 렌더링 - 압력 및 점 형태
src/features/editor/components/canvas/CanvasBoard.tsx
펜/지우개 크기 상수 및 MIN_POINT_DISTANCE 추가, getPointerPressure 정규화 업데이트, getStrokePolygon을 eraser 여부로 분기하여 다른 perfect-freehand 옵션 적용, 포인트 부족 시 DotShape(압력 기반 반경/합성 모드 포함)로 렌더링.
도구 크기 관리 - 펜과 지우개
src/features/editor/components/canvas/CanvasBoard.tsx
penSize/eraserSize 상태 분리, isStrokeTool 및 활성 크기/범위 파생값 계산, 펜/지우개 선택 시에만 범위 슬라이더 표시 UI 추가.
포인터 캡처 및 이벤트 헬퍼
src/features/editor/components/canvas/CanvasBoard.tsx
activePointerIdRef로 캡처된 포인터 추적, handlePointerDown에서 setPointerCapture 시도, pushStrokePointerEvent/pushStrokePointerEvents 헬퍼로 coalesced 이벤트 처리.
문서 레벨 이벤트 핸들러 - 포인터 이동 및 종료
src/features/editor/components/canvas/CanvasBoard.tsx
handlePointerMove에서 선 포인트 추가 로직 제거(초안만 업데이트), 문서 레벨 pointermove/pointerup/pointercancel 리스너 추가로 activePointerIdRef 필터링된 이벤트만 처리, handlePointerUp에서 포인터 캡처 해제 및 조건부 선 커밋.

시퀀스 다이어그램

sequenceDiagram
  participant User as 사용자
  participant Divider as Divider
  participant useResizable as useResizable
  participant Browser as 브라우저
  
  User->>Divider: 핸들 터치 시작
  Divider->>useResizable: handleTouchStart(e)
  useResizable->>Browser: preventDefault()
  useResizable->>useResizable: isDragging=true
  
  User->>Browser: 터치 드래그
  Browser->>useResizable: touchmove 리스너
  useResizable->>useResizable: RAF로 폭 갱신
  
  User->>Browser: 터치 끝
  Browser->>useResizable: touchend 리스너
  useResizable->>useResizable: isDragging=false
Loading
sequenceDiagram
  participant Canvas as 캔버스
  participant handleDown as handlePointerDown
  participant Document as 문서
  participant handleUp as handlePointerUp
  
  Canvas->>handleDown: 포인터다운
  handleDown->>Canvas: setPointerCapture
  handleDown->>Canvas: activeStroke 생성
  
  Canvas->>Document: pointermove (캡처됨)
  Document->>Document: 선 포인트 추가
  
  Canvas->>Document: pointerup (캡처됨)
  Document->>handleUp: 호출
  handleUp->>Canvas: releasePointerCapture
  handleUp->>Canvas: 선 커밋
Loading

예상 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~50분

제안 라벨

fix, enhancement

제안 리뷰어

  • dev-ldy03
  • Mingyeong-Kang

터치와 펜으로 그려낸 선들,
모바일의 손끝이 이제 춤춘다 🎨
포인터 캡처의 섬세한 손길,
압력 기반의 부드러운 획,
마우스의 영역에 터치의 봄이 왔네 🐰
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 요약하고 있으며, 이슈 번호(#264)와 핵심 기능(모바일/태블릿 Divider 터치 지원, 캔버스 필기감 개선)을 포함하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션(관련 이슈, PR 타입, 작업 내용, 체크리스트, 기타 참고사항)을 모두 포함하고 있으며, 구체적인 기술 변경사항이 명확하게 기술되어 있습니다.
Linked Issues check ✅ Passed PR의 모든 변경사항이 이슈 #264의 요구사항을 충족합니다: touchstart/touchmove/touchend 이벤트 핸들러 구현, Divider 컴포넌트에 onTouchStart prop 추가, useResizable에 터치 이벤트 지원 추가 등이 모두 이루어졌습니다.
Out of Scope Changes check ✅ Passed CanvasBoard.tsx의 필기감 개선 작업(perfect-freehand 옵션 튜닝)은 PR 설명에 명시되어 있으며, 사용자가 인지하는 변경사항입니다. 이슈 #264의 범위를 넘지만 PR 목표에 포함되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/264-mobile-viewer-bug

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@stringnine stringnine added bug 버그 발생 fix 코드 및 버그 수정 labels May 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/features/editor/components/canvas/CanvasBoard.tsx (3)

1066-1084: 💤 Low value

슬라이더 step=0.2인데 표시값은 Math.round로 정수화됩니다.

사용자가 슬라이더를 한 칸 움직여도 표시값이 동일하게 보이는 구간이 생겨 “반응 없음”처럼 느껴질 수 있습니다. 표시 정밀도와 step을 맞추는 편이 자연스럽습니다(예: step={1} 또는 toFixed(1)).

💄 제안
-              step={0.2}
+              step={1}

또는 toFixed(1)로 표시:

-            <span className="w-[28px] text-right tabular-nums">
-              {Math.round(activeToolSize)}
-            </span>
+            <span className="w-[32px] text-right tabular-nums">
+              {activeToolSize.toFixed(1)}
+            </span>
🤖 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 `@src/features/editor/components/canvas/CanvasBoard.tsx` around lines 1066 -
1084, The displayed tool size is being rounded with Math.round(activeToolSize)
while the slider uses step={0.2}, causing perceived non-responsiveness; update
the display to match the slider precision (e.g., replace
Math.round(activeToolSize) with activeToolSize.toFixed(1)) or change the slider
step to 1 so step and display align, and ensure this reflects the same state
used by setPenSize/setEraserSize and the activeToolSize variable for
consistency.

168-177: 💤 Low value

단일 포인트 정규화의 +0.01 매직 넘버에 의도 주석을 추가하면 좋겠습니다.

perfect-freehandgetStroke가 단일 포인트에서 폴리곤을 만들기 위한 알려진 우회 처리지만, 의도가 코드만으로는 드러나지 않습니다. 또한 이 경로 결과는 보통 StrokeShapelinePoints.length < 6 가드에 의해 DotShape로 떨어지는 것이 정상 흐름이라는 점도 함께 명시하면 후속 변경 시 회귀를 막기 좋습니다.

🤖 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 `@src/features/editor/components/canvas/CanvasBoard.tsx` around lines 168 -
177, The normalization of single-point input in normalizedPoints currently adds
a magic +0.01 offset without explanation; update the code near normalizedPoints
to add a concise comment explaining that this small offset is a deliberate
workaround for perfect-freehand's getStroke behavior to force a minimal polygon
from a single point, and note that the resulting path is expected to be handled
by the StrokeShape guard (linePoints.length < 6) and converted to a DotShape in
normal flow to avoid regressions; reference points, normalizedPoints, getStroke,
StrokeShape, DotShape, and linePoints.length < 6 in the comment so future
maintainers understand intent.

763-779: ⚡ Quick win

document 레벨 pointermovepassive: false가 컴포넌트 마운트 동안 상시 부착됩니다.

passive: false 리스너가 document에 붙어 있는 한, 페이지의 모든 pointermove에 대해 브라우저가 핸들러 완료를 기다린 뒤 스크롤/제스처를 진행합니다. 핸들러가 빠르게 early return 하더라도 전역 스크롤·터치 반응성에 마진을 깎아 먹는 패턴입니다(특히 pointerup/pointercancel은 스크롤 측면에서 preventDefault가 의미 있는 이벤트도 아닙니다).

setPointerCapture가 잡혀 있으면 캡처된 포인터의 pointermove는 stage container로 전달되어 bubble로 document에도 도달하므로, 스트로크 시작 시 부착 → 종료 시 분리 패턴이면 충분합니다.

♻️ 제안 리팩터링
-  useEffect(() => {
-    document.addEventListener("pointermove", handleDocumentPointerMove, {
-      passive: false,
-    });
-    document.addEventListener("pointerup", handleDocumentPointerEnd, {
-      passive: false,
-    });
-    document.addEventListener("pointercancel", handleDocumentPointerEnd, {
-      passive: false,
-    });
-
-    return () => {
-      document.removeEventListener("pointermove", handleDocumentPointerMove);
-      document.removeEventListener("pointerup", handleDocumentPointerEnd);
-      document.removeEventListener("pointercancel", handleDocumentPointerEnd);
-    };
-  }, [handleDocumentPointerEnd, handleDocumentPointerMove]);

대신 handlePointerDown의 pen/eraser 분기에서 캡처 직후 부착하고, handlePointerUp에서 해제하는 식으로 라이프사이클을 좁히는 것을 권장합니다. 부수적으로 tool이 바뀔 때마다 핸들러 참조가 갱신되어 useEffect가 재실행되는 churn도 함께 사라집니다.

🤖 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 `@src/features/editor/components/canvas/CanvasBoard.tsx` around lines 763 -
779, The document-level pointer listeners are attached for the entire component
lifecycle with passive:false, hurting scroll/gesture responsiveness; instead,
remove the global useEffect attachment and attach
document.addEventListener("pointermove", handleDocumentPointerMove,
{passive:false}) and pointerup/pointercancel to handleDocumentPointerEnd
immediately after you call setPointerCapture in handlePointerDown (pen/eraser
branch), and remove those same listeners in
handlePointerUp/handleDocumentPointerEnd when the stroke ends; update
handlePointerDown, handlePointerUp (and any capture logic) to manage
adding/removing handleDocumentPointerMove and handleDocumentPointerEnd so
listeners only exist during an active stroke.
🤖 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 `@src/features/chat/hooks/useChatPanel.ts`:
- Around line 22-35: activeTab currently always prefers the raw URL param
(panelParam) so clicking tabs doesn't change the visible tab because
handleTabChange only updates fallbackTab; modify handleTabChange (and any
tab-click handlers in useChatPanel) to update both fallbackTab via
setFallbackTab(newTab) and the URL searchParams (use the existing
searchParams/setSearchParams mechanism) so the "panel" query param is set to the
new tab (or removed/normalized when switching to the default viewer behavior),
and ensure closing/clearing viewer/file logic also removes the "panel" param so
activeTab and the URL stay in sync.

In `@src/features/chat/hooks/useResizable.ts`:
- Around line 67-70: handleTouchStart currently sets isDragging without
recording which touch to follow, causing jumpy behavior in multi-touch; modify
handleTouchStart to capture and store the initiating touch's identifier into
activeTouchIdRef (or create that ref if missing), update handleTouchMove to
locate the touch by comparing touch.identifier against activeTouchIdRef.current
using e.changedTouches or e.touches (ignore e.touches[0] directly), and clear
activeTouchIdRef.current = null inside handleTouchEnd (and stop dragging) so the
tracked identifier is reset; update references to activeTouchIdRef,
handleTouchMove, and handleTouchEnd accordingly.
- Around line 136-164: When cleaning up the useEffect that attaches drag
listeners (the effect depending on isDragging, handleMouseMove, handleMouseUp,
handleTouchMove, handleTouchEnd), also cancel any pending requestAnimationFrame
to avoid calling setWidth after unmount; add or use the RAF handle (e.g., rafId
or rafRef used by your drag logic) and call cancelAnimationFrame(rafId) in the
effect cleanup in addition to removing the event listeners and resetting
document styles so any scheduled RAF started during dragging is aborted on
unmount.

In `@src/features/editor/components/canvas/CanvasBoard.tsx`:
- Around line 635-651: handlePointerUp must accept an Event (or PointerEvent)
parameter and early-return unless event.pointerId === activePointerIdRef.current
so that only the pointer that started the stroke can end it; update the Stage
onPointerUp/onPointerCancel handlers to pass the event through to
handlePointerUp, keep handleDocumentPointerEnd calling handlePointerUp() with no
args (it already filters by pointerId) and retain the
releasePointerCapture/cleanup logic inside handlePointerUp using
activePointerIdRef/current; reference functions/refs: handlePointerUp,
handleDocumentPointerEnd, activePointerIdRef, stageRef.

---

Nitpick comments:
In `@src/features/editor/components/canvas/CanvasBoard.tsx`:
- Around line 1066-1084: The displayed tool size is being rounded with
Math.round(activeToolSize) while the slider uses step={0.2}, causing perceived
non-responsiveness; update the display to match the slider precision (e.g.,
replace Math.round(activeToolSize) with activeToolSize.toFixed(1)) or change the
slider step to 1 so step and display align, and ensure this reflects the same
state used by setPenSize/setEraserSize and the activeToolSize variable for
consistency.
- Around line 168-177: The normalization of single-point input in
normalizedPoints currently adds a magic +0.01 offset without explanation; update
the code near normalizedPoints to add a concise comment explaining that this
small offset is a deliberate workaround for perfect-freehand's getStroke
behavior to force a minimal polygon from a single point, and note that the
resulting path is expected to be handled by the StrokeShape guard
(linePoints.length < 6) and converted to a DotShape in normal flow to avoid
regressions; reference points, normalizedPoints, getStroke, StrokeShape,
DotShape, and linePoints.length < 6 in the comment so future maintainers
understand intent.
- Around line 763-779: The document-level pointer listeners are attached for the
entire component lifecycle with passive:false, hurting scroll/gesture
responsiveness; instead, remove the global useEffect attachment and attach
document.addEventListener("pointermove", handleDocumentPointerMove,
{passive:false}) and pointerup/pointercancel to handleDocumentPointerEnd
immediately after you call setPointerCapture in handlePointerDown (pen/eraser
branch), and remove those same listeners in
handlePointerUp/handleDocumentPointerEnd when the stroke ends; update
handlePointerDown, handlePointerUp (and any capture logic) to manage
adding/removing handleDocumentPointerMove and handleDocumentPointerEnd so
listeners only exist during an active stroke.
🪄 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: c5bb248a-c624-43bd-bc19-02504043ce30

📥 Commits

Reviewing files that changed from the base of the PR and between 377b032 and 04c86c9.

📒 Files selected for processing (5)
  • src/features/chat/components/divider/Divider.tsx
  • src/features/chat/hooks/useChatPanel.ts
  • src/features/chat/hooks/useResizable.ts
  • src/features/editor/components/canvas/CanvasBoard.tsx
  • src/pages/ChatPage.tsx

Comment thread src/features/chat/hooks/useChatPanel.ts
Comment thread src/features/chat/hooks/useResizable.ts
Comment thread src/features/chat/hooks/useResizable.ts
Comment thread src/features/editor/components/canvas/CanvasBoard.tsx Outdated
Copy link
Copy Markdown
Collaborator

@L0521 L0521 left a comment

Choose a reason for hiding this comment

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

개선이 잘 되어 있는 것 같습니다!

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

Labels

bug 버그 발생 fix 코드 및 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] 모바일/태블릿 환경에서 뷰어-채팅 영역 Divider 드래그 동작 불가

2 participants