Skip to content

feat: 오답노트 — 질문 북마크 + 모아보기 (B-4) - #178

Merged
i3months merged 4 commits into
devfrom
feature/question-bookmarks
Aug 19, 2026
Merged

feat: 오답노트 — 질문 북마크 + 모아보기 (B-4)#178
i3months merged 4 commits into
devfrom
feature/question-bookmarks

Conversation

@i3months

Copy link
Copy Markdown
Member

변경 사항

감사 백로그 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)에서만 켠다(bookmarkable prop).

모범 답안은 접혀 있다. 먼저 스스로 떠올려 보고 펼치는 게 복습의 핵심이다. 펼치기 전에는 질문과 평가 관점만 보인다.

답변·피드백 기록이 없는 질문(답변 전에 담았거나 피드백 생성 전)은 빈 아코디언 대신 이유를 적어 준다.

practice 러너 재사용에 대해

원래 백로그에는 "practice 러너 재사용"이라고 적었지만, 실제로 보니 PracticeRunner 는 정적 QuestionBank 를 로드하는 usePracticeSession(track) 에 묶인 전체 화면 레이아웃(헤더의 나가기, 푸터)이었다. 워크스페이스 안에 끼워 넣으려면 상태 훅과 표현을 갈라내는 리팩터가 필요한데, 그 리팩터를 이 PR 에 섞으면 연습 모드까지 회귀 위험을 지게 된다.

대신 목록에서 모범 답안을 접어 두는 것으로 같은 효과(회상 → 확인)를 냈다. 한 문제씩 넘기는 드릴이 필요하면 러너 상태 훅(useQuestionRunner) 추출을 별도 PR 로 하는 게 안전하다.

테스트

레이어 신규 내용
backend QuestionBookmarkServiceTest (8) 표시, 멱등, 답변 표시 거부, 타인 세션 거부, 다른 세션의 메시지 id 끼워넣기 거부, 답변·코칭 결합, 답변 없는 질문도 노출, 북마크 0건일 때 답변 조회 스킵
frontend BookmarkList.test.tsx (5) 빈 상태, 모범답안 접힘→펼침, 복습 재료 없을 때 안내, 출처 면접 링크, 빼기 인자
frontend QuestionBubble.test.tsx (3) 기본은 별 미노출, 담기/빼기 인자

backend test 통과(ArchUnit 포함) · frontend vitest 98/98 · eslint 0 · tsc · build.

영향 범위

  • DB 마이그레이션: 있음 (V26 — 컬럼 추가 + 부분 인덱스. 기존 행은 FALSE)
  • API contract 변경: 추가만 (엔드포인트 2개, MessageResponse.bookmarked)
  • 환경변수: 없음
  • 신규 에러코드: MESSAGE_NOT_FOUND(404), MESSAGE_NOT_BOOKMARKABLE(422)

리뷰어 체크포인트

  • 세션을 지우면 그 질문의 북마크도 사라진다 — 의도한 동작이다. "질문은 남기고 싶다"면 별도 테이블로 복사해야 하는데, 그건 출처를 잃은 질문이 된다고 봤다
  • 라이브 중 별을 숨긴 판단. "그 순간 담고 싶다"는 요구가 있으면 되돌리기 쉽다

다시 볼 질문을 표시해 두고 나중에 모아 복습한다.

- `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
i3months merged commit 30361a3 into dev Aug 19, 2026
5 checks passed
@i3months
i3months deleted the feature/question-bookmarks branch August 19, 2026 02:23
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.

1 participant