[Feat/native/#227] 이어서 풀기 관련 로직 수정(DOING handling 추가)#229
[Feat/native/#227] 이어서 풀기 관련 로직 수정(DOING handling 추가)#229sterdsterd merged 8 commits intodevelopfrom
Conversation
…l child problems in incorrect path
…up resume support
…itch to API attemptCount
…Pointing in PointingScreen
…sed screen routing
…pdate retry logic in ProblemScreen
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR implements the "resume from where you left off" functionality for problem-solving sessions with proper handling of the DOING state. It adds support for tracking attempt counts and progress information, allowing students to continue their problem-solving session from the exact point where they stopped.
Changes:
- Added API schema updates to support resume functionality (LastProgressInfo, attemptCount, publishId fields)
- Implemented MAIN_PROBLEM_RETRY phase and resume state computation logic in problem session store
- Updated all entry points (deep links, notifications, problem set) to use initWithResume instead of init
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/native/src/types/api/schema.d.ts | Added LastProgressInfo type, attemptCount fields, and optional publishId to PointingFeedbackRequest |
| apps/native/src/stores/problemSessionStore.ts | Added MAIN_PROBLEM_RETRY phase, initWithResume action, and computeResumeState function to handle session resumption logic |
| apps/native/src/hooks/useDeepLinkHandler.ts | Updated to use initWithResume and navigate to appropriate screen based on session phase |
| apps/native/src/features/student/problem/screens/ProblemScreen.tsx | Added handling for MAIN_PROBLEM_RETRY phase and updated attempt count tracking |
| apps/native/src/features/student/problem/screens/PointingScreen.tsx | Updated pointing feedback submission to include publishId and fixed CTA button labels for different paths |
| apps/native/src/features/student/home/screens/notifications/NotificationsScreen.tsx | Updated to use initWithResume and phase-based navigation |
| apps/native/src/features/student/home/components/ProblemSet.tsx | Updated to use initWithResume and phase-based navigation |
| apps/native/src/apis/controller/student/study/postPointing.ts | Added optional publishId parameter to pointing feedback API |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
|
|
||
| const findNextPointingIndex = ( | ||
| pointings: PointingWithFeedbackResp[], | ||
| lastAnsweredNo: number | undefined | null | ||
| ): number => { | ||
| if (lastAnsweredNo == null) return 0; | ||
| const lastIdx = pointings.findIndex((p) => p.no === lastAnsweredNo); | ||
| if (lastIdx === -1) return 0; | ||
| return lastIdx + 1; |
There was a problem hiding this comment.
The findNextPointingIndex helper function is defined but never used in the code. Consider removing it if it's not needed, or use it if it was intended to be part of the resume logic.
| }; | |
| const findNextPointingIndex = ( | |
| pointings: PointingWithFeedbackResp[], | |
| lastAnsweredNo: number | undefined | null | |
| ): number => { | |
| if (lastAnsweredNo == null) return 0; | |
| const lastIdx = pointings.findIndex((p) => p.no === lastAnsweredNo); | |
| if (lastIdx === -1) return 0; | |
| return lastIdx + 1; |
| const totalChildPointingCount = children.reduce((s, c) => s + (c.pointings ?? []).length, 0); | ||
| const childPointingsDone = answeredChildPointingCount >= totalChildPointingCount; | ||
|
|
||
| if (!childPointingsDone) { |
There was a problem hiding this comment.
The condition mainPointings.every((p) => p.isUnderstood == null) is redundant because if all pointings are unanswered, mainNextPIdx would already be 0 (the first unanswered pointing). The check mainNextPIdx === 0 alone is sufficient for determining whether to go to MAIN_PROBLEM_RETRY phase.
| if (!childPointingsDone) { | |
| if (mainNextPIdx === 0) { |
| if (mainNextPIdx !== -1 && mainNextPIdx > 0) { | ||
| return { | ||
| phase: 'MAIN_POINTINGS', | ||
| childIndex: INITIAL_INDEX, | ||
| pointingIndex: mainNextPIdx, | ||
| pointingTarget: 'MAIN', | ||
| mainCorrect: false, | ||
| }; | ||
| } | ||
|
|
||
| if (mainNextPIdx === 0 || mainPointings.every((p) => p.isUnderstood == null)) { | ||
| return { | ||
| phase: 'MAIN_PROBLEM_RETRY', | ||
| childIndex: INITIAL_INDEX, | ||
| pointingIndex: INITIAL_INDEX, | ||
| pointingTarget: undefined, | ||
| mainCorrect: false, | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
When the main problem is incorrect (isMainCorrect === false) and there are no child problems (totalChildren === 0), the logic falls through to line 245 which directs to MAIN_POINTINGS. However, according to the incorrect path flow, the student should retry the main problem first (MAIN_PROBLEM_RETRY) before doing main pointings. Consider adding a condition to handle the case when !isMainCorrect && totalChildren === 0 to direct to MAIN_PROBLEM_RETRY phase.
| if (mainNextPIdx !== -1 && mainNextPIdx > 0) { | |
| return { | |
| phase: 'MAIN_POINTINGS', | |
| childIndex: INITIAL_INDEX, | |
| pointingIndex: mainNextPIdx, | |
| pointingTarget: 'MAIN', | |
| mainCorrect: false, | |
| }; | |
| } | |
| if (mainNextPIdx === 0 || mainPointings.every((p) => p.isUnderstood == null)) { | |
| return { | |
| phase: 'MAIN_PROBLEM_RETRY', | |
| childIndex: INITIAL_INDEX, | |
| pointingIndex: INITIAL_INDEX, | |
| pointingTarget: undefined, | |
| mainCorrect: false, | |
| }; | |
| } | |
| // ... some existing logic for when all children are solved ... | |
| // (not shown in the snippet) | |
| } | |
| // When the main problem is incorrect and there are no child problems, | |
| // the student should retry the main problem before doing main pointings. | |
| if (!isMainCorrect && totalChildren === 0) { | |
| return { | |
| phase: 'MAIN_PROBLEM_RETRY', | |
| childIndex: INITIAL_INDEX, | |
| pointingIndex: INITIAL_INDEX, | |
| pointingTarget: undefined, | |
| mainCorrect: false, | |
| }; | |
| } | |
| const mainNextPIdx = mainPointings.findIndex((p) => p.isUnderstood == null); | |
| if (mainNextPIdx !== -1) { | |
| return { | |
| phase: 'MAIN_POINTINGS', | |
| childIndex: INITIAL_INDEX, | |
| pointingIndex: mainNextPIdx, | |
| pointingTarget: 'MAIN', | |
| mainCorrect: false, | |
| }; | |
| } | |
| return { | |
| phase: 'ANALYSIS', | |
| childIndex: INITIAL_INDEX, | |
| pointingIndex: INITIAL_INDEX, | |
| pointingTarget: undefined, | |
| mainCorrect: false, | |
| }; |
| mainCorrect: true, | ||
| phase: 'MAIN_POINTINGS', | ||
| pointingTarget: 'MAIN', | ||
| pointingIndex: 0, | ||
| }); | ||
| } else { | ||
| set({ | ||
| mainCorrect: true, | ||
| phase: 'ANALYSIS', | ||
| pointingTarget: undefined, | ||
| pointingIndex: INITIAL_INDEX, | ||
| }); | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
When the main problem is answered incorrectly on the first attempt and there are no child problems, the logic goes directly to MAIN_POINTINGS or ANALYSIS (lines 353-367). However, according to the new MAIN_PROBLEM_RETRY flow, the student should be directed to retry the main problem first. Consider changing this logic to direct to MAIN_PROBLEM_RETRY phase instead, or clarify if this path should skip the retry (which would be inconsistent with the incorrect path after child problems are completed).
|
|
||
| const lastChildIdx = findChildIndexByNo(children, lastChildNo); | ||
| if (lastChildIdx !== -1) { | ||
| const child = children[lastChildIdx]; | ||
| const childAttempts = child.attemptCount ?? 0; | ||
| const childCorrect = child.progress === 'CORRECT' || child.progress === 'SEMI_CORRECT'; | ||
| if (!childCorrect && childAttempts < MAX_RETRY_ATTEMPTS) { |
There was a problem hiding this comment.
lastChildIdx could be -1 when findChildIndexByNo returns -1 (child not found). Using it directly to compute nextChildIdx would result in nextChildIdx being 0, which could incorrectly navigate to the first child problem instead of handling the error case properly. Consider adding a check to handle the case when lastChildIdx === -1 before computing nextChildIdx.
| const lastChildIdx = findChildIndexByNo(children, lastChildNo); | |
| if (lastChildIdx !== -1) { | |
| const child = children[lastChildIdx]; | |
| const childAttempts = child.attemptCount ?? 0; | |
| const childCorrect = child.progress === 'CORRECT' || child.progress === 'SEMI_CORRECT'; | |
| if (!childCorrect && childAttempts < MAX_RETRY_ATTEMPTS) { | |
| const nextChildIdx = lastChildIdx + 1; | |
| if (nextChildIdx < children.length) { | |
| return { | |
| phase: 'CHILD_PROBLEM', | |
| childIndex: nextChildIdx, | |
| pointingIndex: INITIAL_INDEX, | |
| pointingTarget: undefined, | |
| mainCorrect: false, | |
| }; | |
| } | |
| } else { | |
| // lastSolvedChildProblemNo did not correspond to any child; start from the first child. | |
| return { | |
| phase: 'CHILD_PROBLEM', | |
| childIndex: 0, |
| [isSubmittingUnderstanding, pointing?.id] | ||
| ); |
There was a problem hiding this comment.
publishId is used inside the callback but is missing from the dependency array. This could cause the callback to capture a stale value of publishId. Add publishId to the dependency array.
📌 Related Issue Number
✅ Key Changes
DOING핸들링 구현attemptCount,lastProgressInfo기반 마지막 상태에서 이어서 풀기 구현