[refactor/native/scrap-#234] 스크랩 전체 리팩토링 및 drawing 기능 성능 향상#235
[refactor/native/scrap-#234] 스크랩 전체 리팩토링 및 drawing 기능 성능 향상#235
Conversation
… for strokes and texts, optimize history management, and enhance Y coordinate calculations
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b0nsu
left a comment
There was a problem hiding this comment.
리뷰 남깁니다. 회귀 리스크 중심으로 확인했습니다.
- [P1] DrawingToolbar의 pointerEvents 적용 위치
- 파일: apps/native/src/features/student/scrap/components/scrap/DrawingToolbar.tsx
pointerEvents를 style 객체에 넣어 실제로는 적용되지 않습니다. text mode에서 selector가 숨겨져도 터치를 계속 잡을 수 있습니다.- 제안:
<View pointerEvents={isTextMode ? 'none' : 'auto'} style={{ opacity: isTextMode ? 0 : 1 }}>형태로 분리 적용
- [P1] drawing.tsx에서 setState updater 외부의 즉시 saveToHistory/onChange 호출
- 파일: apps/native/src/features/student/scrap/utils/skia/drawing.tsx (finalizeStroke, confirmTextInput)
- updater 내부에서 ref를 갱신하고, 외부에서 즉시
onChange/saveToHistory를 호출하고 있어 타이밍에 따라 최신 변경이 history에 누락될 수 있습니다. - 제안:
next데이터를 먼저 계산해 state/ref/onChange/history에 동일값으로 반영하거나, history 저장 타이밍을 effect로 고정
- [P2] Split 레이아웃에서 flex + translateX(px) 혼용
- 파일: apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx
- 섹션 폭은
flex비율로 계산하는데 핸들은leftWidthpx 기준으로 이동해서 경계선과 실제 폭이 어긋날 수 있습니다. - 제안: width 기반으로 통일하거나 핸들 위치도 동일 비율 계산 기준으로 맞추기
There was a problem hiding this comment.
Pull request overview
This PR refactors the native “Scrap” feature UI/data flow (case-1) and focuses on improving drawing/handwriting performance, simplifying toolbar responsiveness, and fixing a highlighted-text styling issue.
Changes:
- Refactors drawing canvas internals (history restore/load, deep copy helpers, safe max, keyboard listener management) aimed at performance and stability.
- Refactors Scrap list/grid cards to inject actions via props (move/permanent delete) and reduces unnecessary rerenders via memoization.
- Simplifies responsive behavior in the drawing toolbar and removes unnecessary save-indicator display behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/native/src/features/student/scrap/utils/skia/drawing.tsx | Drawing performance refactor (deep copy/safeMax), history/undo behavior changes, keyboard listener cleanup, gesture adjustments |
| apps/native/src/features/student/scrap/utils/HighlightedText.tsx | Fixes highlighted text styling by avoiding empty string className |
| apps/native/src/features/student/scrap/screens/SearchScrapScreen.tsx | Minor layout spacing adjustments for search sections |
| apps/native/src/features/student/scrap/screens/ScrapScreen.tsx | Memoizes grid data to reduce rerenders |
| apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx | Removes save-indicator logic, adjusts split-pane layout logic, passes narrow-flag to toolbar |
| apps/native/src/features/student/scrap/components/scrap/FilterBar.tsx | Memoizes scroll handler and avoids redundant state updates |
| apps/native/src/features/student/scrap/components/scrap/DrawingToolbar.tsx | Replaces width-based prop with isNarrow flag and simplifies size selector rendering |
| apps/native/src/features/student/scrap/components/Tooltip/ScrapItemTooltip.tsx | Replaces effect-based title sync with local override pattern |
| apps/native/src/features/student/scrap/components/Modal/LoadQnaImageModal.tsx | Avoids redundant layout state updates (floored width + change guard) |
| apps/native/src/features/student/scrap/components/Header/ScrapDetailHeader.tsx | Replaces effect-based name sync with local override pattern |
| apps/native/src/features/student/scrap/components/Card/cards/TrashCard.tsx | Injects permanent-delete action via prop; internalizes selection dispatch callback |
| apps/native/src/features/student/scrap/components/Card/cards/ScrapCard.tsx | Injects move action via prop; internalizes selection dispatch callback |
| apps/native/src/features/student/scrap/components/Card/ScrapCardGrid.tsx | Memoizes placeholder-filled data and wires injected card actions via context/hooks |
Comments suppressed due to low confidence (1)
apps/native/src/features/student/scrap/utils/skia/drawing.tsx:213
restoreFromHistorydeep-copiesstate.strokesintorestoredStrokesand sets component state/refs to the restored copies, but still callsonChange?.(state.strokes). This means the callback receives a different array than the actual current canvas state, and it also exposes the history snapshot to accidental mutation. Prefer callingonChange?.(restoredStrokes)(or a deep copy) so external consumers stay consistent with what was restored.
const restoredStrokes = deepCopyStrokes(state.strokes);
const restoredTexts = deepCopyTexts(state.texts);
const newPaths = restoredStrokes.map((stroke) => buildSmoothPath(stroke.points));
setStrokes(restoredStrokes);
setPaths(newPaths);
setTexts(restoredTexts);
strokesRef.current = restoredStrokes;
textsRef.current = restoredTexts;
// 최대 Y 좌표 재계산
let maxYValue = 0;
if (state.strokes.length > 0) {
const strokesMaxY = safeMax(
state.strokes.flatMap((stroke) => stroke.points.map((p) => p.y))
);
maxYValue = Math.max(maxYValue, strokesMaxY);
}
if (state.texts.length > 0) {
const textsMaxY = safeMax(state.texts.map((text) => text.y));
maxYValue = Math.max(maxYValue, textsMaxY);
}
if (maxYValue > 0) {
maxY.current = maxYValue;
canvasHeight.current = Math.max(800, maxY.current + 200);
} else {
maxY.current = 0;
canvasHeight.current = 800;
}
onChange?.(state.strokes);
// 상태 변경으로 자동 리렌더링
notifyHistoryChange();
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 상태 업데이트를 분리 (React 18 automatic batching으로 한 번에 처리됨) | ||
| setStrokes((prev) => { | ||
| const next = [...prev, strokeData]; | ||
| setPaths((prevPaths) => [...prevPaths, newPath]); | ||
| strokesRef.current = next; | ||
| onChange?.(next); | ||
|
|
||
| // 히스토리에 저장 | ||
| setTimeout(() => saveToHistory(), 0); | ||
|
|
||
| return next; | ||
| }); | ||
| setPaths((prev) => [...prev, newPath]); | ||
|
|
||
| currentPoints.current = []; | ||
| livePath.current = Skia.Path.Make(); | ||
| livePath.current.reset(); | ||
|
|
||
| // 사이드이펙트는 updater 외부에서 | ||
| onChange?.([...strokesRef.current]); | ||
| saveToHistory(); | ||
| }, [strokeColor, strokeWidth, onChange, saveToHistory]); |
There was a problem hiding this comment.
finalizeStroke calls onChange and saveToHistory() immediately after setStrokes((prev) => ...). Because the functional state updater is not guaranteed to run synchronously, strokesRef.current may still contain the previous value here, causing onChange/history to miss the newly finalized stroke (undo/redo desync). Consider computing nextStrokes from strokesRef.current (or prev) outside, updating strokesRef.current first, then calling setStrokes(nextStrokes) and only then onChange(nextStrokes) / saveToHistory() (or have saveToHistory accept an explicit state snapshot).
| @@ -753,21 +677,6 @@ const DrawingCanvas = forwardRef<DrawingCanvasRef, Props>( | |||
| : text | |||
| ); | |||
| textsRef.current = next; | |||
|
|
|||
| // 텍스트의 실제 줄 수 계산하여 최대 Y 좌표 업데이트 | |||
| const lineCount = calculateTextLineCount(activeTextInput.value); | |||
| const totalTextHeight = lineCount * 22.5; | |||
| const textBottomY = activeTextInput.y + totalTextHeight; | |||
|
|
|||
| if (textBottomY > maxY.current) { | |||
| maxY.current = textBottomY; | |||
| canvasHeight.current = Math.max(800, maxY.current + 200); | |||
| setTick((t) => t + 1); | |||
| } | |||
|
|
|||
| // 히스토리에 저장 (텍스트 수정) | |||
| setTimeout(() => saveToHistory(), 0); | |||
|
|
|||
| return next; | |||
| }); | |||
| } else { | |||
| @@ -781,37 +690,28 @@ const DrawingCanvas = forwardRef<DrawingCanvasRef, Props>( | |||
| color: '#1E1E21', // 고정 텍스트 색상 | |||
| }; | |||
|
|
|||
| // 텍스트의 실제 줄 수 계산하여 최대 Y 좌표 업데이트 | |||
| const lineCount = calculateTextLineCount(activeTextInput.value); | |||
| const totalTextHeight = lineCount * 22.5; // 고정 줄 높이 22.5px | |||
| const textBottomY = activeTextInput.y + totalTextHeight; | |||
|
|
|||
| if (textBottomY > maxY.current) { | |||
| maxY.current = textBottomY; | |||
| canvasHeight.current = Math.max(800, maxY.current + 200); | |||
| setTick((t) => t + 1); // 캔버스 높이 변경을 위한 리렌더링 | |||
| } | |||
|
|
|||
| setTexts((prev) => { | |||
| const next = [...prev, newText]; | |||
| textsRef.current = next; | |||
|
|
|||
| // 히스토리에 저장 (텍스트 추가) | |||
| setTimeout(() => saveToHistory(), 0); | |||
|
|
|||
| return next; | |||
| }); | |||
| } | |||
| // 상태 변경으로 자동 리렌더링 | |||
|
|
|||
| // 사이드이펙트는 updater 외부에서 (가이드 수정 2 원칙) | |||
| if (textBottomY > maxY.current) { | |||
| maxY.current = textBottomY; | |||
| canvasHeight.current = Math.max(800, maxY.current + 200); | |||
| setTick((t) => t + 1); | |||
| } | |||
|
|
|||
| saveToHistory(); | |||
| } | |||
There was a problem hiding this comment.
confirmTextInput calls saveToHistory() right after setTexts((prev) => ...), but saveToHistory reads from textsRef.current. Since the functional updater may not have executed yet, history can capture the pre-edit/pre-add text list (breaking undo/redo for text changes). A safer approach is to build nextTexts from textsRef.current/texts, assign textsRef.current = nextTexts first, then setTexts(nextTexts) and call saveToHistory().
apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
✅ Key Changes