Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
083f880
feat: add @repo/pointer-native-drawing package
b0nsu Apr 7, 2026
a82f1eb
refactor(drawing): migrate to shared package and remove unsupported p…
b0nsu Apr 7, 2026
5e70dd0
chore(metro): add singleton resolution for native dependencies
b0nsu Apr 7, 2026
0510409
chore: remove old pointer-native-drawing source for subtree replacement
b0nsu Apr 8, 2026
5afe37c
Merge commit '3733c900d7a6cc41884b8d3b35dae10205c931c8' as 'packages/…
b0nsu Apr 8, 2026
3733c90
Squashed 'packages/pointer-native-drawing/' content from commit ac29ede5
b0nsu Apr 8, 2026
f714ade
refactor(drawing): integrate pointer-native-drawing as monorepo package
b0nsu Apr 9, 2026
c1c16a0
Squashed 'packages/pointer-native-drawing/' changes from ac29ede5..50…
b0nsu Apr 12, 2026
4b8836c
Merge commit 'c1c16a078cd71cd912fffcf0f5dd08197eef387c' into refactor…
b0nsu Apr 12, 2026
c090157
feat(drawing): update DrawingCanvas with pencilOnly, zoomPan, scroll …
b0nsu Apr 12, 2026
d9e3544
feat(drawing): add undo/redo, color persistence, textbox save/load
b0nsu Apr 12, 2026
a5bbc6f
merge: resolve conflicts with origin/develop
b0nsu Apr 15, 2026
b579888
chore: remove unused ProblemViewer import and update lockfile
b0nsu Apr 15, 2026
4d96481
fix: autosave 미작동, lastColor 복원 누락, textBoxes stale state 수정
b0nsu Apr 15, 2026
8012a15
chore: PR 리뷰 TODO 주석 추가 (smoothing import type, clampTransform NaN gu…
b0nsu Apr 15, 2026
5be3c24
fix: handleSave stale closure, clampTransform NaN guard, import type 분리
b0nsu Apr 15, 2026
d18d767
fix: onDirty 콜백 추가로 텍스트 박스 변경 시 autosave 누락 수정, 툴바 타입 에러 해결
b0nsu Apr 16, 2026
b64b5b6
merge: sync with origin/develop (MAT-277 no-deprecated, MAT-281 홈스크린,…
b0nsu Apr 16, 2026
8432e3f
chore(drawing): example 앱 삭제 및 docs git 추적 해제
b0nsu Apr 16, 2026
d2bb8b3
chore(drawing): ci:lint 설정 및 lint fix (MAT-240)
b0nsu Apr 16, 2026
764419c
chore: PR-262-REVIEW-PLAN.md 제거 및 prettier 포맷 수정
b0nsu Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ CLAUDE.md
AGENTS.md
.claude
.omc
.omx/
6 changes: 5 additions & 1 deletion apps/native/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const DEDUPE_MODULES = {
react: path.resolve(projectRoot, 'node_modules/react'),
'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
'react-native-webview': path.resolve(projectRoot, 'node_modules/react-native-webview'),
'react-native-reanimated': path.resolve(projectRoot, 'node_modules/react-native-reanimated'),
'react-native-gesture-handler': path.resolve(
projectRoot,
'node_modules/react-native-gesture-handler'
),
};
config.resolver.extraNodeModules = {
...(config.resolver.extraNodeModules ?? {}),
Expand All @@ -39,7 +44,6 @@ config.resolver.blockList = [
/apps\/native\/dist\/.*/, // 프로젝트 자체의 dist만 차단
];

// react-native-css-interop의 jsx-runtime을 직접 resolve
const originalResolveRequest = config.resolver.resolveRequest;
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (moduleName === 'react-native-css-interop/jsx-runtime') {
Expand Down
3 changes: 2 additions & 1 deletion apps/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@react-navigation/native": "^7.1.8",
"@react-navigation/native-stack": "^7.8.0",
"@react-navigation/stack": "^7.1.1",
"@repo/pointer-native-drawing": "workspace:*",
"@repo/pointer-content-renderer": "workspace:*",
"@shopify/react-native-skia": "2.2.12",
"@tanstack/react-query": "^5.66.0",
Expand Down Expand Up @@ -79,7 +80,7 @@
"react-native-gesture-handler": "~2.28.0",
"react-native-image-viewing": "^0.2.2",
"react-native-popover-view": "^6.1.0",
"react-native-reanimated": "~4.1.5",
"react-native-reanimated": "~4.1.6",
Comment thread
b0nsu marked this conversation as resolved.
"react-native-safe-area-context": "~5.4.0",
"react-native-screens": "~4.16.0",
"react-native-sse": "^1.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,26 @@ import { colors } from '@theme/tokens';
import { AnimatedPressable } from '@components/common';

interface ProblemDrawingToolbarProps {
canUndo: boolean;
canRedo: boolean;
onUndo: () => void;
onRedo: () => void;
isEraserMode: boolean;
onPenModePress: () => void;
onEraserModePress: () => void;
canUndo?: boolean;
canRedo?: boolean;
onUndo: () => void;
onRedo: () => void;
}

export const ProblemDrawingToolbar = ({
canUndo,
canRedo,
onUndo,
onRedo,
isEraserMode,
onPenModePress,
onEraserModePress,
canUndo = false,
canRedo = false,
onUndo,
onRedo,
}: ProblemDrawingToolbarProps) => {
return (
<View className='gap-[10px] rounded-[10px] bg-gray-300 p-[8px]'>
{/* Undo/Redo 그룹 */}
<View className='gap-[10px]'>
{/* Undo 버튼 */}
<AnimatedPressable
onPress={onUndo}
disabled={!canUndo}
className={`size-[36px] items-center justify-center rounded-lg ${
canUndo ? 'bg-black' : 'bg-gray-100'
}`}>
<Undo2 size={16} color={canUndo ? '#fff' : colors['gray-500']} strokeWidth={1.33} />
</AnimatedPressable>

{/* Redo 버튼 */}
<AnimatedPressable
onPress={onRedo}
disabled={!canRedo}
className={`size-[36px] items-center justify-center rounded-lg ${
canRedo ? 'bg-black' : 'bg-gray-100'
}`}>
<Redo2 size={16} color={canRedo ? '#fff' : colors['gray-500']} strokeWidth={1.33} />
</AnimatedPressable>
</View>

{/* 구분선 */}
<View className='h-[2px] w-[22px] self-center bg-gray-500' />

{/* Pencil/Eraser 그룹 */}
<View className='gap-[10px]'>
{/* Eraser 버튼 */}
Expand All @@ -75,6 +49,25 @@ export const ProblemDrawingToolbar = ({
/>
</AnimatedPressable>
</View>

{/* 구분선 */}
<View className='h-[2px] w-[22px] self-center bg-gray-500' />

{/* Undo/Redo 그룹 */}
<View className='gap-[6px]'>
<AnimatedPressable
onPress={onUndo}
disabled={!canUndo}
className='size-[36px] items-center justify-center rounded-lg border border-gray-500 bg-white'>
<Undo2 size={16} color={canUndo ? colors['gray-900'] : colors['gray-500']} />
</AnimatedPressable>
<AnimatedPressable
onPress={onRedo}
disabled={!canRedo}
className='size-[36px] items-center justify-center rounded-lg border border-gray-500 bg-white'>
<Redo2 size={16} color={canRedo ? colors['gray-900'] : colors['gray-500']} />
</AnimatedPressable>
</View>
</View>
);
};
61 changes: 27 additions & 34 deletions apps/native/src/features/student/problem/screens/ProblemScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Text,
View,
} from 'react-native';
// TODO: runOnJS는 reanimated 4.x에서 deprecated. useAnimatedReaction 콜백 방식으로 교체 필요.
import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';

Expand Down Expand Up @@ -444,6 +445,7 @@ const ProblemScreen = ({ navigation }: ProblemScreenProps) => {
}, [problemProgress, currentProblem?.progress]);

const canvasRef = useRef<DrawingCanvasRef>(null);
const [undoState, setUndoState] = useState({ canUndo: false, canRedo: false });
const drawingState = useDrawingState();

const screenHeight = Dimensions.get('window').height;
Expand All @@ -470,10 +472,6 @@ const ProblemScreen = ({ navigation }: ProblemScreenProps) => {
pointerEvents='box-none'>
<View pointerEvents='auto'>
<ProblemDrawingToolbar
canUndo={drawingState.canUndo}
canRedo={drawingState.canRedo}
onUndo={() => canvasRef.current?.undo()}
onRedo={() => canvasRef.current?.redo()}
isEraserMode={drawingState.isEraserMode}
onPenModePress={drawingState.setPenMode}
onEraserModePress={() => {
Expand All @@ -483,40 +481,35 @@ const ProblemScreen = ({ navigation }: ProblemScreenProps) => {
drawingState.setEraserMode();
}
}}
canUndo={undoState.canUndo}
canRedo={undoState.canRedo}
onUndo={() => canvasRef.current?.undo()}
onRedo={() => canvasRef.current?.redo()}
/>
</View>
</View>

<ScrollView>
<ContentInset className='flex-1'>
{/* Problem */}
<View
className='my-[12px] overflow-hidden rounded-[8px]'
style={{ position: 'relative', height: screenHeight - 200 }}>
<PointerContentView
initMessage={problemInitMessage}
minHeight={200}
style={{ maxWidth: 720 }}
/>

{/* 위층: DrawingCanvas - ProblemViewer 위에 겹쳐짐 */}
<View
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
pointerEvents='box-none'>
<View style={{ flex: 1 }} pointerEvents='auto'>
<DrawingCanvas
ref={canvasRef}
strokeColor='#1E1E21'
strokeWidth={2}
eraserMode={drawingState.isEraserMode}
eraserSize={12}
onHistoryChange={drawingState.setHistoryState}
/>
</View>
</View>
</View>
</ContentInset>
</ScrollView>
<ContentInset className='flex-1'>
<DrawingCanvas
ref={canvasRef}
strokeColor='#1E1E21'
strokeWidth={2}
activeTool={drawingState.mode}
eraserSize={12}
pencilOnly
enableZoomPan
onUndoStateChange={setUndoState}
backgroundColor='transparent'
// NOTE: 현재 문제 콘텐츠가 screenHeight*2를 초과하는 케이스는 없음.
// 만약 초과할 경우 PointerContentView 높이 측정 후 동적 설정 필요.
minCanvasHeight={screenHeight * 2}>
<PointerContentView
initMessage={problemInitMessage}
minHeight={200}
style={{ maxWidth: 720 }}
/>
</DrawingCanvas>
</ContentInset>
<AnswerKeyboardSheet
ref={bottomSheetRef}
bottomInset={bottomBarHeight}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import { Undo2, Redo2, Type } from 'lucide-react-native';
import { Type, Undo2, Redo2 } from 'lucide-react-native';

import { colors } from '@theme/tokens';
import { PencilFilledIcon, EraserFilledIcon } from '@components/system/icons';
Expand All @@ -9,25 +9,25 @@ import { SizeSelector } from '@features/student/scrap/components/scrap/SizeSelec
import { IconButton } from '../../../problem/components/WritingArea';

export interface DrawingToolbarProps {
// Undo/Redo
canUndo: boolean;
canRedo: boolean;
onUndo: () => void;
onRedo: () => void;

// Mode selection
isEraserMode: boolean;
isTextMode: boolean;
isTextBoxMode: boolean;
onPenModePress: () => void;
onEraserModePress: () => void;
onTextModePress: () => void;
onTextBoxModePress: () => void;

// Size selection
strokeWidth: number;
eraserSize: number;
onStrokeWidthChange: (size: number) => void;
onEraserSizeChange: (size: number) => void;

// Undo/Redo
canUndo?: boolean;
canRedo?: boolean;
onUndo: () => void;
onRedo: () => void;

// Narrow layout flag (drawingAreaWidth < 380)
isNarrow?: boolean;
}
Expand All @@ -36,23 +36,25 @@ const STROKE_SIZES = [2, 1.2, 0.7];
const ERASER_SIZES = [22, 14, 8];

export const DrawingToolbar = ({
canUndo,
canRedo,
onUndo,
onRedo,
isEraserMode,
isTextMode,
isTextBoxMode,
onPenModePress,
onEraserModePress,
onTextModePress,
onTextBoxModePress,
strokeWidth,
eraserSize,
onStrokeWidthChange,
onEraserSizeChange,
canUndo = false,
canRedo = false,
onUndo,
onRedo,
isNarrow = false,
}: DrawingToolbarProps) => {
const SizeSelectorComponent = (
<View pointerEvents={isTextMode ? 'none' : 'auto'} style={{ opacity: isTextMode ? 0 : 1 }}>
<View
pointerEvents={isTextBoxMode ? 'none' : 'auto'}
style={{ opacity: isTextBoxMode ? 0 : 1 }}>
{isEraserMode ? (
<SizeSelector
type='eraser'
Expand All @@ -77,39 +79,11 @@ export const DrawingToolbar = ({
<View
className='flex-row items-center gap-[10px] px-[14px] py-[6px]'
style={{ borderBottomWidth: isNarrow ? 1 : 0, borderColor: '#DFE2E7' }}>
{/* Undo/Redo */}
<View className='flex-row items-center gap-[10px]'>
<IconButton
icon={Undo2}
backgroundColor='bg-gray-700'
disabledBackgroundColor='bg-gray-100'
iconColor='white'
onPress={onUndo}
disabled={!canUndo}
disabledColor={colors['gray-500']}
radius={8}
size={36}
/>
<IconButton
icon={Redo2}
backgroundColor='bg-gray-700'
disabledBackgroundColor='bg-gray-100'
iconColor='white'
onPress={onRedo}
disabled={!canRedo}
disabledColor={colors['gray-500']}
size={36}
radius={8}
/>
</View>

<View className='h-[22px] w-[2px] bg-gray-500' />

{/* Mode Selection */}
<View className='flex-row items-center gap-[10px]'>
<IconButton
icon={PencilFilledIcon}
disabled={isTextMode || isEraserMode}
disabled={isTextBoxMode || isEraserMode}
backgroundColor='bg-blue-200'
disabledBackgroundColor='bg-gray-100'
iconColor={colors['primary-500']}
Expand All @@ -131,12 +105,40 @@ export const DrawingToolbar = ({
/>
<IconButton
icon={Type}
disabled={!isTextMode}
disabled={!isTextBoxMode}
backgroundColor='bg-blue-200'
disabledBackgroundColor='bg-gray-100'
iconColor={colors['primary-500']}
disabledColor={colors['gray-700']}
onPress={onTextModePress}
onPress={onTextBoxModePress}
size={36}
radius={8}
/>
</View>

<View className='h-[22px] w-[2px] bg-gray-500' />

{/* Undo/Redo */}
<View className='flex-row items-center gap-[6px]'>
<IconButton
icon={Undo2}
disabled={!canUndo}
backgroundColor='bg-gray-100'
disabledBackgroundColor='bg-gray-100'
iconColor={colors['gray-900']}
disabledColor={colors['gray-500']}
onPress={onUndo}
size={36}
radius={8}
/>
<IconButton
icon={Redo2}
disabled={!canRedo}
backgroundColor='bg-gray-100'
disabledBackgroundColor='bg-gray-100'
iconColor={colors['gray-900']}
disabledColor={colors['gray-500']}
onPress={onRedo}
size={36}
radius={8}
/>
Expand Down
Loading
Loading