Skip to content

[refactor/native/scrap-#234] 스크랩 전체 리팩토링 및 drawing 기능 성능 향상#235

Open
b0nsu wants to merge 9 commits intodevelopfrom
refactor/native/scrap-#234
Open

[refactor/native/scrap-#234] 스크랩 전체 리팩토링 및 drawing 기능 성능 향상#235
b0nsu wants to merge 9 commits intodevelopfrom
refactor/native/scrap-#234

Conversation

@b0nsu
Copy link
Collaborator

@b0nsu b0nsu commented Mar 5, 2026

✅ Key Changes

이번 PR에서 작업한 내용을 간략히 설명해주세요

  • case-1 전체 리팩토링 : useEffect 제거, prop 주입 패턴 등 (11파일)
  • 필기 기능 deep copy, safeMax 유틸 수정으로 성능 향상
  • DrawingToolbar SizeSelector 단순화
  • HighlightedText font 스타일 미적용되는 이슈 해결
  • ScrapDetailScreen save indicator 필요없는 display 동작 제거

@b0nsu b0nsu requested a review from sterdsterd March 5, 2026 14:16
@vercel
Copy link

vercel bot commented Mar 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pointer-admin Ready Ready Preview, Comment Mar 10, 2026 2:07pm

@b0nsu b0nsu linked an issue Mar 5, 2026 that may be closed by this pull request
@b0nsu b0nsu changed the title Refactor/native/scrap #234 [refactor/native/scrap-#234] 스크랩 전체 리팩토링 및 drawing 기능 성능 향상 Mar 5, 2026
Copy link
Collaborator Author

@b0nsu b0nsu left a comment

Choose a reason for hiding this comment

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

리뷰 남깁니다. 회귀 리스크 중심으로 확인했습니다.

  1. [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 }}> 형태로 분리 적용
  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로 고정
  1. [P2] Split 레이아웃에서 flex + translateX(px) 혼용
  • 파일: apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx
  • 섹션 폭은 flex 비율로 계산하는데 핸들은 leftWidth px 기준으로 이동해서 경계선과 실제 폭이 어긋날 수 있습니다.
  • 제안: width 기반으로 통일하거나 핸들 위치도 동일 비율 계산 기준으로 맞추기

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

  • restoreFromHistory deep-copies state.strokes into restoredStrokes and sets component state/refs to the restored copies, but still calls onChange?.(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 calling onChange?.(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.

Comment on lines 438 to 452
// 상태 업데이트를 분리 (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]);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines 668 to 708
@@ -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();
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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().

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@b0nsu b0nsu Mar 10, 2026

Choose a reason for hiding this comment

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

@copilot This has already been addressed in 835b9a0.
confirmTextInput now computes nextTexts first, assigns textsRef.current = nextTexts, then calls setTexts(nextTexts) and saveToHistory(), so history no longer depends on the functional updater timing.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor/native/scrap] SCRAP 전반적인 성능 향상

2 participants