백로그
0할 일
0진행 중
0리뷰
0완료 (30일)
2BibleHighlight 모델 + 마이그레이션 + CRUD API
## 목표 BibleHighlight 모델 생성 + CRUD API 컨트롤러 구현 ## 1단계: 모델 + 마이그레이션 ### 마이그레이션 ```ruby create_table :bible_highlights, id: :string do |t| t.string :user_id, null: false t.string :book_abbrev, null: false # gen, exo, lev... t.integer :chapter, null: false t.integer :verse, null: false t.string :color, default: "yellow", null: false # yellow, green, blue, pink, purple t.text :note t.timestamps end add_index :bible_highlights, [:user_id, :book_abbrev, :chapter, :verse], unique: true, name: "idx_bible_highlights_unique" add_index :bible_highlights, :user_id add_foreign_key :bible_highlights, :users ``` ### 모델 (app/models/bible_highlight.rb) - belongs_to :user - validates :book_abbrev, :chapter, :verse, presence: true - validates :color, inclusion: { in: %w[yellow green blue pink purple] } - validates :verse, uniqueness: { scope: [:user_id, :book_abbrev, :chapter] } - validates :chapter, :verse, numericality: { greater_than: 0 } ### User 모델 업데이트 - has_many :bible_highlights, dependent: :destroy ### Fixture (test/fixtures/bible_highlights.yml) ```yaml genesis_1_1: id: "bh-genesis-1-1" user: daniel book_abbrev: "gen" chapter: 1 verse: 1 color: "yellow" note: "태초에 하나님이 천지를 창조하시니라" psalm_23_1: id: "bh-psalm-23-1" user: daniel book_abbrev: "psa" chapter: 23 verse: 1 color: "green" ``` ### 모델 테스트 - valid with required attributes - invalid without book_abbrev/chapter/verse - invalid with wrong color - unique constraint (same user+book+chapter+verse) - chapter/verse must be positive integers ## 2단계: CRUD API ### 라우트 (config/routes.rb) ```ruby namespace :api do namespace :bible do resources :highlights, only: [:index, :create, :destroy] end end ``` ### 컨트롤러 (app/controllers/api/bible/highlights_controller.rb) ```ruby class Api::Bible::HighlightsController < ApplicationController before_action :authenticate_user! def index highlights = current_user.bible_highlights .where(book_abbrev: params[:book], chapter: params[:chapter]) render json: highlights.map { |h| { id: h.id, verse: h.verse, color: h.color, note: h.note } } end def create highlight = current_user.bible_highlights.find_or_initialize_by( book_abbrev: highlight_params[:book_abbrev], chapter: highlight_params[:chapter], verse: highlight_params[:verse] ) highlight.assign_attributes(highlight_params) if highlight.save render json: { id: highlight.id, verse: highlight.verse, color: highlight.color, note: highlight.note }, status: :created else render json: { errors: highlight.errors.full_messages }, status: :unprocessable_entity end end def destroy highlight = current_user.bible_highlights.find(params[:id]) highlight.destroy head :no_content rescue ActiveRecord::RecordNotFound head :not_found end private def highlight_params params.require(:highlight).permit(:book_abbrev, :chapter, :verse, :color, :note) end end ``` ### 컨트롤러 테스트 - GET index: 특정 book+chapter 하이라이트 조회 - GET index: 다른 사용자 하이라이트 안 보임 - POST create: 새 하이라이트 생성 → 201 - POST create: 같은 위치 재생성 → upsert (색상 변경) - POST create: 잘못된 파라미터 → 422 - DELETE destroy: 본인 하이라이트 삭제 → 204 - DELETE destroy: 다른 사용자 하이라이트 삭제 → 404 - 미인증 요청 → 리다이렉트 ## 주의사항 - UUID PK: ApplicationRecord의 set_uuid 패턴 사용 (id: :string) - SQLite: parallelize(workers: 1) 필수 - 마이그레이션 후 `bin/rails db:migrate` 실행 - structure.sql 갱신: `bin/rails db:schema:dump` - 기존 테스트 전체 통과 확인
하이라이트 Stimulus 컨트롤러 + UI
## 목표 bible_passage_controller.js를 확장하여 성경 절 하이라이트 기능 구현 ## 의존성 - highlight-backend 에이전트가 모델 + API를 먼저 완성해야 함 - API 엔드포인트: GET/POST/DELETE /api/bible/highlights ## 구현 내용 ### 1. bible_passage_controller.js 수정 기존 bible_passage_controller.js에 하이라이트 기능을 직접 추가합니다. #### 새 Stimulus values 추가 ```javascript static values = { passage: String, highlightable: { type: Boolean, default: false } // 하이라이트 활성화 여부 } ``` #### 새 Stimulus targets 추가 ```javascript static targets = ["content", "loading", "error", "colorPopover"] ``` #### 주요 메서드 1. **renderVerses() 수정** - 기존 절 렌더링에 하이라이트 CSS 적용 - 각 verse를 `<span data-verse="N" data-action="click->bible-passage#toggleHighlight">` 으로 감싸기 - 기존 하이라이트가 있으면 배경색 클래스 적용 2. **loadHighlights()** - 페이지 로드 시 API에서 기존 하이라이트 조회 ```javascript async loadHighlights() { if (!this.highlightableValue) return const resp = await fetch(`/api/bible/highlights?book=${this.bookAbbrev}&chapter=${this.chapter}`) const highlights = await resp.json() highlights.forEach(h => this.applyHighlight(h.verse, h.color)) } ``` 3. **toggleHighlight(event)** - 절 클릭 시 색상 팝오버 표시 ```javascript toggleHighlight(event) { if (!this.highlightableValue) return const verse = event.currentTarget.dataset.verse this.showColorPopover(event.currentTarget, verse) } ``` 4. **showColorPopover(target, verse)** - 색상 선택 팝오버 - 5색 버튼: yellow, green, blue, pink, purple - 이미 하이라이트된 절이면 "삭제" 버튼도 표시 - 절 위에 popover로 표시 (position: absolute) - 외부 클릭 시 닫기 5. **selectColor(event)** - 색상 선택 → API 호출 ```javascript async selectColor(event) { const color = event.currentTarget.dataset.color const verse = this.currentVerse const resp = await fetch('/api/bible/highlights', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ highlight: { book_abbrev: this.bookAbbrev, chapter: this.chapter, verse: parseInt(verse), color } }) }) if (resp.ok) { this.applyHighlight(verse, color) this.hideColorPopover() } } ``` 6. **removeHighlight(event)** - 하이라이트 삭제 ```javascript async removeHighlight() { const highlightId = this.highlightMap.get(parseInt(this.currentVerse)) await fetch(`/api/bible/highlights/${highlightId}`, { method: 'DELETE', headers: { 'X-CSRF-Token': this.csrfToken } }) this.clearHighlight(this.currentVerse) this.hideColorPopover() } ``` 7. **applyHighlight(verse, color)** - DOM에 배경색 적용 - 색상 매핑: - yellow: bg-yellow-100 dark:bg-yellow-900/30 - green: bg-emerald-100 dark:bg-emerald-900/30 - blue: bg-blue-100 dark:bg-blue-900/30 - pink: bg-pink-100 dark:bg-pink-900/30 - purple: bg-purple-100 dark:bg-purple-900/30 ### 2. 색상 팝오버 UI ```html <!-- 팝오버는 JS로 동적 생성 --> <div class="absolute z-50 bg-surface-primary rounded-lg shadow-lg border border-border-default p-2 flex gap-1"> <button data-color="yellow" class="w-7 h-7 rounded-full bg-yellow-300 hover:ring-2 ring-yellow-500"></button> <button data-color="green" class="w-7 h-7 rounded-full bg-emerald-300 hover:ring-2 ring-emerald-500"></button> <button data-color="blue" class="w-7 h-7 rounded-full bg-blue-300 hover:ring-2 ring-blue-500"></button> <button data-color="pink" class="w-7 h-7 rounded-full bg-pink-300 hover:ring-2 ring-pink-500"></button> <button data-color="purple" class="w-7 h-7 rounded-full bg-purple-300 hover:ring-2 ring-purple-500"></button> <!-- 이미 하이라이트된 절이면 --> <button data-action="remove" class="w-7 h-7 rounded-full bg-gray-200 hover:bg-red-200 flex items-center justify-center"> <svg class="w-4 h-4">✕</svg> </button> </div> ``` ### 3. qt/today.html.erb 수정 기존 bible_passage 파셜 사용 부분에 `highlightable: true` 추가: ```erb <div data-controller="bible-passage" data-bible-passage-passage-value="<%= @qt_content.reading_passage %>" data-bible-passage-highlightable-value="true"> ``` ### 4. 색상 팝오버 파셜 (선택) - 동적 JS 생성이면 파셜 불필요 - 정적 파셜이면 `app/views/shared/_color_popover.html.erb` ## 주의사항 - CSRF 토큰: `document.querySelector('meta[name="csrf-token"]').content` - 기존 bible_passage_controller.js의 parsePassageReference()에서 bookAbbrev 추출 필요 - 하이라이트 Map으로 verse→{id, color} 매핑 관리 - 다크모드 대응 필수 (bg-yellow-100 + dark:bg-yellow-900/30) - 모바일: 탭으로 절 선택 (hover 없음) - 기존 테스트 전체 통과 확인