feat: 오답노트 — 질문 북마크 + 모아보기 (B-4) - #178
Merged
Merged
Conversation
다시 볼 질문을 표시해 두고 나중에 모아 복습한다.
- `PUT /api/sessions/{sid}/messages/{mid}/bookmark` — 표시/해제
- `GET /api/users/me/bookmarks` — 모아보기
## 설계 판단
**질문(INTERVIEWER)에만 걸 수 있다** (`MESSAGE_NOT_BOOKMARKABLE`, 422). 답변을 표시해 봐야
복습할 게 없다.
**토글이 아니라 명시적 상태를 받는다.** 토글이면 재전송·더블클릭이 상태를 뒤집어 놓는다.
같은 요청이 두 번 와도 결과가 같다.
**목록은 질문 + 그때 내 답변 + 모범답안/코칭을 한 묶음으로** 반환한다. 질문만 모아 봐야
"내가 뭘 틀렸는지"를 알 수 없다. 답변은 `findByParentMessage_IdIn` 으로 한 번에 받아
매핑한다 — 질문마다 조회하면 N+1 이다.
**별도 테이블을 만들지 않았다.** V26 으로 `interview_messages.bookmarked` + 부분 인덱스
(`WHERE bookmarked = TRUE`). 북마크의 수명은 질문의 수명과 같다 — 세션을 지우면 그 질문도
사라지는 게 맞다(목록 쿼리가 `session.deleted = false` 로 거른다).
엔티티 변경 메서드는 `applyBookmark` 다. `setBookmarked` 로 두면 ArchUnit 의
"엔티티 public setter 금지" 룰에 걸린다.
`QuestionBookmarkController` 는 URL 이 `/api/users/me/*` 지만 `UserStatsController` 와 같은
이유로 session 슬라이스에 둔다(user → session 직접 의존 회피).
## 테스트
`QuestionBookmarkServiceTest` 신설 (8) — 표시, 멱등, 답변 표시 거부, 타인 세션 거부,
**다른 세션의 메시지 id 끼워넣기 거부**, 답변·코칭 결합, 답변 없는 질문도 목록에 노출,
북마크 0건일 때 답변 조회 스킵.
- 종료 세션 기록(`InterviewTranscript`)의 질문 버블에 별 버튼. **라이브 중엔 안 띄운다** — 답변에 집중해야 할 때 끼어드는 조작이다(`bookmarkable` prop 으로 게이팅) - 워크스페이스에 '오답노트' 메뉴 + `/workspace/bookmarks` - **모범 답안은 접혀 있다.** 먼저 스스로 떠올려 보고 펼치는 게 복습의 핵심이라, 펼치기 전에는 질문과 평가 관점만 보인다 - 답변·피드백 기록이 없는 질문(답변 전에 담았거나 피드백 전)은 빈 아코디언 대신 이유를 적어 준다 - 담기/빼기 모두 명시적 상태를 보낸다(서버 계약과 동일 — 재전송이 상태를 뒤집지 않게) 북마크는 면접 질문이라 별도 feature 슬라이스로 쪼개지 않고 `features/interview` 에 뒀다. 쪼갰다면 질문 버블의 토글이 feature 간 import 가 돼 FSD 규칙에 걸린다. 테스트: `BookmarkList.test.tsx`(5) — 빈 상태, 모범답안 접힘→펼침, 복습 재료 없을 때 안내, 출처 면접 링크, 빼기 인자. `QuestionBubble.test.tsx` 신설(3) — 기본은 별 미노출, 담기/빼기 인자. vitest 98/98 · eslint 0 · tsc · build 통과.
- `docs/database.md` — interview_messages.bookmarked(V26) + 부분 인덱스 - `backend/CLAUDE.md` — 엔드포인트·설계 판단(질문 전용, 명시적 상태, N+1 회피, 컨트롤러 위치) - `frontend/src/features/CLAUDE.md` — interview 슬라이스 책임에 오답노트 추가와 그 이유
i3months
added a commit
to i3months/stackup
that referenced
this pull request
Aug 28, 2026
## 드릴 상태 기계 추출 (Team-StackUp#178 에서 미뤘던 것) `usePracticeSession` 안에 섞여 있던 "한 문제씩 넘기며 답을 적고 정답을 확인하는" 상태를 `useQuestionRunner(questionIds, storageKey?)` 로 뽑았다. 질문 id 목록만 받고 질문을 어디서 얻는지는 모른다. **`shared/hooks` 에 뒀다.** 처음엔 `features/practice` 에 두고 오답노트에서 가져다 썼는데, features 끼리 import 는 FSD 위반이다(§3). 도메인 타입에 의존하지 않으므로 shared 가 맞다. `PracticeRunner` 는 손대지 않았다 — 정적 질문 은행을 쓰는 전체 화면 레이아웃 그대로고, 내부 상태만 공용 훅으로 바뀌었다. ## A-7 — 연습 모드 답변이 새로고침하면 사라지던 문제 `storageKey` 를 주면 답변 메모를 localStorage 에 남긴다. 사용자가 직접 쓴 것이라 잃으면 손실이 크다(질문 목록은 다시 뽑으면 그만). 트랙별로 키를 나눈다. - 저장 실패(용량 초과·프라이빗 모드)는 무시한다 — 드릴이 멈추면 안 된다 - 읽을 때 형식을 검사한다. 다른 코드가 같은 키를 썼거나 형식이 바뀌어도 빈 상태로 시작할 뿐 `reset()` 에서 `removeItem` 을 호출했다가 걷어냈다 — answers 변경으로 저장 이펙트가 곧바로 `"{}"` 를 다시 써서 무의미했다(테스트가 잡았다). 저장은 이펙트 하나가 책임진다. ## 오답노트 복습 드릴 목록 위 '복습 시작' 으로 진입. 한 문제씩 다시 답해 본 뒤 **정답 확인**을 눌러야 그때 내 답변· 모범 답안·코칭이 보인다. 여기 적는 답도 localStorage 에 남는다. 기록이 없는 질문(답변 전에 담았거나 피드백 전)도 드릴에 포함하고, 펼쳤을 때 이유를 알려준다. ## 테스트 - `useQuestionRunner.test.ts` 신설 (7) — 순서·마지막·완료, 다음 질문에서 정답 재차단, **저장/복원**, storageKey 없으면 미저장, reset, **망가진 저장값 방어 3종**, 질문 목록이 짧아질 때 범위 접힘(오답노트에서 항목을 빼는 경우) - `BookmarkDrill.test.tsx` 신설 (6) — 정답 확인 전 미노출, 다음 질문에서 재차단, 기록 없는 질문 안내, 완료 화면, 나가기, 재진입 후 답변 유지 vitest 111/111 · eslint 0 · tsc · build 통과.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
변경 사항
감사 백로그 B-4. 다시 볼 질문을 표시해 두고 나중에 모아 복습한다.
PUT /api/sessions/{sid}/messages/{mid}/bookmark— 표시/해제GET /api/users/me/bookmarks— 모아보기/workspace/bookmarks설계 판단
질문(INTERVIEWER)에만 걸 수 있다 (
MESSAGE_NOT_BOOKMARKABLE, 422). 답변을 표시해 봐야 복습할 게 없다.토글이 아니라 명시적 상태를 받는다.
{"bookmarked": true|false}. 토글이면 재전송·더블클릭이 상태를 뒤집어 놓는다. 같은 요청이 두 번 와도 결과가 같다.목록은 질문 + 그때 내 답변 + 모범답안/코칭을 한 묶음으로 반환한다. 질문만 모아 봐야 "내가 뭘 틀렸는지"를 알 수 없다. 답변은
findByParentMessage_IdIn으로 한 번에 받아 매핑한다 — 질문마다 조회하면 N+1 이다.별도 테이블을 만들지 않았다. V26 으로
interview_messages.bookmarked+ 부분 인덱스(WHERE bookmarked = TRUE). 북마크의 수명은 질문의 수명과 같다 — 세션을 지우면 그 질문도 사라지는 게 맞고, 목록 쿼리가session.deleted = false로 거른다.엔티티 변경 메서드 이름은
applyBookmark다.setBookmarked로 두면 ArchUnit 의 "엔티티 public setter 금지" 룰에 걸린다.UX 판단
라이브 면접 중에는 별을 띄우지 않는다. 답변에 집중해야 할 때 끼어드는 조작이라, 종료 세션 기록(
InterviewTranscript)에서만 켠다(bookmarkableprop).모범 답안은 접혀 있다. 먼저 스스로 떠올려 보고 펼치는 게 복습의 핵심이다. 펼치기 전에는 질문과 평가 관점만 보인다.
답변·피드백 기록이 없는 질문(답변 전에 담았거나 피드백 생성 전)은 빈 아코디언 대신 이유를 적어 준다.
practice 러너 재사용에 대해
원래 백로그에는 "practice 러너 재사용"이라고 적었지만, 실제로 보니
PracticeRunner는 정적 QuestionBank 를 로드하는usePracticeSession(track)에 묶인 전체 화면 레이아웃(헤더의 나가기, 푸터)이었다. 워크스페이스 안에 끼워 넣으려면 상태 훅과 표현을 갈라내는 리팩터가 필요한데, 그 리팩터를 이 PR 에 섞으면 연습 모드까지 회귀 위험을 지게 된다.대신 목록에서 모범 답안을 접어 두는 것으로 같은 효과(회상 → 확인)를 냈다. 한 문제씩 넘기는 드릴이 필요하면 러너 상태 훅(
useQuestionRunner) 추출을 별도 PR 로 하는 게 안전하다.테스트
QuestionBookmarkServiceTest(8)BookmarkList.test.tsx(5)QuestionBubble.test.tsx(3)backend
test통과(ArchUnit 포함) · frontend vitest 98/98 · eslint 0 · tsc · build.영향 범위
MessageResponse.bookmarked)MESSAGE_NOT_FOUND(404),MESSAGE_NOT_BOOKMARKABLE(422)리뷰어 체크포인트