Skip to content

[feat/MAT-685] 집중학습 카드 어드민 + 발행 출제근거 매핑#328

Open
anjm1020 wants to merge 2 commits into
developfrom
feat/MAT-685
Open

[feat/MAT-685] 집중학습 카드 어드민 + 발행 출제근거 매핑#328
anjm1020 wants to merge 2 commits into
developfrom
feat/MAT-685

Conversation

@anjm1020
Copy link
Copy Markdown
Contributor

Summary

GNB

  • 집중학습 신규 nav section 추가 (문제 관리 다음 위치). SUPER admin 한정 노출 — BE admin_menu 시드 마이그레이션 follow-up 필요 (FOCUS_CARD, FOCUS_CARD_ISSUANCE).
  • canAccessPath 를 longest-prefix-wins (getMostSpecificNavItem) 로 리팩터 — /focus-card 권한이 /focus-card/issuance 까지 흡수하던 누수 제거. GNB active state 도 동일 규칙으로 통일 (TanStack Link 기본 prefix 매칭으로 중복 highlight 되던 회귀 fix).

신규 페이지

  • 집중학습 카드 목록 (/focus-card): 카드 그리드 + 삭제. 빈 상태 안내.
  • 집중학습 카드 생성 (/focus-card/register): FocusCardActionNodePickerGET /api/admin/concept/graph/node?onlyFocusCardCandidates=true이미 카드가 발급된 ActionNode 자동 제외. 제목/설명/본문 TipTap 에디터 3개.
  • 집중학습 카드 상세/수정 (/focus-card/$focusCardId): ActionNode read-only chip + nodeType.label 배지 + 설명/예시/포인팅 (InlineProblemViewer 2줄). 제목/설명/본문 부분 수정 (POST /focus-card/{id}/content). 헤더 삭제 버튼.
  • 집중학습 카드 발급 (/focus-card/issuance): 사이드바 선택 학생 + 일자별 발급 카드. 학생 취약점 패널 (GET /api/admin/analytics/concept-history/{studentId} — 시도 5회 이상 + 정답률 낮은 순 top-5). 수동 발급 모달(ActionNode picker), 발급 취소, 자동발급 트리거 (MAT-620 의 autoIssueForStudent 가 PR #75 vulnerability index 대기 중이라 현재는 빈 결과 가능).
  • 발행 상세 (/publish/$publishId): 캘린더 Day hover Eye 버튼으로 진입. 문제별 매핑된 카드 chip + 사후 매핑 추가 모달. 모달 후보는 GET /api/admin/focus-card/issuance/candidates?studentId&problemId&targetDate이 문제의 pointing 버블 액션과 매칭되는 카드만 표시.

수정 페이지

  • 발행 생성 (/publish/register/$publishDate/$studentId): 선택 세트 펼침 영역에 '출제근거 카드 매핑' 섹션 추가. 일괄 후보 조회 (POST /api/admin/publish/focus-card-link-candidates, read-only 이지만 BE 가 POST 노출 — useQuery('post', ..., { body }) 패턴). 문제별로 매칭되는 카드만 toggle 가능. 매칭 0개 문제는 회색 안내, toggle hide. linkMap 은 세트 전환 시 보존(현재 세트 item 만 발행 시 filter). PUBLISH_FOCUS_CARD_LINK_001/002 BE 검증 실패가 UI에서 사전 차단됨.
  • 발행 캘린더 (/publish): Day cell hover 액션에 Eye(상세 보기) 버튼 추가 (BarChart3/Trash2 옆).

API 추가/확장

  • 신규 컨트롤러: focusCard/{getList,getById,post,postContent,delete,postIssuance,getIssuanceByDate,getIssuanceCandidates,deleteIssuance,postAutoIssue}, publish/{postFocusCardLink,deleteFocusCardLink,getFocusCardLinkCandidates}, analytics/getConceptHistory.
  • conceptGraph/getNode{ onlyFocusCardCandidates?: boolean } 옵션 파라미터 (기본 false, 기존 호출자 무영향).

기타

  • useInvalidate 에 4개 invalidator 추가: focus card list/item, issuance by-date, publish detail.
  • useActionNodeDetail / useActionNodeTypeId 훅 추가 (hooks/).
  • 컨벤션 정리: 컨트롤러 param 인터페이스를 @types 로 이동, hook 을 hooks/ 로 배치, POST 후보 조회를 useQuery 로 통일.

Linear

Test plan

카드 템플릿 CRUD

  • SUPER admin 로그인 → GNB '집중학습 > 집중학습 카드' 노출 확인
  • /focus-card → '신규 생성' → ActionNode picker 가 카드 없는 노드만 보여줌 → 제목/설명/본문 입력 → 저장 → 목록에 노출
  • 같은 ActionNode 로 재생성 시도 → picker 후보에서 자동 제외돼 선택 불가
  • /focus-card/$id → ActionNode 정보(nodeType 배지 + 설명/예시/포인팅) 노출 → 본문 수정 → 저장 토스트
  • 카드 삭제 → 확인 모달 → 목록에서 제거

학생 카드 발급

  • 사이드바 학생 선택 → '집중학습 카드 발급' 진입 → 일자 변경 시 발급 카드 리스트 갱신
  • 학생 취약점 패널: 시도 5회 이상 개념 top-5 (정답률 낮은 순). 시도 부족 시 안내 메시지.
  • 수동 발급 모달 → ActionNode 선택 → 발급 → 리스트 갱신
  • 자동 발급 실행 → 토스트 (현재 BE stub 이라 0개 발급될 수 있음)
  • 발급 취소 → 확인 모달 → 리스트에서 제거
  • 학생 미선택 시 헤더 버튼 hide + 빈 상태 안내

발행 inline 매핑

  • /publish 캘린더에서 학생 선택 후 빈 날짜 클릭 → 세트 검색 → 세트 선택 시 출제근거 매핑 섹션 자동 노출
  • 매칭 카드 있는 문제만 toggle 가능 / 매칭 0개 문제는 회색 안내
  • toggle on → 발행 → publish 상세에서 focusCards chip 노출
  • 세트 A 토글 → 세트 B 선택 → 다시 A 선택 시 A 의 매핑 복원
  • 매칭 안 되는 카드 시도 차단 (애초에 후보로 안 떠야 함)

발행 사후 매핑

  • /publish 캘린더 Day hover → Eye 클릭 → 상세 진입
  • 문제별 chip 노출. '카드 매핑' 클릭 → 모달에 매칭 카드만 (다른 문제 후보 X)
  • 후보 0개 문제: '매칭되는 발급 카드가 없습니다' 안내
  • 카드 클릭 → 매핑 추가 → chip 추가됨
  • chip X → 삭제 → 매핑 제거
  • 학생 미선택 시 amber 경고 배너 + 후보 fetch 차단

권한

  • SUPER admin: '집중학습' 섹션 전체 노출
  • ROLE_BASED admin (BE seed 전): 미노출, 직접 URL 입력 시 redirect

Known follow-ups

  • BE admin_menu 시드 마이그레이션에 FOCUS_CARD, FOCUS_CARD_ISSUANCE 추가 + 컨트롤러 메뉴 ACL 연동.
  • MAT-620 의 autoIssueForStudent (vulnerability index PR #75 머지 후 활성화). 그때까진 자동발급 결과가 빈 ListResp.
  • PublishRespstudentId 추가 (선택) → 발행 상세에서 사이드바 의존 제거 가능.
  • 규모상 단일 PR 검토 부담 시: Phase A(템플릿 CRUD + GNB) / B(발급 + 취약점) / C(발행 매핑) 로 분할 가능.

🤖 Generated with Claude Code

anjm1020 and others added 2 commits May 12, 2026 16:13
- Regen OpenAPI schema from QA exposing new focus-card and
  publish-focus-card-link endpoints
- focusCard/* controllers for template CRUD, per-student issuance,
  candidates lookup
- publish/* controllers for focus-card-link CRUD and bulk candidates
  query (POST-as-read via useQuery + body)
- analytics/getConceptHistory for student vulnerability lookup
- Extend conceptGraph/getNode with optional onlyFocusCardCandidates
  param (default false, backwards-compatible)
- Add 3 param interfaces to types/api/queryParams.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /focus-card: template list + delete
- /focus-card/register: new template with FocusCardActionNodePicker
  (uses onlyFocusCardCandidates to hide nodes that already have a card)
- /focus-card/$focusCardId: template detail/content edit with
  ActionNode description / payload (example, pointingExample) panel
- /focus-card/issuance: per-student issuance management — by-date
  list, manual issue modal, revoke, auto-issue trigger, student
  vulnerability panel (top-5 weak concepts via analytics)
- /publish/$publishId: publish detail with focus card link chips per
  problem + add-modal that loads only problem-matching candidates
- /publish (modify): Day cell hover Eye button → publish detail
- /publish/register/$publishDate/$studentId (modify): inline focus
  card mapping section using bulk candidates query
  (POST /publish/focus-card-link-candidates); only matching cards
  selectable; submit body includes focusCardLinks
- Add useActionNodeDetail / useActionNodeTypeId hooks
- Add 4 invalidators: focus card list/item, issuance by-date,
  publish detail
- Add FOCUS_CARD / FOCUS_CARD_ISSUANCE menus under new 집중학습 nav
  section (between 문제 관리 and 개념 그래프). SUPER admin only until
  BE menu seed lands
- Refactor canAccessPath to longest-prefix-wins
  (getMostSpecificNavItem) so /focus-card permission does not leak
  into /focus-card/issuance; GNB active state follows the same rule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented May 12, 2026

MAT-685

@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

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

Project Deployment Actions Updated (UTC)
pointer-admin Ready Ready Preview, Comment May 12, 2026 7:16am

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62ca4f6ee1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +35 to +36
const problemSetId = publish?.problemSet?.id ?? 0;
const { data: problemSet } = getProblemSetById({ id: problemSetId });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer problem-set fetch until publish data is available

getProblemSetById is invoked with problemSetId defaulting to 0 before getPublishById resolves, so this page sends an unnecessary /api/admin/problem-set/0 request on initial render. That call is guaranteed to fail for normal data, adds avoidable network/error noise, and can trigger retries before the real problem-set id is known. Gate this query with an enabled check (or conditional call) until publish?.problemSet?.id is present.

Useful? React with 👍 / 👎.

Comment on lines +109 to +111
onSuccess: () => {
invalidateFocusCardIssuanceByDate(studentId, issuedDate);
toast.success('카드가 발급되었습니다');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate publish-link candidates after issuance mutations

After issuing/revoking cards, this code only invalidates issuance/by-date; it never invalidates the candidate query used by /publish/register (/api/admin/publish/focus-card-link-candidates). Because queries are configured as perpetually fresh (staleTime: Infinity), previously viewed candidate results remain cached and newly issued cards may not appear in mapping UI until a hard reload. Add candidate-query invalidation (or force refetch semantics) in these mutation success handlers.

Useful? React with 👍 / 👎.

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