Files
Hyungi Ahn 63ed4d81e5 feat(study): study_topics 학습 워크스페이스 컨테이너 도입
필기 세션과 자료(library document)를 한 학습 주제(예: 가스기사) 아래로 묶는
1차 컨테이너. 향후 단어장/오디오/문제세트 등 학습 자산이 같은 묶음으로 들어올 수
있도록 응답 구조(sections + stats)를 dict 기반으로 설계.

데이터 모델 (migrations 179~185):
- study_topics: user_id × name partial unique (active 행만), soft delete
- study_sessions.study_topic_id: 1:N nullable FK (ON DELETE SET NULL)
- study_topic_documents: 자료 N:M 매핑 (user_id 반정규화로 권한 격리)

설계 원칙:
- documents.category(자료실 UI 축)와 직교 → 자료실 facet/카테고리 미터치
- StudySession.certification/subject/topic 보존 (세부 메타로 계속 사용)
- study_type은 느슨한 분류 (강한 enum 미사용, jlpt_n3 등 확장 여지)
- polymorphic study_topic_items 영구 금지 → 자산 타입별 조인 테이블 추가 방식

API: /api/study-topics CRUD + /by-document/{id} + 자료/세션 매핑 엔드포인트.
프론트: /study/topics 목록 + /study/topics/[id] 통합 뷰(필기·자료 두 트랙) +
        write 폼에 워크스페이스 드롭다운 + study hub 진입 카드.

후속 PR-2 어학 UX, PR-3 오디오 자산, PR-4 AI retrieval scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 07:06:37 +09:00

29 lines
1.5 KiB
SQL

-- 179_study_topics.sql (1/6)
-- 학습 주제(study_topic) — 학습 워크스페이스 1차 컨테이너.
-- 필기 세션 + 자료(library document)를 한 주제(예: 가스기사) 아래로 묶는다.
--
-- 핵심 개념:
-- - 단순 폴더/태그가 아닌 학습 워크스페이스 컨테이너 (향후 단어장/오디오/문제세트 등 자산이 같은 컨테이너로 들어옴).
-- - documents.category(자료실 UI 축) 와 직교. 자료실 facet/카테고리는 건드리지 않는다.
-- - study_sessions.certification/subject/topic 컬럼은 보존, 본 컨테이너 와 직교한 세부 메타로 계속 사용.
--
-- study_type 권장값: certification / language / school / work / general.
-- 백엔드는 강한 enum 으로 강제하지 않음 (DB 제약 / Pydantic Literal 모두 사용 금지).
-- 어학 세부(jlpt_n3 등) 같은 분류 확장 여지 확보.
--
-- soft delete (deleted_at): 묶음 해제 시 매핑·세션 보호.
-- partial unique index (180) 가 active 행끼리만 중복 방지 → 삭제된 주제명 재사용 가능.
CREATE TABLE IF NOT EXISTS study_topics (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(120) NOT NULL,
description TEXT,
color VARCHAR(20),
study_type VARCHAR(40),
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);