From e6717adf57c091f76a969f403a43ed6bbfb333b9 Mon Sep 17 00:00:00 2001 From: b0nsu <125778250+b0nsu@users.noreply.github.com> Date: Sun, 10 May 2026 23:09:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(problem):=20MAT-658=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8C=85=20=ED=99=94=EB=A9=B4=20=ED=95=84?= =?UTF-8?q?=EA=B8=B0=20toolbar=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PointingDrawingToolbar: floating-toolbar/ 안에 신규. ProblemDrawingToolbar 와 동일한 useFloatingToolbarSnap hook + shared.tsx 재사용. ExpandedToolbar 마지막에 divider + 2x2 색상 grid (POINTING_BRUSH_COLORS). - POINTING_BRUSH_COLORS = ['#000000', '#FF0900', '#0004FF', '#1BB52A'] export. - PointingScreen: 우측 문제 영역에 DrawingCanvas + PointingDrawingToolbar 통합. 4-corner snap 영역과 drawing 영역을 PointerContentView 영역으로 정렬. brushColor state + handleSelectBrushColor (eraser 모드면 자동 pen 전환). - index.ts: PointingDrawingToolbar / POINTING_BRUSH_COLORS export 추가. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PointingDrawingToolbar.tsx | 254 ++++++++++++++++++ .../components/floating-toolbar/index.ts | 1 + .../problem/screens/PointingScreen.tsx | 88 +++++- 3 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx diff --git a/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx new file mode 100644 index 00000000..7cdaec3a --- /dev/null +++ b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx @@ -0,0 +1,254 @@ +import { View } from 'react-native'; +import { GestureDetector } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import { Undo2, Redo2 } from 'lucide-react-native'; + +import { AnimatedPressable } from '@components/common'; +import { EraserFilledIcon, PencilFilledIcon } from '@components/system/icons'; +import { colors } from '@theme/tokens'; + +import { + BUTTON_RADIUS, + BUTTON_SIZE, + COLLAPSED_RADIUS, + COLLAPSED_W, + COLOR_GRID_GAP, + COLOR_GRID_W, + ColorSwatch, + type Corner, + DIVIDER_WIDTH, + EXPANDED_RADIUS, + GAP, + ICON_SIZE, + PADDING, + SHADOW, + TOOLBAR_H, + ToolbarButton, + ToolbarDivider, +} from './shared'; +import { useFloatingToolbarSnap } from './useFloatingToolbarSnap'; + +export const POINTING_BRUSH_COLORS = ['#000000', '#FF0900', '#0004FF', '#1BB52A'] as const; + +const EXPANDED_W = + PADDING + + BUTTON_SIZE + + GAP + + BUTTON_SIZE + + GAP + + DIVIDER_WIDTH + + GAP + + BUTTON_SIZE + + GAP + + BUTTON_SIZE + + GAP + + DIVIDER_WIDTH + + GAP + + COLOR_GRID_W + + PADDING; + +interface PointingDrawingToolbarProps { + canUndo: boolean; + canRedo: boolean; + onUndo: () => void; + onRedo: () => void; + isEraserMode: boolean; + onPenModePress: () => void; + onEraserModePress: () => void; + collapsed: boolean; + onCollapsedChange: (collapsed: boolean) => void; + containerWidth: number; + containerHeight: number; + initialCorner?: Corner; + selectedBrushColor: string; + onSelectBrushColor: (color: string) => void; +} + +export const PointingDrawingToolbar = ({ + canUndo, + canRedo, + onUndo, + onRedo, + isEraserMode, + onPenModePress, + onEraserModePress, + collapsed, + onCollapsedChange, + containerWidth, + containerHeight, + initialCorner = 'bottom-right', + selectedBrushColor, + onSelectBrushColor, +}: PointingDrawingToolbarProps) => { + const toolbarWidth = collapsed ? COLLAPSED_W : EXPANDED_W; + const { composedGesture, animatedStyle, ready } = useFloatingToolbarSnap({ + containerWidth, + containerHeight, + toolbarWidth, + initialCorner, + }); + + return ( + + + {collapsed ? ( + onCollapsedChange(false)} /> + ) : ( + + )} + + + ); +}; + +const ExpandedToolbar = ({ + canUndo, + canRedo, + onUndo, + onRedo, + isEraserMode, + onPenModePress, + onEraserModePress, + selectedBrushColor, + onSelectBrushColor, +}: Pick< + PointingDrawingToolbarProps, + | 'canUndo' + | 'canRedo' + | 'onUndo' + | 'onRedo' + | 'isEraserMode' + | 'onPenModePress' + | 'onEraserModePress' + | 'selectedBrushColor' + | 'onSelectBrushColor' +>) => ( + + + } + isActive={canUndo} + /> + + } + isActive={canRedo} + /> + + + } + isActive={!isEraserMode} + /> + + } + isActive={isEraserMode} + /> + + + {POINTING_BRUSH_COLORS.map((c) => ( + onSelectBrushColor(c)} + /> + ))} + + +); + +const CollapsedToolbar = ({ + isEraserMode, + onPress, +}: { + isEraserMode: boolean; + onPress: () => void; +}) => ( + + + {isEraserMode ? ( + + ) : ( + + )} + + +); diff --git a/apps/native/src/features/student/problem/components/floating-toolbar/index.ts b/apps/native/src/features/student/problem/components/floating-toolbar/index.ts index fc4e8576..e0b6dabb 100644 --- a/apps/native/src/features/student/problem/components/floating-toolbar/index.ts +++ b/apps/native/src/features/student/problem/components/floating-toolbar/index.ts @@ -1 +1,2 @@ export { ProblemDrawingToolbar } from './ProblemDrawingToolbar'; +export { PointingDrawingToolbar, POINTING_BRUSH_COLORS } from './PointingDrawingToolbar'; diff --git a/apps/native/src/features/student/problem/screens/PointingScreen.tsx b/apps/native/src/features/student/problem/screens/PointingScreen.tsx index c3e5b040..8acb6372 100644 --- a/apps/native/src/features/student/problem/screens/PointingScreen.tsx +++ b/apps/native/src/features/student/problem/screens/PointingScreen.tsx @@ -1,7 +1,7 @@ -import { Alert, View } from 'react-native'; +import { Alert, type LayoutChangeEvent, StyleSheet, View } from 'react-native'; import { XIcon } from 'lucide-react-native'; import { type NativeStackScreenProps } from '@react-navigation/native-stack'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { AnswerEventPayload } from '@repo/pointer-content-renderer'; import { useShallow } from 'zustand/react/shallow'; @@ -30,6 +30,9 @@ import { toChatScenario, toUserAnswers, } from '../transforms/contentRendererTransforms'; +import { POINTING_BRUSH_COLORS, PointingDrawingToolbar } from '../components/floating-toolbar'; +import { DrawingCanvas, type DrawingCanvasRef } from '../../scrap/utils/skia'; +import { useDrawingState } from '../../scrap/hooks/useDrawingState'; const PointingScreen = ({ navigation, @@ -158,6 +161,41 @@ const PointingScreen = ({ const { leftWidth, rightWidth } = useSplitPanelLayout(); + const canvasRef = useRef(null); + const drawingState = useDrawingState(); + const [toolbarCollapsed, setToolbarCollapsed] = useState(false); + const [toolbarArea, setToolbarArea] = useState({ width: 0, height: 0 }); + const [brushColor, setBrushColor] = useState(POINTING_BRUSH_COLORS[0]); + + const handleToolbarAreaLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + const { width, height } = nativeEvent.layout; + setToolbarArea((prev) => + prev.width === width && prev.height === height ? prev : { width, height } + ); + }, []); + + const handlePenModePress = useCallback(() => { + drawingState.setPenMode(); + }, [drawingState]); + + const handleEraserModePress = useCallback(() => { + if (drawingState.isEraserMode) { + drawingState.setPenMode(); + } else { + drawingState.setEraserMode(); + } + }, [drawingState]); + + const handleSelectBrushColor = useCallback( + (color: string) => { + setBrushColor(color); + if (drawingState.isEraserMode) { + drawingState.setPenMode(); + } + }, + [drawingState] + ); + const redirectToHome = useCallback(() => { resetSession(); navigation?.navigate('StudentTabs', { screen: 'Home' }); @@ -234,11 +272,47 @@ const PointingScreen = ({ badge={badgeStatus} paddingHorizontal={0} /> - + + + + + + setToolbarCollapsed(true)} + /> + + + + + canvasRef.current?.undo()} + onRedo={() => canvasRef.current?.redo()} + isEraserMode={drawingState.isEraserMode} + onPenModePress={handlePenModePress} + onEraserModePress={handleEraserModePress} + collapsed={toolbarCollapsed} + onCollapsedChange={setToolbarCollapsed} + containerWidth={toolbarArea.width} + containerHeight={toolbarArea.height} + selectedBrushColor={brushColor} + onSelectBrushColor={handleSelectBrushColor} + /> + + From fb63feed3cfcbae7ef639fd72fcb19fd72b6f0d0 Mon Sep 17 00:00:00 2001 From: b0nsu <125778250+b0nsu@users.noreply.github.com> Date: Sun, 10 May 2026 23:33:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(problem):=20MAT-658=20=E2=80=94=20copil?= =?UTF-8?q?ot=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PointingScreen: CONTENT_MAX_WIDTH 상수 추출. inner wrapper 와 PointerContentView 둘 다 같은 cap 사용 → wide iPad 에서 4-corner snap 영역 과 contentviewer 영역 일치 (720 magic number 한 곳에서 관리). - PointingDrawingToolbar: Animated.View 에 pointerEvents 게이트 추가 (measured 전 invisible 영역의 GestureDetector 가 터치 가로채지 않도록). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/floating-toolbar/PointingDrawingToolbar.tsx | 1 + .../src/features/student/problem/screens/PointingScreen.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx index 7cdaec3a..d23e8818 100644 --- a/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx +++ b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx @@ -91,6 +91,7 @@ export const PointingDrawingToolbar = ({ return ( >) => { @@ -273,12 +275,12 @@ const PointingScreen = ({ paddingHorizontal={0} />