부모 티켓
168개 티켓

백로그

0
티켓 없음

할 일

0
티켓 없음

진행 중

0
티켓 없음

리뷰

0
티켓 없음

완료 (30일)

168
긴급 62bb0471
부모 티켓

[P0-SETUP] Rails 프로젝트 초기화 + Docker 환경

## 개요 Rails 8.1.2 프로젝트 초기화 및 개발 환경 구성 ## 범위 - Rails 8.1.2 new 프로젝트 생성 (SQLite3, Importmap, Propshaft) - Tailwind CSS v4 설정 - Docker Compose 개발 환경 (WSL2 호환) - Kamal 2.10.1 배포 설정 (DigitalOcean) - CI 스크립트 (bin/ci) - 기본 ApplicationRecord에 UUID PK 설정 ## 완료 기준 - [ ] `rails s`로 Welcome 페이지 표시 - [ ] Docker Compose로 개발 환경 구동 - [ ] Tailwind CSS v4 동작 확인 - [ ] UUID PK 기본 설정 완료 - [ ] Kamal deploy 설정 파일 존재 ## 참고 - docs/migration/index.md §9 기술 결정 사항 - templates/ 디렉토리 참조

2/2
팀리드
9 days
긴급 7225a7a8

[Auth] 인증 코어 교체 - Devise → OmniAuth 직접 세션 관리

## 목표 Devise를 제거하고 session[:user_id] 기반 직접 인증으로 전환 ## 작업 내용 1. ApplicationController에 current_user, authenticate_user!, user_signed_in? 직접 구현 2. SessionsController 신규 생성 (로그인 페이지, 로그아웃) 3. OmniauthCallbacksController - Devise 상속 제거 → 독립 컨트롤러 4. 라우트: devise_for 제거 → OmniAuth + sessions 직접 라우트 5. User 모델: devise 매크로 제거, from_omniauth에서 Devise.friendly_token 제거 6. 사이드바 로그아웃 버튼 경로 수정 ## 완료 기준 - session[:user_id]로 로그인/로그아웃 작동 - current_user, user_signed_in?, authenticate_user! 정상 동작 - Google/Kakao OmniAuth 콜백 정상 처리 - 기존 26개 뷰/컨트롤러에서 current_user 사용 이상 없음

미배정 9 days
보통 9e79a789
부모 티켓

디자인 시스템

사용자 경험 관점에서 UI UX 디자인 컴포넌트 설계하고, 전체 프로젝트에 적용하기. 사용자가 입력하기 편하고 세련되고 깔끔해야함.

4/4
팀리드
9 days
높음 66e8e018
서브 티켓 디자인 시스템

[DS-1] 디자인 토큰 정리 + 기반 파셜 개선

## 목표 디자인 토큰을 정리하고 핵심 파셜을 개선하여 전체 디자인 시스템의 기반을 강화한다. ## 작업 내용 ### 1. 레거시 tailwind.config.js 정리 - `templates/tailwind.config.js` 삭제 (v3 레거시, 실제 미사용) - `app/assets/tailwind/application.css`가 유일한 진실 소스임을 확인 ### 2. _button 파셜 확장 (가장 중요!) - `tag:` 옵션 추가 → `:button` (기본), `:a`, `:submit` 지원 - `:a`일 때 `href:` 파라미터 지원, `link_to` 없이 `<a>` 태그 렌더 - `:submit`일 때 `<input type="submit">` 또는 `<button type="submit">` 렌더 - 기존 variant(primary/secondary/outline/ghost/danger), size(sm/md/lg) 유지 - `icon:` 옵션 추가 (SVG 아이콘을 텍스트 앞에 배치) ### 3. _empty_state 파셜 신규 생성 - locals: `icon:` (SVG), `title:`, `description:`, `action_text:`, `action_path:`, `action_variant:` - 중앙 정렬, 아이콘 + 제목 + 설명 + CTA 버튼 - `_button` 파셜 재활용 ### 4. _pagination 파셜 신규 생성 - locals: `pagy:` (pagy 객체) 또는 `current_page:, total_pages:, base_path:` - 이전/다음 + 페이지 번호 표시 - 모바일: 이전/다음만, 데스크톱: 전체 번호 ### 5. auth 레이아웃 토큰 적용 - `app/views/layouts/auth.html.erb`에서 `bg-gray-50` → `bg-surface-muted`, `dark:bg-gray-900` → 토큰 사용 ## 완료 기준 - `templates/tailwind.config.js` 삭제됨 - `_button` 파셜이 tag: :a, :submit 지원 - `_empty_state`, `_pagination` 파셜 생성됨 - auth 레이아웃이 시멘틱 토큰 사용 - 기존 테스트 전부 통과 (bin/rails test)

D
ds-foundation
9 days
보통 a3cc36a1
부모 티켓

날짜 버튼

날짜 입력이 어렵게 되어 있음 날짜 관련된 버튼 점검 한국날짜 계산 확인

3/3
팀리드
9 days
긴급 78906aa0
서브 티켓 날짜 버튼

[DATE-1] 한국 시간대(KST) 설정 + 날짜 계산 수정

## 목표 Rails 앱의 시간대를 Asia/Seoul로 설정하고, 모든 날짜 계산이 한국 시간 기준으로 동작하도록 수정 ## 작업 내용 ### 1. config/application.rb 시간대 설정 - `config.time_zone = "Asia/Seoul"` 주석 해제/추가 - 이것만으로 Date.current, Time.current 등이 KST 기준으로 동작 ### 2. 기존 Date.current 사용처 검증 (12곳) - `app/controllers/qt_controller.rb` - QT 오늘 일차 계산 - `app/controllers/qt/sessions_controller.rb` - 오늘 묵상 통계, 월별 랭킹 - `app/controllers/prayers_controller.rb` - 기도 체크 날짜 - `app/controllers/bible_readings_controller.rb` - 통독 기록 날짜 - `app/controllers/sermons_controller.rb` - 새 설교 기본값 - `app/controllers/stats_controller.rb` - 연속 묵상 계산 - `app/jobs/notification_cron_job.rb` - 알림 발송 (이미 in_time_zone 사용 중) - `app/views/qt/meditations/_form.html.erb` - 묵상 날짜 기본값 ### 3. session_form_controller.js 시간대 수정 - `new Date(start)` → UTC 파싱 이슈 해결 - `new Date(start + 'T00:00:00+09:00')` 또는 날짜 문자열 직접 파싱 ### 4. 테스트 작성 - 시간대 설정 확인 테스트 - Date.current가 KST 기준인지 확인 - QT 일차 계산이 KST 기준인지 확인 ## 완료 기준 - `config.time_zone = "Asia/Seoul"` 설정됨 - 기존 테스트 모두 통과 - 새 시간대 관련 테스트 추가됨 - JS 날짜 계산도 KST 기준으로 동작 ## 담당 파일 - config/application.rb - app/javascript/controllers/session_form_controller.js - test/ 관련 테스트 파일

T
timezone-dev
9 days
높음 cfb377e3

스키마 중복 정리

## 목표 users 테이블의 user_settings와 중복되는 3개 컬럼 제거 ## 제거 대상 컬럼 - `notification_enabled` (users) → user_settings.notification_enabled 사용 중 - `notification_time` (users) → user_settings.notification_time 사용 중 - `current_session_id` (users) → user_settings.current_session_id 사용 중 ## 완료 기준 - [ ] 마이그레이션: remove_column :users, :notification_enabled / :notification_time / :current_session_id - [ ] 코드에서 users.notification_enabled 등 참조 없음 확인 - [ ] 기존 테스트 전체 통과 - [ ] structure.sql 갱신 ## 주의사항 - SQLite remove_column 시 테이블 재생성됨 → uuid 타입 소실 주의 - Phase 0: 다른 기능 구현 전 선행 필수

미배정 8 days
높음 d2a5dae6

[P2] 기도 페이지 4탭 통합 + 인터랙션 개선

H1: 기도 4탭 통합 (Today/목록/동역자/통계), H2: Today 기도 진행률, H5: CSS 애니메이션, H6: 하단 네비 스크롤 숨김

P
phase2-agent
8 days
긴급 4035ebfa

[모임] 스키마/모델 생성 (Group, GroupMember, AttendanceRecord)

## 구현 내용 Group, GroupMember, AttendanceRecord 3개 모델 생성 ### Group (모임) - name (string) - 모임 이름 - description (text) - 모임 설명 - token (string, unique) - 초대 링크용 토큰 - invite_code (string, unique) - 6자리 초대코드 - recurrence_type (string) - daily/weekly/monthly/custom - meeting_day (integer, nullable) - 요일(0-6) 또는 일(1-31) - meeting_time (time, nullable) - 모임 시간 - late_minutes (integer, nullable) - 지각 기준 분 - starts_on (date) - 시작일 - expires_on (date, nullable) - 만료일 - qt_enabled (boolean, default: true) - QT 진행 여부 - user_id (string) - 생성자 - status (string, default: active) - active/archived ### GroupMember (모임 멤버) - group_id (string) - user_id (string) - role (string, default: member) - creator/admin/member - is_active (boolean, default: true) - joined_at (datetime) ### AttendanceRecord (출석 기록) - group_id (string) - group_member_id (string) - attended_on (date) - 출석 날짜 - checked_at (datetime) - 체크 시각 - status (string) - present/late/absent ## 완료 기준 - 마이그레이션 3개 생성 및 실행 - 모델 관계 설정 (User has_many :groups, Group has_many :group_members, has_many :attendance_records) - 유효성 검증 (presence, uniqueness) - UUID PK, FK 금지 (belongs_to만) - 기본 모델 테스트 통과

미배정 8 days
높음 ad6f0585

[모임] 출석 QR 이미지 렌더링 + 다운로드 기능

## 목표 출석 QR을 텍스트 URL 대신 실제 QR 이미지로 렌더링하고, 초대/출석 QR 모두 다운로드 가능하게 함 ## 작업 내용 1. `show.html.erb` 출석 섹션(line 178-183)에 qr Stimulus 컨트롤러 추가 (attendance_check_url) 2. `qr_controller.js`에 download() 액션 추가 (canvas → PNG 다운로드) 3. 초대/출석 QR 모두 다운로드 버튼 추가 ## 수정 파일 - app/views/groups/show.html.erb - app/javascript/controllers/qr_controller.js ## 완료 기준 - 출석 QR이 canvas 이미지로 렌더링됨 - 다운로드 버튼 클릭 시 PNG 파일 저장 - 기존 초대 QR 기능 정상 동작

팀리드
8 days
높음 01937374

shadcn/ui 스타일 전면 리디자인

레거시 커스텀 토큰(brand-primary, surface-default 등)을 shadcn/ui 시맨틱 토큰(primary, background, foreground 등)으로 전면 교체. Phase 1-7 단계 실행.

O
orchestrator
8 days
높음 50099c81

[묵상기록] 기록 표시 파셜 분리 + 통독/QT 양탭 통합

## 목표 기존 `_form.html.erb`에서 기록 표시 영역을 분리하여 통독/QT 양쪽 탭에서 묵상 기록을 볼 수 있게 한다. ## 작업 내용 1. `_form.html.erb`에서 기록 표시(personal_meditation, action_plan, prayer_topic 카드)를 `_records.html.erb`로 분리 2. `today.html.erb`의 통독 탭에 `_records.html.erb` 파셜 추가 3. QT 탭에도 동일하게 `_records.html.erb` 파셜 유지 4. 기분(mood_after) 선택 UI를 양쪽 탭에서 접근 가능하게 5. 저장 버튼도 양쪽 탭에 표시 ## 기술 참고 - UserMeditation 모델은 변경 불필요 (qt_content_id는 두 탭이 같은 날의 콘텐츠 공유) - 기존 turbo-frame "meditation_form" 패턴 활용 - 기분 UI: MOOD_EMOJIS peer CSS 패턴 유지 ## 완료 기준 - 통독 탭에서 기존 묵상 기록(묵상/적용/기도제목) 카드가 보인다 - 기분 선택 + 저장이 양쪽 탭에서 동작한다 - 기존 QT 탭 기능이 깨지지 않는다

미배정 8 days
높음 62877583

[레이아웃] 컨테이너 3-tier 폭 시스템 구축

## 개요 application.html.erb의 기본 max-w-2xl(672px)을 3-tier 시스템으로 교체 ## 3-tier 정의 - narrow: max-w-2xl (672px) — 폼, 설정 - default: max-w-4xl (896px) — 일반 콘텐츠 - wide: max-w-6xl (1152px) — 통계, 그리드 ## 구현 ```erb <% width_map = { 'narrow' => 'max-w-2xl', 'wide' => 'max-w-6xl' } container_width = width_map[content_for(:layout_width).presence] || 'max-w-4xl' %> <div class="px-4 py-6 md:px-8 md:py-8 <%= container_width %> mx-auto"> ``` ## 파일 - `app/views/layouts/application.html.erb` (1개) ## 완료 기준 - layout에서 3-tier 분기 동작 - content_for(:wide_layout) 제거 - 기존 페이지가 max-w-4xl 기본값으로 표시됨

미배정 7 days
높음 aa74f714
부모 티켓

[레이아웃] 전체 폭 개선 - Coordination

레이아웃 폭 개선 4개 티켓 일괄 처리 조율 티켓. 3-tier 시스템 구축 → 중복 제거 + narrow/wide 선언 → 특수 페이지 점검

2/2
팀리드
2 days
높음 5dd2b4da
서브 티켓 [레이아웃] 전체 폭 개선 - Coordination

[구현] 3-tier 레이아웃 시스템 + 기본 폭 뷰 정리

## 작업 1: application.html.erb 3-tier 시스템 구축 `app/views/layouts/application.html.erb`에서: - 기존 `content_for?(:wide_layout) ? 'max-w-4xl' : 'max-w-2xl'` 를 3-tier로 교체: ```erb <% width_map = { 'narrow' => 'max-w-2xl', 'wide' => 'max-w-6xl' } container_width = width_map[content_for(:layout_width).presence] || 'max-w-4xl' %> ``` - div에 `<%= container_width %>` 적용 ## 작업 2: 기본 폭 뷰 정리 (중복 max-w-2xl 제거) 아래 11개 뷰에서 자체 `max-w-2xl mx-auto` 와 중복 `px-4 py-6` 제거 (레이아웃 기본 max-w-4xl 사용): - qt/sessions/show.html.erb - qt/sessions/edit.html.erb - qt/sessions/members.html.erb - qt/sessions/rankings.html.erb - qt/sessions/shared_meditations.html.erb - qt/themes/show.html.erb - prayer_partners/search.html.erb - prayer_partners/index.html.erb - bible_highlights/index.html.erb - group_qt_sessions/new.html.erb - pages/home.html.erb (내부 max-w-2xl 섹션) 주의: 내부 spacing (space-y-6 등)은 유지, max-w와 mx-auto와 중복 px/py만 제거

L
layout-dev
2 days
높음 605ca25b

shadcn 스타일 디자인 시스템 정비

현재 어색한 UI를 shadcn/ui 수준으로 정비. 파셜 리디자인 + 뷰 일관성 적용 + 다크모드 보완.

O
orchestrator
5 days
높음 7fec3845

[P0-SETUP] 디자인 시스템 (shared partials)

## 개요 shadcn/ui 컴포넌트를 Tailwind CSS ERB 파셜로 변환한 디자인 시스템 구축 ## 범위 - 공통 레이아웃 (AppLayout, Header, Sidebar, BottomNav) - UI 파셜 16개: Button, Card, Input, Modal, Select, Tabs, Avatar, Badge, Progress, Calendar, Switch, Tooltip, Alert, Dropdown, Separator, Table - 모바일 반응형 하단 네비게이션 (4탭: QT, 묵상, 통독, 기도) - 다크모드 지원 (Tailwind dark: + prefers-color-scheme) ## 완료 기준 - [ ] app/views/shared/ 에 파셜 존재 - [ ] 레이아웃 반응형 (모바일 하단탭 / 데스크톱 사이드바) - [ ] 다크모드 전환 동작 - [ ] Stimulus 컨트롤러: tabs, modal, dropdown, tooltip ## 참고 - docs/migration/index.md §6 UI 구조 - 레거시 src/components/ui/ (27개 shadcn 컴포넌트)

미배정 9 days
높음 e9df7b18

[Auth] Devise gem 및 설정 파일 제거

## 목표 Devise 관련 코드/설정 완전 제거 ## 작업 내용 1. Gemfile에서 gem "devise" 제거 + bundle install 2. config/initializers/devise.rb 삭제 3. app/views/devise/ 디렉토리 삭제 (로그인 뷰는 sessions로 이동 완료 후) 4. app/controllers/users/ 디렉토리 정리 ## 의존성 - 티켓 1 (인증 코어 교체) 완료 후 진행 ## 완료 기준 - Devise gem 없이 서버 정상 기동 - 불필요한 Devise 파일 모두 제거됨

미배정 9 days
높음 c4424c02
서브 티켓 디자인 시스템

[DS-2] 로그인/인증 페이지 + QT 메인 뷰 재설계

## 목표 로그인/인증 페이지와 QT 메인 뷰를 디자인 시스템에 맞게 통일한다. ## 의존성 - [DS-1] 완료 후 진행 (특히 _button 확장 필요) ## 작업 내용 ### 1. 로그인 페이지 재설계 (`sessions/new.html.erb`) - `bg-white` → `bg-surface-default`, `dark:bg-gray-800` → 토큰 사용 - `rounded-2xl` → `rounded-lg` (앱 전체 통일) - 인라인 flash 메시지 → `render "shared/alert"` 사용 - 인라인 버튼 → `render "shared/button"` 파셜 사용 (tag: :a) - Google/Kakao 로그인 버튼: 각 브랜드 색상 유지하되 크기/여백 통일 - 전체적으로 세련되고 깔끔한 로그인 UX ### 2. QT 메인 (`qt/today.html.erb`) 개선 - 이미 `_card`, `_progress` 파셜 잘 사용 중 - 이전/다음 링크 → `_button` 파셜 (tag: :a, variant: :outline) 적용 - 묵상 영역 입력 UX 개선: textarea 높이, 저장 버튼 위치 ### 3. QT 관련 뷰 개선 - `qt/no_session.html.erb`: 인라인 버튼 → `_button` 파셜 + `_empty_state` 활용 - `qt/day.html.erb`: 동일 패턴 적용 - `qt/sessions/` 관련 뷰: 카드, 버튼 파셜 통일 ### 4. 다크모드 하드코딩 수정 - 작업하는 모든 뷰에서 `dark:bg-gray-XXX` → 시멘틱 토큰 사용 - `bg-white` → `bg-surface-default` - `text-gray-900` → `text-text-primary` - `text-gray-500` → `text-text-secondary` ## 완료 기준 - 로그인 페이지가 디자인 토큰 사용, rounded-lg 통일 - QT 뷰에서 인라인 버튼 스타일 0개 - 모든 뷰에서 하드코딩 다크모드 색상 제거 - 기존 테스트 전부 통과

D
ds-views-auth
9 days
보통 8b7321bc
부모 티켓

통독 하기

통독하기에서 성경을 선택하고 통독하기 없음. 레거시 코드 확인

2/2
팀리드
9 days
높음 5e797a37
서브 티켓 날짜 버튼

[DATE-2] 달력 날짜 선택 Stimulus 컨트롤러 구현

## 목표 브라우저 네이티브 date input을 대체하는 커스텀 달력 날짜 선택 컴포넌트 구현 (Stimulus + Tailwind) ## 레거시 참고 레거시(Next.js)에서는 react-day-picker 기반 Calendar + Popover 조합 사용: - 버튼 클릭 → 달력 팝오버 열림 - 날짜 선택 → 자동 닫힘 - 한국어 포맷 ("2026년 3월 2일", "3/2 (월)") - 레거시 파일: /mnt/c/dev/logbible/src/components/ui/calendar.tsx ## 작업 내용 ### 1. Stimulus 달력 컨트롤러 생성 - `app/javascript/controllers/datepicker_controller.js` 생성 - 기능: - 버튼 클릭 → 달력 팝오버 토글 - 월 이동 (이전/다음) - 날짜 클릭 → hidden input에 값 설정 + 팝오버 닫기 - 한국어 요일/월 표시 (일/월/화/수/목/금/토) - 선택된 날짜를 버튼에 한국어 포맷으로 표시 - 외부 클릭 시 팝오버 닫기 - min/max 날짜 제한 지원 (옵션) ### 2. shared/_datepicker 파셜 생성 - `app/views/shared/_datepicker.html.erb` 생성 - 사용법: `<%= render "shared/datepicker", form: f, field: :start_date, label: "시작일", required: true %>` - 구성: - 라벨 - hidden input (실제 form 값) - 트리거 버튼 (선택된 날짜 표시) - 달력 팝오버 (Tailwind 스타일) - 디자인 시스템 일관성: 기존 _input 파셜과 같은 스타일링 토큰 사용 ### 3. Tailwind 스타일링 - 기존 디자인 시스템 색상 토큰 사용 (app/assets/tailwind/application.css) - 오늘 날짜 하이라이트 - 선택된 날짜 하이라이트 - 비활성 날짜 (다른 월) 흐리게 ## 완료 기준 - datepicker_controller.js 생성 + importmap 등록 - shared/_datepicker.html.erb 파셜 생성 - 달력이 한국어로 표시됨 - 날짜 선택 시 hidden input에 YYYY-MM-DD 형식으로 저장 - 모바일에서도 사용 가능 (터치 지원) - 외부 클릭 시 닫힘 ## 담당 파일 (신규 생성) - app/javascript/controllers/datepicker_controller.js - app/views/shared/_datepicker.html.erb

D
datepicker-dev
9 days
높음 8bd8fa27

QT 세션 수정/편집

## 목표 QT 세션의 edit/update 기능 추가 (현재 생성만 가능) ## 구현 내용 - qt/sessions_controller에 edit, update 액션 추가 - edit 뷰 생성 (기존 new 폼 재활용, _form 파셜 분리) - 수정 가능 필드: title, start_date, end_date - 테마 변경 시 콘텐츠 매핑 재설정 - creator만 수정 가능 (권한 체크) ## 완료 기준 - [ ] edit/update 라우트 및 컨트롤러 액션 - [ ] _form 파셜 분리 (new/edit 공유) - [ ] creator 권한 체크 (다른 사용자 수정 불가) - [ ] 통합 테스트 (성공/권한 실패/유효성 실패) - [ ] show 페이지에 "수정" 버튼 추가 ## 관련 파일 - app/controllers/qt/sessions_controller.rb - app/views/qt/sessions/ - config/routes.rb (이미 resources :sessions) - test/controllers/qt/sessions_controller_test.rb

미배정 8 days
높음 4a897e8d

[P3] 묵상 폼 분리 + 통계 확장 + max-width

H3: 묵상 폼 3섹션 Card 분리, H4: 통계 그래프 확장, H7: 콘텐츠 max-w-2xl, M6: 묵상 자동저장, M7: 기분 이모지 통일

P
phase3-agent
8 days
높음 15114fe8

[모임] 모임 CRUD + 설정 (생성/수정/삭제, 일정/시간/만료)

## 구현 내용 GroupsController CRUD + 라우팅 + 설정 관리 ### 라우팅 - resources :groups (full CRUD) - 사이드바 메뉴에 "모임" 항목 추가 ### 컨트롤러 - index: 내 모임 목록 (참여 중 / 내가 만든 모임) - new/create: 모임 생성 폼 (이름, 설명, 일정, 시간, 만료일, 지각시간, QT 여부) - show: 모임 상세 (멤버, 출석현황, QT플랜) - edit/update: 모임 설정 수정 (관리자만) - destroy: 모임 삭제/보관 (생성자만) ### 설정 항목 - 모임이름, 설명 - 반복 일정 (매일/매주/매월) - 모임 시간, 지각 기준 - 만료일 - QT 진행 여부 ## 완료 기준 - CRUD 전체 동작 - 관리자/생성자 권한 검증 - 디자인 시스템 파셜 활용 - 통합 테스트 통과

미배정 8 days
높음 24f9302c

[묵상기록] 하단 고정 입력 UI (Sticky Bottom Input)

## 목표 성경을 읽으면서 중간중간 빠르게 기록할 수 있는 하단 고정 입력 바를 구현한다. ## 작업 내용 1. 하단 고정 입력 바 파셜 생성 (`_quick_input.html.erb`) - 묵상 / 적용 / 기도제목 세그먼트 버튼 (플래그 선택) - 텍스트 입력 (textarea, auto-resize) - 저장 버튼 2. `today.html.erb`의 탭 외부(공통 영역)에 배치 3. bottom_nav(56px) 위에 위치: `fixed bottom-14 md:bottom-0` 4. 성경 본문 스크롤 영역은 하단 입력 바 높이만큼 padding-bottom 추가 ## UX - 플래그 선택 → 해당 필드에 텍스트 매핑 (personal_meditation / action_plan / prayer_topic) - 저장 시 해당 필드에 텍스트 append (또는 replace) - 저장 후 입력 칸 초기화 - 모바일 키보드 올라올 때 레이아웃 대응 ## 기술 참고 - 기존 _bottom_nav.html.erb: `fixed bottom-0 md:hidden` - z-index: bottom_nav(z-40) 위에 배치 (z-50) - Turbo Stream으로 저장 후 기록 카드 업데이트 ## 완료 기준 - 통독/QT 양쪽 탭에서 하단 고정 입력 바가 보인다 - 묵상/적용/기도제목 플래그 전환이 동작한다 - 저장 시 해당 필드에 기록이 반영된다 - 하단 네비와 겹치지 않는다

미배정 8 days
높음 035b7476

[레이아웃] 기본 폭 페이지 뷰 정리 (중복 max-w 제거)

## 개요 레이아웃 기본 폭(max-w-4xl)을 사용할 뷰에서 중복 max-w-2xl mx-auto px-4 py-6 제거 ## 대상 뷰 (11개) - qt/sessions/show, edit, members, rankings, shared_meditations (5개) - qt/themes/show (1개) - prayer_partners/search, index (2개) - bible_highlights/index (1개) - group_qt_sessions/new (1개) - pages/home.html.erb 내부 max-w-2xl 섹션 (1개) ## 완료 기준 - 대상 뷰에서 자체 max-w-2xl 제거 - 레이아웃 기본값(max-w-4xl)으로 표시 - 이중 패딩 해소 (px/py 중복 제거) ## 의존성 - 작업 #1 (3-tier 시스템) 완료 후

미배정 7 days
높음 244d8079
서브 티켓 [레이아웃] 전체 폭 개선 - Coordination

[구현] narrow/wide 페이지 선언 + 특수 페이지 점검

## 작업 1: narrow 페이지 선언 (4개) 아래 뷰에서 자체 `max-w-2xl mx-auto` 제거 + 파일 상단에 `<% content_for(:layout_width, 'narrow') %>` 추가: - settings/show.html.erb - profiles/show.html.erb - groups/edit.html.erb - groups/new.html.erb ## 작업 2: wide 페이지 선언 (5개) 아래 뷰에서 자체 max-w 제거 + 파일 상단에 `<% content_for(:layout_width, 'wide') %>` 추가: - stats/index.html.erb — wide 선언 추가 - tongtok/index.html.erb — `content_for(:wide_layout, true)` → `content_for(:layout_width, 'wide')` 교체 - groups/show.html.erb — 자체 max-w-4xl 제거 + wide 선언 - groups/index.html.erb — 자체 max-w-4xl 제거 + wide 선언 - attendance_stats/show.html.erb — 자체 max-w-4xl 제거 + wide 선언 ## 작업 3: 특수 페이지 점검 아래 페이지는 현재 max-w를 유지 (변경 불필요 확인): - pages/privacy, pages/terms (max-w-3xl) - attendances/show (max-w-md) - sessions/new (auth 레이아웃) - group_joins/show (max-w-lg) - tongtok/read.html.erb (max-w-lg — 성경 읽기 가독성) 주의: 중복 px/py도 함께 제거, 내부 spacing은 유지

W
width-dev
2 days
긴급 f0d09654

[P0] User 모델 + DB 마이그레이션

## 개요 User, UserSetting 모델 생성 및 DB 마이그레이션 ## 범위 - User 모델: email, nickname, provider, provider_id, role(enum), profile_image, phone - UserSetting 모델: timezone, language, notification 설정, preferred_difficulty - UUID PK 사용 (id: :uuid) - 인덱스: email(unique), provider+provider_id(unique) - User has_one :user_setting ## 스키마 참고 ``` users: id(UUID), email, nickname, provider, provider_id, role, profile_image, phone, notification_enabled, notification_time, current_session_id user_settings: id(UUID), user_id(FK), current_session_id, timezone, language, preferred_difficulty, auto_next_day ``` ## 완료 기준 - [ ] 마이그레이션 실행 성공 - [ ] User 모델 validations 동작 (email uniqueness, nickname presence) - [ ] UserSetting 1:1 관계 설정 - [ ] 모델 테스트 통과 ## 참고 - docs/migration/index.md §2-1 핵심 테이블 - 기능 ID: A1-A4

미배정 9 days
보통 ce699602

[Auth] DB 마이그레이션 - Devise 전용 컬럼 제거

## 목표 users 테이블에서 Devise 전용 컬럼 제거 ## 작업 내용 마이그레이션 생성하여 아래 컬럼 제거: - encrypted_password - reset_password_token - reset_password_sent_at - remember_created_at ## 의존성 - 티켓 2 (Devise gem 제거) 완료 후 진행 ## 완료 기준 - 불필요한 컬럼 제거됨 - 기존 데이터 영향 없음 - 마이그레이션 롤백 가능

미배정 9 days
높음 d002f5bb
서브 티켓 디자인 시스템

[DS-3] 기도/설교/프로필/통독/통계 뷰 재설계

## 목표 나머지 CRUD 뷰들을 디자인 시스템에 맞게 통일한다. ## 의존성 - [DS-1] 완료 후 진행 ## 작업 내용 ### 1. 기도 뷰 (`prayers/`) - `prayers/index.html.erb`: 인라인 버튼 → `_button` 파셜 (tag: :a) - `prayers/_form.html.erb`: submit/취소 버튼 → `_button` 파셜 (tag: :submit) - `prayers/_prayer_card.html.erb`: hover 기반 액션 버튼 → 모바일에서도 보이게 변경 - `opacity-0 group-hover:opacity-100` → 항상 보이되 크기 작게, 또는 스와이프 패턴 - 빈 상태 → `_empty_state` 파셜 적용 - `prayers/stats.html.erb`: 통계 카드 정리 ### 2. 설교 뷰 (`sermons/`) - `sermons/index.html.erb`: 인라인 버튼, 검색 폼 → 파셜 적용 - `sermons/show.html.erb`: 수정/삭제 버튼 → `_button` 파셜 - `sermons/_form.html.erb`: submit/취소 → `_button` 파셜 - 페이지네이션 → `_pagination` 파셜 적용 - 빈 상태 → `_empty_state` 파셜 적용 ### 3. 프로필/설정 (`profiles/`, `settings/`) - disabled 이메일 필드: `_input` 파셜에 `disabled:` 옵션 추가하여 적용 - 버튼 파셜 통일 ### 4. 통독/통계/기록 (`tongtok/`, `stats/`, `records/`) - 인라인 스타일 → 파셜 활용으로 통일 - 다크모드 하드코딩 수정 ### 5. 모바일 최적화 - 기도 카드 액션 버튼: 모바일에서 항상 접근 가능하게 - 설교 검색 폼: 모바일에서 레이아웃 개선 (세로 배치) - 하단 네비에 "설교" 탭 추가 고려 (5개 탭) ## 완료 기준 - 모든 뷰에서 인라인 버튼 스타일 0개 - `_empty_state`, `_pagination` 파셜 활용 - 모바일에서 hover 의존 UI 제거 - 다크모드 하드코딩 색상 제거 - 기존 테스트 전부 통과

D
ds-views-crud
9 days
보통 32357a15
부모 티켓

미디어 플레이

통독하기랑 QT 본문 읽기에서 미디어 플레이 구현 안됨. 레거시 코드 확인

2/2
팀리드
9 days
높음 3869362d
서브 티켓 날짜 버튼

[DATE-3] 기존 폼에 달력 날짜 선택 적용 + QT 세션 UX 개선

## 목표 DATE-2에서 만든 datepicker 컴포넌트를 기존 폼에 적용하고, QT 세션 생성 시 종료일 자동 계산 구현 ## 의존성 - DATE-2 (datepicker 컴포넌트) 완료 후 진행 ## 작업 내용 ### 1. QT 세션 생성 폼 개선 (app/views/qt/sessions/new.html.erb) - 기존 `shared/input type: :date` → `shared/datepicker`로 교체 - 시작일 선택 시 종료일 자동 계산 (테마의 total_days 기반) - session_form_controller.js 수정: - datepicker의 change 이벤트 감지 - 시작일 변경 → 종료일 = 시작일 + total_days - 1 자동 설정 - 총 일수 표시 업데이트 ### 2. 설교 노트 폼 개선 (app/views/sermons/_form.html.erb) - 기존 `shared/input type: :date` → `shared/datepicker`로 교체 - 기본값: 오늘 날짜 ### 3. 기존 session_form_controller.js 수정 - datepicker와 연동되도록 이벤트 처리 업데이트 - 시작일 변경 → 종료일 자동 설정 로직 추가 - 수동으로 종료일도 변경 가능하도록 유지 ## 완료 기준 - QT 세션 생성 폼에서 달력으로 날짜 선택 가능 - 시작일 선택 시 종료일이 자동 계산됨 - 설교 노트 폼에서 달력으로 날짜 선택 가능 - 기존 기능(total_days 계산 등)이 정상 동작 - 기존 테스트 모두 통과 ## 담당 파일 - app/views/qt/sessions/new.html.erb - app/views/sermons/_form.html.erb - app/javascript/controllers/session_form_controller.js

D
dateform-dev
9 days
높음 f5ebc8ae
부모 티켓

카카오 알림톡 + CRON

## 목표 NotificationService에 카카오 알림톡 채널 추가 및 CRON 기반 알림 스케줄링 ## 구현 내용 1. **카카오 알림톡 연동** - KakaoNotificationSender 서비스 객체 생성 - 카카오 비즈니스 API 연동 (알림톡 템플릿) - NotificationService.send_all에 kakao 채널 추가 - user_setting.kakao_enabled? 메서드 활용 (이미 존재) 2. **CRON 스케줄링** - NotificationCronJob 확장 (config/recurring.yml 이미 설정됨) - QT 리마인더 알림 (매일 설정 시간) - 기도 체크 리마인더 ## 완료 기준 - [ ] KakaoNotificationSender 서비스 + 테스트 - [ ] NotificationService에 kakao 채널 통합 - [ ] 알림톡 템플릿 관리 (QT 리마인더, 기도 리마인더) - [ ] CRON job에서 사용자별 알림 시간 처리 - [ ] 카카오 API 키 환경변수 설정 - [ ] 실패 시 재시도 로직 ## 관련 파일 - app/services/notification_service.rb - app/jobs/notification_cron_job.rb - config/recurring.yml - app/models/user_setting.rb (kakao_enabled? 존재)

1/1
팀리드
8 days
보통 1d6d2eba

[P4] 네비/레이아웃 세부 개선

M1: 사이드바 접기/펼치기, M4: 동역자 기도 Dialog, M5: 네비 블러 강화

P
phase4-agent
8 days
높음 a7d57f36

[모임] 참여 플로우 (QR/URL/초대코드로 가입)

## 구현 내용 3가지 방법으로 모임 참여 ### 참여 방법 1. **URL 링크**: /join/:token → 모임 소개 페이지 → 참여 버튼 2. **QR 코드**: qrcode.js CDN + qr_controller.js Stimulus → 모임 show에서 QR 표시 3. **초대코드**: 6자리 코드 입력 폼 → 코드 검증 → 참여 ### 플로우 1. 비로그인 → 로그인 페이지 (return_to 저장) 2. 로그인 상태 → 모임 확인 (만료/비활성 체크) 3. 이미 멤버 → 모임 페이지로 리다이렉트 4. 미참여 → GroupMember 생성 (role: member) 5. 참여 완료 화면 ### QR 코드 - importmap에 qrcode.js CDN 추가 - qr_controller.js Stimulus 컨트롤러 - 모임 show 페이지에 QR 표시 + 링크 복사 버튼 ## 완료 기준 - 3가지 참여 방법 모두 동작 - 중복 참여 방지 - 만료된 모임 참여 차단 - QR 코드 정상 렌더링 - 통합 테스트 통과

미배정 8 days
높음 0809387d

[묵상기록] Stimulus 컨트롤러 (quick_input_controller)

## 목표 하단 고정 입력 바의 동작을 제어하는 Stimulus 컨트롤러를 구현한다. ## 작업 내용 1. `quick_input_controller.js` 생성 - 플래그(묵상/적용/기도제목) 전환 → 활성 플래그 상태 관리 - 텍스트 입력 → hidden field에 매핑 (personal_meditation / action_plan / prayer_topic) - textarea auto-resize - 저장 액션 (Turbo Stream submit) - 저장 후 입력 초기화 + 피드백(토스트 등) 2. 기존 autosave_controller, offline_sync_controller 패턴 참고 3. importmap에 등록 ## 기술 참고 - 기존 tab_controller.js의 세그먼트 전환 패턴 참고 - Turbo Stream 저장: `turbo:submit-end` 이벤트 처리 - 오프라인 시 offline_sync_controller와 연동 고려 ## 완료 기준 - 플래그 전환 시 UI 상태가 변경된다 (active 스타일) - 텍스트 입력 후 저장 시 서버에 반영된다 - 저장 후 입력 칸이 초기화된다 - 기존 묵상 데이터가 있으면 해당 플래그의 내용이 표시된다

미배정 8 days
보통 860496c5

[레이아웃] narrow/wide 페이지 폭 선언

## 개요 특수 폭이 필요한 뷰에 content_for(:layout_width) 선언 ## narrow 대상 (4개) - settings/show — 자체 max-w-2xl 제거 + narrow 선언 - profiles/show — 자체 max-w-2xl 제거 + narrow 선언 - groups/edit — 자체 max-w-2xl 제거 + narrow 선언 - groups/new — 자체 max-w-2xl 제거 + narrow 선언 ## wide 대상 (4개+) - stats/index — wide 선언 - tongtok/index — content_for(:wide_layout) → content_for(:layout_width, 'wide') 교체 - groups/show — 자체 max-w-4xl 제거 + wide 선언 - groups/index — 자체 max-w-4xl 제거 + wide 선언 - attendance_stats/show — 자체 max-w-4xl 제거 + wide 선언 ## 완료 기준 - 모든 대상 뷰에서 적절한 폭 적용 - 자체 max-w 제거, content_for 시스템으로 통합 ## 의존성 - 작업 #1 (3-tier 시스템) 완료 후 (작업 #2와 병렬 가능)

미배정 7 days
긴급 e37d0fbf

[P0] OmniAuth 소셜 로그인 (Google/Kakao)

## 개요 Devise + OmniAuth를 이용한 Google/Kakao 소셜 로그인 구현 ## 범위 - Devise 설정 + OmniAuth Google OAuth2 - OmniAuth Kakao 설정 - OAuth 콜백 처리 (users 테이블 upsert) - 세션 관리 (로그인/로그아웃) - 로그인 페이지 UI (소셜 로그인 버튼) - 비로그인 시 로그인 페이지로 리다이렉트 - 세션 만료 시 자동 정리 + 재로그인 유도 ## 완료 기준 - [ ] Google 로그인 → 사용자 생성/로그인 성공 - [ ] Kakao 로그인 → 사용자 생성/로그인 성공 - [ ] 로그아웃 동작 - [ ] 비인증 접근 시 로그인 리다이렉트 - [ ] 통합 테스트 통과 ## 참고 - 레거시: src/contexts/AuthContext.tsx, api/auth/callback/route.ts - 기능 ID: A1-A4, A7

미배정 9 days
높음 af6c4192

[Auth] 테스트 수정 - Devise 헬퍼 → 자체 sign_in 헬퍼

## 목표 Devise::Test::IntegrationHelpers 제거 후 자체 테스트 헬퍼 구현 ## 작업 내용 1. test_helper.rb에서 Devise::Test::IntegrationHelpers 제거 2. 자체 sign_in 헬퍼 구현 (session[:user_id] 설정 방식) 3. 20개 테스트 파일의 sign_in 호출이 새 헬퍼로 작동 확인 ## 의존성 - 티켓 1 (인증 코어 교체)과 동시 진행 가능 ## 완료 기준 - 전체 테스트 통과 (357+ tests, 0 failures) - sign_in 헬퍼가 Devise 없이 동작

미배정 9 days
보통 699c73d6
서브 티켓 디자인 시스템

[DS-4] 네비게이션 개선 + 관리자 뷰 통일

## 목표 사이드바/하단 네비를 개선하고 관리자 뷰를 디자인 시스템에 맞게 통일한다. ## 의존성 - [DS-1] 완료 후 진행 ## 작업 내용 ### 1. 사이드바 개선 (`shared/_sidebar.html.erb`) - 현재 메뉴: QT/묵상/통독/기도/설교/통계/설정 (7개) - 아이콘+텍스트 정렬 확인 및 일관성 개선 - active 상태 스타일 강화 (현재 경로 기반) - 프로필 영역 정리 ### 2. 하단 네비 개선 (`shared/_bottom_nav.html.erb`) - 현재 4탭: QT/묵상/통독/기도 - 설교 탭 추가하여 5탭으로 확장 (QT/묵상/기도/설교/더보기) - 또는 "더보기" 탭으로 나머지 기능 접근 - safe-area-bottom 유지 - 아이콘/레이블 크기 통일 ### 3. 관리자 뷰 (`admin/`) 통일 - `admin_sidebar_controller.js` → Stimulus targets 패턴으로 개선 (getElementById 제거) - 관리자 사이드바 너비 `w-56` → `w-60` (앱과 통일) - 관리자 레이아웃 최대 너비 유지 (`max-w-7xl`은 적절) - 관리자 뷰에서 인라인 스타일 → 파셜 활용 ### 4. 모바일 헤더 (`shared/_header.html.erb`) - 헤더 디자인 개선 - 현재 페이지 제목 표시 추가 고려 ## 완료 기준 - 사이드바 active 상태 명확 - 하단 네비 5탭 or 더보기 구조 - admin_sidebar_controller가 Stimulus targets 사용 - 관리자 사이드바 w-60 통일 - 기존 테스트 전부 통과

D
ds-nav-admin
9 days
높음 f3cfbdd1
서브 티켓 미디어 플레이

[MP-1] YouTube Audio Player Stimulus 컨트롤러 + 파셜 + QT 통합

## 목표 YouTube Iframe API를 사용한 오디오 플레이어 Stimulus 컨트롤러와 파셜을 생성하고, QT 본문 읽기 페이지에 통합한다. ## 작업 내용 ### 1. Stimulus 컨트롤러: `youtube_player_controller.js` - 파일: `app/javascript/controllers/youtube_player_controller.js` - YouTube Iframe API 로드 (글로벌 1회만) - 재생/일시정지, 프로그레스 바, 재생 속도(0.5x~2.0x), 음소거 기능 - Stimulus values: `videoId` (String), `title` (String) - Stimulus targets: `player`, `playBtn`, `pauseBtn`, `progress`, `currentTime`, `duration`, `speed`, `muteBtn` - 에러 처리 (YouTube 로드 실패 시 메시지 표시) - `public/bible-youtube-urls.json`에서 URL 조회하는 헬퍼 함수 포함 ### 2. Rails 파셜: `shared/_youtube_player.html.erb` - 깔끔한 오디오 플레이어 UI (Tailwind CSS v4) - 재생/일시정지 버튼, 프로그레스 바, 시간 표시 - 재생 속도 선택 드롭다운 - 음소거 버튼 - 디자인 시스템 토큰 사용 (text-text-primary, bg-surface-subtle 등) - locals: `video_id:`, `title:` (필수) ### 3. QT 페이지 통합 - `app/views/qt/today.html.erb` 수정 - 성경 구절 카드 아래에 오디오 플레이어 추가 - `@qt_content.bible_passage`에서 책 이름과 장을 파싱하여 YouTube URL 조회 - 여러 장인 경우 각 장별 플레이어 표시 - 통독 구절(`reading_passage`)이 있으면 별도 플레이어도 표시 ### 참고: bible-youtube-urls.json 구조 ```json { "창세기": { "1": "https://www.youtube.com/watch?v=VIDEO_ID", ... }, ... } ``` ### 참고: 기존 bible_passage_controller.js의 BOOK_MAPPINGS - 한글 약어/전체 이름 → 영어 약어 매핑이 있음 - youtube-urls.json은 한글 전체 이름을 키로 사용 - 약어를 전체 이름으로 역변환하는 매핑 필요 ### 완료 기준 - YouTube 플레이어가 QT 성경 구절에서 정상 작동 - 재생/일시정지/속도변경/음소거 동작 - 모바일 반응형 - 테스트: 통합 테스트에서 플레이어 HTML 존재 확인

P
player-dev
9 days
보통 0ec59434
부모 티켓

기도 AI 분석

## 목표 기도 내용을 AI로 분석하여 패턴, 감정, 인사이트 제공 ## 구현 내용 1. **AiPrayerAnalyzer 서비스** - 기도 내용 텍스트 분석 (감정, 주제, 패턴) - 기도 카테고리 자동 분류 - 기간별 기도 패턴 리포트 생성 - ruby-openai gem 활용 (기존 AI 서비스 패턴 따름) 2. **UI 통합** - 기도 상세 페이지에 AI 분석 버튼 - Turbo Stream으로 비동기 결과 표시 - 기도 통계 페이지에 AI 인사이트 섹션 ## 완료 기준 - [ ] AiPrayerAnalyzer 서비스 객체 + 테스트 - [ ] 분석 결과 저장 (prayer_requests에 컬럼 또는 별도 모델) - [ ] Turbo Stream 비동기 분석 UI - [ ] 기도 통계에 AI 인사이트 통합 - [ ] API 키 미설정 시 graceful fallback ## 관련 파일 - app/services/ai_meditation_analyzer.rb (참고 패턴) - app/controllers/prayers_controller.rb - app/views/prayers/

2/2
팀리드
8 days
높음 7546939b

[모임] QT 플랜 연동 (모임 내 QT 생성 + 멤버 자동 참여)

## 구현 내용 모임 관리자가 QT 플랜을 생성하면 모임 멤버가 자동 참여 ### 변경사항 - QtSession에 group_id 컬럼 추가 (nullable, 기존 세션은 null) - 모임 show 페이지에서 "QT 플랜 생성" 버튼 (관리자만) - QT 플랜 생성 시 group_id 설정 → 모임 멤버 자동 QtParticipant 생성 - 새 멤버 가입 시 진행 중인 QT 플랜에 자동 참여 - 모임 페이지에서 QT 플랜 목록 표시 ### 기존 프로세스 변경 - 기존: QT 세션 생성 → 초대코드로 참여자 모집 - 변경: 모임 생성 → 멤버 모집 → 모임 내 QT 플랜 생성 (멤버 자동 참여) ## 완료 기준 - QtSession에 group_id 마이그레이션 - 모임 내 QT 생성 시 멤버 자동 참여 - 새 멤버 가입 시 진행 중 QT 자동 참여 - 기존 독립 QT 세션도 정상 동작 (하위 호환) - 통합 테스트 통과

미배정 8 days
높음 d45636bb
부모 티켓

[묵상기록] 통합 UI 구현 - Coordination

묵상 기록 통합 UI 전체 구현 조율 티켓. 통독/QT 양탭 기록 표시 + 하단 고정 입력 + Stimulus 컨트롤러

1/1
팀리드
8 days
높음 2ce330a1
서브 티켓 [묵상기록] 통합 UI 구현 - Coordination

[묵상기록] 전체 구현: 파셜 분리 + 하단 입력 + Stimulus

통독/QT 양탭 묵상 기록 통합 UI 전체 구현. ## 작업 1: 기록 표시 파셜 분리 - `_form.html.erb`에서 기록 카드(묵상/적용/기도제목)를 `_records.html.erb`로 분리 - 통독 탭(today.html.erb)에 `_records.html.erb` 추가 - QT 탭에도 동일하게 유지 ## 작업 2: 하단 고정 입력 UI - `_quick_input.html.erb` 파셜 생성 - 묵상/적용/기도제목 세그먼트 버튼 + textarea + 저장 - `today.html.erb`의 탭 외부(공통)에 배치 - bottom_nav(h-16, z-30) 위에: `fixed bottom-16 z-40` ## 작업 3: quick_input_controller.js - 플래그 전환, hidden field 매핑, auto-resize, Turbo submit - importmap 등록 ## 작업 4: 기분(mood) + 저장 양탭 공통 - mood 선택 UI를 _quick_input 또는 별도 영역에 배치 ## 작업 5: Turbo Stream 업데이트 - create/update.turbo_stream.erb에서 _records도 갱신하도록 수정

M
meditation-ui-dev
8 days
낮음 5fa75a38

[레이아웃] 정적/특수 페이지 폭 점검

## 개요 정적 페이지와 특수 레이아웃 페이지 확인 및 필요시 조정 ## 유지 대상 (변경 불필요) - pages/privacy, pages/terms (max-w-3xl — 읽기 가독성) - attendances/show (max-w-md — 체크인 전용) - sessions/new (auth 레이아웃, max-w-sm) - group_joins/show (max-w-lg — 초대 페이지) ## 확인 필요 - tongtok/read.html.erb (max-w-lg — 성경 읽기 가독성 유지 여부) ## 완료 기준 - 모든 특수 페이지 확인 후 변경/유지 결정 - 유지 판단한 페이지에 대한 이유 문서화 ## 의존성 - 작업 #1 (3-tier 시스템) 완료 후

미배정 7 days
긴급 9b719191

[P0] QT 핵심 모델 (Theme/Content/Session/Participant)

## 개요 QT 시스템의 핵심 데이터 모델 4개 생성 ## 범위 - QtTheme: title, description, is_default, is_active, total_day, is_ai_generated, generation_status - QtContent: theme_id(FK), day_number, bible_passage, theme_title, content, questions(JSON), difficulty - QtSession: title, theme_id(FK), creator_id(FK), start_date, end_date, invite_code(unique), status(enum) - QtParticipant: session_id(FK), user_id(FK), role(enum), is_active, joined_at - 모든 테이블 UUID PK - 관계 설정: Theme has_many Contents/Sessions, Session has_many Participants ## 완료 기준 - [ ] 4개 모델 마이그레이션 실행 성공 - [ ] 관계(associations) 설정 완료 - [ ] enum, scope, validation 설정 - [ ] 모델 테스트 통과 ## 참고 - docs/migration/index.md §2-2 QT 시스템 테이블 - docs/migration/index.md §8 Rails 모델 설계

미배정 9 days
긴급 4eea7209
부모 티켓

[Auth] Devise 제거 → 자체 세션 관리 전환 (Coordination)

## 목표 Devise gem을 완전히 제거하고 session[:user_id] 기반 자체 인증으로 전환 ## 서브 티켓 1. 인증 코어 교체 (critical) - auth-app-dev 2. Devise gem 및 설정 파일 제거 (high) - auth-app-dev 3. DB 마이그레이션 (medium) - auth-app-dev 4. 테스트 수정 (high) - auth-test-dev ## 의존성 - #1 완료 → #2, #4 진행 가능 - #2 완료 → #3 진행 가능

2/2
팀리드
9 days
긴급 a13af170
서브 티켓 [Auth] Devise 제거 → 자체 세션 관리 전환 (Coordination)

[Auth-1] 인증 코어 교체 + Devise 제거 + DB 마이그레이션

## 목표 Devise를 제거하고 session[:user_id] 기반 직접 인증으로 전환. Gem/설정 제거 + DB 마이그레이션까지 완료. ## 작업 순서 (반드시 이 순서대로) ### Phase 1: 코어 교체 1. **ApplicationController 수정** (`app/controllers/application_controller.rb`) - `before_action :authenticate_user!` 유지 (자체 구현으로 대체) - private 메서드 추가: ```ruby def current_user @current_user ||= User.find_by(id: session[:user_id]) end helper_method :current_user def user_signed_in? current_user.present? end helper_method :user_signed_in? def authenticate_user! unless user_signed_in? redirect_to login_path, alert: "로그인이 필요합니다." end end ``` - `layout_by_resource` 메서드와 `layout :layout_by_resource` 제거 (devise_controller? 참조 없앰) 2. **SessionsController 생성** (`app/controllers/sessions_controller.rb`) ```ruby class SessionsController < ApplicationController skip_before_action :authenticate_user!, only: [:new] layout "devise" def new redirect_to root_path if user_signed_in? end def destroy reset_session redirect_to login_path, notice: "로그아웃되었습니다." end end ``` 3. **OmniauthController 수정** (`app/controllers/omniauth_controller.rb` - 새 위치) - `app/controllers/users/omniauth_callbacks_controller.rb`를 `app/controllers/omniauth_controller.rb`로 이동 - Devise 상속 제거: ```ruby class OmniauthController < ApplicationController skip_before_action :authenticate_user! def google_oauth2 handle_auth("Google") end def kakao handle_auth("Kakao") end def failure redirect_to login_path, alert: "로그인에 실패했습니다. 다시 시도해주세요." end private def handle_auth(kind) @user = User.from_omniauth(request.env["omniauth.auth"]) if @user.persisted? session[:user_id] = @user.id redirect_to root_path, notice: "#{kind}로 로그인되었습니다." else redirect_to login_path, alert: "#{kind} 로그인에 실패했습니다." end end end ``` 4. **라우트 수정** (`config/routes.rb`) - `devise_for :users, ...` 제거 - 자체 라우트 추가: ```ruby # 인증 get "login", to: "sessions#new", as: :login delete "logout", to: "sessions#destroy", as: :logout # OmniAuth 콜백 get "auth/:provider/callback", to: "omniauth#google_oauth2", constraints: { provider: "google_oauth2" } get "auth/:provider/callback", to: "omniauth#kakao", constraints: { provider: "kakao" } post "auth/:provider/callback", to: "omniauth#google_oauth2", constraints: { provider: "google_oauth2" } post "auth/:provider/callback", to: "omniauth#kakao", constraints: { provider: "kakao" } get "auth/failure", to: "omniauth#failure" ``` 5. **User 모델 수정** (`app/models/user.rb`) - `devise :database_authenticatable, :omniauthable, ...` 줄 전체 삭제 - `from_omniauth`에서 `Devise.friendly_token` 제거 (password 할당 불필요): ```ruby def self.from_omniauth(auth) where(provider: auth.provider, provider_id: auth.uid).first_or_create do |user| user.email = auth.info.email user.nickname = auth.info.name || auth.info.email.split("@").first user.profile_image = auth.info.image end end ``` 6. **뷰 이동** - `app/views/devise/sessions/new.html.erb` → `app/views/sessions/new.html.erb` - 뷰 내용에서 Devise 경로 수정: - `user_google_oauth2_omniauth_authorize_path` → `/auth/google_oauth2` - `user_kakao_omniauth_authorize_path` → `/auth/kakao` - `new_user_session_path` → `login_path` 7. **사이드바 수정** - 로그아웃 링크 경로 확인 - `destroy_user_session_path` → `logout_path` - method: :delete 유지 ### Phase 2: OmniAuth 설정 이전 8. **OmniAuth 이니셜라이저 생성** (`config/initializers/omniauth.rb`) ```ruby Rails.application.config.middleware.use OmniAuth::Builder do provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"], scope: "email,profile" require_relative "../../lib/omniauth/strategies/kakao" provider :kakao, ENV["KAKAO_CLIENT_ID"], ENV["KAKAO_CLIENT_SECRET"] end OmniAuth.config.allowed_request_methods = [:post, :get] OmniAuth.config.silence_get_warning = true ``` ### Phase 3: Devise 제거 9. **Gemfile에서 devise 제거** + `bundle install` 10. **config/initializers/devise.rb 삭제** 11. **app/views/devise/ 디렉토리 삭제** (views 이동 완료 후) 12. **app/controllers/users/ 디렉토리 삭제** (controller 이동 완료 후) ### Phase 4: DB 마이그레이션 13. **마이그레이션 생성** - Devise 전용 컬럼 제거: ```ruby class RemoveDeviseColumnsFromUsers < ActiveRecord::Migration[8.1] def change remove_index :users, :reset_password_token remove_column :users, :encrypted_password, :string remove_column :users, :reset_password_token, :string remove_column :users, :reset_password_sent_at, :datetime remove_column :users, :remember_created_at, :datetime end end ``` 14. `bin/rails db:migrate` 실행 ## 주의사항 - UUID PK 사용 (`before_create :set_uuid` in ApplicationRecord) - ERB 멀티라인 주석 안에 ERB 태그 금지 (SystemStackError) - 모든 변경 후 서버가 기동 가능한지 확인 - test/ 디렉토리는 auth-test-dev가 담당 - 건드리지 말 것 - `bundle install` 실행 후 Gemfile.lock 업데이트 확인

A
auth-app-dev
9 days
높음 a99c551d
서브 티켓 미디어 플레이

[MP-2] 통독하기 미디어 플레이 통합

## 목표 통독하기 페이지에서 각 성경 장의 오디오를 들을 수 있는 기능을 추가한다. ## 사전 조건 - `shared/_youtube_player.html.erb` 파셜과 `youtube_player_controller.js`가 이미 구현되어 있어야 함 (MP-1에서 구현) - 해당 파일들이 없으면 기다리지 말고 직접 확인 후 존재하면 진행 ## 작업 내용 ### 1. 통독 장별 재생 버튼 추가 - `app/views/tongtok/_book_card.html.erb` 수정 - 각 책 카드에 "듣기" 버튼 추가 (Stimulus 컨트롤러로 토글) - 듣기 버튼 클릭 시 해당 책의 오디오 플레이어 영역 표시 ### 2. 통독 오디오 플레이어 Stimulus 컨트롤러 - 파일: `app/javascript/controllers/tongtok_player_controller.js` - 책 이름을 받아 `bible-youtube-urls.json`에서 해당 책의 모든 장 URL 조회 - 장 선택 UI: 드롭다운 또는 가로 스크롤 장 번호 리스트 - 선택한 장의 오디오 재생 (기존 youtube_player_controller 활용 또는 독립 구현) - 연속 재생 옵션: 현재 장 끝나면 다음 장 자동 재생 ### 3. UI 디자인 - 각 book_card 하단에 접을 수 있는 플레이어 영역 - 또는 book_card의 장 번호 옆에 작은 재생 아이콘 - Tailwind CSS v4 디자인 시스템 토큰 사용 - 모바일 우선 반응형 ### 참고: bible-youtube-urls.json 구조 ```json { "창세기": { "1": "https://www.youtube.com/watch?v=VIDEO_ID", ... }, ... } ``` ### 참고: 현재 book_card 구조 - `book[:name]`: 한글 전체 이름 (예: "창세기") → JSON 키와 일치 - `book[:chapters]`: 총 장 수 - 장 번호 그리드 (1~N)에 읽음/안읽음 토글 버튼 ### 완료 기준 - 각 책 카드에서 듣기 기능 접근 가능 - 장 선택 후 오디오 재생 동작 - 모바일 반응형 - 기존 읽음/안읽음 토글 기능 유지

T
tongtok-dev
9 days
보통 ee2747e9
부모 티켓

통계 차트 Chart.js

## 목표 묵상/기도/통독 통계를 Chart.js 차트로 시각화 ## 구현 내용 1. **Chart.js 설치** - Importmap으로 Chart.js CDN 추가 - chart_controller Stimulus 컨트롤러 생성 2. **차트 종류** - 묵상 통계: 주간/월간 완료율 라인 차트 - 기도 통계: 카테고리별 파이 차트, 응답률 바 차트 - 통독 현황: 66권 진행률 프로그레스 바 (기존) + 월별 트렌드 3. **데이터 API** - 기존 stats 컨트롤러에서 JSON 데이터 제공 - 또는 data-* 속성으로 직접 전달 ## 완료 기준 - [ ] Chart.js importmap 설정 - [ ] chart_controller Stimulus 컨트롤러 - [ ] 묵상 통계 차트 (라인) - [ ] 기도 통계 차트 (파이 + 바) - [ ] 통독 월별 트렌드 차트 - [ ] 다크모드 대응 (차트 색상) - [ ] 모바일 반응형 ## 관련 파일 - app/views/qt/stats/ - app/views/prayers/stats.html.erb - app/views/tongtok/ - config/importmap.rb

3/3
팀리드
8 days
보통 6d8e4b5a

[모임] 출석 체크 시스템 (출석 QR + 체크인 + 지각 판정)

## 구현 내용 출석 전용 QR 생성 + 멤버 출석 체크 ### 출석 QR - 모임 show에서 "출석 체크 QR" 생성 버튼 (관리자) - 출석 QR URL: /attend/:group_token (모임 참여 QR과 별도) - 당일 모임일인 경우만 출석 체크 가능 ### 체크인 플로우 1. /attend/:token 접속 2. 로그인 확인 3. 모임 멤버 확인 (비멤버 → 안내 메시지) 4. 오늘이 모임일인지 확인 (recurrence_type 기반) 5. 출석 기록 생성 (AttendanceRecord) 6. 지각 자동 판정: meeting_time + late_minutes vs 현재 시각 7. 중복 출석 방지 (같은 날 같은 멤버) 8. 출석 완료 화면 ### 모임일 판정 로직 - daily: 매일 - weekly: meeting_day(요일)과 오늘 요일 비교 - monthly: meeting_day(일)과 오늘 일 비교 ## 완료 기준 - 출석 QR 생성 및 렌더링 - 멤버만 출석 가능 - 모임일에만 출석 가능 - 지각 자동 판정 - 중복 출석 방지 - 통합 테스트 통과

미배정 8 days
높음 514952b2

[P0] QT 시드 데이터 (레거시 import)

## 개요 레거시 Supabase에서 QT 테마/콘텐츠 데이터를 Rails로 가져오는 rake task ## 범위 - lib/tasks/import_legacy.rake 작성 - Supabase REST API로 qt_themes, qt_contents 데이터 export - JSON → Rails seed로 import - UUID 유지 (기존 ID 그대로 사용) - db/seeds.rb에 기본 시드 데이터 포함 ## 완료 기준 - [ ] rake import:qt_data 실행 시 테마/콘텐츠 import 성공 - [ ] UUID 정합성 유지 - [ ] db:seed로 기본 데이터 생성 가능 ## 참고 - 레거시: sql/insert_qt_themes.sql - docs/migration/index.md §10 데이터 마이그레이션 전략

미배정 9 days
높음 76e78b59
서브 티켓 [Auth] Devise 제거 → 자체 세션 관리 전환 (Coordination)

[Auth-2] 테스트 수정 - Devise 헬퍼 → 자체 sign_in 헬퍼

## 목표 Devise::Test::IntegrationHelpers를 제거하고 session[:user_id] 기반 자체 테스트 헬퍼로 전환. 전체 테스트 통과 확인. ## 작업 내용 ### 1. test_helper.rb 수정 - `include Devise::Test::IntegrationHelpers` 제거 - 자체 sign_in 헬퍼 추가: ```ruby class ActionDispatch::IntegrationTest private def sign_in(user) post "/auth/google_oauth2/callback", env: { "omniauth.auth" => OmniAuth::AuthHash.new( provider: user.provider || "google_oauth2", uid: user.provider_id || "test_uid_#{user.id}", info: { email: user.email, name: user.nickname, image: user.profile_image } ) } end end ``` **주의**: 위 sign_in 구현이 실제 OmniAuth 콜백 라우트와 맞지 않을 수 있습니다. auth-app-dev가 라우트를 변경하므로, 실제 라우트 파일(`config/routes.rb`)을 먼저 확인하세요. 대안적 접근 (더 단순): ```ruby def sign_in(user) # OmniAuth 테스트 모드가 아닌 직접 세션 설정 # Integration 테스트에서는 직접 세션 설정이 어려우므로 OmniAuth mock 사용 OmniAuth.config.test_mode = true OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: "google_oauth2", uid: user.provider_id || "test_uid", info: { email: user.email, name: user.nickname, image: user.profile_image } ) get "/auth/google_oauth2/callback" end ``` 또는 가장 단순한 방식: ```ruby def sign_in(user) post login_path, params: {}, env: { "rack.session" => { user_id: user.id } } end ``` **실제 구현 시**: auth-app-dev의 코어 교체 결과(routes, controllers)를 확인한 후 가장 적합한 방식을 선택하세요. ### 2. 전체 테스트 파일 확인 - `test/` 디렉토리의 모든 테스트 파일에서 `sign_in` 호출이 새 헬퍼로 작동하는지 확인 - Devise 관련 참조가 남아있지 않은지 확인 (grep으로 검색) ### 3. OmniAuth 테스트 모드 설정 - test_helper.rb에 OmniAuth 테스트 모드 설정 추가: ```ruby OmniAuth.config.test_mode = true ``` ### 4. 전체 테스트 실행 - `bin/rails test` 실행 - 모든 테스트 통과 확인 (430+ tests, 0 failures 기대) ## 주의사항 - auth-app-dev가 app/ 코드를 변경하므로, 그 변경이 완료된 후에 테스트를 실행해야 합니다 - 하지만 test_helper.rb 수정과 테스트 분석은 먼저 시작 가능 - test/ 디렉토리만 수정 - app/ 코드는 건드리지 말 것 - `parallelize(workers: 1)` 유지 필수 (SQLite) - fixture UUID PK → `.to_s` 비교 필요한 곳 있음 - 통합 테스트에서 RecordNotFound → `assert_response :not_found` 패턴 사용

A
auth-test-dev
9 days
높음 c6c18cb8
서브 티켓 통독 하기

[통독-1] 다중 선택 UI + 통독하기 버튼

## 목표 통독 현황 페이지에서 성경 장을 다중 선택하고 "통독하기" 버튼으로 읽기 페이지로 이동하는 기능 구현 ## 현재 상태 - app/views/tongtok/index.html.erb: 통독 현황 페이지 (구약/신약 탭) - app/views/tongtok/_book_card.html.erb: 책 카드 (각 장을 button_to로 읽음/미읽음 토글) - 현재는 장 클릭 시 바로 POST/DELETE로 읽음 토글됨 ## 구현 내용 ### 1. 모드 전환 UI - index.html.erb 상단에 "기록 모드" / "통독 모드" 토글 버튼 추가 - **기록 모드 (기본)**: 현재처럼 장 클릭 시 읽음/미읽음 즉시 토글 (button_to 유지) - **통독 모드**: 장 클릭 시 선택/해제 (파란색 테두리 표시) ### 2. Stimulus 컨트롤러 - `app/javascript/controllers/tongtok_select_controller.js` 생성 - data-controller="tongtok-select" 를 index 컨테이너에 적용 - 기능: - `toggle()`: 모드 전환 (기록 ↔ 통독) - `selectChapter(event)`: 장 선택/해제 - `startReading()`: 선택된 장들을 URL 파라미터로 읽기 페이지 이동 - `clearSelection()`: 선택 초기화 - 타겟: - `modeButton`: 모드 전환 버튼 - `selectionBar`: 하단 플로팅 바 - `selectionCount`: 선택 수 텍스트 - `chapterGrid`: 장 그리드 컨테이너들 ### 3. _book_card.html.erb 수정 - 기존 button_to 유지하되, 통독 모드일 때는 숨기고 JS 버튼 표시 - 각 장 버튼에 `data-action="tongtok-select#selectChapter"` 추가 - `data-book-name`, `data-chapter` 데이터 속성 추가 - 선택 시 CSS: `ring-2 ring-brand-primary bg-brand-primary/10` ### 4. 플로팅 액션 바 - index.html.erb 하단에 고정 바 추가 (통독 모드에서만 표시) - 내용: "N장 선택됨" + "선택 초기화" + "통독하기" 버튼 - 통독하기 클릭 시 → `/tongtok/read?selections=창세기:1,2,3;출애굽기:1,2` 형식으로 이동 ## 담당 파일 (다른 파일 수정 금지) - app/javascript/controllers/tongtok_select_controller.js (생성) - app/views/tongtok/index.html.erb (수정) - app/views/tongtok/_book_card.html.erb (수정) ## 완료 기준 - 기록 모드에서 기존 동작 유지 (button_to 읽음 토글) - 통독 모드에서 다중 장 선택 가능 - 선택 시 플로팅 바에 선택 수 표시 - "통독하기" 클릭 시 /tongtok/read?selections=... 으로 이동 - 기존 테스트 깨지지 않을 것

T
tongtok-select
9 days
보통 9edc6510
부모 티켓

월별 랭킹 미세 조정

## 목표 이미 구현된 월별 랭킹의 미세 조정 및 완성 ## 현황 - rankings 액션에 전체/이번달 period 필터 이미 존재 - 기본 랭킹 UI 구현됨 ## 조정 내용 - [ ] 월별 필터가 정확히 동작하는지 검증 - [ ] 이전 달 조회 기능 (월 선택) - [ ] 랭킹 순위 표시 UI 개선 (1/2/3위 뱃지) - [ ] 본인 순위 하이라이트 - [ ] 빈 데이터 시 empty_state 파셜 적용 ## 관련 파일 - app/controllers/qt/sessions_controller.rb (rankings 액션) - app/views/qt/sessions/rankings.html.erb

1/1
팀리드
8 days
보통 69e25b03

[모임] 출석 통계/리포트 (출석률, 결석, 지각 + Chart.js)

## 구현 내용 모임 관리자용 출석 통계 대시보드 ### 통계 - 기간별 출석률 (일/주/월) - 출석/지각/결석 인원 집계 - 멤버별 출석률 랭킹 - 개근자/다결석자 하이라이트 - 일자별 상세 (누가 몇시에 체크했는지) ### 차트 (Chart.js 재사용) - 출석 추이 라인 차트 - 출석/지각/결석 비율 도넛 차트 - 멤버별 출석률 바 차트 ### 접근 권한 - 관리자(creator/admin)만 통계 열람 가능 ## 완료 기준 - 기간별 통계 정상 집계 - Chart.js 차트 렌더링 - 관리자 권한 검증 - SQL GROUP BY 집계 (N+1 방지) - 다크모드 대응

미배정 8 days
긴급 964eecbd

[P0] QT 메인 페이지 (오늘의 QT)

## 개요 QT 메인 페이지 - 오늘의 QT 조회 및 특정 일차 조회 ## 범위 - QtController: today, day 액션 - 현재 세션 기준 일차 계산 로직 - QT 콘텐츠 표시 (성경구절, 테마제목, 본문, 질문 5개) - 묵상 기록 폼 (MeditationForm) - Turbo Frame - 일차 네비게이션 (이전/다음) - 반응형 레이아웃 (모바일 우선) ## 완료 기준 - [ ] /qt 접근 시 오늘의 QT 콘텐츠 표시 - [ ] /qt/day?day=N 으로 특정 일차 조회 - [ ] 묵상 기록 폼 표시 - [ ] 세션 미참여 시 세션 선택/생성 안내 - [ ] 시스템 테스트 통과 ## 참고 - 레거시: app/qt/page.tsx, api/qt/route.ts - 기능 ID: Q1-Q2

미배정 9 days
높음 1424b0ac
서브 티켓 통독 하기

[통독-2] 성경 읽기 페이지 + 일괄 완료 처리

## 목표 선택된 성경 장들의 본문을 표시하는 읽기 페이지와, 읽기 완료 후 일괄 저장 기능 구현 ## 현재 상태 - TongtokController: index 액션만 있음 - BibleReadingsController: create(단일), destroy(단일) 만 있음 - bible_passage_controller.js: 성경 본문 파싱/렌더링 Stimulus 컨트롤러 (이미 존재) - public/data/bible/books/*.json: 66권 성경 JSON 데이터 (이미 존재) - BibleReadingLog 모델: user_id, book_name, chapter, read_date ## 구현 내용 ### 1. 라우트 추가 config/routes.rb에 추가: ```ruby get "tongtok/read", to: "tongtok#read" post "bible_readings/batch", to: "bible_readings#batch_create" ``` ### 2. TongtokController#read 액션 ```ruby def read # params[:selections] = "창세기:1,2,3;출애굽기:1,2" @selections = parse_selections(params[:selections]) if @selections.empty? redirect_to tongtok_path, alert: "읽을 장을 선택해주세요." return end end ``` - parse_selections 헬퍼: "창세기:1,2,3;출애굽기:1,2" → [{book_name: "창세기", chapters: [1,2,3]}, ...] ### 3. 읽기 뷰 (app/views/tongtok/read.html.erb) - 상단: 뒤로가기 버튼 (← 통독 현황) - 선택된 장 요약 (예: "창세기 1-3장, 출애굽기 1-2장") - 각 장별 성경 본문: - 기존 bible_passage_controller.js를 활용 - 각 장을 div[data-controller="bible-passage"][data-bible-passage-passage-value="창세기 1장"] 으로 렌더링 - 장 간 구분선 - 맨 아래: "통독 완료" 버튼 (sticky) - 맨 위로 스크롤 버튼 ### 4. BibleReadingsController#batch_create 액션 ```ruby def batch_create chapters = JSON.parse(params[:chapters]) # chapters = [{"book_name": "창세기", "chapter": 1}, ...] chapters.each do |ch| current_user.bible_reading_logs.find_or_create_by( book_name: ch["book_name"], chapter: ch["chapter"], read_date: Date.current ) end redirect_to tongtok_path, notice: "#{chapters.size}장 통독 완료!" end ``` ### 5. 통독 완료 처리 (Stimulus) - app/javascript/controllers/tongtok_reader_controller.js 생성 - "통독 완료" 버튼 클릭 시: - 선택된 장 정보를 hidden field에 담아 POST /bible_readings/batch 전송 - 완료 후 통독 현황 페이지로 리다이렉트 ### 6. 테스트 - test/controllers/tongtok_controller_test.rb에 read 액션 테스트 추가: - 선택 없이 접근 시 리다이렉트 - 정상 선택 시 페이지 렌더링 - test/controllers/bible_readings_controller_test.rb에 batch_create 테스트 추가: - 다중 장 일괄 저장 - 중복 저장 방지 ## 디자인 참고 - 읽기 페이지는 깔끔한 읽기 모드 (책 읽듯이) - 본문 텍스트: text-body leading-relaxed - 절 번호: sup 태그로 작게 표시 - 장 제목: text-heading font-semibold border-b - 플로팅 "통독 완료" 버튼: sticky bottom, bg-brand-primary text-white ## 담당 파일 (다른 파일 수정 금지) - config/routes.rb (수정 - 2줄 추가) - app/controllers/tongtok_controller.rb (수정 - read 액션 + parse_selections) - app/controllers/bible_readings_controller.rb (수정 - batch_create 액션) - app/views/tongtok/read.html.erb (생성) - app/javascript/controllers/tongtok_reader_controller.js (생성) - test/controllers/tongtok_controller_test.rb (수정) - test/controllers/bible_readings_controller_test.rb (수정) ## 완료 기준 - /tongtok/read?selections=창세기:1,2 접근 시 성경 본문 표시 - 선택 없이 접근 시 리다이렉트 - "통독 완료" 클릭 시 선택한 장들이 BibleReadingLog에 저장 - 저장 후 통독 현황 페이지로 리다이렉트 + 성공 메시지 - 기존 테스트 깨지지 않을 것 - 새 테스트 통과

T
tongtok-reader
9 days
보통 3ef54083
부모 티켓

프로필 이미지 업로드

## 목표 사용자 프로필 이미지 직접 업로드 기능 (현재 OAuth 이미지만 사용) ## 구현 내용 1. **Active Storage 설정** - User 모델에 has_one_attached :avatar - 이미지 리사이즈 (image_processing gem) - 기존 profile_image(URL)와 avatar 우선순위 처리 2. **UI** - 프로필 편집에 이미지 업로드 영역 - 이미지 미리보기 (Stimulus 컨트롤러) - 크롭/리사이즈 클라이언트 처리 3. **표시** - avatar || profile_image || 기본 아바타 fallback - 전체 뷰에서 프로필 이미지 헬퍼 사용 ## 완료 기준 - [ ] Active Storage 설정 + User.has_one_attached :avatar - [ ] 프로필 편집 이미지 업로드 UI - [ ] 이미지 미리보기 Stimulus 컨트롤러 - [ ] user_avatar_url 헬퍼 (avatar > profile_image > default) - [ ] 파일 크기/형식 유효성 검사 - [ ] 테스트 ## 관련 파일 - app/models/user.rb - app/controllers/profiles_controller.rb - app/views/profiles/edit.html.erb - app/helpers/

1/1
팀리드
8 days
보통 c1b2c8b8

[모임] 관리자 설정 + 네비게이션 통합

## 구현 내용 ### 관리자 설정 - 모임 멤버 목록에서 관리자 추가/제거 (creator만 가능) - role 변경: member ↔ admin - admin 권한: QT 플랜 생성, 출석 QR 생성, 통계 열람, 모임 설정 수정 - creator 권한: admin 권한 + 관리자 설정 + 모임 삭제 ### 네비게이션 - 사이드바에 "모임" 메뉴 추가 - active 상태 스타일링 (border-l-3 border-brand-secondary) - 모임 관련 경로 highlight ## 완료 기준 - 관리자 추가/제거 동작 - 권한별 UI 분기 (버튼 표시/숨김) - 사이드바 메뉴 정상 동작 - 통합 테스트 통과

미배정 8 days
긴급 3ddd1e52

[P0] 묵상 기록 저장/조회

## 개요 UserMeditation 모델 및 묵상 기록 CRUD ## 범위 - UserMeditation 모델/마이그레이션 - 묵상 저장: personal_meditation, action_plan, prayer_topic, mood_before/after, highlights - 통독 완료 체크 (is_tongtok_completed) - 공유 설정 (is_personal_meditation_shared 등) - 자동 저장 (Turbo Stream or Stimulus debounce) - 기존 묵상 조회/수정 ## 완료 기준 - [ ] 묵상 기록 저장 성공 (POST /qt/meditations) - [ ] 기존 묵상 수정 성공 - [ ] 기분(mood) 1-5 선택 동작 - [ ] 하이라이트 JSON 저장/표시 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/qt/meditation/route.ts, components/qt/MeditationForm.tsx - 기능 ID: Q3

미배정 9 days
낮음 ca1b5b01
부모 티켓

묵상 오프라인 저장

## 목표 오프라인에서도 묵상 기록을 작성/저장하고, 온라인 복귀 시 자동 동기화 ## 구현 내용 1. **Service Worker 캐시 확장** - QT 콘텐츠 페이지 캐시 (읽기) - 묵상 폼 오프라인 접근 가능 2. **IndexedDB 로컬 저장** - 오프라인 묵상 기록 임시 저장 - 구조: { qt_content_id, content, created_at, synced: false } 3. **동기화** - 온라인 복귀 시 Background Sync API 활용 - 서버에 POST → synced: true 마킹 - 충돌 처리 (서버 우선) ## 완료 기준 - [ ] Service Worker: QT 콘텐츠 페이지 캐시 - [ ] IndexedDB: 오프라인 묵상 CRUD - [ ] 오프라인 상태 표시 UI - [ ] Background Sync 자동 동기화 - [ ] 충돌 해결 로직 - [ ] 동기화 상태 표시 ## 관련 파일 - public/service-worker.js - app/javascript/ (새 모듈) - app/views/qt/meditations/

2/2
팀리드
8 days
긴급 642577e5
부모 티켓

[모임] 전체 기능 구현 - Coordination

모임 기능 전체 구현 조율 티켓. 7개 서브 티켓을 에이전트 팀으로 병렬 처리.

4/4
팀리드
8 days
긴급 b1db95ed
서브 티켓 [모임] 전체 기능 구현 - Coordination

[모임] 스키마/모델 + CRUD + 네비게이션

Group, GroupMember, AttendanceRecord 3개 모델 마이그레이션 + GroupsController CRUD + 사이드바 네비게이션 통합. 상세 내용은 기존 todo 티켓 참조. UUID PK, FK 금지, belongs_to만 사용.

S
schema-dev
8 days
높음 b9b3dd28

[P0] QT 플랜(세션) 관리

## 개요 QT 세션(플랜) 생성, 참여, 탈퇴, 선택 기능 ## 범위 - 세션 생성: 테마 선택 + 시작일 + 초대코드 자동 생성 (8자) - 세션 목록 조회 (활성/완료 분류) - 세션 수정 (소유자만) - 초대코드로 참여 (join) - 세션 탈퇴 (leave) - 현재 활성 세션 변경 (select) - 초대 코드 조회 페이지 - QT 시작 처리 ## 완료 기준 - [ ] 세션 생성 → 초대코드 발급 - [ ] 초대코드 공유 → 참여 성공 - [ ] 활성 세션 전환 동작 - [ ] 세션 탈퇴 동작 - [ ] /qt/join?code=X 참여 페이지 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/qt/sessions/*.ts - 기능 ID: Q6-Q12, Q21

미배정 9 days
높음 a0b28045

QT 세션 스위처 UI

## 목표 QT 메인(today) 상단에 현재 참여 중인 세션 목록 드롭다운 추가 ## 상세 - 현재 사용자의 qt_participants → qt_sessions (active) 목록을 드롭다운으로 표시 - 세션 전환 시 해당 세션의 today 콘텐츠로 이동 - N+1 쿼리 방지: includes(:qt_session, :qt_theme) 사용 - Stimulus 컨트롤러로 드롭다운 토글 ## 완료 기준 - [ ] QT today 페이지 상단에 세션 스위처 드롭다운 표시 - [ ] 참여 중인 active 세션만 목록에 표시 - [ ] 세션 선택 시 해당 세션의 today 페이지로 이동 - [ ] 기존 테스트 전체 통과

미배정 8 days
보통 53394161
서브 티켓 공개 세션 참여 기능

공개 세션 탐색 탭 + max_participants 제한

## 목표 QT 플랜 목록(index)에 "공개 플랜" 탭 추가 + join 시 max_participants 제한 적용 ## 구현 내용 ### 1. SessionsController 수정 - index 액션에 `@public_sessions` 추가: `QtSession.active.where(is_public: true).where.not(id: current_user.qt_sessions.pluck(:id)).includes(:qt_theme, :creator).order(created_at: :desc)` - join 액션에 max_participants 체크 추가: `if @session.max_participants && @session.qt_participants.count >= @session.max_participants` → 거부 메시지 ### 2. index.html.erb 수정 - 기존 탭(활성/전체)에 "공개 플랜" 탭 추가 - 공개 플랜 탭 내용: 공개 세션 카드 목록 + "참여하기" 버튼 - 참여하기 버튼은 invite 페이지로 이동 (기존 invite 뷰 재활용) - 이미 참여 중이면 "참여 중" 뱃지 표시 - max_participants가 설정된 경우 "N/M명" 표시 ### 3. _session_card.html.erb 수정 - 공개 세션용 카드에 참여 인원 수 표시 - "참여하기" 버튼 또는 "참여 중" 뱃지 ### 4. 통합 테스트 작성 - test/controllers/qt/sessions_controller_test.rb에 추가: - 공개 세션 목록 표시 테스트 - max_participants 초과 시 join 거부 테스트 - 정상 join 테스트 (기존 테스트 있으면 확인) ## 관련 파일 (이 파일들만 수정) - app/controllers/qt/sessions_controller.rb - app/views/qt/sessions/index.html.erb - app/views/qt/sessions/_session_card.html.erb - test/controllers/qt/sessions_controller_test.rb - test/fixtures/qt_sessions.yml (필요시 공개 세션 fixture 추가) ## 주의사항 - 디자인 시스템 파셜 사용 (shared/card, shared/button, shared/tabs, shared/badge 등) - N+1 쿼리 방지: includes 사용 - 기존 invite/join 로직 유지, max_participants 체크만 추가 - 기존 테스트 전체 통과 확인 필수

S
session-dev
8 days
높음 db143157
서브 티켓 기도 AI 분석

AiPrayerAnalyzer 서비스 + 마이그레이션

AiPrayerAnalyzer 서비스 객체를 생성하고, prayer_requests 테이블에 ai_analysis 컬럼을 추가하는 마이그레이션을 작성합니다. ## 완료 기준 1. `app/services/ai_prayer_analyzer.rb` 생성 (AiSermonInterpreter 패턴 따름) 2. `db/migrate/xxx_add_ai_analysis_to_prayer_requests.rb` 마이그레이션 3. `test/services/ai_prayer_analyzer_test.rb` 서비스 테스트 4. 모든 테스트 통과

P
prayer-ai-dev
8 days
긴급 5cb8bfeb

API CSRF 보호 수정 (highlights_controller)

## 목표 highlights_controller.rb에서 skip_before_action :verify_authenticity_token 제거 후 JS에서 CSRF 토큰 전송하도록 수정 ## 현황 (보안 리뷰 발견) - 심각도: CRITICAL - 파일: app/controllers/api/bible/highlights_controller.rb:3 - 영향: 공격자가 악성 사이트에서 로그인된 사용자의 하이라이트 데이터 조작 가능 ## 수정 방법 1. `skip_before_action :verify_authenticity_token` 제거 2. JS fetch 호출 시 X-CSRF-Token 헤더 포함: ```javascript headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } ``` 3. 테스트에서 CSRF 토큰 포함 확인 ## 완료 기준 - skip_before_action 제거 - JS에서 CSRF 토큰 전송 - 기존 테스트 통과

미배정 8 days
높음 00454d4b
서브 티켓 [모임] 전체 기능 구현 - Coordination

[모임] 참여 플로우 + QR/초대코드 + 관리자 설정

QR/URL/초대코드 3가지 참여 방법 구현 + qrcode.js CDN importmap + qr_controller.js Stimulus + 관리자 추가/제거 기능. 상세 내용은 기존 todo 티켓 참조.

J
join-dev
8 days
높음 f738244e

[P1] 기도제목 CRUD + 기도 체크

## 개요 기도제목 생성/조회/수정/삭제 및 일일 기도 체크 ## 범위 - PrayerRequest 모델: content, target, category(daily/weekly), response_type, visibility, sort_order - PrayerCheckLog 모델: prayer_request_id, check_date (unique per day) - 기도제목 CRUD (생성, 목록, 수정, 삭제) - 오늘 기도 체크/해제 토글 - 카테고리별 필터 (매일/주간) - 응답 상태 변경 (keep_praying, waiting, yes, no) - 정렬 순서 변경 (drag & drop or 수동) - 기도 페이지 UI ## 완료 기준 - [ ] 기도제목 CRUD 동작 - [ ] 일일 기도 체크 토글 동작 - [ ] 카테고리 필터 동작 - [ ] 응답 상태 변경 동작 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/prayer/route.ts, app/prayer/page.tsx - 기능 ID: P1-P2

미배정 9 days
보통 854a0d35

QT 빈 상태 안내 개선

## 목표 참여 세션이 없을 때 테마 브라우즈 페이지로 안내하는 빈 상태 UI 추가 ## 상세 - 참여 세션 0개인 사용자가 /qt/today 접속 시 안내 화면 표시 - "테마를 둘러보고 플랜을 시작하세요" 메시지 + CTA 버튼 - /qt/themes 페이지로 연결 ## 완료 기준 - [ ] 참여 세션 없는 사용자에게 안내 화면 표시 - [ ] 테마 브라우즈 페이지로 이동하는 CTA 버튼 - [ ] 기존 테스트 전체 통과

미배정 8 days
보통 67958cd9
서브 티켓 하이라이트 관리 페이지

하이라이트 관리 페이지 (목록/필터/편집/삭제)

## 목표 /bible/highlights — 내 하이라이트 전체 목록, 성경별/색상별 필터, 노트 편집, 삭제 ## 구현 내용 ### 1. 라우트 추가 (config/routes.rb) ```ruby resources :bible_highlights, only: [:index, :update, :destroy], path: "bible/highlights" ``` ### 2. BibleHighlightsController (app/controllers/bible_highlights_controller.rb) ```ruby class BibleHighlightsController < ApplicationController before_action :authenticate_user! def index @highlights = current_user.bible_highlights.order(:book_abbrev, :chapter, :verse) @highlights = @highlights.where(book_abbrev: params[:book]) if params[:book].present? @highlights = @highlights.where(color: params[:color]) if params[:color].present? @grouped = @highlights.group_by(&:book_abbrev) @books = current_user.bible_highlights.distinct.pluck(:book_abbrev).sort @colors = %w[yellow green blue pink purple] end def update @highlight = current_user.bible_highlights.find(params[:id]) if @highlight.update(highlight_params) respond_to do |format| format.turbo_stream format.html { redirect_to bible_highlights_path } end else render :edit, status: :unprocessable_entity end end def destroy @highlight = current_user.bible_highlights.find(params[:id]) @highlight.destroy respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.remove(@highlight) } format.html { redirect_to bible_highlights_path, notice: "하이라이트가 삭제되었습니다." } end end private def highlight_params params.require(:bible_highlight).permit(:note) end end ``` ### 3. 뷰 파일들 #### index.html.erb (app/views/bible_highlights/index.html.erb) - 상단: 제목 "내 하이라이트" + 필터 (성경 select + 색상 select) - 필터: form_with GET으로 구현 (Turbo Frame 사용 가능) - 본문: 성경별 그룹핑 (book_abbrev 기준) - 각 그룹 헤더: 성경 이름 + 하이라이트 수 - 각 하이라이트: 장:절 + 색상 뱃지 + 노트 미리보기 + 편집/삭제 버튼 - 성경 본문 이동 링크: qt_day_path 또는 tongtok/read 경로 - 빈 상태: "아직 하이라이트가 없습니다" 안내 - book_abbrev를 한글 성경 이름으로 변환: BibleData::BOOKS에서 abbrev→name 매핑 #### _highlight_item.html.erb (app/views/bible_highlights/_highlight_item.html.erb) - turbo_frame_tag로 감싸기 (인라인 편집용) - 색상별 배경: yellow→bg-yellow-100, green→bg-emerald-100, blue→bg-blue-100, pink→bg-pink-100, purple→bg-purple-100 - 노트 표시 (있으면), 편집 버튼 (Turbo Frame으로 전환) - 삭제 버튼 (button_to DELETE, data-turbo-confirm) #### _highlight_form.html.erb (app/views/bible_highlights/_highlight_form.html.erb) - turbo_frame_tag 안에 노트 편집 폼 - textarea + 저장/취소 버튼 - PATCH 요청 #### update.turbo_stream.erb - turbo_stream.replace로 편집 완료 후 아이템 교체 ### 4. BibleData 헬퍼 확인 - BibleData::BOOKS에서 abbrev→name 변환 메서드 확인 - 없으면 간단한 헬퍼 메서드 추가 ### 5. 통합 테스트 (test/controllers/bible_highlights_controller_test.rb) - index: 하이라이트 목록 표시 - index with filters: 성경/색상 필터 동작 - update: 노트 편집 - destroy: 하이라이트 삭제 - 인증 필수 확인 ## 관련 파일 (이 파일들만 수정/생성) - config/routes.rb (라우트 추가) - app/controllers/bible_highlights_controller.rb (새로 생성) - app/views/bible_highlights/index.html.erb (새로 생성) - app/views/bible_highlights/_highlight_item.html.erb (새로 생성) - app/views/bible_highlights/_highlight_form.html.erb (새로 생성) - app/views/bible_highlights/update.turbo_stream.erb (새로 생성) - test/controllers/bible_highlights_controller_test.rb (새로 생성) - test/fixtures/bible_highlights.yml (필요시) ## 주의사항 - 디자인 시스템 파셜 사용 (shared/card, shared/button, shared/badge, shared/empty_state) - 다크모드 대응 필수 (Tailwind dark: 프리픽스) - N+1 쿼리 방지 - CSRF 토큰 포함 (button_to 기본 포함) - 기존 API 컨트롤러(Api::Bible::HighlightsController)와 충돌 없도록 별도 컨트롤러 - 기존 테스트 전체 통과 확인 필수 - SQLite FK 금지 (belongs_to로만 관계 유지)

H
highlight-dev
8 days
높음 909e710f
서브 티켓 기도 AI 분석

기도 AI 분석 UI (컨트롤러 + Turbo Stream + 뷰)

PrayersController에 analyze 액션을 추가하고, Turbo Stream으로 AI 분석 결과를 표시하는 UI를 구현합니다. ## 완료 기준 1. PrayersController에 `analyze` 액션 추가 2. 라우트 추가: `post :analyze` (member) 3. `app/views/prayers/analyze.turbo_stream.erb` 생성 4. `app/views/prayers/_ai_analysis.html.erb` 파셜 생성 5. `_prayer_card.html.erb`에 AI 분석 버튼/영역 추가 6. `test/controllers/prayers_controller_test.rb`에 analyze 테스트 추가 7. 모든 테스트 통과

P
prayer-ai-dev
8 days
높음 62b323d8

QT 세션 N+1 쿼리 최적화

## 목표 sessions_controller.rb의 N+1 쿼리 3건 해결 ## 발견 사항 (코드 리뷰) 1. [HIGH] members 액션 (L83-94): @participants.map에서 각 참가자별 user.user_meditations 쿼리 → N+1 2. [HIGH] rankings 액션 (L105-133): users.map에서 유저별 4개 쿼리 (meditations, completed, shared, tongtok) → N*4+1 3. [HIGH] _session_card.html.erb:5: session.qt_participants.size → index에서 카드마다 쿼리 ## 수정 방법 - members: includes(:user => :user_meditations) 또는 서브쿼리로 집계 - rankings: counter_cache 또는 SQL 집계 쿼리로 전환 - index: @sessions = ... .includes(:qt_participants) 추가 ## 완료 기준 - N+1 쿼리 제거 확인 (bullet gem 또는 로그) - 기존 테스트 통과

미배정 8 days
높음 e9b0085a
서브 티켓 [모임] 전체 기능 구현 - Coordination

[모임] QT 플랜 연동 + 출석 체크 시스템

QtSession에 group_id 추가 + 모임 내 QT 생성 시 멤버 자동 참여 + 출석 QR 생성 + 체크인 플로우 + 지각 판정. 상세 내용은 기존 todo 티켓 참조.

Q
qt-link-dev
8 days
높음 44e26348

[P1] 설교 노트 CRUD

## 개요 설교 노트 생성/조회/수정/삭제 ## 범위 - SermonNote 모델: title, sermon_date, bible_book, start/end_chapter/verse, passage_reference, personal_meditation, action_plan, prayer_topics, highlights(JSON), mood_after - 설교 노트 목록 (페이지네이션, 검색, 필터) - 설교 노트 생성 폼 (성경구절 선택 + 묵상 입력) - 설교 노트 상세 보기 - 설교 노트 수정/삭제 - 하이라이트 기능 (JSONB) - 공유 설정 ## 완료 기준 - [ ] 설교 노트 CRUD 동작 - [ ] 성경구절 입력 (책/장/절) 동작 - [ ] 페이지네이션 동작 - [ ] 검색/필터 동작 - [ ] 하이라이트 저장/표시 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/sermon/*.ts, app/sermon/ - 기능 ID: S1-S5

미배정 9 days
높음 d23df7e3

QT 테마 브라우즈 페이지

## 목표 /qt/themes — 모든 공개 테마를 카드 형태로 나열하는 페이지 ## 상세 - QtTheme.where(is_active: true) 목록을 카드 UI로 표시 - 각 카드: 제목, 설명, 총 일수, 성경 범위(bible_books) - 라우트: GET /qt/themes → Qt::ThemesController#index - 디자인 시스템 shared/card 파셜 활용 ## 완료 기준 - [ ] /qt/themes 라우트 + 컨트롤러 + 뷰 생성 - [ ] 활성 테마 카드 목록 표시 (제목, 설명, 일수, 성경 범위) - [ ] 각 카드에서 테마 상세 페이지로 링크 - [ ] 기존 테스트 전체 통과

미배정 8 days
보통 134e7895

QT 세션 IDOR 접근 제어 + select 참여자 검증

## 목표 비참여자의 비공개 세션 접근 차단 + select 액션 참여자 확인 ## 발견 사항 (보안 리뷰) 1. [MEDIUM] set_session이 QtSession.find(params[:id])로 무조건 조회 → 비참여자도 show/members/rankings 접근 가능 2. [MEDIUM] select 액션에서 참여자 확인 없이 current_session_id 설정 가능 ## 수정 방법 1. before_action :verify_participant 추가 (show, shared_meditations, members, rankings) - is_public이면 허용, 아니면 qt_participants.exists?(user: current_user) 확인 2. select 액션에 참여자 확인: ```ruby unless @session.qt_participants.exists?(user: current_user) redirect_to qt_sessions_path, alert: "참여 중인 플랜만 선택할 수 있습니다." return end ``` ## 완료 기준 - 비참여자 비공개 세션 접근 차단 - select 참여자 검증 - 테스트 추가

미배정 8 days
보통 6727988f
서브 티켓 [모임] 전체 기능 구현 - Coordination

[모임] 출석 통계/리포트 (Chart.js)

모임 관리자용 출석 통계 대시보드. 기간별 출석률, 멤버별 랭킹, Chart.js 차트 3종 (라인/도넛/바). SQL GROUP BY 집계. 상세 내용은 기존 todo 티켓 참조.

S
stats-dev
8 days
보통 160b7c8a

[P1] 성경 통독 현황

## 개요 성경 66권 통독 기록 및 현황 시각화 ## 범위 - BibleReadingLog 모델: user_id, book_name, chapter, read_date - 성경 읽기 기록 CRUD (같은 장 중복 허용) - 통독 현황 페이지 (/tongtok) - 66권 전체 현황 시각화 (구약 39권 + 신약 27권) - 장별 읽기 체크 UI - 진행률 표시 ## 완료 기준 - [ ] 성경 장 읽기 기록 생성/삭제 - [ ] 66권 통독 현황 시각화 - [ ] 진행률 (%) 표시 - [ ] 통합 테스트 통과 ## 참고 - 레거시: app/tongtok/page.tsx, api/user/bible-reading/route.ts - 기능 ID: T1-T3 - 유틸리티: src/utils/bible.ts (66권 정보)

미배정 9 days
높음 dcdbeaf9

QT 테마 상세 + 구독(세션 생성)

## 목표 /qt/themes/:id — 테마 상세 정보 + "이 테마로 플랜 시작" 버튼 ## 상세 - QtTheme 상세 정보 표시: 제목, 설명, 총 일수, 성경 범위, 콘텐츠 미리보기 - "이 테마로 플랜 시작" 버튼 → QtSession 자동 생성 (start_date=today, end_date 자동 계산) - 이미 같은 테마로 active 세션이 있으면 안내 메시지 - 라우트: GET /qt/themes/:id → Qt::ThemesController#show - POST /qt/themes/:id/subscribe → 세션 생성 + 참여자 등록 ## 완료 기준 - [ ] 테마 상세 페이지 (제목, 설명, 일수, 성경 범위, 콘텐츠 목록) - [ ] "플랜 시작" 버튼으로 세션+참여자 자동 생성 - [ ] 중복 구독 방지 (같은 테마 active 세션 경고) - [ ] 기존 테스트 전체 통과

미배정 8 days
보통 a3bcc073
서브 티켓 프로필 이미지 업로드

Active Storage + 프로필 이미지 업로드 전체 구현

Active Storage 설치, User avatar 연결, 프로필 편집 이미지 업로드 UI, Stimulus 미리보기, avatar_url 헬퍼, 기존 avatar 파셜 수정, 테스트 작성 ## 완료 기준 1. Active Storage 마이그레이션 설치 + 실행 2. User 모델에 has_one_attached :avatar + 유효성 검사 3. User#avatar_url 메서드 (avatar > profile_image > nil) 4. ProfilesController에 :avatar permit 추가 5. profiles/show.html.erb에 파일 업로드 UI (URL 입력 → 파일 업로드 대체) 6. image_preview_controller.js Stimulus 컨트롤러 7. shared/_avatar.html.erb에서 User 객체의 avatar_url 활용 8. 테스트 작성 + 전체 테스트 통과

P
profile-img-dev
8 days
보통 59d90501

[P1] 묵상 통계/히스토리

## 개요 묵상 통계 대시보드 및 히스토리 페이지 ## 범위 - 묵상 통계: 총 묵상수, 완료율, 연속일수, 평균 기분 - 월별 통계: 월별 묵상 카운트/평균 - 묵상 히스토리 목록 (/records) - 통계 페이지 (/stats) - 차트 (Chart.js + Stimulus 또는 Chartkick) - 성경 챕터 통계 - 달력 기반 히스토리 (/history) - P2 가능 ## 완료 기준 - [ ] /stats 통계 페이지 차트 표시 - [ ] /records 묵상 기록 리스트 표시 - [ ] 월별/연간 통계 동작 - [ ] 연속일수 계산 정확 - [ ] 통합 테스트 통과 ## 참고 - 레거시: app/stats/page.tsx, app/records/page.tsx - 기능 ID: R1-R6

미배정 9 days
보통 4ae3b0a7
부모 티켓

공개 세션 참여 기능

## 목표 공개 세션 목록 + invite_code로 참여하는 기능 ## 상세 - 공개 세션(is_public: true) 목록 페이지 - invite_code 입력으로 비공개 세션 참여 - 참여 시 QtParticipant 자동 생성 (role: member) - max_participants 초과 시 참여 거부 ## 완료 기준 - [ ] 공개 세션 목록 페이지 - [ ] invite_code 입력 UI + 참여 처리 - [ ] max_participants 제한 동작 - [ ] 기존 테스트 전체 통과

1/1
팀리드
8 days
보통 fb191778
서브 티켓 묵상 오프라인 저장

오프라인 묵상 프론트엔드 (IndexedDB + Stimulus + Service Worker)

## 목표 오프라인에서 묵상 기록을 작성/저장하고, 온라인 복귀 시 자동 동기화하는 프론트엔드 구현 ## 구현 내용 ### 1. IndexedDB 래퍼 모듈 (`app/javascript/lib/offline_store.js`) - IndexedDB 'logbible' 데이터베이스 생성 (version 1) - 'pending_meditations' object store: { id(auto), qt_content_id, meditation_date, personal_meditation, action_plan, prayer_topic, mood_after, is_tongtok_completed, created_at, synced } - CRUD 메서드: save(), getAll(), getPending(), markSynced(), delete() - importmap에 등록 ### 2. offline_sync Stimulus 컨트롤러 (`app/javascript/controllers/offline_sync_controller.js`) - data-controller="offline-sync"를 묵상 폼에 추가 - 온라인/오프라인 상태 감지 (navigator.onLine + online/offline 이벤트) - 오프라인 시 폼 제출 가로채기: IndexedDB에 저장 + 성공 메시지 표시 - 온라인 복귀 시 자동 동기화: pending entries를 POST /qt/meditations/sync로 전송 - 동기화 상태 표시: 배지/아이콘으로 "N건 동기화 대기" 표시 ### 3. Service Worker 캐시 확장 (`app/views/pwa/service-worker.js`) - QT 페이지 캐시 전략 추가: /qt/today, /qt/day?day=N 페이지를 Network First + Cache Fallback - 기존 precache에 추가하지 말고, 동적 캐시로 처리 (방문한 페이지만 캐시) - 캐시 이름: 'qt-pages-v1' - 오프라인 시 캐시된 QT 페이지 제공 (기존 offline.html 대신) ### 4. 오프라인 상태 UI - 오프라인 배너: 상단에 "오프라인 모드 - 묵상이 로컬에 저장됩니다" 표시 - 동기화 진행 표시: "동기화 중..." → "동기화 완료" 토스트 - 묵상 폼에 로컬 저장 아이콘 표시 (오프라인 시) - 디자인 시스템 활용: shared/_card, shared/_badge 파셜 패턴 참고 ### 5. 묵상 폼 수정 (`app/views/qt/meditations/_form.html.erb`) - 폼에 data-controller="offline-sync" 추가 - data-action="submit->offline-sync#handleSubmit" 추가 - 동기화 상태 표시 영역 추가 ## 파일 목록 (이 파일들만 수정/생성) - `app/javascript/lib/offline_store.js` (생성) - `app/javascript/controllers/offline_sync_controller.js` (생성) - `app/views/pwa/service-worker.js` (수정 - 캐시 전략 추가만) - `app/views/qt/meditations/_form.html.erb` (수정 - data attributes 추가) - `app/views/qt/today.html.erb` (수정 - 오프라인 배너 추가) - `config/importmap.rb` (수정 - offline_store 핀 추가) ## 주의사항 - Importmap 환경: npm 패키지 사용 불가, 순수 JS만 사용 - 기존 Stimulus 컨트롤러 패턴 참고 (app/javascript/controllers/) - Tailwind CSS v4 사용 중 (app/assets/tailwind/application.css 참고) - 기존 Service Worker 구조 유지하면서 확장 (덮어쓰기 금지) - turbo:submit-end 이벤트 활용 가능 - 테스트는 백엔드 에이전트가 담당 (프론트엔드는 구현만)

O
offline-frontend
8 days
긴급 84934226
부모 티켓

보안+성능 일괄 수정 (CSRF, IDOR, N+1)

## 목표 리뷰에서 발견된 보안/성능 이슈 3건 일괄 수정 ## 서브 티켓 1. [CRITICAL] API CSRF 보호 수정 (5cb8bfeb) 2. [HIGH] QT 세션 N+1 쿼리 최적화 (62b323d8) 3. [MEDIUM] QT 세션 IDOR 접근 제어 (134e7895) ## 팀 구성 - csrf-fixer: highlights_controller.rb + JS CSRF 토큰 처리 - sessions-fixer: sessions_controller.rb N+1 + IDOR 수정

2/2
팀리드
8 days
긴급 4062e079
서브 티켓 보안+성능 일괄 수정 (CSRF, IDOR, N+1)

API CSRF 보호 수정 + JS CSRF 토큰 전송

## 작업 내용 1. app/controllers/api/bible/highlights_controller.rb에서 `skip_before_action :verify_authenticity_token` 제거 2. JS fetch 호출에서 X-CSRF-Token 헤더 포함하도록 수정 3. 관련 Stimulus 컨트롤러/JS 파일에서 CSRF 토큰 전송 추가 4. 기존 테스트 수정 (CSRF 토큰 포함) ## 완료 기준 - skip_before_action 제거 - JS에서 CSRF 토큰 전송 - 전체 테스트 통과 (bin/rails test)

C
csrf-fixer
8 days
보통 cb5e09d7

[P1] 프로필/설정 + 정적 페이지

## 개요 사용자 프로필 수정, 설정, 약관/개인정보처리방침 페이지 ## 범위 - 프로필 페이지 (/profile): 닉네임, 프로필 이미지, 전화번호 변경 - 설정 페이지 (/settings): 알림 설정, 타임존, 언어, 난이도 - 개인정보처리방침 (/privacy) - 정적 페이지 - 이용약관 (/terms) - 정적 페이지 - 프로필 이미지 업로드 (ActiveStorage) ## 완료 기준 - [ ] 프로필 수정 동작 - [ ] 설정 변경 저장 동작 - [ ] 정적 페이지 표시 - [ ] 프로필 이미지 업로드/표시 - [ ] 통합 테스트 통과 ## 참고 - 레거시: app/profile/page.tsx, app/settings/page.tsx - 기능 ID: A5, E2-E3

미배정 9 days
높음 a73d80d0
부모 티켓

QT today 페이지 통독/QT 탭 분리

## 목표 QT today 페이지에서 통독과 QT 묵상을 별도 탭으로 분리 ## 상세 - today 페이지에 두 개 탭 구현: - **통독 탭**: bible_passage(통독 범위) 표시 + 성경 본문 렌더링 + 통독 완료 체크 - **QT 탭**: reading_passage(QT 본문) 표시 + 묵상 질문 + 묵상 입력 폼 - Stimulus 컨트롤러로 탭 전환 (Turbo 없이 클라이언트 사이드) - 통독 완료 체크 → BibleReadingLog로 저장 - QT 묵상은 reading_passage 기준으로만 동작 - 탭 상태 유지 (URL hash 또는 localStorage) ## 완료 기준 - [ ] today 페이지에 통독/QT 2개 탭 UI - [ ] 통독 탭: bible_passage + 성경 본문 + 완료 체크 - [ ] QT 탭: reading_passage + 질문 + 묵상 입력 - [ ] 탭 전환 Stimulus 컨트롤러 - [ ] 기존 테스트 전체 통과

2/2
팀리드
9 days
보통 66ece3fb
서브 티켓 묵상 오프라인 저장

오프라인 묵상 백엔드 (Sync API + 테스트)

## 목표 오프라인 저장된 묵상 데이터를 서버에 동기화하는 API 엔드포인트 + 테스트 ## 구현 내용 ### 1. Sync API 라우트 (`config/routes.rb`) ```ruby namespace :qt do resources :meditations, only: [:create, :update] do member do post :organize end collection do post :sync # 새로 추가 end end end ``` ### 2. Sync 액션 (`app/controllers/qt/meditations_controller.rb`) 기존 MeditationsController에 sync 액션 추가: ```ruby def sync results = [] sync_params[:meditations].each do |med_data| content = current_user.qt_sessions_contents.find_by(id: med_data[:qt_content_id]) next unless content meditation = current_user.user_meditations.find_or_initialize_by( qt_content_id: med_data[:qt_content_id], meditation_date: med_data[:meditation_date] ) # 충돌 해결: 서버에 이미 데이터가 있고 더 최신이면 서버 우선 if meditation.persisted? && meditation.updated_at > Time.parse(med_data[:created_at]) results << { qt_content_id: med_data[:qt_content_id], status: "conflict", server_data: meditation_json(meditation) } next end meditation.assign_attributes( personal_meditation: med_data[:personal_meditation], action_plan: med_data[:action_plan], prayer_topic: med_data[:prayer_topic], mood_after: med_data[:mood_after], is_tongtok_completed: med_data[:is_tongtok_completed] ) if meditation.save results << { qt_content_id: med_data[:qt_content_id], status: "synced", id: meditation.id } else results << { qt_content_id: med_data[:qt_content_id], status: "error", errors: meditation.errors.full_messages } end end render json: { results: results } end ``` ### 3. Strong Parameters ```ruby def sync_params params.permit(meditations: [:qt_content_id, :meditation_date, :personal_meditation, :action_plan, :prayer_topic, :mood_after, :is_tongtok_completed, :created_at]) end ``` ### 4. 테스트 (`test/controllers/qt/meditations_controller_test.rb`) 기존 테스트 파일에 sync 테스트 추가: - POST /qt/meditations/sync 인증 필요 - POST /qt/meditations/sync 새 묵상 생성 (synced) - POST /qt/meditations/sync 기존 묵상 업데이트 (synced) - POST /qt/meditations/sync 충돌 시 서버 우선 (conflict) - POST /qt/meditations/sync 잘못된 qt_content_id 무시 - POST /qt/meditations/sync 빈 배열 처리 - POST /qt/meditations/sync 여러 건 배치 처리 ### 5. qt_sessions_contents 헬퍼 sync에서 qt_content_id 유효성 검사를 위해 현재 사용자가 접근 가능한 콘텐츠 확인 필요. 기존 QtController에 이미 세션→콘텐츠 관계가 있으므로 참고: - User → qt_participants → qt_sessions → qt_theme → qt_contents ## 파일 목록 (이 파일들만 수정/생성) - `config/routes.rb` (수정 - sync 라우트 추가) - `app/controllers/qt/meditations_controller.rb` (수정 - sync 액션 + sync_params 추가) - `test/controllers/qt/meditations_controller_test.rb` (수정 - sync 테스트 추가) ## 주의사항 - UUID PK: ApplicationRecord의 set_uuid 패턴 사용 - SQLite: parallelize(workers: 1) 필수 - 기존 create/update/organize 액션 절대 수정 금지 - 기존 테스트 전체 통과 확인 필수 (bin/rails test) - JSON 응답 (render json:) 사용 - Turbo Stream 아님 - 충돌 해결: 서버 데이터가 더 최신이면 서버 우선, 그렇지 않으면 클라이언트 데이터 적용 - content의 접근 권한 검증: 사용자가 참여 중인 세션의 콘텐츠만 동기화 허용

O
offline-backend
8 days
높음 450befd8
서브 티켓 보안+성능 일괄 수정 (CSRF, IDOR, N+1)

QT 세션 N+1 쿼리 최적화 + IDOR 접근 제어

## 작업 내용 (2개 티켓 통합) ### A. N+1 쿼리 최적화 1. members 액션: @participants.map에서 user.user_meditations N+1 → includes 또는 SQL 집계 2. rankings 액션: users.map에서 유저별 4개 쿼리 → SQL 집계 쿼리로 전환 3. _session_card.html.erb: session.qt_participants.size → index에서 includes(:qt_participants) 추가 ### B. IDOR 접근 제어 1. before_action :verify_participant 추가 (show, shared_meditations, members, rankings) - is_public이면 허용, 아니면 qt_participants.exists?(user: current_user) 확인 2. select 액션에 참여자 확인 추가 3. 비참여자 접근 차단 테스트 추가 ## 완료 기준 - N+1 쿼리 제거 - 비참여자 비공개 세션 접근 차단 - select 참여자 검증 - 전체 테스트 통과 (bin/rails test)

S
sessions-fixer
8 days
보통 8d72626a

[P2] 기도 동역자 시스템

## 개요 기도 동역자 검색, 요청, 수락/거절, 기도제목 공유 ## 범위 - PrayerPartner 모델: requester_id, receiver_id, status(pending/accepted/rejected) - 동역자 검색 (이메일/닉네임) - 동역자 요청 보내기 - 받은 요청 목록 - 수락/거절/삭제 - 동역자의 공개 기도제목 조회 - 동역자 관리 페이지 (/prayer/partners) ## 완료 기준 - [ ] 동역자 검색 동작 - [ ] 요청 → 수락/거절 플로우 동작 - [ ] 동역자 기도제목 조회 동작 - [ ] 자기 자신에게 요청 방지 (CHECK 제약) - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/prayer/partners/*.ts - 기능 ID: P6-P10

미배정 9 days
보통 30a2362f

통독 탭 상세 기능 (진행률 + 체크)

## 목표 통독 탭 내에서 66권 진행률 표시 + 장 단위 체크 기능 ## 상세 - 통독 탭 하단에 66권 진행률 요약 (완료/전체) - 장 단위 체크박스로 BibleReadingLog 저장 (Turbo Frame 비동기) - 구약/신약 분류 토글 - 기존 /bible/progress 뷰의 데이터 활용 ## 완료 기준 - [ ] 통독 탭에 진행률 요약 표시 - [ ] 장 단위 체크 → BibleReadingLog 저장 - [ ] 기존 테스트 전체 통과

미배정 9 days
긴급 eed2c213
부모 티켓

코드 품질 일괄 개선 (CSRF + N+1 + IDOR)

보안 및 성능 이슈 3건 일괄 처리:\n1. [CRITICAL] API CSRF 보호 수정 (highlights_controller)\n2. [HIGH] QT 세션 N+1 쿼리 최적화\n3. [MEDIUM] QT 세션 IDOR 접근 제어 + select 참여자 검증

3/3
팀리드
8 days
보통 edb84c7f
서브 티켓 카카오 알림톡 + CRON

카카오 알림톡 프론트엔드 (설정 UI 활성화 + 전화번호 입력)

## 목표 카카오 알림톡 설정 UI 활성화 + 전화번호 입력 필드 추가 ## 구현 내용 ### 1. 설정 페이지 수정 (`app/views/settings/show.html.erb`) - 카카오톡 체크박스: `disabled` 제거, "준비 중" 배지 제거 - 카카오톡 체크박스 활성화 시 전화번호 입력 필드 표시 - 전화번호 형식: 010-XXXX-XXXX (한국 모바일) - 안내 텍스트: "카카오톡으로 QT 알림을 받으려면 전화번호를 입력해주세요" - Stimulus 컨트롤러 활용: 기존 `notification-methods` 컨트롤러에 kakao 토글 로직 추가 ### 2. Users 테이블 phone 컬럼 확인 - users 테이블에 이미 `phone` varchar 컬럼이 존재함 - SettingsController에서 phone 업데이트 가능하도록 수정 필요 ### 3. SettingsController 수정 (`app/controllers/settings_controller.rb`) - `settings_params`에서 phone을 user params로 분리 처리 - 또는 UserSetting에 phone 저장 (설계 선택) - **중요**: User 모델의 phone 컬럼 활용 (이미 존재) - update 액션에서 user.phone도 함께 업데이트: ```ruby current_user.update(phone: params[:phone]) if params[:phone].present? ``` ### 4. 전화번호 Stimulus 컨트롤러 (`app/javascript/controllers/notification_methods_controller.js`) - 기존 notification_methods_controller.js 수정 - kakao 체크박스 토글 시 전화번호 입력 필드 표시/숨김 - 전화번호 유효성 검사 (010으로 시작, 11자리) ### 5. 테스트 (`test/controllers/settings_controller_test.rb`) - PATCH /settings에서 phone 업데이트 확인 - kakao 포함한 notification_methods 저장 확인 ## 파일 목록 (이 파일들만 수정/생성) - `app/views/settings/show.html.erb` (수정 - 카카오 활성화 + 전화번호) - `app/controllers/settings_controller.rb` (수정 - phone 파라미터 처리) - `app/javascript/controllers/notification_methods_controller.js` (수정 - kakao 토글) - `test/controllers/settings_controller_test.rb` (수정 - kakao + phone 테스트) ## 주의사항 - Tailwind CSS v4 사용 (app/assets/tailwind/application.css 참고) - 기존 디자인 시스템 파셜 패턴 활용 (shared/_card, _button 등) - 기존 notification-methods Stimulus 컨트롤러 패턴 유지 - users 테이블에 phone 컬럼이 이미 있는지 먼저 확인 (없으면 마이그레이션 불필요 - 이미 존재) - bin/rails test로 전체 테스트 통과 확인 필수 - 다른 에이전트(kakao-backend)가 서비스 파일을 수정하므로, 서비스 파일은 절대 수정하지 마세요

K
kakao-frontend
8 days
보통 3210e3b7

[P2] 공유 묵상 + 랭킹

## 개요 세션 내 묵상 공유, 멤버 관리, 통독/묵상 랭킹 ## 범위 - 공유 묵상 조회 (같은 세션 멤버의 공개 묵상) - 멤버 목록 조회 - 특정 멤버의 묵상 조회 - 랭킹: 통독/묵상/공유 순위 (전체/월별) - 내 완료 현황 (일차별 묵상/통독 완료 상태) - 내 세션 목록 ## 완료 기준 - [ ] 공유 묵상 목록 표시 - [ ] 멤버 목록 표시 - [ ] 랭킹 (전체/월별) 표시 - [ ] 내 완료 현황 표시 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/qt/sessions/shared-meditations, rankings, members - 기능 ID: Q14-Q20

미배정 9 days
높음 657861d7

BibleHighlight 모델 + 마이그레이션

## 목표 성경 하이라이트를 저장할 BibleHighlight 모델 생성 ## 상세 - 컬럼: id(uuid), user_id(fk), book_abbrev(string), chapter(integer), verse(integer), color(string, default: 'yellow'), note(text), created_at, updated_at - 인덱스: [user_id, book_abbrev, chapter, verse] unique - belongs_to :user - validates: book_abbrev, chapter, verse presence - color enum 또는 validates inclusion: %w[yellow green blue pink purple] ## 완료 기준 - [ ] 마이그레이션 생성 + 실행 - [ ] BibleHighlight 모델 (validations, associations) - [ ] 모델 단위 테스트 - [ ] 기존 테스트 전체 통과

미배정 8 days
긴급 02012328
서브 티켓 코드 품질 일괄 개선 (CSRF + N+1 + IDOR)

API CSRF 보호 수정 + 테스트 강화

## 목표 api/bible/highlights_controller.rb의 CSRF 보호를 명시적으로 강화하고 테스트 추가 ## 수정 내용 1. ApplicationController 확인: protect_from_forgery가 활성화되어 있는지 검증 2. api/bible/highlights_controller.rb에 skip_before_action :verify_authenticity_token이 없는지 확인 (없으면 OK) 3. test/controllers/api/bible/highlights_controller_test.rb에 CSRF 토큰 없이 POST/DELETE 시 거부되는지 테스트 추가 4. app/javascript/controllers/bible_passage_controller.js의 fetch 호출이 X-CSRF-Token 헤더를 올바르게 전송하는지 확인 ## 담당 파일 (이 파일들만 수정) - app/controllers/api/bible/highlights_controller.rb - test/controllers/api/bible/highlights_controller_test.rb - app/javascript/controllers/bible_passage_controller.js (필요시만) ## 완료 기준 - CSRF 토큰 없는 POST/DELETE 요청이 422로 거부됨 (테스트 검증) - 기존 테스트 전체 통과 - bin/rails test 실행 결과 0 failures, 0 errors

C
csrf-dev
8 days
보통 a61b6d56

[P2] AI 묵상 분석 (월별 리포트)

## 개요 AI를 활용한 월별 묵상 패턴 분석 및 리포트 생성 ## 범위 - MonthlyAnalysisReport 모델: user_id, year, month, analysis_report(JSON), data_summary(JSON) - ruby-openai gem으로 Gemini/OpenAI API 연동 - 월별 묵상 데이터 수집 → AI 분석 요청 - 분석 결과 저장 및 표시 - 달력 기반 히스토리 (/history) ## 완료 기준 - [ ] AI 분석 요청 → 결과 저장 동작 - [ ] 월별 리포트 조회 동작 - [ ] 달력 히스토리 페이지 동작 - [ ] API 키 환경변수 관리 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/qt/analysis/route.ts - 기능 ID: Q22, R7

미배정 9 days
높음 dc0d9f90

하이라이트 CRUD API

## 목표 성경 절 하이라이트 저장/삭제/조회 API ## 상세 - POST /api/bible/highlights — 하이라이트 생성 (book_abbrev, chapter, verse, color, note) - DELETE /api/bible/highlights/:id — 하이라이트 삭제 - GET /api/bible/highlights?book=gn&chapter=1 — 특정 장의 하이라이트 조회 - JSON 응답 (Stimulus 컨트롤러에서 fetch 사용) - authenticate_user! 필수 ## 완료 기준 - [ ] API 컨트롤러 (Api::Bible::HighlightsController) - [ ] CRUD 엔드포인트 동작 - [ ] 인증 필수 확인 - [ ] 컨트롤러 테스트 - [ ] 기존 테스트 전체 통과

미배정 8 days
높음 de40ed9f

카카오 알림톡 백엔드 (서비스 + NotificationService 통합 + 테스트)

## 목표 카카오 알림톡 API 클라이언트 서비스 + NotificationService 통합 + 테스트 ## 구현 내용 ### 1. KakaoAlimtalkSender 서비스 (`app/services/kakao_alimtalk_sender.rb`) - Net::HTTP 기반 카카오 알림톡 API 클라이언트 - 초기화: `KakaoAlimtalkSender.new(phone:, template_code:, variables:)` - `call` 메서드: API 호출 → `{ success: true/false, message_id:, error: }` 반환 - 환경변수: `KAKAO_ALIMTALK_API_KEY`, `KAKAO_SENDER_KEY` - API 키가 없으면 graceful하게 skip ### 2. NotificationService 수정 (`app/services/notification_service.rb`) - send_all에 kakao 채널 추가 - send_kakao private 메서드 추가 ### 3. 테스트 - `test/services/kakao_alimtalk_sender_test.rb` (생성) - `test/services/notification_service_test.rb` (수정) ## 파일 목록 - `app/services/kakao_alimtalk_sender.rb` (생성) - `app/services/notification_service.rb` (수정) - `test/services/kakao_alimtalk_sender_test.rb` (생성) - `test/services/notification_service_test.rb` (수정) 부모 티켓: f5ebc8ae-a57c-4589-ae3a-dad0b64edd25

K
kakao-backend
8 days
보통 d5a83809

[P2] AI 설교 해석

## 개요 Gemini/OpenAI를 활용한 성경 구절 AI 해석 ## 범위 - ruby-openai gem으로 AI API 호출 - 설교 노트에서 성경 구절 선택 → AI 해석 요청 - 해석 결과를 sermon_notes.ai_interpretation에 저장 - Turbo Stream으로 비동기 결과 표시 - 프롬프트 엔지니어링 (한국어 성경 해석에 최적화) ## 완료 기준 - [ ] AI 해석 요청 → 결과 표시 동작 - [ ] 결과 저장 동작 - [ ] 로딩 상태 표시 - [ ] 에러 처리 (API 실패, 할당량 초과) - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/sermon/ai-interpretation/route.ts - 기능 ID: S6

미배정 9 days
높음 e22eab14

하이라이트 Stimulus 컨트롤러

## 목표 bible_passage_controller.js 확장 — 절 선택 시 하이라이트 색상 팝오버 + 저장된 하이라이트 렌더링 ## 상세 - 기존 bible_passage_controller.js에 하이라이트 기능 통합 - 절 탭/롱프레스 시 색상 선택 팝오버 표시 (yellow, green, blue, pink, purple) - 색상 선택 시 API 호출 (POST /api/bible/highlights) - 페이지 로드 시 기존 하이라이트 조회 + 렌더링 (배경색 적용) - 하이라이트된 절 재탭 시 삭제 옵션 ## 완료 기준 - [ ] 절 선택 → 색상 팝오버 표시 - [ ] 색상 선택 → API 저장 + UI 반영 - [ ] 페이지 로드 시 기존 하이라이트 표시 - [ ] 하이라이트 삭제 동작 - [ ] 기존 테스트 전체 통과

미배정 8 days
높음 1173c5e4
서브 티켓 코드 품질 일괄 개선 (CSRF + N+1 + IDOR)

QT 세션 N+1 쿼리 최적화

## 목표 qt/sessions_controller.rb의 N+1 쿼리 3건 해결 ## 수정 내용 ### 1. members 액션 (L84-101) - 문제: @participants.map에서 각 participant.user.user_meditations → N+1 - 해결: SQL 집계 쿼리로 변환 (COUNT 서브쿼리 또는 joins + group) ```ruby # 예시 접근법 meditations = UserMeditation.joins(:qt_content) .where(qt_contents: { qt_theme_id: theme.id }) .where(user_id: @participants.select(:user_id)) .group(:user_id) .select("user_id, COUNT(*) as total_count, COUNT(CASE WHEN personal_meditation IS NOT NULL AND personal_meditation != '' THEN 1 END) as completed_count") ``` ### 2. rankings 액션 (L103-153) - 문제: users.map에서 각 유저별 meditations, completed, shared, tongtok = N*4+1 - 해결: SQL 집계 쿼리 1개로 4개 통계를 한번에 조회 ```ruby # 예시: 한 번의 쿼리로 모든 통계 집계 stats = UserMeditation.joins(:qt_content) .where(qt_contents: { qt_theme_id: theme.id }) .where(user_id: participant_user_ids) .group(:user_id) .select("user_id, COUNT(*) as meditation_count, COUNT(CASE WHEN personal_meditation IS NOT NULL AND personal_meditation != '' THEN 1 END) as completed_count, COUNT(CASE WHEN is_personal_meditation_shared = 1 THEN 1 END) as shared_count, COUNT(CASE WHEN is_tongtok_completed = 1 THEN 1 END) as tongtok_count") ``` ### 3. _session_card.html.erb:5 - 문제: session.qt_participants.size → 이미 index에서 includes(:qt_participants) 있으므로 .size 사용 시 메모리에서 계산됨 - 확인: index 액션의 includes에 :qt_participants가 포함되어 있는지 확인. @my_sessions에는 없으므로 추가 필요: ```ruby @my_sessions = current_user.qt_sessions.includes(:qt_theme, :creator, :qt_participants).order(created_at: :desc) ``` ## 담당 파일 (이 파일들만 수정) - app/controllers/qt/sessions_controller.rb (members, rankings, index 액션) - test/controllers/qt/sessions_controller_test.rb (기존 테스트 유지 + 최적화 검증) ## 완료 기준 - N+1 쿼리 3건 모두 제거 - members/rankings 액션이 참여자 수에 관계없이 고정 횟수 쿼리 실행 - bin/rails test 실행 결과 0 failures, 0 errors

N
n1-dev
8 days
보통 73410efc
부모 티켓

[P2] 알림 시스템 (Push/Email/Kakao)

## 개요 Web Push, 이메일, 카카오톡 알림 시스템 ## 범위 - PushSubscription 모델: user_id, subscription(JSON), is_active, browser_info - Web Push 구독/해제 (web-push gem, VAPID) - 푸시 알림 발송 - Solid Queue로 CRON 알림 (시간별 대상자 조회 + 발송) - Action Mailer 이메일 알림 - Solapi 카카오톡 알림 (선택적) - 알림 설정 (방식, 시간) ## 완료 기준 - [ ] Web Push 구독/발송 동작 - [ ] Solid Queue CRON 예약 알림 동작 - [ ] 알림 설정 변경 동작 - [ ] 이메일 알림 발송 동작 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/push/*.ts, api/notifications/cron/route.ts - 기능 ID: N1-N4

3/3
팀리드
9 days
보통 7a8f17bb
부모 티켓

하이라이트 관리 페이지

## 목표 /bible/highlights — 내 하이라이트 목록, 성경별 필터, 노트 편집 ## 상세 - 전체 하이라이트 목록 (성경별 그룹핑) - 성경/색상별 필터 - 노트 인라인 편집 (Turbo Frame) - 하이라이트 삭제 - 각 하이라이트에서 해당 성경 본문으로 이동 링크 ## 완료 기준 - [ ] /bible/highlights 라우트 + 뷰 - [ ] 성경별 그룹핑 목록 - [ ] 필터 (성경/색상) - [ ] 노트 편집 + 삭제 - [ ] 기존 테스트 전체 통과

1/1
팀리드
8 days
보통 4e80e385
서브 티켓 코드 품질 일괄 개선 (CSRF + N+1 + IDOR)

QT 세션 IDOR 접근 제어 + select 참여자 검증

## 목표 비참여자의 비공개 세션 접근 차단 + select 액션 참여자 확인 ## 수정 내용 ### 1. before_action :verify_participant 추가 - 적용 대상: show, shared_meditations, members, rankings - 로직: - is_public이면 허용 - 아니면 @session.qt_participants.exists?(user: current_user) 확인 - 비참여자면 redirect_to qt_sessions_path, alert: "비공개 플랜입니다." ### 2. select 액션 참여자 검증 ```ruby def select unless @session.qt_participants.exists?(user: current_user) redirect_to qt_sessions_path, alert: "참여 중인 플랜만 선택할 수 있습니다." return end ensure_user_setting.update!(current_session_id: @session.id) redirect_to root_path, notice: "#{@session.title} 플랜으로 전환했습니다." end ``` ### 3. 테스트 추가 - 비참여자가 비공개 세션에 접근 시 리다이렉트 확인 - 참여자가 비공개 세션에 접근 시 정상 응답 확인 - 공개 세션은 비참여자도 접근 가능 확인 - select 액션: 비참여자 → 리다이렉트, 참여자 → 정상 작동 ## 담당 파일 (이 파일들만 수정) - app/controllers/qt/sessions_controller.rb (verify_participant, select 수정) - test/controllers/qt/sessions_controller_test.rb (IDOR 테스트 추가) ## 주의사항 - n1-dev 에이전트도 같은 파일(qt/sessions_controller.rb)을 수정합니다 - **이 에이전트는 private 메서드 영역과 select/show 등 액션의 접근 제어만 수정합니다** - members, rankings 액션의 쿼리 로직은 건드리지 마세요 (n1-dev 담당) - before_action 라인과 private 메서드, select 액션만 수정하세요 ## 완료 기준 - 비참여자 비공개 세션 접근 차단 (리다이렉트) - select 액션 참여자 검증 - 테스트 추가 + bin/rails test 0 failures, 0 errors

I
idor-dev
8 days
낮음 2dd7645c

[P2] 세션 통계/멤버 관리

## 개요 QT 세션 통계 및 멤버 상세 관리 ## 범위 - 세션 통계: 참여자수, 통독완료수, QT완료수 - 멤버 목록 조회 - 기도 통계: 응답율, 카테고리별 통계 - QT에서 기도제목 가져오기 (import_from_qt) - 기도제목 AI 분석 (선택적) ## 완료 기준 - [ ] 세션 통계 표시 - [ ] 멤버 목록 표시 - [ ] 기도 통계 표시 - [ ] QT → 기도제목 가져오기 동작 - [ ] 통합 테스트 통과 ## 참고 - 레거시: api/qt/sessions/stats, members - 기능 ID: Q13, Q17, P3, P5

미배정 9 days
높음 a9055c68
부모 티켓

Phase 0+1: 스키마 정리 + QT 기능 묶음

## 조율 티켓 (Coordination) ### Phase 0 1. 스키마 중복 정리 - users 중복 컬럼 3개 제거 ### Phase 1 - QT 기능 2. QT 세션 수정/편집 - edit/update 액션 3. QT 세션 스위처 UI - today 상단 드롭다운 4. QT 빈 상태 안내 개선 - 세션 없을 때 안내 5. QT 테마 브라우즈 페이지 - 테마 목록 6. QT 테마 상세 + 구독 - 플랜 시작 ## 에이전트 배정 - schema-dev: #1 - session-dev: #2, #3, #4 - theme-dev: #5, #6

3/3
팀리드
8 days
높음 ca4ecb1e
부모 티켓

QT 테마 브라우즈 + 구독 (조율)

QT 테마 브라우즈 페이지 + 테마 상세/구독 기능을 팀으로 구현하는 조율 티켓

2/2
팀리드
8 days
높음 3ce383bb
서브 티켓 Phase 0+1: 스키마 정리 + QT 기능 묶음

[P0-1] users 테이블 중복 컬럼 제거 마이그레이션

## 목표 users 테이블에서 user_settings와 중복되는 3개 컬럼을 제거하는 마이그레이션 생성 및 실행 ## 제거 대상 - `notification_enabled` (boolean, default: false) - `notification_time` (integer) - `current_session_id` (string) 이 3개 컬럼은 user_settings 테이블에도 동일하게 존재하며, 코드에서는 모두 `user_setting.xxx`로 접근합니다. ## 작업 순서 1. `grep -r "users\.\(notification_enabled\|notification_time\|current_session_id\)" app/` 로 코드 참조 없음 재확인 2. `bin/rails generate migration RemoveDuplicateColumnsFromUsers` 실행 3. 마이그레이션 파일에 remove_column 3개 작성 4. `bin/rails db:migrate` 실행 5. `bin/rails test` 전체 테스트 실행 → 0 failures 확인 ## 주의사항 - SQLite remove_column은 테이블 재생성 → uuid 타입 소실 가능성 확인 - db/structure.sql 자동 갱신 확인 - 마이그레이션 후 User 모델에서 해당 컬럼 참조가 없는지 확인 - seeds.rb에서 해당 컬럼 사용 여부 확인 ## 완료 기준 - 마이그레이션 성공 - 전체 테스트 통과 (430+ tests, 0 failures) - structure.sql에서 3개 컬럼 제거 확인

S
schema-dev
8 days
낮음 97c901f1
부모 티켓

[P2] 세션 통계/멤버 관리

## 개요 QT 세션 통계 및 멤버 상세 관리 ## 범위 - 세션 통계: 참여자수, 통독완료수, QT완료수 - 멤버 목록 조회 - 기도 통계: 응답율, 카테고리별 통계 - QT에서 기도제목 가져오기 (import_from_qt) ## 완료 기준 - [ ] 세션 통계 표시 - [ ] 멤버 목록 표시 - [ ] 기도 통계 표시 - [ ] QT → 기도제목 가져오기 동작 ## 참고 - 기능 ID: Q13, Q17, P3, P5

2/2
팀리드
9 days
높음 82f97aa8
서브 티켓 Phase 0+1: 스키마 정리 + QT 기능 묶음

[P1-1] QT 세션 edit/update + 세션 스위처 + 빈 상태

## 목표 3가지 QT 세션 관련 기능을 구현합니다. ### 작업 1: QT 세션 수정/편집 (edit/update) 현재 `config/routes.rb`에서 sessions는 `only: [:index, :new, :create, :show]`만 있습니다. 1. **라우트 수정**: `config/routes.rb`에서 sessions에 `:edit, :update` 추가 2. **컨트롤러**: `app/controllers/qt/sessions_controller.rb`에 edit, update 액션 추가 - `edit`: set_session으로 세션 로드, @themes = QtTheme.active - `update`: session_params로 업데이트, creator만 수정 가능 - 권한 체크: `@session.creator_id != current_user.id` → redirect with alert 3. **뷰**: `app/views/qt/sessions/new.html.erb`의 폼을 `_form.html.erb` 파셜로 추출 - new.html.erb와 edit.html.erb 모두 _form 사용 - edit.html.erb 제목: "플랜 수정" 4. **show 페이지**: creator에게만 "수정" 버튼 표시 (shared/_button 파셜 사용) 5. **테스트**: `test/controllers/qt/sessions_controller_test.rb`에 추가 - test_edit_session, test_update_session_valid - test_update_non_creator_redirects (권한 체크) - test_update_invalid_params ### 작업 2: QT 세션 스위처 UI QT today 페이지(qt_controller.rb의 today 액션) 상단에 현재 참여 중인 세션 목록 드롭다운 추가. 1. 먼저 `app/controllers/qt_controller.rb`를 읽어서 today 액션과 뷰 위치 확인 2. today 뷰 상단에 세션 스위처 드롭다운 추가: - `current_user.qt_sessions.active.includes(:qt_theme)` 목록 - 현재 세션 표시 (user_setting.current_session_id) - 세션 선택 시 `POST /qt/sessions/:id/select`로 전환 후 리다이렉트 3. Stimulus 컨트롤러로 드롭다운 토글 (기존 dropdown controller가 있으면 재사용) ### 작업 3: QT 빈 상태 안내 참여 세션이 0개인 사용자가 /qt/today 접속 시 안내 화면. 1. today 뷰에서 `@session.nil?` 체크 2. shared/_empty_state 파셜 활용: ```erb <%= render "shared/empty_state", title: "참여 중인 플랜이 없습니다", description: "테마를 둘러보고 묵상 플랜을 시작하세요", action_text: "테마 둘러보기", action_path: qt_themes_path %> ``` 3. 참고: qt_themes_path는 theme-dev가 생성할 예정 → 임시로 qt_sessions_path 사용 가능 ## 파일 범위 (이 에이전트만 수정) - app/controllers/qt/sessions_controller.rb - app/controllers/qt_controller.rb (today 뷰에 스위처/빈상태 추가) - app/views/qt/sessions/ (new, edit, show, _form, _session_card) - app/views/qt/ (today 관련 뷰) - config/routes.rb (sessions only 수정) - test/controllers/qt/sessions_controller_test.rb ## 수정 금지 파일 - app/models/ (theme-dev 영역) - app/controllers/qt/themes_controller.rb (theme-dev 영역) - db/migrate/ (schema-dev 영역) ## 완료 기준 - edit/update 동작 + 권한 체크 - 세션 스위처 드롭다운 동작 - 빈 상태 안내 표시 - 전체 테스트 통과

S
session-dev
8 days
보통 4537cfbf
부모 티켓

[P3] 관리자 대시보드

## 개요 관리자 전용 대시보드 및 권한 체크 ## 범위 - 관리자 레이아웃 (admin namespace) - role='admin' 권한 체크 미들웨어 - 관리자 대시보드 메인 페이지 - 사용자 통계 요약 ## 완료 기준 - [ ] /admin 접근 시 권한 체크 - [ ] 관리자 대시보드 표시 - [ ] 비관리자 접근 차단 ## 참고 - 기능 ID: AD1, A6

2/2
팀리드
9 days
높음 b4cd7dc3
서브 티켓 Phase 0+1: 스키마 정리 + QT 기능 묶음

[P1-2] QT 테마 브라우즈 + 상세 + 구독(플랜 시작)

## 목표 일반 사용자용 QT 테마 브라우즈 페이지와 테마 상세/구독 기능 구현 ### 작업 1: QT 테마 브라우즈 페이지 (/qt/themes) 1. **라우트**: `config/routes.rb`의 qt 네임스페이스에 추가 ```ruby namespace :qt do resources :themes, only: [:index, :show] do member do post :subscribe end end # 기존 sessions... end ``` 2. **컨트롤러**: `app/controllers/qt/themes_controller.rb` 생성 ```ruby class Qt::ThemesController < ApplicationController def index @themes = QtTheme.active.includes(:qt_contents).order(:created_at) end def show @theme = QtTheme.find(params[:id]) @contents = @theme.qt_contents.order(:day_number) @existing_session = current_user.qt_sessions.active.find_by(qt_theme: @theme) end def subscribe @theme = QtTheme.find(params[:id]) # 중복 체크 if current_user.qt_sessions.active.exists?(qt_theme: @theme) redirect_to qt_theme_path(@theme), alert: "이미 이 테마로 진행 중인 플랜이 있습니다." return end session = QtSession.create!( qt_theme: @theme, creator: current_user, title: @theme.title, start_date: Date.current, end_date: Date.current + (@theme.total_day - 1).days, total_days: @theme.total_day, status: :active ) session.qt_participants.create!(user: current_user, role: :creator) ensure_user_setting current_user.user_setting.update!(current_session_id: session.id) redirect_to qt_session_path(session), notice: "#{@theme.title} 플랜을 시작했습니다!" end private def ensure_user_setting current_user.create_user_setting! unless current_user.user_setting end end ``` 3. **뷰: index.html.erb** - 테마 카드 그리드 - shared/_card 파셜이 있으면 활용, 없으면 Tailwind 카드 직접 구현 - 각 카드: 제목, 설명(truncate 50), 총 일수, 성경 범위(bible_books) - 카드 클릭 → show 페이지 - 디자인 시스템의 시멘틱 토큰 사용 (bg-surface-default, text-text-primary 등) 4. **뷰: show.html.erb** - 테마 상세 - 테마 정보: 제목, 설명, 총 일수, 성경 범위 - 콘텐츠 목록: day_number, theme_title, bible_passage (접힌 아코디언 형태) - "이 테마로 플랜 시작" 버튼 (shared/_button 파셜, variant: :primary) - 이미 진행 중이면 "이미 진행 중인 플랜이 있습니다" 안내 + 해당 세션 링크 5. **테스트**: `test/controllers/qt/themes_controller_test.rb` 생성 - test_index_shows_active_themes - test_show_displays_theme_details - test_subscribe_creates_session_and_participant - test_subscribe_prevents_duplicate - test_unauthenticated_redirects ## 파일 범위 (이 에이전트만 수정) - app/controllers/qt/themes_controller.rb (신규 생성) - app/views/qt/themes/ (index.html.erb, show.html.erb 신규 생성) - config/routes.rb (qt 네임스페이스에 themes 추가만) - test/controllers/qt/themes_controller_test.rb (신규 생성) ## 수정 금지 파일 - app/controllers/qt/sessions_controller.rb (session-dev 영역) - app/controllers/qt_controller.rb (session-dev 영역) - db/migrate/ (schema-dev 영역) - app/models/ (수정 필요 없음, 기존 모델 사용) ## 참고 패턴 - sessions_controller.rb의 create 액션 참고 (세션 생성 로직) - shared/_button 파셜: `render "shared/button", text: "...", variant: :primary, tag: :a, href: ...` - shared/_empty_state 파셜: 빈 데이터 표시 - 디자인 시스템: bg-surface-default, text-text-primary, border-border-default 등 시멘틱 토큰 ## 완료 기준 - /qt/themes에서 활성 테마 카드 목록 표시 - /qt/themes/:id에서 테마 상세 + 콘텐츠 목록 - "플랜 시작" 시 세션+참여자 자동 생성 - 중복 구독 방지 - 전체 테스트 통과

T
theme-dev
8 days
보통 76139f0f
부모 티켓

[P3] QT 테마/콘텐츠 관리 CRUD

## 개요 관리자용 QT 테마 및 콘텐츠 CRUD ## 범위 - 테마 목록/생성/편집/삭제 - 테마별 콘텐츠 목록/생성/편집 - 테마 활성화/비활성화 - 테마 발행 상태 관리 (draft → generating → completed → published) - 콘텐츠 일괄 편집 ## 완료 기준 - [ ] 테마 CRUD 동작 - [ ] 콘텐츠 CRUD 동작 - [ ] 발행 상태 변경 동작 ## 참고 - 기능 ID: AD2-AD6

2/2
팀리드
9 days
높음 39414c99
서브 티켓 QT 테마 브라우즈 + 구독 (조율)

QT 테마 라우트 + 컨트롤러 + 테스트

## 목표 Qt::ThemesController 생성 (index, show, subscribe) + 라우트 + 컨트롤러 테스트 ## 1. 라우트 (config/routes.rb) qt namespace 안에 추가: ```ruby resources :themes, only: [:index, :show] do member do post :subscribe end end ``` ## 2. 컨트롤러 (app/controllers/qt/themes_controller.rb) - `index`: QtTheme.active 목록 (is_default 먼저, 나머지 최신순) - `show`: @theme = QtTheme.active.find(params[:id]), @contents = @theme.qt_contents.order(:day_number), @existing_session 중복 체크 - `subscribe`: - QtSession 생성 (title: theme.title, qt_theme: theme, creator: current_user, start_date: Date.current, end_date: Date.current + theme.total_day - 1, total_days: theme.total_day) - QtParticipant 생성 (role: :creator, user: current_user) - 이미 같은 테마로 active 세션 있으면 redirect with alert - 성공 시 redirect_to qt_session_path(session), notice: "플랜이 시작되었습니다!" ## 3. 테스트 (test/controllers/qt/themes_controller_test.rb) - GET /qt/themes - 로그인 필수, active 테마만 표시, inactive 미표시 - GET /qt/themes/:id - 테마 상세 표시, inactive 테마 404 - POST /qt/themes/:id/subscribe - 세션+참여자 생성, 중복 구독 방지 - 비로그인 시 redirect ## 참고 파일 - app/controllers/qt/sessions_controller.rb (패턴 참고) - app/models/qt_theme.rb (scope :active) - app/models/qt_session.rb (generate_invite_code) - test/fixtures/qt_themes.yml (default_theme, ai_theme, inactive_theme) ## 완료 기준 - 라우트 동작, 컨트롤러 3개 액션, 테스트 전체 통과

T
theme-backend
8 days
낮음 ad0a61e4
부모 티켓

[P3] AI 콘텐츠 자동 생성

## 개요 Gemini API로 QT 테마/콘텐츠 자동 생성 + 음성 묵상 정리 ## 범위 - AI 기반 QT 테마 자동 생성 (성경 범위 지정 → 콘텐츠 생성) - 생성 상태 추적 (Solid Queue 백그라운드 작업) - 음성인식 텍스트 → 묵상 정리 (organize_meditation) - 프롬프트 템플릿 관리 ## 완료 기준 - [ ] AI 테마 생성 요청 → 콘텐츠 자동 생성 - [ ] 백그라운드 생성 상태 추적 - [ ] 음성 텍스트 정리 동작 ## 참고 - 기능 ID: AD7, Q23

2/2
팀리드
9 days
높음 0875583c
서브 티켓 QT 테마 브라우즈 + 구독 (조율)

QT 테마 뷰 (index + show + 카드 파셜)

## 목표 QT 테마 브라우즈 + 상세 페이지 뷰 구현 ## 1. _theme_card 파셜 (app/views/qt/themes/_theme_card.html.erb) - 디자인 시스템 shared/card 파셜 활용 - 표시: 제목, 설명(truncate), 총 일수, 성경 범위(bible_books), AI생성 뱃지 - 링크: qt_theme_path(theme)로 상세 이동 ## 2. index 뷰 (app/views/qt/themes/index.html.erb) - 페이지 제목: "QT 테마" - 기본 테마 섹션 (is_default: true) + 일반 테마 섹션 - 반응형 그리드: grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 - 테마 없을 때 shared/empty_state 파셜 사용 ## 3. show 뷰 (app/views/qt/themes/show.html.erb) - 테마 정보: 제목, 설명, 총 일수, 성경 범위 - 콘텐츠 미리보기: @contents에서 처음 3일분만 표시 (day_number, theme_title, bible_passage) - "이 테마로 플랜 시작" 버튼 → POST subscribe_qt_theme_path(@theme), method: :post - @existing_session이 있으면 "이미 진행 중인 플랜이 있습니다" 안내 + 해당 세션 링크 - 뒤로가기: qt_themes_path ## 디자인 패턴 참고 - app/views/qt/sessions/index.html.erb (카드 레이아웃 패턴) - app/views/shared/_card.html.erb, _button.html.erb, _badge.html.erb, _empty_state.html.erb - 색상/텍스트: text-heading, text-body, text-small, text-caption - 다크모드: bg-surface-default, text-text-primary 등 디자인 토큰 사용 ## 완료 기준 - 3개 뷰 파일 생성, 디자인 시스템 일관성, 반응형 동작

T
theme-views
8 days
낮음 bb6b65e4

[P3] PWA + 모바일 지원

## 개요 PWA 지원 및 Capacitor 모바일 앱 연동 ## 범위 - 서비스워커 설정 - Web App Manifest (아이콘, 테마 색상) - 오프라인 지원 (기본) - Capacitor 설정 (Android/iOS WebView) - 네이티브 기능 연동 (카메라, 알림 등) ## 완료 기준 - [ ] PWA 설치 가능 (A2HS) - [ ] 서비스워커 등록/캐싱 - [ ] Capacitor 빌드 성공 - [ ] Android APK 생성 ## 참고 - 기능 ID: E5, E6

미배정 9 days
높음 27518a53
서브 티켓 QT today 페이지 통독/QT 탭 분리

탭 UI 구현 (Stimulus + 뷰 재구성)

## 작업 내용 1. `app/javascript/controllers/tab_controller.js` Stimulus 컨트롤러 신규 생성 - static targets = ["tab", "panel"] - static values = { defaultTab: { type: String, default: "tongtok" } } - switch(event) → 탭 active 스타일 토글 + 패널 show/hide - URL hash로 탭 상태 유지 (#tongtok, #qt) 2. `app/views/qt/today.html.erb` 전면 재구성 - 상단 카드 (테마 정보 + 진행률) 유지 - 그 아래에 탭 네비게이션 추가: [통독] [QT] 두 개 탭 - **통독 탭 패널**: - bible_passage 제목 카드 (성경 아이콘 + bible_passage 텍스트) - 유튜브 플레이어 (bible_passage) - 성경 본문 (bible-passage Stimulus 컨트롤러, passage=bible_passage) - 통독 완료 체크 영역: turbo-frame id="reading_check" (reading-dev가 구현할 파셜 자리) - **QT 탭 패널**: - reading_passage 제목 카드 (reading_passage 텍스트, 없으면 bible_passage) - 유튜브 플레이어 (reading_passage, 없으면 생략) - 성경 본문 (bible-passage Stimulus 컨트롤러, passage=reading_passage 또는 bible_passage) - 묵상 내용 카드 (content) - 묵상 질문 카드 (questions) - 묵상 기록 폼 (turbo-frame meditation_form) - 일차 네비게이션 (이전/다음) 탭 외부 하단 유지 3. 탭 디자인: Tailwind CSS - 탭 바: flex, border-b, gap-0 - 활성 탭: text-brand-primary, border-b-2 border-brand-primary, font-semibold - 비활성 탭: text-text-muted, hover:text-text-secondary - 패널 전환: hidden 클래스 토글 ## 기존 코드 참고 - 현재 today.html.erb: 통독과 QT가 혼재 (bible_passage가 통독, reading_passage가 QT) - 디자인 시스템: shared/card, shared/progress, shared/button, shared/alert, shared/youtube_player 파셜 사용 ## 완료 기준 - tab_controller.js 생성 - today.html.erb 탭 구조로 재구성 - 기존 테스트 전체 통과 (rails test) - reading_check turbo-frame 자리는 빈 프레임으로 둘 것 (reading-dev가 채움)

T
tab-dev
8 days
긴급 367f6db9
서브 티켓 [P0-SETUP] Rails 프로젝트 초기화 + Docker 환경

Rails 8.1.2 프로젝트 생성 + Gemfile + UUID PK + Tailwind v4 + CI

## 작업 내용 기존 디렉토리(`/home/daniel/dev/logbile2.0.0`)에 Rails 8.1.2 프로젝트를 생성하고 기본 설정을 완료합니다. ### 1단계: Rails new ```bash cd /home/daniel/dev/logbile2.0.0 rails new . --database=sqlite3 --asset-pipeline=propshaft --javascript=importmap --force --skip-jbuilder ``` - `--force`: 기존 파일(README.md 등) 덮어쓰기 허용 - 단, CLAUDE.md, docs/, memory/, templates/는 보존되어야 함 (rails new가 건드리지 않는 경로) ### 2단계: Gemfile 추가 gems ```ruby # 인증 gem "devise" gem "omniauth" gem "omniauth-google-oauth2" gem "omniauth-kakao", github: "nicholasngai/omniauth-kakao" gem "omniauth-rails_csrf_protection" # CSS gem "tailwindcss-rails" # AI gem "ruby-openai" # 기타 gem "solid_queue" gem "solid_cable" gem "solid_cache" ``` `bundle install` 실행 ### 3단계: UUID PK 기본 설정 `config/initializers/generators.rb`: ```ruby Rails.application.config.generators do |g| g.orm :active_record, primary_key_type: :uuid end ``` `app/models/application_record.rb`: ```ruby class ApplicationRecord < ActiveRecord::Base primary_abstract_class self.implicit_order_column = "created_at" end ``` ### 4단계: Tailwind CSS v4 설치 ```bash rails tailwindcss:install ``` - templates/tailwind.config.js 참고하여 설정 적용 ### 5단계: CI 스크립트 - templates/bin/ci → bin/ci 복사 - config/ci.rb → config/ci.rb 복사 (이미 존재) - 실행 권한 부여: `chmod +x bin/ci` ### 6단계: 기본 파셜 복사 - templates/app/views/shared/*.html.erb → app/views/shared/ 복사 ### 7단계: 검증 - `rails db:prepare` 실행 - `rails s` 동작 확인 (포트 지정) - 테스트 실행: `rails test` ## 완료 기준 - rails s로 앱 기동 성공 - UUID PK 제너레이터 설정 완료 - Tailwind CSS v4 동작 - bundle install 성공 - bin/ci 스크립트 존재

R
rails-setup
9 days
긴급 646cd001
부모 티켓

[P0-COORD] 핵심 기반 구축 (User + QT 모델 + 인증 + 디자인 시스템)

## 목표 P0 핵심 기반 4개 티켓을 병렬 팀으로 처리 ## 포함 티켓 1. [P0] User 모델 + DB 마이그레이션 2. [P0] QT 핵심 모델 (Theme/Content/Session/Participant) 3. [P0] OmniAuth 소셜 로그인 (Google/Kakao) 4. [P0-SETUP] 디자인 시스템 (shared partials) ## 의존성 - User 모델 → QT 모델 (FK 참조) - User 모델 → OmniAuth (Devise 연동) - 디자인 시스템: 독립적 (병렬 가능) ## 팀 구성 - db-models: 전체 DB 모델 (User + QT) - auth-dev: 소셜 로그인 - ui-dev: 디자인 시스템

3/3
팀리드
9 days
긴급 a7717af1
서브 티켓 [P0-COORD] 핵심 기반 구축 (User + QT 모델 + 인증 + 디자인 시스템)

DB 모델 구축: User + UserSetting + QT 핵심 모델 4개

## 목표 Rails 8.1.2 프로젝트에 핵심 DB 모델 6개를 TDD로 구축합니다. ## 작업 범위 ### 1. Devise 설치 및 User 모델 - `rails generate devise:install` 실행 - User 모델 마이그레이션 (UUID PK): email, nickname, provider, provider_id, role(enum: user/admin), profile_image, phone, notification_enabled, notification_time, current_session_id - 인덱스: email(unique), [provider, provider_id](unique) - Devise 모듈: :database_authenticatable, :omniauthable (omniauth_providers: [:google_oauth2, :kakao]) ### 2. UserSetting 모델 - user_id(FK unique), timezone(default: Asia/Seoul), language(default: ko), preferred_difficulty(default: 3), auto_next_day(default: true), current_session_id - User has_one :user_setting ### 3. QtTheme 모델 - title, description, is_default, is_active, total_day, is_ai_generated, generation_status(enum: draft/generating/completed/published), user_id(FK nullable) - User has_many :qt_themes ### 4. QtContent 모델 - theme_id(FK), day_number, bible_passage, theme_title, content, questions(JSON array), difficulty(default: 3), week_number, reading_passage, estimated_minutes, tags, bible_chapter, bible_verse - QtTheme has_many :qt_contents ### 5. QtSession 모델 - title, theme_id(FK), creator_id(FK->users), start_date, end_date, total_days, is_public, invite_code(unique 8자), max_participants, status(enum: active/completed/cancelled) - QtTheme has_many :qt_sessions ### 6. QtParticipant 모델 - session_id(FK), user_id(FK), role(enum: creator/member), is_active, joined_at - unique index: [session_id, user_id] - QtSession has_many :qt_participants ## 기술 요구사항 - 모든 테이블 UUID PK (id: :uuid) - ApplicationRecord에 `self.implicit_order_column = "created_at"` 이미 설정됨 - enum은 Rails 8 방식 사용 - 모델 테스트 (validations, associations, scopes) 작성 - fixtures 작성 ## 참고 - docs/migration/index.md §2 데이터 모델 참조

D
db-models
9 days
긴급 440dd6b8
부모 티켓

[P0-COORD] QT 핵심 기능 (메인 페이지 + 묵상 + 세션 + 시드)

## 목표 QT 핵심 사용 플로우 4개 티켓을 병렬 팀으로 구현 ## 포함 티켓 1. [P0] QT 메인 페이지 (오늘의 QT) + 묵상 기록 저장/조회 2. [P0] QT 플랜(세션) 관리 3. [P0] QT 시드 데이터 (레거시 import) ## 의존성 - 모든 작업은 이전 배치에서 생성된 QT 모델에 의존 (완료됨) - QT 메인 페이지와 묵상 기록은 밀접하게 연관 (같은 페이지) - 세션 관리와 시드 데이터는 독립적 ## 팀 구성 (3명) - qt-core: QT 메인 페이지 + 묵상 기록 (UserMeditation 모델 포함) - session-dev: QT 세션 관리 - seed-dev: QT 시드 데이터

3/3
팀리드
9 days
긴급 cbdf9ccb
서브 티켓 [P0-COORD] QT 핵심 기능 (메인 페이지 + 묵상 + 세션 + 시드)

QT 메인 페이지 + 묵상 기록 CRUD

## 목표 QT 메인 페이지(오늘의 QT)와 묵상 기록 저장/조회 기능을 구현합니다. ## 작업 범위 ### 1. UserMeditation 모델 + 마이그레이션 ```ruby create_table :user_meditations, id: :uuid do |t| t.references :user, type: :uuid, null: false, foreign_key: true t.references :qt_content, type: :uuid, null: false, foreign_key: true t.date :meditation_date, null: false t.text :personal_meditation t.text :action_plan t.text :prayer_topic t.integer :mood_before # 1-5 t.integer :mood_after # 1-5 t.integer :completion_minutes t.boolean :is_tongtok_completed, default: false t.boolean :is_personal_meditation_shared, default: false t.boolean :is_action_plan_shared, default: false t.boolean :is_prayer_topic_shared, default: false t.json :highlights t.integer :bible_chapter t.integer :bible_verse t.timestamps end add_index :user_meditations, [:user_id, :qt_content_id], unique: true ``` - User has_many :user_meditations - QtContent has_many :user_meditations ### 2. QtController - `today` 액션: 현재 세션 기준 오늘의 QT 콘텐츠 조회 - current_user.current_session_id 또는 user_setting.current_session_id로 세션 찾기 - 세션의 start_date와 오늘 날짜 차이로 day_number 계산 - 해당 day_number의 qt_content 조회 - 기존 묵상 기록이 있으면 함께 로드 - `day` 액션: params[:day]로 특정 일차 QT 조회 ### 3. MeditationsController - `create`: 묵상 기록 생성 (Turbo Stream 응답) - `update`: 기존 묵상 수정 (Turbo Stream 응답) - 자동 저장: Stimulus debounce + Turbo Stream ### 4. QT 뷰 - app/views/qt/today.html.erb: QT 메인 페이지 - 성경 구절 표시 (bible_passage) - 테마 제목 (theme_title) - 본문 내용 (content) - 질문 5개 표시 (questions JSON array) - 일차 네비게이션 (이전/다음 버튼) - 묵상 기록 폼 (Turbo Frame) - 세션 미참여 시 안내 메시지 - app/views/qt/_meditation_form.html.erb: 묵상 폼 파셜 - 개인 묵상 textarea - 실천 계획 textarea - 기도 제목 textarea - 기분 선택 (mood_before/after) 1-5 이모지/스타 - 통독 완료 체크박스 - 공유 설정 토글 - 저장 버튼 - app/views/qt/_no_session.html.erb: 세션 미참여 안내 ### 5. 라우팅 config/routes.rb에 추가 (authenticated 블록 내): ```ruby get "qt", to: "qt#today" get "qt/day", to: "qt#day" resources :meditations, only: [:create, :update] ``` ### 6. 테스트 - test/models/user_meditation_test.rb - test/controllers/qt_controller_test.rb - test/controllers/meditations_controller_test.rb ## 스타일 - 디자인 시스템 파셜 활용 (shared/card, shared/button, shared/input, shared/tabs 등) - Tailwind CSS, 모바일 우선, 다크모드 지원 ## 참고 - docs/migration/index.md §2-2 user_meditations 스키마 - docs/migration/index.md §3-2 QT 기능 목록 Q1-Q3

Q
qt-core
9 days
높음 d1015e2b
부모 티켓

[P1-COORD] 핵심 기능 (기도제목 + 설교 노트 + 프로필/설정)

## 목표 P1 핵심 사용 기능 3개 티켓을 병렬 팀으로 구현 ## 포함 티켓 1. [P1] 기도제목 CRUD + 기도 체크 2. [P1] 설교 노트 CRUD 3. [P1] 프로필/설정 + 정적 페이지 ## 의존성 - QT 핵심 기능 완료 (P0 완료) - 기존 모델: User, UserSetting, QtSession, QtParticipant, QtTheme, QtContent, UserMeditation - 기존 UI: shared 파셜 19개 + Stimulus 컨트롤러 6개 ## 팀 구성 (3명) - prayer-dev: 기도제목 CRUD + 기도 체크 - sermon-dev: 설교 노트 CRUD - profile-dev: 프로필/설정 + 정적 페이지

3/3
팀리드
9 days
높음 80d5a4d4
서브 티켓 [P1-COORD] 핵심 기능 (기도제목 + 설교 노트 + 프로필/설정)

기도제목 CRUD + 기도 체크

## 목표 PrayerRequest, PrayerCheckLog 모델 생성 + 기도제목 CRUD + 일일 기도 체크 기능 ## 모델 ### PrayerRequest - user_id: UUID FK NOT NULL - content: text NOT NULL - target: text NULL (기도 대상) - category: integer enum (daily:0, weekly:1) DEFAULT daily - day_of_week: integer NULL (0-6, weekly일 때) - response_type: integer enum (keep_praying:0, waiting:1, yes:2, no:3) DEFAULT keep_praying - visibility: integer enum (private:0, partners:1, qt_plan:2, partners_qt_plan:3) DEFAULT private - is_active: boolean DEFAULT true - sort_order: integer DEFAULT 0 ### PrayerCheckLog - user_id: UUID FK NOT NULL - prayer_request_id: UUID FK NOT NULL - check_date: date NOT NULL - UNIQUE(user_id, prayer_request_id, check_date) ## 컨트롤러 PrayersController: index, new, create, edit, update, destroy, check(toggle), reorder - 카테고리 필터 (daily/weekly) - 응답 상태 변경 - 오늘 기도 체크 토글 (Turbo Stream) ## 뷰 - app/views/prayers/ (index, new, edit, _prayer_card, _form) - Turbo Frame/Stream 활용 ## 라우트 resources :prayers do member do post :check patch :reorder end end ## 테스트 - 모델 테스트 (PrayerRequest, PrayerCheckLog) - 컨트롤러 테스트 (CRUD + check toggle)

P
prayer-dev
9 days
높음 d5f99127
부모 티켓

[P1-COORD] 마무리 (성경 통독 + 묵상 통계)

## 목표 P1 마지막 2개 티켓을 병렬 팀으로 구현하여 MVP 핵심 기능 완성 ## 포함 티켓 1. [P1] 성경 통독 현황 - BibleReadingLog 모델, 66권 시각화, 진행률 2. [P1] 묵상 통계/히스토리 - 통계 대시보드, 기록 목록, 연속일수 ## 팀 구성 (2명) - tongtok-dev: 성경 통독 현황 (모델 + 컨트롤러 + 뷰) - stats-dev: 묵상 통계/히스토리 + routes.rb 관리

2/2
팀리드
9 days
높음 6625f49d
서브 티켓 [P1-COORD] 마무리 (성경 통독 + 묵상 통계)

성경 통독 현황 (BibleReadingLog + 66권 시각화)

## 목표 BibleReadingLog 모델 생성 + 성경 66권 통독 현황 시각화 페이지 구현 ## 모델 ### BibleReadingLog - user_id: UUID FK NOT NULL - book_name: string NOT NULL (한국어 책명: "창세기", "출애굽기" 등) - chapter: integer NOT NULL - read_date: date NOT NULL - INDEX: (user_id, book_name, chapter, read_date) - 같은 장 같은 날짜 중복 허용 (여러 번 읽기 가능) ## 성경 데이터 유틸리티 ### lib/bible_data.rb (모듈) - BIBLE_BOOKS 상수: 구약 39권 + 신약 27권 데이터 - 각 책: { name: "창세기", abbrev: "창", chapters: 50, testament: :old } - 총 1189장 - OLD_TESTAMENT, NEW_TESTAMENT 상수 - TOTAL_CHAPTERS = 1189 - 책명으로 장수 조회 메서드 ## 컨트롤러 ### TongtokController - index: 통독 현황 페이지 - params[:year] (기본 올해) - BibleReadingLog에서 해당 연도 데이터 집계 - { "창세기" => { 1 => 2, 2 => 1 }, ... } 형태로 책별/장별 읽기 횟수 - 전체 진행률 계산 (읽은 고유 장 수 / 1189 * 100) ### BibleReadingsController - create: 통독 기록 저장 - params: { bible_reading: { book_name:, chapter:, read_date: } } - 또는 배치: { chapters: [{ book_name:, chapter: }] } (같은 날짜) - destroy: 통독 기록 삭제 ## 뷰 ### tongtok/index.html.erb - 제목: "성경 통독 현황" - 연도 선택 드롭다운 - 전체 진행률 표시 (N / 1189장, N%) - 구약 / 신약 탭 - 각 책을 카드로 표시: - 책이름 + 읽은 장/전체 장 - 장별 그리드 (읽은 횟수에 따라 색상 변화: 0=회색, 1=연초록, 2+=진초록) - 장 클릭 시 읽기 기록 생성/삭제 토글 (Turbo Stream) - data-testid="tongtok-book" (각 책 카드), data-testid="progress-bar" (진행률) - 기존 shared 파셜 활용 (card, badge, tabs, progress) ## 라우트 (routes.rb는 수정하지 마세요! stats-dev가 관리합니다) - 필요한 라우트를 팀리드에게 SendMessage로 전달하면 stats-dev가 추가합니다 - 예상 라우트: ```ruby get "tongtok", to: "tongtok#index" resources :bible_readings, only: [:create, :destroy] ``` ## 테스트 - 모델 테스트: BibleReadingLog validations, associations - 컨트롤러 테스트: TongtokController (index, 진행률), BibleReadingsController (create, destroy) - lib/bible_data 테스트 (optional) ## 파일 담당 (다른 에이전트 파일 수정 금지) - 수정 가능: app/models/bible_reading_log.rb, app/models/user.rb (has_many 추가), db/migrate/*, app/controllers/tongtok_controller.rb, app/controllers/bible_readings_controller.rb, app/views/tongtok/*, lib/bible_data.rb, test/* - 수정 금지: config/routes.rb, app/controllers/stats_controller.rb, app/controllers/records_controller.rb

T
tongtok-dev
9 days
높음 9376259f
부모 티켓

[P2-COORD] 소셜 기능 (기도 동역자 + 공유 묵상/랭킹)

## 목표 P2 소셜 기능 2개 티켓을 병렬 팀으로 구현 ## 포함 티켓 1. [P2] 기도 동역자 시스템 - PrayerPartner 모델, 요청/수락/거절, 공유 기도제목 2. [P2] 공유 묵상 + 랭킹 - 세션 내 묵상 공유, 멤버 관리, 통독/묵상 랭킹 ## 팀 구성 (2명) - partner-dev: 기도 동역자 시스템 (모델 + 컨트롤러 + 뷰) - social-dev: 공유 묵상/랭킹 + routes.rb 관리

2/2
팀리드
9 days
높음 95ca71a8
서브 티켓 [P2-COORD] 소셜 기능 (기도 동역자 + 공유 묵상/랭킹)

기도 동역자 시스템 (PrayerPartner + 검색/요청/수락)

## 목표 PrayerPartner 모델을 생성하고 동역자 검색, 요청 보내기, 수락/거절, 동역자 기도제목 공유 기능 구현 ## 모델 ### PrayerPartnership (중간 테이블) - requester_id: UUID FK → users (요청 보낸 사람) - receiver_id: UUID FK → users (요청 받은 사람) - status: integer enum (pending: 0, accepted: 1, rejected: 2) - INDEX: (requester_id, receiver_id) UNIQUE - 자기 자신 요청 방지: validate requester != receiver ## 컨트롤러 ### PrayerPartnersController - index: 내 동역자 목록 (accepted) + 받은 요청 (pending) - search: 동역자 검색 (params[:q] → email/nickname LIKE) - create: 동역자 요청 보내기 (requester=current_user, receiver=params[:user_id]) - accept: 요청 수락 (status → accepted) - reject: 요청 거절 (status → rejected) - destroy: 동역자 관계 삭제 - partner_prayers: 특정 동역자의 공개 기도제목 조회 ## 뷰 - index: 동역자 목록 + 받은 요청 + 검색 - _partner_card: 동역자 카드 (아바타, 닉네임, 상태) - _request_card: 요청 카드 (수락/거절 버튼) - partner_prayers: 동역자 기도제목 목록 ## 파일 담당 - 수정 가능: app/models/prayer_partnership.rb, app/models/user.rb (has_many 추가), db/migrate/*, app/controllers/prayer_partners_controller.rb, app/views/prayer_partners/*, test/* - 수정 금지: config/routes.rb (social-dev가 관리) ## 테스트 - 모델: PrayerPartnership validations, self-request 방지 - 컨트롤러: index, search, create, accept, reject, destroy, partner_prayers

P
partner-dev
9 days
높음 63f19cd1
부모 티켓

[P2] AI 기능 - 묵상 분석 + 설교 해석 (coordination)

## 목표 P2 AI 기능 2개를 에이전트 팀으로 병렬 구현 ## 서브 티켓 1. AI 묵상 분석 (월별 리포트) - ai-meditation-dev 2. AI 설교 해석 - ai-sermon-dev ## 관련 기존 티켓 - a61b6d56: [P2] AI 묵상 분석 (월별 리포트) - d5a83809: [P2] AI 설교 해석

2/2
팀리드
9 days
높음 2baf3ad7
서브 티켓 [P2] AI 기능 - 묵상 분석 + 설교 해석 (coordination)

AI 묵상 분석 - MonthlyAnalysisReport + 서비스 + 컨트롤러 + 뷰

## 목표 AI를 활용한 월별 묵상 패턴 분석 및 리포트 생성 ## 구현 항목 ### 1. 마이그레이션 - MonthlyAnalysisReport: user_id(uuid FK), year(integer), month(integer), analysis_report(json), data_summary(json), status(string, default: "pending") - unique index: [user_id, year, month] - 타임스탬프: 20260302150000 ### 2. 모델 - MonthlyAnalysisReport - belongs_to :user - validates year, month, uniqueness scope - enum status: pending/analyzing/completed/failed - scope :recent, :for_month ### 3. User 모델 - has_many :monthly_analysis_reports, dependent: :destroy 추가 ### 4. 서비스 객체 - app/services/ai_meditation_analyzer.rb - initialize(user, year, month) - call: 월별 묵상 데이터 수집 → 프롬프트 구성 → OpenAI API 호출 → 결과 파싱/저장 - 프롬프트: 한국어, 묵상 패턴 분석, 성장 포인트, 추천사항 - API 키: ENV["OPENAI_API_KEY"] 또는 ENV["GEMINI_API_KEY"] - OpenAI client: OpenAI::Client.new (ruby-openai gem) - 에러 처리: API 실패 시 status를 failed로 변경 ### 5. 컨트롤러 - MeditationReportsController - index: 리포트 목록 (최근순) - show: 리포트 상세 - create: 월별 리포트 생성 요청 (year, month 파라미터) ### 6. 뷰 - meditation_reports/index.html.erb: 리포트 카드 목록 + 생성 버튼 - meditation_reports/show.html.erb: 분석 결과 표시 (카드 레이아웃) - 기존 shared 파셜 활용: _card, _badge, _separator, _button 등 ### 7. 라우트 - config/routes.rb에 추가: ```ruby resources :meditation_reports, only: [:index, :show, :create] ``` - 기존 라우트 구조 유지, `# AI Reports` 주석과 함께 stats/records 근처에 배치 ### 8. 테스트 - test/models/monthly_analysis_report_test.rb - test/controllers/meditation_reports_controller_test.rb - test/fixtures/monthly_analysis_reports.yml (과거 날짜 사용!) - API 호출은 stub으로 처리 ## 주의사항 - UUID PK 사용 (ApplicationRecord의 set_uuid 자동 처리) - fixture 날짜는 과거 고정 날짜 사용 (Date.current 충돌 방지) - shared 파셜의 strict locals 준수 - ERB 멀티라인 주석 사용 금지 (각 줄 단일 라인 주석) - 전체 테스트 통과 확인: bin/rails test

A
ai-meditation-dev
9 days
높음 1cc93ea1
서브 티켓 [P3] 관리자 대시보드

Admin 백엔드 - BaseController + DashboardController + 라우트 + 테스트

## 목표 Admin namespace 백엔드 인프라 구축 (컨트롤러, 라우트, 테스트) ## 구현 항목 ### 1. Admin::BaseController (신규) - 파일: `app/controllers/admin/base_controller.rb` - ApplicationController 상속 - `before_action :authorize_admin!` - `authorize_admin!` 메서드: `current_user.admin?` 체크, 실패 시 root_path redirect + alert - `layout "admin"` 선언 ### 2. Admin::DashboardController (신규) - 파일: `app/controllers/admin/dashboard_controller.rb` - Admin::BaseController 상속 - `index` 액션에서 통계 수집: - @total_users = User.count - @total_meditations = UserMeditation.count - @total_sessions = QtSession.count - @total_prayers = PrayerRequest.count - @total_sermons = SermonNote.count - @total_readings = BibleReadingLog.count - @recent_users = User.order(created_at: :desc).limit(5) - @recent_meditations = UserMeditation.order(created_at: :desc).limit(5) ### 3. 라우트 수정 - 파일: `config/routes.rb` - 기존 라우트 유지, 맨 아래(authenticated 블록 밖)에 추가: ```ruby # Admin namespace :admin do root "dashboard#index" end ``` ### 4. 테스트 (신규) - 파일: `test/controllers/admin/dashboard_controller_test.rb` - 테스트 케이스: - admin 사용자로 /admin 접근 → 200 - 일반 사용자로 /admin 접근 → redirect - 미인증 사용자로 /admin 접근 → redirect - 통계 데이터가 올바르게 표시되는지 확인 - Fixture 사용: users(:admin), users(:daniel) - 인증: `sign_in users(:admin)` (Devise test helper) ## 주의사항 - UUID PK 사용 (ApplicationRecord의 set_uuid 자동 처리) - `parallelize(workers: 1)` 테스트 설정 확인 - 기존 routes.rb 구조 깨지지 않게 주의 - 기존 테스트 전체 통과 확인: `bin/rails test`

A
admin-backend
9 days
높음 23994de5
서브 티켓 QT today 페이지 통독/QT 탭 분리

통독 완료 체크 백엔드 + 파셜

## 작업 내용 1. `app/controllers/qt_controller.rb` 수정 - today/day 액션에서 BibleReadingLog 데이터 로드 추가 - @reading_completed = 현재 사용자가 오늘 해당 bible_passage의 장을 이미 읽었는지 확인 - bible_passage 파싱하여 book_name + chapter 추출 필요 (예: "창세기 1-4장" → 창세기 1,2,3,4장) 2. `app/views/qt/_reading_check.html.erb` 신규 파셜 생성 - turbo-frame id="reading_check" 래핑 - 통독 범위의 각 장에 대해 체크박스 표시 - 이미 읽은 장은 체크된 상태 - 체크 시 BibleReadingsController#create 호출 (Turbo Frame) - 해제 시 BibleReadingsController#destroy 호출 - 전체 완료 시 축하 메시지 or 배지 - 디자인: shared/card 파셜 사용, 체크박스 리스트 3. `app/views/qt/today.html.erb`는 건드리지 말 것! - tab-dev가 today.html.erb를 수정하고, turbo-frame id="reading_check"를 배치할 예정 - 이 에이전트는 _reading_check.html.erb 파셜만 생성 4. bible_passage 파싱 헬퍼 (QtController private 메서드 또는 모델 메서드) - "창세기 1-4장" → [{book_name: "창세기", chapter: 1}, ..., {chapter: 4}] - "시편 23편" → [{book_name: "시편", chapter: 23}] - 기존 BibleData 클래스 활용 가능 (tongtok에서 사용 중) 5. 기존 BibleReadingsController는 수정하지 않음 (이미 create/destroy/batch_create 있음) ## 기존 코드 참고 - BibleReadingsController: create(book_name, chapter), destroy(id), batch_create(chapters JSON) - BibleReadingLog: user_id, book_name, chapter, read_date - BibleData: 66권 성경 데이터 (app/models/bible_data.rb 또는 관련 파일 확인) - tongtok 뷰: 기존 통독 체크 UI 참고 (app/views/tongtok/) ## 완료 기준 - QtController에 reading log 데이터 로드 - _reading_check.html.erb 파셜 생성 - 통독 장별 체크/해제 동작 - 기존 테스트 전체 통과 (rails test)

R
reading-dev
8 days
높음 97aaa952
서브 티켓 [P0-SETUP] Rails 프로젝트 초기화 + Docker 환경

Docker Compose 개발환경 + Kamal 2.10.1 배포 설정

## 작업 내용 Rails 프로젝트의 Docker 개발 환경과 Kamal 배포 설정을 구성합니다. ### 주의: rails-setup 에이전트가 먼저 Rails 프로젝트를 생성해야 합니다. TaskList에서 rails-setup의 태스크가 완료되었는지 확인 후 작업을 시작하세요. ### 1단계: Dockerfile 확인/수정 - Rails 8.1.2가 생성한 기본 Dockerfile 활용 - 멀티스테이지 빌드 확인 - 필요시 수정 (SQLite3 + Tailwind 빌드 지원) ### 2단계: Docker Compose (개발용) `docker-compose.yml` 또는 `compose.yaml` 생성: ```yaml services: web: build: . ports: - "3000:3000" volumes: - .:/rails - bundle:/usr/local/bundle environment: - RAILS_ENV=development command: bin/rails server -b 0.0.0.0 css: build: . volumes: - .:/rails command: bin/rails tailwindcss:watch volumes: bundle: ``` - WSL2 환경 고려 (파일 시스템 성능) ### 3단계: Kamal 배포 설정 `config/deploy.yml`: ```yaml service: logbible image: logbible servers: web: hosts: - 167.172.82.126 registry: server: ghcr.io username: - KAMAL_REGISTRY_USERNAME password: - KAMAL_REGISTRY_PASSWORD builder: arch: amd64 env: secret: - RAILS_MASTER_KEY - GOOGLE_CLIENT_ID - GOOGLE_CLIENT_SECRET - KAKAO_CLIENT_ID - KAKAO_CLIENT_SECRET ``` - `.kamal/secrets` 파일 템플릿 생성 (.gitignore에 추가) ### 4단계: .dockerignore 확인 - 불필요한 파일 제외 (node_modules, .git, tmp, log 등) ### 5단계: 검증 - `docker compose build` 성공 - `docker compose up` 으로 앱 기동 - Kamal 설정 파일 구문 검증 ## 완료 기준 - Docker Compose로 개발 환경 구동 가능 - Kamal deploy 설정 파일 존재 - .dockerignore 적절히 설정 - .kamal/secrets 템플릿 존재

D
devops
9 days
긴급 23c025d7
서브 티켓 [P0-COORD] 핵심 기반 구축 (User + QT 모델 + 인증 + 디자인 시스템)

OmniAuth 소셜 로그인 (Google/Kakao) + 세션 관리

## 목표 Devise + OmniAuth로 Google/Kakao 소셜 로그인을 구현합니다. ## 전제 조건 - User 모델이 이미 Devise와 통합되어 있다고 가정합니다 - User 모델에 provider, provider_id, email, nickname 등 컬럼이 있습니다 - devise gem, omniauth gem이 Gemfile에 이미 있습니다 ## 작업 범위 ### 1. Gemfile 추가 - `omniauth-kakao` gem 추가 - `bundle install` 실행 ### 2. Devise 설정 - config/initializers/devise.rb 설정 (mailer, secret_key 등) - OmniAuth provider 설정 (Google OAuth2, Kakao) - 환경변수: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, KAKAO_CLIENT_ID, KAKAO_CLIENT_SECRET ### 3. OmniAuth 콜백 컨트롤러 - app/controllers/users/omniauth_callbacks_controller.rb - Google 콜백: 사용자 find_or_create - Kakao 콜백: 사용자 find_or_create - User.from_omniauth 클래스 메서드 ### 4. 세션 관리 - 로그인/로그아웃 플로우 - 비인증 사용자 → 로그인 페이지 리다이렉트 - authenticate_user! before_action 설정 ### 5. 로그인 페이지 UI - app/views/devise/sessions/new.html.erb - Google 로그인 버튼 + Kakao 로그인 버튼 - Tailwind CSS 스타일링 (모바일 반응형) - 로고 + 앱 소개 텍스트 ### 6. 라우팅 - config/routes.rb에 devise_for :users 추가 - OmniAuth 콜백 라우트 설정 ### 7. 테스트 - OmniAuth mock을 사용한 통합 테스트 - 로그인/로그아웃 플로우 테스트 ## 참고 - 레거시: src/contexts/AuthContext.tsx 분석 - docs/migration/index.md §3-1 인증/세션 API

A
auth-dev
9 days
높음 701354b3
서브 티켓 [P0-COORD] QT 핵심 기능 (메인 페이지 + 묵상 + 세션 + 시드)

QT 플랜(세션) 관리: 생성/참여/탈퇴/선택

## 목표 QT 세션(플랜) 생성, 참여, 탈퇴, 선택 기능을 구현합니다. ## 기존 모델 (이미 생성됨) - QtSession: title, theme_id, creator_id, start_date, end_date, total_days, is_public, invite_code(unique), status(enum: active/completed/cancelled) - QtParticipant: session_id, user_id, role(enum: creator/member), is_active, joined_at - QtTheme: title, description, total_day, is_active 등 - User: current_session_id 컬럼 있음 ## 작업 범위 ### 1. Qt::SessionsController - `index`: 내 세션 목록 (활성/완료 분류) - `new`: 새 세션 생성 폼 (테마 선택 + 시작일) - `create`: 세션 생성 + 참여자(creator) 자동 추가 + current_session_id 설정 - `show`: 세션 상세 (멤버 목록, 통계 등) - `edit`/`update`: 세션 수정 (소유자만) - `join`: 초대코드로 참여 (POST) - `leave`: 세션 탈퇴 (DELETE) - `select`: 현재 활성 세션 변경 (POST) → user.current_session_id 업데이트 ### 2. 초대 참여 페이지 - GET /qt/join?code=XXXX → 세션 정보 표시 + 참여 버튼 - 이미 참여 중이면 안내 메시지 ### 3. 뷰 - app/views/qt/sessions/index.html.erb: 세션 목록 (활성/완료 탭) - app/views/qt/sessions/new.html.erb: 생성 폼 (테마 선택 드롭다운 + 시작일 date input) - app/views/qt/sessions/show.html.erb: 세션 상세 (초대코드, 멤버, 통계) - app/views/qt/sessions/edit.html.erb: 수정 폼 - app/views/qt/sessions/join.html.erb: 초대 참여 페이지 - 공통 파셜: _session_card.html.erb (세션 요약 카드) ### 4. 라우팅 config/routes.rb에 추가: ```ruby namespace :qt do resources :sessions do member do post :select delete :leave end collection do get :join post :join, action: :process_join end end end ``` ### 5. 비즈니스 로직 - 세션 생성 시: total_days = theme.total_day, end_date = start_date + total_days - 초대코드: QtSession 모델에 이미 자동생성 로직 있음 - 참여 시: QtParticipant 생성 (role: :member) + is_active: true + joined_at: Time.current - 탈퇴 시: QtParticipant.is_active = false (soft delete) - 세션 선택: User.current_session_id 업데이트 ### 6. 테스트 - test/controllers/qt/sessions_controller_test.rb (CRUD + join/leave/select) - fixture에 테스트 세션/참여자 데이터 ## 스타일 - 디자인 시스템 파셜 활용 (shared/card, shared/button, shared/input, shared/badge, shared/tabs 등) - Tailwind CSS, 모바일 우선, 다크모드 지원 ## 참고 - QtSession 모델: app/models/qt_session.rb 참조 - QtParticipant 모델: app/models/qt_participant.rb 참조 - docs/migration/index.md §3-2 기능 Q6-Q12, Q21

S
session-dev
9 days
높음 9f2af9b5
서브 티켓 [P1-COORD] 핵심 기능 (기도제목 + 설교 노트 + 프로필/설정)

설교 노트 CRUD

## 목표 SermonNote 모델 생성 + 설교 노트 CRUD (목록/생성/상세/수정/삭제) ## 모델 ### SermonNote - user_id: UUID FK NOT NULL - title: text NOT NULL - sermon_date: date NOT NULL - bible_book: string NOT NULL - bible_book_abbrev: string NOT NULL - start_chapter: integer NOT NULL - start_verse: integer NOT NULL - end_chapter: integer NULL - end_verse: integer NULL - passage_reference: text NOT NULL - personal_meditation: text NULL - action_plan: text NULL - prayer_topics: text NULL - ai_interpretation: text NULL - highlights: text NULL (JSON) - is_shared: boolean DEFAULT false - mood_after: integer NULL (1-5) ## 컨트롤러 SermonsController: index, new, create, show, edit, update, destroy - 페이지네이션 (pagy gem 또는 수동) - 검색 (제목/성경구절) - 날짜 필터 ## 뷰 - app/views/sermons/ (index, new, show, edit, _form, _sermon_card) - 성경구절 입력 UI (책/장/절 선택) - 하이라이트 표시 ## 라우트 resources :sermons ## 테스트 - 모델 테스트 (SermonNote validations) - 컨트롤러 테스트 (전체 CRUD + 검색/페이지네이션)

S
sermon-dev
9 days
높음 d3fcfb29
서브 티켓 [P1-COORD] 마무리 (성경 통독 + 묵상 통계)

묵상 통계/히스토리 + routes.rb 관리

## 목표 묵상 통계 대시보드 + 묵상 히스토리 목록 + routes.rb 통합 관리 ## routes.rb 관리 (최우선!) 이 티켓의 에이전트가 routes.rb 유일한 관리자입니다. 아래 라우트를 모두 추가하세요: ```ruby # Tongtok (tongtok-dev가 구현) get "tongtok", to: "tongtok#index", as: :tongtok resources :bible_readings, only: [:create, :destroy] # Stats & Records get "stats", to: "stats#show", as: :stats get "records", to: "records#index", as: :records ``` ## 컨트롤러 ### StatsController - show: 묵상 통계 대시보드 - params[:year] (기본 올해) - UserMeditation 기반 통계 계산: - 총 묵상 수 (count) - 완료 묵상 수 (personal_meditation 또는 action_plan이 존재하는 것) - 완료율 (%) - 평균 기분 (mood_after 평균, 1-5) - 연속 묵상일수 (current_streak: 오늘부터 거슬러 올라가며 연속된 날 수) - 최대 연속일수 (max_streak) - 월별 묵상 수 배열 (1-12월) - @stats 해시로 뷰에 전달 ### RecordsController - index: 묵상 기록 목록 - params[:page] (기본 1), params[:month] (YYYY-MM 형태) - current_user.user_meditations.includes(:qt_content).order(meditation_date: :desc) - 페이지네이션 (10개/페이지) - 월별 필터 ## 통계 계산 로직 (StatsHelper 또는 concern) ### 연속일수 (Streak) 계산 ```ruby # current_streak: 오늘(또는 가장 최근 묵상일)부터 연속된 날 수 dates = user.user_meditations.where(meditation_date: year_range).pluck(:meditation_date).uniq.sort.reverse streak = 0 expected = Date.current dates.each do |d| break unless d == expected streak += 1 expected -= 1.day end # max_streak: 전체 기간 중 최대 연속 ``` ### 완료 판단 ```ruby # personal_meditation 또는 action_plan 중 하나에 내용이 있으면 완료 scope :completed, -> { where("personal_meditation IS NOT NULL AND personal_meditation != '' OR action_plan IS NOT NULL AND action_plan != ''") } ``` ## 뷰 ### stats/show.html.erb - 제목: "묵상 통계" (h1) - 연도 선택 - 핵심 지표 카드 4개: 총 묵상, 완료율, 연속일수, 평균 기분 - 월별 묵상 차트 (bar chart - 순수 HTML/CSS 또는 SVG 바 차트) - 외부 JS 라이브러리 사용 하지 마세요. 순수 Tailwind CSS로 바 차트 구현: ```erb <div class="flex items-end gap-1 h-40"> <% @stats[:monthly_data].each_with_index do |count, i| %> <div class="flex-1 bg-brand-primary rounded-t" style="height: <%= count > 0 ? [count.to_f / max * 100, 5].max : 0 %>%" title="<%= i+1 %>월: <%= count %>회"> </div> <% end %> </div> ``` - data-testid="stat-card" (각 지표 카드) - data-testid="monthly-chart" (월별 차트) ### records/index.html.erb - 제목: "묵상 기록" (h1) - 월별 필터 (month input) - 기록 목록: 날짜, 성경구절, 묵상 내용 미리보기, 기분 이모지 - 페이지네이션 (이전/다음) - 빈 상태 처리 - data-testid="record-item" (각 기록) ## 테스트 - 컨트롤러 테스트: StatsController (show, 통계값 검증), RecordsController (index, 페이지네이션, 필터) - 연속일수 계산 정확성 검증 ## Fixtures - 기존 user_meditations.yml에 테스트용 데이터가 있으면 활용 - 없으면 stats 테스트용 fixture 추가 (연속일수 테스트를 위해 여러 날짜의 묵상) ## 파일 담당 (다른 에이전트 파일 수정 금지) - 수정 가능: config/routes.rb, app/controllers/stats_controller.rb, app/controllers/records_controller.rb, app/views/stats/*, app/views/records/*, test/*, app/models/user_meditation.rb (scope 추가) - 수정 금지: app/models/bible_reading_log.rb, app/controllers/tongtok_controller.rb, app/controllers/bible_readings_controller.rb, app/views/tongtok/*

S
stats-dev
9 days
높음 cbcd6c48
서브 티켓 [P2-COORD] 소셜 기능 (기도 동역자 + 공유 묵상/랭킹)

공유 묵상 + 랭킹 + routes.rb 관리

## 목표 세션 내 공유 묵상 조회, 멤버 관리, 통독/묵상 랭킹 구현 + routes.rb 통합 관리 ## routes.rb 관리 (최우선!) 기존 라우트 아래, `# Static pages` 주석 위에 추가: ```ruby # Prayer Partners (동역자) resources :prayer_partners, only: [:index, :create, :destroy] do collection do get :search end member do post :accept post :reject get :prayers end end # Social (공유 묵상/랭킹) namespace :qt do resources :sessions do member do get :shared_meditations get :members get :rankings end end end ``` 주의: 기존 qt namespace 안에 sessions 라우트가 이미 있으므로, 새 member 라우트를 기존 block에 추가해야 합니다. ## 컨트롤러 ### Qt::SharedMeditationsController (또는 sessions 확장) - shared_meditations: 같은 세션 멤버의 공개 묵상 목록 (is_personal_meditation_shared == true) - members: 세션 멤버 목록 + 각 멤버의 진행 상황 - rankings: 통독/묵상/공유 순위 (전체/월별) ## 뷰 - qt/sessions/shared_meditations: 공유 묵상 카드 목록 - qt/sessions/members: 멤버 목록 + 진행 현황 - qt/sessions/rankings: 랭킹 테이블 ## 파일 담당 - 수정 가능: config/routes.rb, app/controllers/qt/sessions_controller.rb (액션 추가), app/views/qt/sessions/*, test/* - 수정 금지: app/models/prayer_partnership.rb, app/controllers/prayer_partners_controller.rb ## 테스트 - 컨트롤러: shared_meditations, members, rankings - 랭킹 계산 정확성 검증

S
social-dev
9 days
높음 8ba3eeec
서브 티켓 [P2] AI 기능 - 묵상 분석 + 설교 해석 (coordination)

AI 설교 해석 - 서비스 + interpret 액션 + Turbo Stream

## 목표 설교 노트에서 성경 구절 AI 해석 기능 추가 ## 구현 항목 ### 1. 서비스 객체 - app/services/ai_sermon_interpreter.rb - initialize(sermon_note) - call: 설교 정보로 프롬프트 구성 → OpenAI API 호출 → 해석 결과 반환 - 프롬프트: 한국어, 성경 구절(passage_reference), 설교 제목, 개인 묵상 기반 - 성경 구절 해석 + 적용 포인트 + 핵심 메시지 정리 - API 키: ENV["OPENAI_API_KEY"] 또는 ENV["GEMINI_API_KEY"] - OpenAI client: OpenAI::Client.new (ruby-openai gem) - 에러 처리: API 실패 시 에러 메시지 반환 ### 2. 컨트롤러 수정 - SermonsController에 interpret 액션 추가: ```ruby def interpret @sermon = current_user.sermon_notes.find(params[:id]) service = AiSermonInterpreter.new(@sermon) result = service.call if result[:success] @sermon.update(ai_interpretation: result[:interpretation]) respond_to do |format| format.turbo_stream format.html { redirect_to sermon_path(@sermon) } end else respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.replace("ai-interpretation", partial: "sermons/ai_error", locals: { error: result[:error] }) } format.html { redirect_to sermon_path(@sermon), alert: result[:error] } end end end ``` - before_action :set_sermon에 :interpret 추가 ### 3. 뷰 수정 - sermons/show.html.erb 수정: - AI 해석 섹션에 turbo_frame_tag "ai-interpretation" 추가 - ai_interpretation이 없으면 "AI 해석 요청" 버튼 표시 - ai_interpretation이 있으면 내용 표시 + "재해석" 버튼 - sermons/interpret.turbo_stream.erb: AI 해석 결과 turbo_stream replace - sermons/_ai_interpretation.html.erb: AI 해석 결과 파셜 - sermons/_ai_error.html.erb: 에러 표시 파셜 ### 4. 라우트 수정 - config/routes.rb에서 기존 sermons 리소스에 멤버 라우트 추가: ```ruby resources :sermons do member do post :interpret end end ``` ### 5. 테스트 - test/controllers/sermons_controller_test.rb에 interpret 테스트 추가: - POST /sermons/:id/interpret 인증 필수 - POST /sermons/:id/interpret AI 호출 성공 시 결과 저장 - POST /sermons/:id/interpret AI 호출 실패 시 에러 처리 - POST /sermons/:id/interpret 타인 설교 접근 불가 - API 호출은 stub으로 처리 ## 기존 코드 참고 - sermon_notes 테이블에 ai_interpretation 컬럼 이미 존재 - sermons/show.html.erb 64-71줄에 AI 해석 표시 코드 이미 존재 - sermons_controller.rb의 sermon_params에 :ai_interpretation 이미 포함 - SermonsController의 set_sermon: `current_user.sermon_notes.find(params[:id])` ## 주의사항 - 기존 sermons 테스트가 깨지지 않게 주의 - fixture 날짜는 과거 고정 날짜 사용 (Date.current 충돌 방지) - shared 파셜의 strict locals 준수 - ERB 멀티라인 주석 사용 금지 - 전체 테스트 통과 확인: bin/rails test

A
ai-sermon-dev
9 days
높음 fd7910db
서브 티켓 [P3] 관리자 대시보드

Admin 프론트엔드 - 레이아웃 + 사이드바 + 대시보드 뷰

## 목표 Admin 전용 레이아웃, 사이드바, 대시보드 뷰 구현 ## 구현 항목 ### 1. Admin 레이아웃 (신규) - 파일: `app/views/layouts/admin.html.erb` - 기존 application.html.erb 구조 참고하되, 관리자 전용으로 구성: - 상단 헤더: "LogBible Admin" 타이틀 - 좌측 사이드바: admin 전용 네비게이션 - 메인 컨텐츠 영역 - 하단 네비 불필요 (데스크톱 중심) - Tailwind CSS v4 의미 기반 색상 사용: - bg-surface-default, text-text-primary - brand-primary (#2563EB), surface-muted (#F9FAFB) - `<%= yield %>` 로 컨텐츠 삽입 - flash 메시지: `<%= render "shared/flash" %>` - JS/CSS: `<%= stylesheet_link_tag :app %>`, `<%= javascript_importmap_tags %>` ### 2. Admin 사이드바 (신규) - 파일: `app/views/admin/shared/_sidebar.html.erb` - 네비게이션 메뉴: - 대시보드 (admin_root_path) - 집 아이콘 - 구분선 - "사이트로 돌아가기" (root_path) - 화살표 아이콘 - SVG 아이콘 직접 인라인 사용 (외부 라이브러리 금지) - 현재 페이지 하이라이트: `current_page?` helper 활용 ### 3. 대시보드 뷰 (신규) - 파일: `app/views/admin/dashboard/index.html.erb` - 페이지 제목: "관리자 대시보드" - 통계 카드 그리드 (2x3 또는 3x2): - 전체 사용자 수 (@total_users) - 묵상 기록 수 (@total_meditations) - QT 세션 수 (@total_sessions) - 기도 제목 수 (@total_prayers) - 설교 노트 수 (@total_sermons) - 성경 읽기 수 (@total_readings) - 각 카드: 기존 shared/_card.html.erb 파셜 활용 - 카드 내부: 아이콘 + 숫자 + 라벨 - 숫자는 크게 (text-display 또는 text-2xl font-bold) - 라벨은 작게 (text-small text-text-secondary) - 최근 활동 섹션: - 최근 가입 사용자 목록 (@recent_users) - 이름, 이메일, 가입일 - 최근 묵상 기록 (@recent_meditations) - 사용자명, 날짜 - 기존 shared/_table.html.erb 파셜 활용 ## 기존 shared 파셜 참고 - `_card.html.erb`: `title:`, `padding:` (기본 "default") 파라미터 - `_table.html.erb`: `headers:` (배열), `rows:` (2차원 배열) 파라미터 - `_badge.html.erb`: `text:`, `variant:` (default/success/warning/error/info) 파라미터 - `_separator.html.erb`: `margin:` (기본 "default") 파라미터 ## 주의사항 - ERB 멀티라인 주석 안에 ERB 태그 금지 (SystemStackError) - shared 파셜의 strict locals 정확히 준수 - Tailwind CSS v4 의미 기반 클래스 사용 (하드코딩 색상 금지) - 반응형: 기본 모바일, md: 이상에서 사이드바 표시 - 기존 테스트 전체 통과 확인: `bin/rails test`

A
admin-frontend
9 days
높음 20c31d85
부모 티켓

BibleHighlight 기능 구현 (조율)

## 목표 성경 하이라이트 기능 전체 구현 (모델 + API + UI) ## 서브 티켓 1. BibleHighlight 모델 + 마이그레이션 2. 하이라이트 CRUD API 3. 하이라이트 Stimulus 컨트롤러 (bible_passage_controller 확장) ## 관련 기존 티켓 - 657861d7: BibleHighlight 모델 + 마이그레이션 - dc0d9f90: 하이라이트 CRUD API - e22eab14: 하이라이트 Stimulus 컨트롤러

2/2
팀리드
8 days
높음 43b6fc59
서브 티켓 BibleHighlight 기능 구현 (조율)

BibleHighlight 모델 + 마이그레이션 + 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` - 기존 테스트 전체 통과 확인

H
highlight-backend
8 days
높음 5ddc5663
서브 티켓 [P0-COORD] 핵심 기반 구축 (User + QT 모델 + 인증 + 디자인 시스템)

디자인 시스템: UI 파셜 + 레이아웃 + Stimulus 컨트롤러

## 목표 Tailwind CSS v4 기반 디자인 시스템을 구축합니다. shadcn/ui 스타일을 ERB 파셜로 변환합니다. ## 현재 상태 - 이미 존재하는 파셜: _button.html.erb, _card.html.erb, _flash.html.erb, _input.html.erb - 기존 파셜을 확인하고, 부족한 부분을 보완합니다 ## 작업 범위 ### 1. 기존 파셜 검토 및 보완 - app/views/shared/ 디렉토리의 기존 4개 파셜 확인 - 필요시 개선 (다크모드, 접근성, 변형 옵션) ### 2. 추가 UI 파셜 생성 (12개) - _modal.html.erb: 모달 다이얼로그 (backdrop + 애니메이션) - _select.html.erb: 커스텀 셀렉트 드롭다운 - _tabs.html.erb: 탭 네비게이션 - _avatar.html.erb: 프로필 아바타 (이미지 + 이니셜 폴백) - _badge.html.erb: 상태 배지 (success, warning, error, info) - _progress.html.erb: 진행률 바 - _switch.html.erb: 토글 스위치 - _tooltip.html.erb: 툴팁 - _alert.html.erb: 알림 배너 - _dropdown.html.erb: 드롭다운 메뉴 - _separator.html.erb: 구분선 - _table.html.erb: 테이블 ### 3. 앱 레이아웃 - app/views/layouts/application.html.erb 수정: - 모바일: 하단 네비게이션 (4탭: QT, 묵상, 통독, 기도) - 데스크톱: 사이드바 네비게이션 - 헤더: 앱 이름 + 프로필 메뉴 - 다크모드 토글 - app/views/shared/_header.html.erb - app/views/shared/_sidebar.html.erb - app/views/shared/_bottom_nav.html.erb ### 4. Stimulus 컨트롤러 - app/javascript/controllers/modal_controller.js (열기/닫기/ESC) - app/javascript/controllers/dropdown_controller.js (토글/외부클릭닫기) - app/javascript/controllers/tabs_controller.js (탭 전환) - app/javascript/controllers/tooltip_controller.js (표시/숨김) - app/javascript/controllers/dark_mode_controller.js (다크모드 전환) - app/javascript/controllers/sidebar_controller.js (사이드바 토글) ### 5. 다크모드 - Tailwind dark: 클래스 사용 - prefers-color-scheme 미디어쿼리 지원 - localStorage에 사용자 설정 저장 ## 스타일 가이드 - 색상: 주 색상 indigo-600, 보조 amber-500 - 모바일 우선 (min-width breakpoint) - 간격: p-4, gap-4 기본 - 모서리: rounded-lg 기본 - 그림자: shadow-sm ~ shadow-md - 폰트: font-sans (기본 시스템 폰트) ## 파셜 컨벤션 - 모든 파셜은 local variable로 옵션을 받습니다 - 예: render "shared/button", text: "저장", variant: :primary, size: :md - 기본값은 파셜 내부에서 설정 ## 참고 - docs/migration/index.md §6 UI 구조 - 레거시 src/components/ui/ (27개 shadcn 컴포넌트) - 레거시 src/components/layout/ (AppLayout, Header, Sidebar, BottomNav)

U
ui-dev
9 days
높음 ea9bbdee
서브 티켓 [P0-COORD] QT 핵심 기능 (메인 페이지 + 묵상 + 세션 + 시드)

QT 시드 데이터: 레거시 import + db:seed

## 목표 QT 테마/콘텐츠 시드 데이터를 생성하고, 레거시 import rake task를 작성합니다. ## 작업 범위 ### 1. 레거시 데이터 확인 - /mnt/c/dev/logbible/sql/ 디렉토리에서 SQL insert 파일 확인 - 테마와 콘텐츠 데이터 구조 파악 - 사용 가능한 데이터가 없으면 샘플 데이터 직접 생성 ### 2. db/seeds.rb 기본 시드 최소한 아래 데이터를 포함: - 기본 QT 테마 1-2개 (is_default: true) - 각 테마에 7-14일치 콘텐츠 - 콘텐츠 형식: ``` day_number: 1 bible_passage: "창세기 1:1-31" theme_title: "천지 창조" content: "하나님이 천지를 창조하시니라..." questions: ["배경 설명", "핵심 메시지", "성찰 질문 1", "성찰 질문 2", "위로와 격려"] difficulty: 3 ``` - 테스트용 사용자 1명 (admin) - 테스트용 QT 세션 1개 - 테스트용 참여자 1명 ### 3. lib/tasks/import_legacy.rake ```ruby namespace :import do desc "Import QT themes and contents from legacy SQL/JSON" task qt_data: :environment do # 레거시 SQL 또는 JSON 파일에서 데이터 읽기 # QtTheme, QtContent 모델로 import # UUID 보존 (기존 ID 유지) end end ``` ### 4. 레거시 데이터 변환 - SQL INSERT 문 파싱 → Ruby 해시 변환 - 또는 Supabase REST API 호출 스크립트 - JSON 중간 파일 생성 (db/seed_data/*.json) ### 5. 테스트 - test/tasks/import_legacy_test.rb (rake task 실행 테스트) - 또는 seeds 실행 후 데이터 검증 ## 기존 모델 (참고) - QtTheme: title, description, is_default, is_active, total_day, user_id(nullable) - QtContent: qt_theme_id, day_number, bible_passage, theme_title, content, questions(JSON), difficulty - User: email, nickname, role(enum) - QtSession: title, qt_theme_id, creator_id, start_date, end_date, total_days, invite_code ## 중요 사항 - DB 마이그레이션은 건드리지 마세요 (모델은 이미 있음) - Gemfile은 수정하지 마세요 - config/routes.rb는 수정하지 마세요 - 성경 데이터는 한국어로 작성 ## 참고 - 레거시 경로: /mnt/c/dev/logbible - docs/migration/index.md §10 데이터 마이그레이션 전략

S
seed-dev
9 days
보통 ec2e1e8d
서브 티켓 [P1-COORD] 핵심 기능 (기도제목 + 설교 노트 + 프로필/설정)

프로필/설정 + 정적 페이지

## 목표 프로필 수정, 설정 페이지, 정적 페이지(약관/개인정보) 구현 ## 구현 범위 ### 1. 프로필 페이지 (/profile) - ProfilesController: show, update - 닉네임 변경 - 프로필 이미지 URL 변경 (ActiveStorage는 나중에, 우선 URL 직접 입력) - 이메일 표시 (읽기 전용) - 로그아웃 버튼 ### 2. 설정 페이지 (/settings) - SettingsController: show, update - UserSetting 모델 활용 (이미 존재) - 알림 설정 (notification_enabled, notification_time) - 언어 설정 (language) - 타임존 (timezone) - 선호 난이도 (preferred_difficulty) - 자동 다음날 (auto_next_day) - 다크모드 토글 (클라이언트 사이드) ### 3. 정적 페이지 - PagesController: privacy, terms - app/views/pages/privacy.html.erb - 개인정보처리방침 - app/views/pages/terms.html.erb - 이용약관 ## 라우트 resource :profile, only: [:show, :update] resource :settings, only: [:show, :update] get 'privacy', to: 'pages#privacy' get 'terms', to: 'pages#terms' ## 테스트 - 컨트롤러 테스트 (ProfilesController, SettingsController, PagesController)

P
profile-dev
9 days
높음 d9674ebd
서브 티켓 [P2] 알림 시스템 (Push/Email/Kakao)

알림 기반: Gem 추가 + PushSubscription 모델 + 마이그레이션

## 목표 알림 시스템의 DB 기반을 구축합니다. ## 작업 내용 ### 1. Gemfile에 web-push gem 추가 ```ruby gem "web-push" ``` - `bundle install` 실행 ### 2. PushSubscription 모델 + 마이그레이션 생성 ```bash bin/rails generate model PushSubscription \ user:references \ endpoint:string \ p256dh:string \ auth:string \ is_active:boolean \ browser_info:string \ device_info:string \ last_notification_sent:datetime ``` - 마이그레이션 파일 수정: - `id: :string` (UUID PK) - `user_id`는 `type: :string` (UUID FK) - `is_active` default: true - `endpoint` NOT NULL - `p256dh`, `auth` NOT NULL - unique index: `[:user_id, :endpoint]` - 레거시에서는 subscription을 JSONB로 저장했지만, SQLite3에서는 endpoint/p256dh/auth를 개별 칼럼으로 분리 ### 3. user_settings에 notification_methods 칼럼 추가 ```bash bin/rails generate migration AddNotificationMethodsToUserSettings notification_methods:string ``` - default: "push" (콤마 구분 문자열: "push,email,kakao") - SQLite3에서 배열 타입 미지원이므로 string으로 저장 ### 4. 모델 코드 **PushSubscription 모델:** ```ruby class PushSubscription < ApplicationRecord belongs_to :user validates :endpoint, presence: true, uniqueness: { scope: :user_id } validates :p256dh, presence: true validates :auth, presence: true scope :active, -> { where(is_active: true) } scope :by_device, ->(type) { where("device_info LIKE ?", "%#{type}%") if type.present? } end ``` **User 모델 업데이트:** - `has_many :push_subscriptions, dependent: :destroy` 추가 **UserSetting 모델 업데이트:** - notification_methods 관련 헬퍼 메서드 추가: ```ruby def notification_methods_array (notification_methods || "push").split(",") end def push_enabled? notification_methods_array.include?("push") end def email_enabled? notification_methods_array.include?("email") end def kakao_enabled? notification_methods_array.include?("kakao") end ``` ### 5. 마이그레이션 실행 ```bash bin/rails db:migrate ``` ### 6. 테스트 실행 ```bash bin/rails test ``` ## 완료 기준 - web-push gem 설치됨 - push_subscriptions 테이블 생성됨 (UUID PK) - user_settings에 notification_methods 칼럼 존재 - User has_many :push_subscriptions 관계 설정됨 - bin/rails test 통과

N
notification-foundation
9 days
높음 7ee09279
서브 티켓 BibleHighlight 기능 구현 (조율)

하이라이트 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 없음) - 기존 테스트 전체 통과 확인

H
highlight-frontend
8 days
높음 9ecebc07
서브 티켓 [P2] 알림 시스템 (Push/Email/Kakao)

알림 백엔드: 컨트롤러 + Job + Mailer + 라우팅

## 목표 웹 푸시 구독 관리, 알림 발송 Job, 이메일 Mailer를 구현합니다. ## 선행 조건 - notification-foundation 에이전트가 PushSubscription 모델과 마이그레이션을 완료한 후 시작 ## 작업 내용 ### 1. PushSubscriptionsController 생성 파일: `app/controllers/push_subscriptions_controller.rb` ```ruby class PushSubscriptionsController < ApplicationController before_action :authenticate_user! # POST /push/subscribe - 구독 등록 def create subscription = current_user.push_subscriptions.find_or_initialize_by( endpoint: params[:endpoint] ) subscription.assign_attributes( p256dh: params[:p256dh], auth: params[:auth], browser_info: params[:browser_info], device_info: params[:device_info], is_active: true ) if subscription.save render json: { success: true, id: subscription.id } else render json: { success: false, errors: subscription.errors.full_messages }, status: :unprocessable_entity end end # GET /push/vapid_public_key - VAPID 공개키 반환 def vapid_public_key render json: { vapid_public_key: Rails.application.credentials.dig(:vapid, :public_key) || ENV["VAPID_PUBLIC_KEY"] } end # DELETE /push/unsubscribe - 구독 해제 def destroy subscription = current_user.push_subscriptions.find_by(endpoint: params[:endpoint]) if subscription subscription.update(is_active: false) render json: { success: true } else render json: { success: false }, status: :not_found end end end ``` ### 2. 라우팅 추가 파일: `config/routes.rb`에 추가: ```ruby # 푸시 알림 scope :push do post "subscribe", to: "push_subscriptions#create" get "vapid_public_key", to: "push_subscriptions#vapid_public_key" delete "unsubscribe", to: "push_subscriptions#destroy" end ``` ### 3. NotificationService 생성 파일: `app/services/notification_service.rb` - 웹 푸시 발송 로직 (web-push gem 사용) - 이메일 발송 로직 (Action Mailer) - 발송 방식 선택 로직 (push → email → kakao 순서) ```ruby class NotificationService def initialize(user, title:, message:, url: nil) @user = user @title = title @message = message @url = url || "/" end def send_all results = { push: nil, email: nil } setting = @user.user_setting methods = setting&.notification_methods_array || ["push"] results[:push] = send_web_push if methods.include?("push") results[:email] = send_email if methods.include?("email") results end private def send_web_push subscriptions = @user.push_subscriptions.active return { success: false, reason: "no_subscriptions" } if subscriptions.empty? vapid = { subject: "mailto:#{ENV.fetch('VAPID_EMAIL', 'admin@logbible.co.kr')}", public_key: ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key)), private_key: ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key)) } payload = { title: @title, body: @message, icon: "/icon-192x192.png", badge: "/badge-72x72.png", url: @url, actions: [{ action: "open", title: "열기" }] }.to_json sent = 0 failed = 0 subscriptions.find_each do |sub| begin WebPush.payload_send( message: payload, endpoint: sub.endpoint, p256dh: sub.p256dh, auth: sub.auth, vapid: vapid ) sub.update(last_notification_sent: Time.current) sent += 1 rescue WebPush::ExpiredSubscription sub.update(is_active: false) failed += 1 rescue => e Rails.logger.error("Push failed for #{sub.id}: #{e.message}") failed += 1 end end { success: sent > 0, sent: sent, failed: failed } end def send_email NotificationMailer.qt_reminder(@user, @title, @message, @url).deliver_later { success: true } rescue => e Rails.logger.error("Email failed for #{@user.id}: #{e.message}") { success: false, reason: e.message } end end ``` ### 4. NotificationCronJob 생성 파일: `app/jobs/notification_cron_job.rb` ```ruby class NotificationCronJob < ApplicationJob queue_as :default def perform current_hour = Time.current.in_time_zone("Asia/Seoul").hour # notification_enabled이고, notification_time이 현재 시간인 사용자 조회 UserSetting.where(notification_enabled: true) .where("CAST(strftime('%H', notification_time) AS INTEGER) = ?", current_hour) .includes(user: [:push_subscriptions], current_session: { qt_theme: :qt_contents }) .find_each do |setting| next unless setting.current_session # 활성 세션 없으면 skip session = setting.current_session theme = session.qt_theme # 오늘 날짜 기준 day_number 계산 day_number = (Date.current - session.start_date.to_date).to_i + 1 content = theme.qt_contents.find_by(day_number: day_number) next unless content # 오늘 콘텐츠 없으면 skip title = "📖 QT 시간입니다!" message = "#{theme.title} - #{content.bible_chapter}" url = "/qt/today" NotificationService.new( setting.user, title: title, message: message, url: url ).send_all end end end ``` ### 5. Solid Queue 반복 스케줄 설정 파일: `config/recurring.yml` 생성: ```yaml production: notification_cron: class: NotificationCronJob schedule: every hour description: "매 시간 알림 발송 체크" ``` ### 6. NotificationMailer 생성 파일: `app/mailers/notification_mailer.rb` ```ruby class NotificationMailer < ApplicationMailer def qt_reminder(user, title, message, url) @user = user @title = title @message = message @url = url mail(to: @user.email, subject: title) end end ``` 뷰 파일: `app/views/notification_mailer/qt_reminder.html.erb` - 간단한 HTML 이메일 템플릿 (Tailwind 미사용, 인라인 CSS) - LogBible 로고, 제목, 메시지, CTA 버튼 ### 7. SettingsController 업데이트 - `settings_params`에 `notification_methods` 추가 ## 완료 기준 - POST /push/subscribe 동작 - GET /push/vapid_public_key 동작 - DELETE /push/unsubscribe 동작 - NotificationCronJob이 올바른 사용자에게 알림 발송 - NotificationMailer 이메일 발송 - config/recurring.yml 존재 - bin/rails test 통과

N
notification-backend
9 days
높음 4a6ffcc4
서브 티켓 통계 차트 Chart.js

Chart.js 설정 + chart_controller Stimulus 컨트롤러

## 목표 Chart.js를 Importmap으로 설치하고, 범용 chart_controller Stimulus 컨트롤러를 생성 ## 구현 내용 ### 1. config/importmap.rb에 Chart.js 추가 ```ruby pin "chart.js", to: "https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" ``` ### 2. app/javascript/controllers/chart_controller.js 생성 - Stimulus 컨트롤러로 범용 차트 렌더링 - data-chart-type-value: "line" | "bar" | "pie" | "doughnut" - data-chart-data-value: JSON 문자열 (labels + datasets) - data-chart-options-value: JSON 문자열 (선택적 옵션 오버라이드) - 다크모드 대응: prefers-color-scheme 감지하여 텍스트/그리드 색상 자동 전환 - 모바일 반응형: aspectRatio 조절, 범례 위치 조정 - canvas target으로 차트 렌더링 - disconnect() 시 차트 인스턴스 destroy() ### 3. 다크모드 색상 매핑 ```javascript // 라이트모드 gridColor: 'rgba(0, 0, 0, 0.1)' textColor: '#374151' // 다크모드 gridColor: 'rgba(255, 255, 255, 0.1)' textColor: '#d1d5db' ``` ### 4. 차트 색상 팔레트 (디자인 시스템 연동) ```javascript const CHART_COLORS = { primary: '#4f46e5', // brand-primary secondary: '#f59e0b', // brand-secondary success: '#10b981', // status-success warning: '#f59e0b', // status-warning error: '#ef4444', // status-error info: '#3b82f6', // status-info } ``` ## 완료 기준 - [ ] importmap.rb에 Chart.js pin 추가 - [ ] chart_controller.js 생성 (line/bar/pie/doughnut 지원) - [ ] 다크모드 자동 감지 + 색상 전환 - [ ] 모바일 반응형 (aspectRatio) - [ ] disconnect 시 메모리 정리 - [ ] 기존 테스트 전체 통과 (bin/rails test) ## 담당 파일 - config/importmap.rb (수정) - app/javascript/controllers/chart_controller.js (신규)

C
chart-foundation
8 days
보통 654045ca
서브 티켓 월별 랭킹 미세 조정

월별 랭킹 미세 조정 구현

## 목표 랭킹 페이지 UI/UX 미세 조정: 월 선택, 뱃지 개선, 본인 하이라이트, 빈 상태 ## 구현 내용 ### 1. app/controllers/qt/sessions_controller.rb - rankings 액션 수정 현재 period="all"/"month" 기반 → month 파라미터 추가: ```ruby def rankings theme = @session.qt_theme participant_user_ids = @session.qt_participants.pluck(:user_id) users = User.where(id: participant_user_ids) @period = params[:period] || "all" @selected_month = params[:month] # "2026-03" 형식 @rankings = users.map do |user| meditations = user.user_meditations.joins(:qt_content).where(qt_contents: { qt_theme_id: theme.id }) if @period == "month" if @selected_month.present? begin date = Date.parse("#{@selected_month}-01") meditations = meditations.where(meditation_date: date.beginning_of_month..date.end_of_month) rescue Date::Error meditations = meditations.where("user_meditations.meditation_date >= ?", Date.current.beginning_of_month) end else meditations = meditations.where("user_meditations.meditation_date >= ?", Date.current.beginning_of_month) end end completed = meditations.where.not(personal_meditation: [nil, ""]) shared = meditations.where(is_personal_meditation_shared: true) tongtok = meditations.where(is_tongtok_completed: true) { user: user, meditation_count: meditations.count, completed_count: completed.count, shared_count: shared.count, tongtok_count: tongtok.count, score: completed.count * 3 + shared.count * 2 + tongtok.count } end.sort_by { |r| -r[:score] } # 월 선택을 위한 가용 월 목록 (세션 시작부터 현재까지) @available_months = if @session.start_date.present? start = @session.start_date.beginning_of_month months = [] current = Date.current.beginning_of_month while start <= current months << current.strftime("%Y-%m") current = current.prev_month.beginning_of_month end months else [Date.current.strftime("%Y-%m")] end end ``` ### 2. app/views/qt/sessions/rankings.html.erb 전체 교체 ```erb <div class="max-w-2xl mx-auto px-4 py-6 space-y-6"> <%# 헤더 %> <div class="flex items-center gap-3"> <%= link_to qt_session_path(@session), class: "text-text-secondary hover:text-text-primary transition-colors" do %> <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> </svg> <% end %> <h1 class="text-heading font-bold text-text-primary"><%= @session.title %> 랭킹</h1> </div> <%# 기간 필터 %> <div class="flex items-center gap-2 flex-wrap"> <%= link_to "전체", rankings_qt_session_path(@session, period: "all"), class: "px-4 py-2 text-small font-medium rounded-lg transition-colors #{@period == 'all' ? 'bg-brand-primary text-white' : 'bg-surface-subtle text-text-secondary hover:bg-gray-200 dark:hover:bg-gray-600'}" %> <%= link_to "이번 달", rankings_qt_session_path(@session, period: "month"), class: "px-4 py-2 text-small font-medium rounded-lg transition-colors #{@period == 'month' && @selected_month.blank? ? 'bg-brand-primary text-white' : 'bg-surface-subtle text-text-secondary hover:bg-gray-200 dark:hover:bg-gray-600'}" %> <%# 월 선택 드롭다운 %> <% if @available_months.size > 1 %> <select onchange="if(this.value) window.location.href=this.value" class="px-3 py-2 text-small font-medium rounded-lg bg-surface-subtle text-text-secondary border-0 focus:ring-2 focus:ring-brand-primary"> <option value="">월 선택</option> <% @available_months.each do |month| %> <% month_label = Date.parse("#{month}-01").strftime("%Y년 %m월") %> <option value="<%= rankings_qt_session_path(@session, period: "month", month: month) %>" <%= "selected" if @selected_month == month %>> <%= month_label %> </option> <% end %> </select> <% end %> </div> <%# 랭킹 목록 %> <% if @rankings.empty? || @rankings.all? { |r| r[:score] == 0 } %> <%= render "shared/empty_state", title: "아직 랭킹 데이터가 없습니다", description: "묵상을 완료하면 랭킹에 반영됩니다.", action_text: "오늘의 묵상하기", action_path: qt_today_path(session_id: @session.id) %> <% else %> <%= render "shared/card", padding: :md do %> <div class="space-y-1"> <% @rankings.each_with_index do |ranking, index| %> <% rank = index + 1 %> <% is_me = ranking[:user].id == current_user.id %> <div class="flex items-center gap-3 p-3 rounded-lg <%= if is_me 'bg-brand-primary/10 dark:bg-brand-primary/20 ring-1 ring-brand-primary/30' elsif rank <= 3 'bg-amber-50/80 dark:bg-amber-900/20' end %>"> <%# 순위 %> <div class="w-8 text-center shrink-0"> <% if rank == 1 %> <span class="text-xl">🥇</span> <% elsif rank == 2 %> <span class="text-xl">🥈</span> <% elsif rank == 3 %> <span class="text-xl">🥉</span> <% else %> <span class="text-body font-bold text-text-secondary"><%= rank %></span> <% end %> </div> <%# 아바타 + 이름 %> <%= render "shared/avatar", name: ranking[:user].nickname, size: :sm %> <div class="flex-1 min-w-0"> <div class="flex items-center gap-2"> <span class="text-body font-medium text-text-primary truncate"><%= ranking[:user].nickname %></span> <% if is_me %> <%= render "shared/badge", text: "나", variant: :info %> <% end %> </div> <div class="flex gap-3 text-caption text-text-muted mt-0.5"> <span>묵상 <%= ranking[:completed_count] %></span> <span>공유 <%= ranking[:shared_count] %></span> <span>통독 <%= ranking[:tongtok_count] %></span> </div> </div> <%# 점수 %> <div class="text-right shrink-0"> <span class="text-subheading font-bold text-brand-primary"><%= ranking[:score] %></span> <p class="text-caption text-text-muted">점</p> </div> </div> <% end %> </div> <% end %> <%# 점수 계산 안내 %> <%= render "shared/card", padding: :md do %> <h3 class="text-small font-medium text-text-secondary mb-2">점수 계산</h3> <div class="flex gap-4 text-caption text-text-muted"> <span>묵상 완료 ×3</span> <span>공유 ×2</span> <span>통독 ×1</span> </div> <% end %> <% end %> </div> ``` ### 3. 핵심 변경 요약 1. **월 선택**: @available_months + select 드롭다운 (세션 시작월~현재) 2. **본인 하이라이트**: `is_me` 체크 → brand-primary/10 배경 + ring + "나" 뱃지 3. **다크모드 뱃지**: `dark:bg-amber-900/20` 추가 4. **빈 상태**: shared/empty_state 파셜 사용 (score 0인 경우도 포함) ### 4. 테스트 추가 test/controllers/qt/sessions_controller_test.rb에 추가: ```ruby test "should get rankings with specific month" do sign_in @daniel get rankings_qt_session_path(@active_session, period: "month", month: Date.current.strftime("%Y-%m")) assert_response :success end ``` ## 완료 기준 - [ ] 월 선택 드롭다운 동작 - [ ] 본인 순위 하이라이트 (배경색 + "나" 뱃지) - [ ] 다크모드 대응 (amber, brand 배경) - [ ] 빈 데이터 시 empty_state 파셜 - [ ] 기존 테스트 + 새 테스트 통과 (bin/rails test) ## 담당 파일 - app/controllers/qt/sessions_controller.rb (rankings 액션 수정) - app/views/qt/sessions/rankings.html.erb (전체 교체) - test/controllers/qt/sessions_controller_test.rb (테스트 추가)

R
ranking-dev
8 days
보통 b5874896
서브 티켓 [P2] 알림 시스템 (Push/Email/Kakao)

알림 프론트엔드: Service Worker + Settings UI + Stimulus

## 목표 브라우저 Service Worker, 알림 설정 UI, Stimulus 컨트롤러를 구현합니다. ## 선행 조건 - notification-foundation 에이전트가 모델/마이그레이션 완료 후 시작 - notification-backend 에이전트가 라우트(/push/subscribe, /push/vapid_public_key)를 설정한 후 시작 ## 작업 내용 ### 1. Service Worker 생성 파일: `public/service-worker.js` ```javascript self.addEventListener("push", function(event) { const data = event.data ? event.data.json() : {}; const title = data.title || "LogBible"; const options = { body: data.body || "새 알림이 있습니다.", icon: data.icon || "/icon-192x192.png", badge: data.badge || "/badge-72x72.png", data: { url: data.url || "/" }, actions: data.actions || [{ action: "open", title: "열기" }], requireInteraction: true }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", function(event) { event.notification.close(); const url = event.notification.data?.url || "/"; event.waitUntil( clients.matchAll({ type: "window", includeUncontrolled: true }).then(function(clientList) { for (const client of clientList) { if (client.url.includes(url) && "focus" in client) { return client.focus(); } } return clients.openWindow(url); }) ); }); ``` ### 2. Stimulus Push Notification 컨트롤러 생성 파일: `app/javascript/controllers/push_notification_controller.js` ```javascript import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["status", "toggleBtn"] async connect() { this.updateStatus(); } async togglePush() { if (!("serviceWorker" in navigator) || !("PushManager" in window)) { alert("이 브라우저는 푸시 알림을 지원하지 않습니다."); return; } const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { // 이미 구독 중이면 해제 await this.unsubscribe(subscription); } else { // 구독 시작 await this.subscribe(registration); } this.updateStatus(); } async subscribe(registration) { try { const permission = await Notification.requestPermission(); if (permission !== "granted") { alert("알림 권한이 거부되었습니다. 브라우저 설정에서 허용해주세요."); return; } // VAPID 키 가져오기 const response = await fetch("/push/vapid_public_key"); const { vapid_public_key } = await response.json(); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: this.urlBase64ToUint8Array(vapid_public_key) }); const sub = subscription.toJSON(); // 서버에 구독 등록 const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; await fetch("/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify({ endpoint: sub.endpoint, p256dh: sub.keys.p256dh, auth: sub.keys.auth, browser_info: navigator.userAgent, device_info: /Mobile|Android/i.test(navigator.userAgent) ? "mobile" : "desktop" }) }); } catch (error) { console.error("Push subscription failed:", error); alert("푸시 알림 등록에 실패했습니다."); } } async unsubscribe(subscription) { const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; await fetch("/push/unsubscribe", { method: "DELETE", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); await subscription.unsubscribe(); } async updateStatus() { if (!("serviceWorker" in navigator)) { if (this.hasStatusTarget) this.statusTarget.textContent = "미지원"; return; } const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (this.hasStatusTarget) { this.statusTarget.textContent = subscription ? "활성" : "비활성"; } if (this.hasToggleBtnTarget) { this.toggleBtnTarget.textContent = subscription ? "푸시 알림 해제" : "푸시 알림 등록"; } } urlBase64ToUint8Array(base64String) { const padding = "=".repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } } ``` ### 3. Service Worker 등록 (application.js 또는 별도) 파일: `app/javascript/controllers/service_worker_controller.js` ```javascript import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/service-worker.js") .then(reg => console.log("Service Worker registered:", reg.scope)) .catch(err => console.error("Service Worker registration failed:", err)); } } } ``` - application layout의 body에 `data-controller="service-worker"` 추가 ### 4. Settings UI 업데이트 파일: `app/views/settings/show.html.erb` 기존 알림 설정 섹션에 notification_methods 체크박스 추가: - 알림 활성화 토글 (notification_enabled) - 알림 시간 설정 (notification_time) - **알림 방식 선택** (notification_methods) - 체크박스: - [ ] 푸시 알림 (push) + 푸시 등록/해제 버튼 - [ ] 이메일 알림 (email) - [ ] 카카오톡 알림 (kakao) - 비활성화 표시 "준비 중" Tailwind CSS 사용, 반응형 디자인. 푸시 알림 섹션에 `data-controller="push-notification"` 연결. 레거시 UI 참고: 기존 settings/show.html.erb의 스타일을 따릅니다. ### 5. SettingsController 파라미터 업데이트 파일: `app/controllers/settings_controller.rb` - `settings_params`에 `notification_methods` 추가 ## 완료 기준 - public/service-worker.js 존재 - Stimulus push_notification_controller 동작 - Service Worker 자동 등록 - 설정 페이지에서 알림 방식 선택 가능 - 푸시 알림 등록/해제 버튼 동작 - notification_methods가 서버에 저장됨 - bin/rails test 통과

N
notification-frontend
9 days
높음 0b01be45
서브 티켓 통계 차트 Chart.js

묵상 통계 라인 차트 구현

## 목표 stats/index.html.erb의 기존 HTML 막대 그래프를 Chart.js 라인 차트로 교체 ## 의존성 - chart-foundation 에이전트가 chart_controller.js를 먼저 완성해야 함 ## 구현 내용 ### 1. app/views/stats/index.html.erb 수정 현재 "월별 묵상 추이" 섹션에 순수 HTML로 만든 막대 그래프가 있음 → Chart.js 라인 차트로 교체 ```erb <%# 기존 HTML 막대 그래프 영역을 아래로 교체 %> <div data-controller="chart" data-chart-type-value="line" data-chart-data-value='<%= { labels: @monthly_data.map { |d| d[:month] }, datasets: [{ label: "묵상 횟수", data: @monthly_data.map { |d| d[:count] }, borderColor: "#4f46e5", backgroundColor: "rgba(79, 70, 229, 0.1)", fill: true, tension: 0.4 }] }.to_json %>'> <canvas data-chart-target="canvas"></canvas> </div> ``` ### 2. 추가 고려사항 - 기존 핵심 지표 4개 카드 (총 묵상, 완료율, 스트릭 등)는 그대로 유지 - 월별 추이 섹션만 차트로 교체 - 차트 아래 "묵상 기록 보기" 버튼 유지 - 기존 shared/_card 파셜 안에 차트 배치 ## 현재 stats/index.html.erb 구조 1. 핵심 지표 2x2 그리드 (총 묵상, 완료율, 현재 스트릭, 최대 스트릭) 2. 평균 기분 카드 (이모지 표시) 3. 월별 묵상 추이 (HTML 막대 그래프) ← 여기를 Chart.js로 교체 4. "묵상 기록 보기" 버튼 ## 완료 기준 - [ ] 월별 묵상 추이가 Chart.js 라인 차트로 표시 - [ ] 다크모드에서 차트 가독성 확인 - [ ] 모바일에서 차트 크기 적절 - [ ] 기존 테스트 전체 통과 (bin/rails test) ## 담당 파일 - app/views/stats/index.html.erb (수정)

C
chart-meditation
8 days
높음 85338cf4
서브 티켓 [P2] 세션 통계/멤버 관리

세션 통계 - show 페이지 오늘 현황 카드 + stats 액션 + 테스트

## 목표 QT 세션 show 페이지에 오늘 현황 통계 카드를 추가하고, 별도 stats 액션도 구현 ## 구현 항목 ### 1. Qt::SessionsController show 액션 수정 - 파일: `app/controllers/qt/sessions_controller.rb` - `show` 액션에서 오늘 통계 계산 추가: ```ruby def show # 기존 코드 유지 @session = current_user_sessions.find(params[:id]) @participants = @session.qt_participants.includes(:user).where(is_active: true) # 오늘 통계 추가 theme = @session.qt_theme today = Date.current participant_user_ids = @participants.pluck(:user_id) today_meditations = UserMeditation.joins(:qt_content) .where(qt_contents: { qt_theme_id: theme.id }) .where(meditation_date: today) .where(user_id: participant_user_ids) @today_stats = { total_participants: @participants.count, tongtok_completed: today_meditations.where(is_tongtok_completed: true).count, qt_completed: today_meditations.where.not(personal_meditation: [nil, ""]).count } end ``` ### 2. show.html.erb 뷰 수정 - 파일: `app/views/qt/sessions/show.html.erb` - 기존 세션 정보 카드 아래, 3개 버튼(공유묵상/멤버/랭킹) 위에 "오늘 현황" 카드 추가 - 디자인: - shared/_card 파셜 사용 - 3열 그리드: 참여자 | 통독 완료 | QT 완료 - 각 항목: 숫자 크게 + 라벨 작게 - Tailwind CSS 사용 - 예시: ```erb <%# 오늘 현황 %> <%= render "shared/card" do %> <h3 class="font-semibold text-gray-900 mb-3">오늘 현황</h3> <div class="grid grid-cols-3 gap-4 text-center"> <div> <p class="text-2xl font-bold text-blue-600"><%= @today_stats[:total_participants] %></p> <p class="text-xs text-gray-500">참여자</p> </div> <div> <p class="text-2xl font-bold text-green-600"><%= @today_stats[:tongtok_completed] %></p> <p class="text-xs text-gray-500">통독 완료</p> </div> <div> <p class="text-2xl font-bold text-purple-600"><%= @today_stats[:qt_completed] %></p> <p class="text-xs text-gray-500">QT 완료</p> </div> </div> <% end %> ``` ### 3. 테스트 - 파일: `test/controllers/qt/sessions_controller_test.rb` - 기존 테스트에 추가: ```ruby test "show displays today stats" do sign_in users(:daniel) get qt_session_path(qt_sessions(:active_session)) assert_response :success assert_select ".grid-cols-3" # 오늘 현황 3열 그리드 end ``` ## 주의사항 - 기존 show 뷰의 구조를 깨지 않을 것 - shared 파셜의 strict locals 준수 (variant:, padding: 등) - UUID PK 사용 (ApplicationRecord의 set_uuid 자동 처리) - 전체 테스트 통과 확인: `bin/rails test` - `parallelize(workers: 1)` 테스트 설정 확인

S
session-stats-dev
9 days
보통 f851fdae
부모 티켓

[P3] PWA + 모바일 지원 (coordination)

## 목표 PWA 설치 가능 + Capacitor 모바일 앱 기반 구축 ## 서브 티켓 1. PWA Core - manifest, service worker, offline, routes, 레이아웃, install prompt 2. Capacitor Setup - npm init, @capacitor/core, capacitor.config.ts ## 관련 기존 티켓 - bb6b65e4: [P3] PWA + 모바일 지원

2/2
팀리드
9 days
높음 0eef9d88
서브 티켓 [P3] PWA + 모바일 지원 (coordination)

PWA Core - Manifest + Service Worker + 오프라인 + 레이아웃 + Install Prompt

## 목표 PWA 설치 가능(A2HS), Service Worker 캐싱, 오프라인 페이지 구현 ## 구현 항목 ### 1. Manifest 커스터마이징 (app/views/pwa/manifest.json.erb) 기존 파일 수정: ```json { "name": "LogBible - 묵상 기록", "short_name": "LogBible", "icons": [ { "src": "/icon.png", "type": "image/png", "sizes": "512x512" }, { "src": "/icon.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }, { "src": "/icon-192x192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/icon-192x192.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" } ], "start_url": "/", "display": "standalone", "scope": "/", "description": "크리스천 통독 묵상 기록 플랫폼", "theme_color": "#6366f1", "background_color": "#f8fafc", "orientation": "portrait", "categories": ["lifestyle", "education"], "lang": "ko" } ``` - theme_color: brand-primary (#6366f1 indigo-500 계열, 기존 디자인 시스템 확인) - 192x192 아이콘 생성 (icon.png에서 리사이즈 - ImageMagick 또는 간단한 복사) ### 2. 아이콘 생성 - public/icon-192x192.png: `convert public/icon.png -resize 192x192 public/icon-192x192.png` (ImageMagick) - ImageMagick 없으면 icon.png을 복사하여 icon-192x192.png으로 (브라우저가 리사이즈) ### 3. Service Worker (app/views/pwa/service-worker.js) 기존 주석 파일을 교체: ```javascript // LogBible Service Worker - Cache Strategy const CACHE_VERSION = "v1"; const CACHE_NAME = `logbible-${CACHE_VERSION}`; const OFFLINE_URL = "/offline"; // 캐시할 정적 리소스 const PRECACHE_URLS = [ "/offline", "/icon.png", "/icon-192x192.png" ]; // Install - 정적 리소스 프리캐시 self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(PRECACHE_URLS); }) ); self.skipWaiting(); }); // Activate - 이전 캐시 정리 self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) ); self.clients.claim(); }); // Fetch - Network First (HTML), Cache First (정적) self.addEventListener("fetch", (event) => { const { request } = event; const url = new URL(request.url); // 같은 origin만 처리 if (url.origin !== location.origin) return; // API/POST 요청은 패스 if (request.method !== "GET") return; // HTML 요청: Network First + 오프라인 폴백 if (request.headers.get("Accept")?.includes("text/html")) { event.respondWith( fetch(request) .catch(() => caches.match(OFFLINE_URL)) ); return; } // 정적 리소스 (JS/CSS/이미지): Cache First if (url.pathname.match(/\.(js|css|png|jpg|svg|ico|woff2?)$/)) { event.respondWith( caches.match(request).then((cached) => { return cached || fetch(request).then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); } return response; }); }) ); return; } }); // Push Notification 처리 (향후 알림 시스템용) self.addEventListener("push", (event) => { const data = event.data ? event.data.json() : {}; const title = data.title || "LogBible"; const options = { body: data.body || "새 알림이 있습니다.", icon: data.icon || "/icon-192x192.png", badge: "/icon-192x192.png", data: { url: data.url || "/" } }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const url = event.notification.data?.url || "/"; event.waitUntil( clients.matchAll({ type: "window" }).then((clientList) => { for (const client of clientList) { if (new URL(client.url).pathname === url && "focus" in client) { return client.focus(); } } return clients.openWindow(url); }) ); }); ``` ### 4. 오프라인 페이지 - app/controllers/pwa_controller.rb 생성: ```ruby class PwaController < ApplicationController skip_before_action :authenticate_user!, raise: false layout false def offline render "pwa/offline" end end ``` - app/views/pwa/offline.html.erb 생성: - 심플한 오프라인 안내 페이지 (standalone HTML, Tailwind 인라인) - "인터넷 연결이 없습니다. 연결 후 다시 시도해주세요." 메시지 - 새로고침 버튼 ### 5. 라우트 (config/routes.rb) 기존 파일에 추가: ```ruby # PWA get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker get "offline" => "pwa#offline" ``` - Rails 8에서 제공하는 `rails/pwa` 컨트롤러 활용 - get "up" 라우트 근처에 배치 ### 6. 레이아웃 메타 태그 (app/views/layouts/application.html.erb) ```erb <meta name="theme-color" content="#6366f1"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <link rel="manifest" href="/manifest.json"> ``` - 기존 주석 처리된 manifest 링크를 활성화 - theme-color 메타 태그 추가 - apple-mobile-web-app-status-bar-style 추가 - devise.html.erb에도 동일 적용 ### 7. PWA Install Prompt (Stimulus 컨트롤러) - app/javascript/controllers/pwa_install_controller.js 생성: ```javascript import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["banner"] connect() { this.deferredPrompt = null; window.addEventListener("beforeinstallprompt", this.handleInstallPrompt.bind(this)); window.addEventListener("appinstalled", this.handleInstalled.bind(this)); } handleInstallPrompt(event) { event.preventDefault(); this.deferredPrompt = event; if (this.hasBannerTarget) { this.bannerTarget.classList.remove("hidden"); } } async install() { if (!this.deferredPrompt) return; this.deferredPrompt.prompt(); const { outcome } = await this.deferredPrompt.userChoice; this.deferredPrompt = null; if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } dismiss() { if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } handleInstalled() { this.deferredPrompt = null; if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } disconnect() { window.removeEventListener("beforeinstallprompt", this.handleInstallPrompt); window.removeEventListener("appinstalled", this.handleInstalled); } } ``` - application.html.erb body에 install banner 추가: ```erb <div data-controller="pwa-install" class="fixed bottom-20 md:bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 z-50"> <div data-pwa-install-target="banner" class="hidden bg-brand-primary text-white rounded-lg shadow-lg p-4"> <div class="flex items-center justify-between"> <div> <p class="font-medium text-small">LogBible 앱 설치</p> <p class="text-caption opacity-80">홈 화면에 추가하세요</p> </div> <div class="flex gap-2"> <button data-action="pwa-install#install" class="bg-white text-brand-primary px-3 py-1 rounded text-small font-medium">설치</button> <button data-action="pwa-install#dismiss" class="text-white/80 hover:text-white text-small">닫기</button> </div> </div> </div> </div> ``` ### 8. 테스트 - test/controllers/pwa_controller_test.rb: - GET /offline 200 응답 - GET /manifest.json 200 응답 - GET /service-worker.js 200 응답 ## 기존 코드 참고 - app/views/pwa/manifest.json.erb 존재 (기본 템플릿) - app/views/pwa/service-worker.js 존재 (전부 주석) - application.html.erb 15줄: manifest 링크 주석 처리됨 - theme-color 등 PWA 메타 태그 누락 - Tailwind 디자인 시스템: brand-primary 사용 ## 주의사항 - 기존 테스트가 깨지지 않게 주의 - ERB 멀티라인 주석 사용 금지 - routes.rb 수정 시 기존 구조 유지 - skip_before_action으로 인증 우회 (offline, manifest, service-worker) - bin/rails test로 전체 테스트 통과 확인

P
pwa-core-dev
9 days
높음 687b92f9
서브 티켓 [P3] AI 콘텐츠 자동 생성

AI QT 콘텐츠 자동 생성 - 서비스 + Job + Admin 컨트롤러/뷰 + 테스트

## 목표 Admin에서 QT 테마를 선택하고 AI 생성 요청 시 Solid Queue Job으로 콘텐츠를 자동 생성하는 전체 플로우 구현 ## 스키마 현황 (마이그레이션 불필요!) - QtTheme: is_ai_generated, ai_prompt, bible_books, generation_status(enum: draft/generating/completed/published) 컬럼 이미 존재 - QtContent: day_number, bible_passage, reading_passage, theme_title, content, questions(json), difficulty 등 컬럼 존재 ## 구현 항목 ### 1. AiQtGenerator 서비스 - 파일: `app/services/ai_qt_generator.rb` - 기존 서비스 패턴 (AiMeditationAnalyzer, AiSermonInterpreter)을 따를 것 - 핵심: ```ruby class AiQtGenerator def initialize(theme) @theme = theme end def call # 1. 테마 정보로 프롬프트 구성 # 2. OpenAI::Client로 API 호출 # 3. 응답 JSON 파싱 → QtContent 레코드 생성 # 4. 결과 반환 end private def build_prompt # 한국어 QT 콘텐츠 생성 프롬프트 # 입력: theme.title, theme.description, theme.total_day, theme.bible_books # 출력 형식: JSON 배열 [{day_number, bible_passage, reading_passage, theme_title, content, questions: [...5개], difficulty, estimated_minutes}] end def api_key ENV["OPENAI_API_KEY"] || ENV["GEMINI_API_KEY"] || raise("AI API 키가 설정되지 않았습니다") end def ai_model ENV["OPENAI_API_KEY"] ? "gpt-4o-mini" : "gemini-2.0-flash" end end ``` - **프롬프트 가이드**: 레거시에서 참고. 성경 통독 QT 콘텐츠 생성용: - total_day 일분의 콘텐츠를 JSON 배열로 생성 - 각 콘텐츠: bible_passage(통독 범위), reading_passage(핵심 구절), theme_title, content(본문 해석), questions(5개 묵상 질문 배열) - difficulty 1-5, estimated_minutes 기본 15 - 한국어로 생성 - 개역개정 성경 기준 ### 2. GenerateQtContentsJob - 파일: `app/jobs/generate_qt_contents_job.rb` - Solid Queue 백그라운드 처리: ```ruby class GenerateQtContentsJob < ApplicationJob queue_as :default def perform(theme_id) theme = QtTheme.find(theme_id) theme.generating! # 상태 변경 result = AiQtGenerator.new(theme).call if result[:success] theme.completed! else theme.draft! # 실패 시 롤백 Rails.logger.error("QT generation failed for theme #{theme_id}: #{result[:error]}") end rescue => e theme&.draft! Rails.logger.error("QT generation job error: #{e.message}") raise # Solid Queue가 재시도하도록 end end ``` ### 3. Admin::QtThemesController - 파일: `app/controllers/admin/qt_themes_controller.rb` - Admin::BaseController 상속 (이미 authorize_admin! 포함) - 액션: index, show, generate ```ruby module Admin class QtThemesController < BaseController def index @themes = QtTheme.order(created_at: :desc) end def show @theme = QtTheme.find(params[:id]) @contents = @theme.qt_contents.order(:day_number) end def generate @theme = QtTheme.find(params[:id]) if @theme.generating? redirect_to admin_qt_theme_path(@theme), alert: "이미 생성 중입니다." return end # 기존 콘텐츠 삭제 후 재생성 @theme.qt_contents.destroy_all if @theme.qt_contents.any? GenerateQtContentsJob.perform_later(@theme.id) redirect_to admin_qt_theme_path(@theme), notice: "AI 콘텐츠 생성이 시작되었습니다." end end end ``` ### 4. 라우트 수정 - 파일: `config/routes.rb` - 기존 admin namespace에 추가: ```ruby namespace :admin do root "dashboard#index" resources :qt_themes, only: [:index, :show] do member do post :generate end end end ``` - **중요**: 기존 routes.rb를 먼저 읽고, admin namespace 안에 추가 ### 5. Admin 뷰 - 파일: `app/views/admin/qt_themes/index.html.erb` (신규) - 테마 목록 테이블 (제목, 일수, 상태 뱃지, AI 여부, 생성 버튼) - shared/_table, _badge 파셜 활용 - admin layout 사용 확인 - 파일: `app/views/admin/qt_themes/show.html.erb` (신규) - 테마 상세 정보 - 콘텐츠 목록 (day_number, bible_passage, theme_title) - generation_status에 따른 UI 분기: - draft: "AI 생성" 버튼 표시 - generating: "생성 중..." 로딩 표시 - completed: "생성 완료" 뱃지 + 콘텐츠 목록 - published: "발행됨" 뱃지 - "AI 생성" 버튼: `button_to generate_admin_qt_theme_path(@theme), method: :post` ### 6. 테스트 - 파일: `test/services/ai_qt_generator_test.rb` (신규) - API 호출 stub - 정상 생성 테스트 - 에러 처리 테스트 - 파일: `test/jobs/generate_qt_contents_job_test.rb` (신규) - job enqueue 테스트 - 상태 전환 테스트 - 파일: `test/controllers/admin/qt_themes_controller_test.rb` (신규) - admin 사용자 index 접근 - admin 사용자 show 접근 - admin 사용자 generate 요청 - 일반 사용자 접근 차단 - 비인증 사용자 접근 차단 ## 기존 코드 참고 (반드시 먼저 읽을 것) - `app/services/ai_meditation_analyzer.rb` - AI 서비스 패턴 - `app/services/ai_sermon_interpreter.rb` - AI 서비스 패턴 - `app/controllers/admin/base_controller.rb` - Admin 인증 - `app/controllers/admin/dashboard_controller.rb` - Admin 컨트롤러 예시 - `app/models/qt_theme.rb` - enum, 관계 - `app/models/qt_content.rb` - validates, 컬럼 - `app/views/admin/` - 기존 admin 레이아웃/뷰 구조 - `test/fixtures/qt_themes.yml` - fixture 구조 - `test/fixtures/qt_contents.yml` - fixture 구조 - `config/routes.rb` - 현재 라우트 구조 ## 주의사항 - shared 파셜의 strict locals 준수 (반드시 파셜 파일을 읽어서 필요한 locals 확인) - ERB 멀티라인 주석 안에 ERB 태그 금지 (SystemStackError) - UUID PK 사용 - parallelize(workers: 1) 테스트 설정 확인 - 기존 테스트가 깨지지 않게 주의 - admin layout이 있는지 확인: `app/views/layouts/admin.html.erb` - SQLite JSON 컬럼은 자동 파싱 안 됨 → JSON.parse() 사용 필요할 수 있음 - 전체 테스트 통과 확인: bin/rails test

A
ai-gen-dev
9 days
높음 d5b723d1
서브 티켓 통계 차트 Chart.js

기도 통계 차트 + 통독 월별 트렌드 차트

## 목표 기도 통계에 도넛/바 차트 추가 + 통독에 월별 트렌드 바 차트 추가 ## 의존성 - chart-foundation 에이전트가 chart_controller.js를 먼저 완성해야 함 ## 구현 내용 ### 1. 기도 통계 차트 (app/views/prayers/stats.html.erb) #### A. 응답 현황 도넛 차트 현재 텍스트로만 표시되는 응답 현황(keep_praying/waiting/yes/no)을 도넛 차트로 시각화 ```erb <div data-controller="chart" data-chart-type-value="doughnut" data-chart-data-value='<%= { labels: ["계속 기도 중", "응답 대기", "응답 받음", "응답 없음"], datasets: [{ data: [ @prayer_stats[:by_response][:keep_praying], @prayer_stats[:by_response][:waiting], @prayer_stats[:by_response][:yes], @prayer_stats[:by_response][:no] ], backgroundColor: ["#4f46e5", "#f59e0b", "#10b981", "#ef4444"] }] }.to_json %>'> <canvas data-chart-target="canvas"></canvas> </div> ``` #### B. 기존 텍스트 통계 카드는 유지 (차트와 함께 표시) ### 2. 통독 월별 트렌드 차트 #### A. app/controllers/tongtok_controller.rb 수정 index 액션에 월별 통독 데이터 추가: ```ruby @monthly_reading = current_user.bible_reading_logs .where(read_date: 6.months.ago.beginning_of_month..Date.current.end_of_month) .group("strftime('%Y-%m', read_date)") .count # → {"2025-09" => 15, "2025-10" => 22, ...} 형태 ``` #### B. app/views/tongtok/index.html.erb 수정 전체 진행률 카드 아래에 월별 트렌드 바 차트 추가: ```erb <div data-controller="chart" data-chart-type-value="bar" data-chart-data-value='<%= { labels: @monthly_reading.keys.map { |k| Date.parse("#{k}-01").strftime("%m월") }, datasets: [{ label: "읽은 장 수", data: @monthly_reading.values, backgroundColor: "rgba(79, 70, 229, 0.7)", borderRadius: 6 }] }.to_json %>'> <canvas data-chart-target="canvas"></canvas> </div> ``` ## 현재 파일 구조 ### prayers/stats.html.erb 구조 1. 헤더 (뒤로가기 + 제목) 2. 전체 통계 카드 (총 기도, 활성, 응답률) 3. 응답 현황 카드 ← 여기에 도넛 차트 추가 4. 카테고리별 카드 5. 기도 실천 카드 ### tongtok/index.html.erb 구조 1. 헤더 (제목, 모드 버튼) 2. 전체 진행률 카드 ← 이 아래에 월별 트렌드 추가 3. 구약/신약 탭 + 책 카드 그리드 ## 완료 기준 - [ ] 기도 통계에 응답 현황 도넛 차트 표시 - [ ] 통독에 월별 트렌드 바 차트 표시 - [ ] tongtok_controller.rb에 @monthly_reading 데이터 추가 - [ ] 다크모드 대응 - [ ] 모바일 반응형 - [ ] 기존 테스트 전체 통과 (bin/rails test) ## 담당 파일 - app/views/prayers/stats.html.erb (수정) - app/controllers/tongtok_controller.rb (수정) - app/views/tongtok/index.html.erb (수정)

C
chart-prayer-tongtok
8 days
높음 e8a43807
서브 티켓 [P2] 세션 통계/멤버 관리

기도 통계 + QT→기도제목 가져오기 - stats/import_from_qt 액션 + 뷰 + 테스트

## 목표 기도 통계 페이지와 QT에서 기도제목 가져오기 기능 구현 ## 구현 항목 ### 1. PrayersController에 stats 액션 추가 - 파일: `app/controllers/prayers_controller.rb` - `stats` 액션 추가: ```ruby def stats prayers = current_user.prayer_requests active_prayers = prayers.where(is_active: true) @prayer_stats = { total: prayers.count, active: active_prayers.count, by_response: { keep_praying: prayers.where(response_type: :keep_praying).count, waiting: prayers.where(response_type: :waiting).count, yes: prayers.where(response_type: :yes).count, no: prayers.where(response_type: :no).count }, answer_rate: calculate_answer_rate(prayers), by_category: { daily: prayers.where(category: :daily).count, weekly: prayers.where(category: :weekly).count }, recent_30days_check_rate: calculate_check_rate(current_user), total_checks: current_user.prayer_check_logs.count } end private def calculate_answer_rate(prayers) total = prayers.count return 0 if total.zero? answered = prayers.where(response_type: [:yes, :no, :waiting]).count (answered * 100.0 / total).round(1) end def calculate_check_rate(user) total_days = 30 checked_days = user.prayer_check_logs .where(check_date: 30.days.ago.to_date..Date.current) .select(:check_date).distinct.count (checked_days * 100.0 / total_days).round(1) end ``` ### 2. PrayersController에 import_from_qt 액션 추가 - `import_from_qt` 액션 (POST): ```ruby def import_from_qt meditation = current_user.user_meditations.find(params[:meditation_id]) if meditation.prayer_topic.blank? redirect_to prayers_path, alert: "가져올 기도제목이 없습니다." return end @prayer = current_user.prayer_requests.build( content: meditation.prayer_topic, category: :daily, response_type: :keep_praying, visibility: :private, is_active: true, sort_order: (current_user.prayer_requests.maximum(:sort_order) || 0) + 1 ) if @prayer.save redirect_to prayers_path, notice: "QT 묵상에서 기도제목을 가져왔습니다." else redirect_to prayers_path, alert: "기도제목 저장에 실패했습니다." end end ``` ### 3. 라우트 수정 - 파일: `config/routes.rb` - 기존 prayers 리소스에 collection 라우트 추가: ```ruby resources :prayers do collection do get :stats post :import_from_qt end # 기존 member 라우트 유지 member do post :check end end ``` - **주의**: 기존 prayers 라우트 구조를 잘 확인하고, 이미 member do ... end가 있으면 그 안에 추가하지 말고 collection do ... end를 별도로 추가 ### 4. 기도 통계 뷰 - 파일: `app/views/prayers/stats.html.erb` (신규) - 디자인: - 상단: 뒤로가기 + "기도 통계" 제목 - 카드 1: 전체 통계 (총 기도수, 활성, 응답률) - 카드 2: 응답 현황 (keep_praying/waiting/yes/no 각각 개수 + 비율) - 카드 3: 카테고리별 (매일/주간) - 카드 4: 기도 실천 (최근 30일 이행률 + 총 체크 수) - shared 파셜 활용: _card, _badge, _progress, _separator ### 5. prayers/index.html.erb 수정 - 기존 index 상단에 "통계 보기" 링크 버튼 추가 ```erb <%= link_to "통계", stats_prayers_path, class: "..." %> ``` ### 6. QT→기도제목 가져오기 UI - UserMeditation의 prayer_topic이 있는 묵상에서 "기도 목록에 추가" 버튼 표시 - 파일: `app/views/qt/_meditation_form.html.erb` 또는 묵상 상세 페이지 - **주의**: 묵상 뷰 파일을 먼저 확인하고, prayer_topic 필드가 어디서 표시되는지 파악한 후 적절한 위치에 버튼 추가 - 버튼은 `button_to import_from_qt_prayers_path(meditation_id: meditation.id), method: :post` 사용 ### 7. 테스트 - 파일: `test/controllers/prayers_controller_test.rb`에 추가 - stats 테스트: ```ruby test "stats shows prayer statistics" do sign_in users(:daniel) get stats_prayers_path assert_response :success end test "stats requires authentication" do get stats_prayers_path assert_redirected_to new_user_session_path end ``` - import_from_qt 테스트: ```ruby test "import_from_qt creates prayer from meditation" do sign_in users(:daniel) meditation = user_meditations(:completed_meditation) # prayer_topic이 있는 fixture assert_difference "PrayerRequest.count", 1 do post import_from_qt_prayers_path, params: { meditation_id: meditation.id } end assert_redirected_to prayers_path end test "import_from_qt rejects blank prayer_topic" do sign_in users(:daniel) meditation = user_meditations(:minimal_meditation) # prayer_topic이 없는 fixture assert_no_difference "PrayerRequest.count" do post import_from_qt_prayers_path, params: { meditation_id: meditation.id } end assert_redirected_to prayers_path end ``` ## 기존 코드 참고 - PrayerRequest 모델: category(daily/weekly), response_type(keep_praying/waiting/yes/no), visibility(private/partners/qt_plan/partners_qt_plan) - PrayerCheckLog: [user_id, prayer_request_id, check_date] 유니크 - UserMeditation: prayer_topic 컬럼 존재 - prayers/index.html.erb: 기존 목록 뷰 확인 후 통계 링크 추가 ## 주의사항 - 기존 prayers 테스트가 깨지지 않게 주의 - fixture 날짜는 과거 고정 날짜 사용 (Date.current 충돌 방지) - shared 파셜의 strict locals 준수 - ERB 멀티라인 주석 사용 금지 (각 줄 단일 라인 주석) - 전체 테스트 통과 확인: `bin/rails test` - `parallelize(workers: 1)` 테스트 설정 확인 - UUID PK 사용

P
prayer-stats-dev
9 days
보통 b093cbc0
서브 티켓 [P3] PWA + 모바일 지원 (coordination)

Capacitor Setup - npm init + 설정 + Android/iOS 기본 구성

## 목표 Capacitor 프로젝트 초기화 및 Android/iOS 빌드 기반 구축 ## 구현 항목 ### 1. npm 프로젝트 초기화 ```bash cd /home/daniel/dev/logbile2.0.0 npm init -y ``` - package.json의 name을 "logbible"로 수정 - description, version 등 기본 정보 설정 ### 2. Capacitor 설치 ```bash npm install @capacitor/core @capacitor/cli npx cap init logbible co.kr.logbible --web-dir public ``` - web-dir: public (Rails의 정적 파일 디렉토리) ### 3. capacitor.config.ts 커스터마이징 ```typescript import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'co.kr.logbible', appName: 'LogBible', webDir: 'public', server: { // 개발 중에는 Rails 서버로 프록시 url: process.env.CAPACITOR_SERVER_URL || undefined, cleartext: true }, plugins: { SplashScreen: { launchAutoHide: true, backgroundColor: '#6366f1', showSpinner: false, androidSplashResourceName: 'splash', splashFullScreen: false, splashImmersive: false, }, StatusBar: { style: 'DARK', backgroundColor: '#6366f1' } } }; export default config; ``` ### 4. Android 플랫폼 추가 ```bash npm install @capacitor/android npx cap add android ``` - android/ 디렉토리 생성됨 - 빌드 실패해도 설정 파일만 있으면 OK (Android SDK 없을 수 있음) ### 5. iOS 플랫폼 추가 (선택적) ```bash npm install @capacitor/ios npx cap add ios ``` - macOS가 아니면 실패할 수 있음 - 에러 무시 ### 6. Capacitor 플러그인 (기본) ```bash npm install @capacitor/splash-screen @capacitor/status-bar @capacitor/browser ``` ### 7. 빌드 스크립트 (package.json scripts) ```json { "scripts": { "cap:sync": "npx cap sync", "cap:open:android": "npx cap open android", "cap:open:ios": "npx cap open ios", "cap:build": "npx cap sync && echo 'Capacitor synced. Open Android Studio to build APK.'" } } ``` ### 8. .gitignore 업데이트 기존 .gitignore에 추가: ``` # Capacitor android/ ios/ node_modules/ ``` - android/, ios/ 디렉토리는 로컬에서 생성하므로 gitignore ### 9. 개발 가이드 (script/capacitor-setup.sh) ```bash #!/bin/bash # Capacitor 개발 환경 설정 스크립트 echo "=== LogBible Capacitor Setup ===" echo "" echo "1. npm install 실행..." npm install echo "" echo "2. Android 플랫폼 추가..." npx cap add android 2>/dev/null || echo "Android SDK가 필요합니다. https://developer.android.com/studio" echo "" echo "3. iOS 플랫폼 추가 (macOS only)..." npx cap add ios 2>/dev/null || echo "macOS + Xcode가 필요합니다." echo "" echo "=== 설정 완료 ===" echo "Android: npx cap open android (Android Studio 필요)" echo "iOS: npx cap open ios (Xcode 필요)" ``` ## 테스트 - `npm install`이 성공하는지 확인 - capacitor.config.ts 파일이 존재하는지 확인 - 기존 Rails 테스트에 영향 없는지 확인: bin/rails test ## 주의사항 - Rails 프로젝트 루트에서 npm 명령어 실행 - package.json은 이 에이전트만 관리 (충돌 방지) - node_modules/는 .gitignore에 추가 - android/, ios/도 .gitignore에 추가 (로컬 빌드 전용) - bin/rails test로 전체 테스트 통과 확인 (Capacitor가 Rails에 영향 없어야 함) - npx cap add 실패해도 OK (SDK 없을 수 있음) - config 파일만 있으면 성공

C
capacitor-dev
9 days
높음 4681034b
서브 티켓 [P3] AI 콘텐츠 자동 생성

AI 묵상 정리 (organize_meditation) - 서비스 + 컨트롤러 + UI + 테스트

## 목표 음성인식으로 입력된 묵상 텍스트를 AI가 정리해주는 기능 구현 (Q23) ## 배경 - 사용자가 음성인식으로 묵상 내용을 입력하면 오타/문법 오류가 많음 - AI가 원본 말투를 유지하면서 오타/문법만 수정하고 반복을 제거 - 가벼운 작업이므로 동기 처리 (백그라운드 Job 불필요) ## 구현 항목 ### 1. AiMeditationOrganizer 서비스 - 파일: `app/services/ai_meditation_organizer.rb` - 기존 서비스 패턴 (AiMeditationAnalyzer, AiSermonInterpreter)을 따를 것 - 핵심: ```ruby class AiMeditationOrganizer def initialize(text, passage_reference: nil) @text = text @passage_reference = passage_reference end def call return { success: false, error: "정리할 텍스트가 없습니다." } if @text.blank? client = OpenAI::Client.new(access_token: api_key) response = client.chat(parameters: { model: ai_model, messages: [{ role: "user", content: build_prompt }], temperature: 0.3 # 낮은 temperature - 내용 보존 우선 }) organized = response.dig("choices", 0, "message", "content") { success: true, organized_text: organized&.strip } rescue => e { success: false, error: e.message } end private def build_prompt # 레거시 프롬프트 기반: # - 원본 말투 유지 # - 오타/문법만 수정 # - 반복 제거 # - 내용 추가 금지 # - passage_reference가 있으면 성경 구절 맥락 제공 end def api_key ENV["OPENAI_API_KEY"] || ENV["GEMINI_API_KEY"] || raise("AI API 키가 설정되지 않았습니다") end def ai_model ENV["OPENAI_API_KEY"] ? "gpt-4o-mini" : "gemini-2.0-flash" end end ``` ### 2. MeditationsController에 organize 액션 추가 - 파일: `app/controllers/qt/meditations_controller.rb` (또는 해당 컨트롤러) - **먼저** 기존 묵상 컨트롤러를 찾아서 읽을 것 (qt/ namespace 확인) - organize 액션 추가: ```ruby def organize @meditation = current_user.user_meditations.find(params[:id]) result = AiMeditationOrganizer.new( @meditation.personal_meditation, passage_reference: @meditation.qt_content&.reading_passage ).call if result[:success] @meditation.update(personal_meditation: result[:organized_text]) respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.replace( "meditation-content", partial: "qt/meditations/meditation_content", locals: { meditation: @meditation } )} format.html { redirect_to qt_meditation_path(@meditation), notice: "묵상이 정리되었습니다." } end else respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.replace( "organize-error", html: "<p class='text-red-500 text-sm mt-2'>#{result[:error]}</p>" )} format.html { redirect_to qt_meditation_path(@meditation), alert: result[:error] } end end end ``` ### 3. 라우트 수정 - 파일: `config/routes.rb` - 기존 묵상 리소스에 member 라우트 추가: ```ruby # meditations 리소스 안에: member do post :organize end ``` - **중요**: 기존 routes.rb를 먼저 읽고, 묵상 관련 라우트 구조를 파악한 후 추가 ### 4. UI - "정리하기" 버튼 - **먼저** 묵상 관련 뷰 파일을 탐색하여 어디에 버튼을 추가할지 결정: - `app/views/qt/meditations/` 디렉토리 확인 - 묵상 show 페이지 또는 form에서 personal_meditation 필드 근처 - 버튼 디자인: - personal_meditation이 있을 때만 표시 - `button_to "AI 정리", organize_qt_meditation_path(@meditation), method: :post` - 또는 Turbo 방식: data-turbo-method="post" - **Turbo Stream** 응답을 위한 파셜 필요: - `qt/meditations/_meditation_content.html.erb` (정리된 내용 표시) ### 5. 테스트 - 파일: `test/services/ai_meditation_organizer_test.rb` (신규) - 정상 정리 테스트 (API stub) - 빈 텍스트 에러 처리 - API 실패 에러 처리 - 파일: 기존 묵상 컨트롤러 테스트에 추가 - organize 인증 필수 - organize 성공 시 텍스트 업데이트 - organize 실패 시 에러 처리 - 타인 묵상 접근 불가 ## 기존 코드 참고 (반드시 먼저 읽을 것) - `app/services/ai_sermon_interpreter.rb` - AI 서비스 패턴 + Turbo Stream 응답 패턴 - `app/controllers/sermons_controller.rb` - interpret 액션 (동일 패턴) - `app/controllers/qt/meditations_controller.rb` 또는 유사 컨트롤러 - 묵상 CRUD - 묵상 관련 뷰 파일들 (qt/ 아래) - `app/models/user_meditation.rb` - 모델 구조 - `test/fixtures/user_meditations.yml` - fixture ## 주의사항 - **반드시** 기존 묵상 컨트롤러와 뷰를 먼저 읽고, 그 패턴에 맞게 구현 - AI 서비스 패턴: ruby-openai gem, OpenAI::Client.new(access_token: api_key) - temperature 0.3 (낮음) - 원본 내용 보존 우선 - shared 파셜의 strict locals 준수 - ERB 멀티라인 주석 안에 ERB 태그 금지 (SystemStackError) - UUID PK 사용 - parallelize(workers: 1) 테스트 설정 확인 - 기존 테스트가 깨지지 않게 주의 - 전체 테스트 통과 확인: bin/rails test - config/routes.rb 수정 시 기존 구조를 깨지 않게 주의

A
ai-organize-dev
9 days
높음 5205ccfd
서브 티켓 [P3] QT 테마/콘텐츠 관리 CRUD

Admin QT 테마 CRUD + 라우트 + 사이드바 + 테스트

## 목표 Admin namespace에 QT 테마 CRUD 구현 ## 구현 항목 ### 1. Admin::QtThemesController (신규) - 파일: `app/controllers/admin/qt_themes_controller.rb` - Admin::BaseController 상속 - 액션: index, show, new, create, edit, update, destroy - index: QtTheme.order(created_at: :desc), 페이지네이션 불필요 (수량 적음) - show: @theme + @theme.qt_contents.order(:day_number) - new/create: QtTheme.new / .build - edit/update: 기본 CRUD - destroy: 연관 콘텐츠 있으면 삭제 경고 - 추가 액션: `toggle_active` (PATCH) - is_active 토글 - 추가 액션: `update_status` (PATCH) - generation_status 변경 - strong params: title, description, is_default, is_active, total_day, is_ai_generated, ai_prompt, bible_books, generation_status ### 2. 라우트 수정 - `config/routes.rb`의 admin namespace에 추가: ```ruby namespace :admin do root "dashboard#index" resources :qt_themes do member do patch :toggle_active patch :update_status end resources :qt_contents end end ``` ### 3. 사이드바 수정 - `app/views/admin/shared/_sidebar.html.erb` - 대시보드 링크와 separator 사이에 QT 테마 메뉴 추가: - "QT 테마 관리" (admin_qt_themes_path) - 책 아이콘 SVG ### 4. 뷰 (신규) - `app/views/admin/qt_themes/index.html.erb`: - 제목 "QT 테마 관리" + "새 테마" 버튼 - shared/_table.html.erb로 목록 (헤더: 제목, 일수, 상태, 활성, 생성일, 액션) - generation_status는 shared/_badge.html.erb로 표시 (draft=info, generating=warning, completed=success, published=success) - is_active는 토글 버튼 - 액션: 보기, 수정, 삭제 링크 - `app/views/admin/qt_themes/show.html.erb`: - 테마 상세 정보 카드 - 콘텐츠 목록 (shared/_table.html.erb) + "콘텐츠 추가" 버튼 - 상태 변경 버튼들 (draft→generating→completed→published) - `app/views/admin/qt_themes/new.html.erb` + `edit.html.erb`: - _form.html.erb 파셜 렌더링 - `app/views/admin/qt_themes/_form.html.erb`: - shared/_input.html.erb 사용: title(required), description(textarea), total_day(number, required), bible_books, ai_prompt - shared/_select.html.erb 사용: generation_status - 체크박스: is_default, is_active, is_ai_generated (Tailwind 스타일 직접 구현) - 제출/취소 버튼 ### 5. 테스트 (신규) - `test/controllers/admin/qt_themes_controller_test.rb` - 테스트 케이스: - admin 로그인 후 테마 목록 조회 (GET /admin/qt_themes) - admin 로그인 후 테마 생성 (POST /admin/qt_themes) - admin 로그인 후 테마 수정 (PATCH /admin/qt_themes/:id) - admin 로그인 후 테마 삭제 (DELETE /admin/qt_themes/:id) - admin 로그인 후 활성화 토글 (PATCH /admin/qt_themes/:id/toggle_active) - admin 로그인 후 상태 변경 (PATCH /admin/qt_themes/:id/update_status) - 일반 사용자 접근 차단 - Fixture: qt_themes(:default_theme), users(:admin) ## 주의사항 - UUID PK (ApplicationRecord의 set_uuid) - ERB 멀티라인 주석 금지 - shared 파셜 strict locals 정확히 준수: - _table: headers:, rows:, empty_message: "데이터가 없습니다." - _badge: text:, variant: :info - _input: form:, field:, label:, type: :text, required: false, hint: nil, placeholder: nil - _select: form:, field:, label:, options:, selected: nil, required: false, include_blank: "선택하세요", hint: nil - _card: variant: :default, padding: :md - _separator: (no locals) - Tailwind CSS v4 의미 기반 색상 사용 - 전체 테스트 통과 확인: bin/rails test

A
admin-themes
9 days
높음 15d4b692
서브 티켓 [P3] QT 테마/콘텐츠 관리 CRUD

Admin QT 콘텐츠 CRUD + 테스트

## 목표 Admin namespace에 QT 콘텐츠 CRUD 구현 (테마 하위 nested resource) ## 선행 조건 - admin-themes 에이전트가 라우트와 테마 CRUD를 완성한 후 시작 ## 구현 항목 ### 1. Admin::QtContentsController (신규) - 파일: `app/controllers/admin/qt_contents_controller.rb` - Admin::BaseController 상속 - before_action :set_theme (모든 액션) - before_action :set_content (show, edit, update, destroy) - 액션: index, show, new, create, edit, update, destroy - index: @theme.qt_contents.order(:day_number) - show: 콘텐츠 상세 - new: @theme.qt_contents.build(day_number: next_day) - create/update: 기본 CRUD - destroy: 삭제 후 테마 show로 redirect - strong params: day_number, week_number, bible_passage, reading_passage, theme_title, content, questions, difficulty, estimated_minutes, tags, bible_chapter, bible_verse - questions 파라미터 처리: JSON 문자열 → 배열 변환 ### 2. 뷰 (신규) - `app/views/admin/qt_contents/index.html.erb`: - 제목 "[테마명] 콘텐츠 관리" + "콘텐츠 추가" 버튼 + "테마로 돌아가기" 링크 - shared/_table.html.erb로 목록 (헤더: 일차, 주차, 성경구절, 제목, 난이도, 액션) - 난이도는 숫자 (1-5) - `app/views/admin/qt_contents/show.html.erb`: - 콘텐츠 상세 (성경구절, 제목, 본문, 질문, 난이도, 소요시간) - 질문은 번호 매기기 (ol 태그) - 수정/삭제 버튼 - `app/views/admin/qt_contents/new.html.erb` + `edit.html.erb`: - _form.html.erb 파셜 렌더링 - `app/views/admin/qt_contents/_form.html.erb`: - shared/_input 사용: day_number(number, required), week_number(number), bible_passage(required), reading_passage, theme_title(required), content(textarea, required), estimated_minutes(number), tags, bible_chapter(number), bible_verse(number) - shared/_select 사용: difficulty (1-5 선택지) - questions: textarea (JSON 배열 형태, 한 줄에 질문 하나, JS 없이 각 줄을 배열로 변환) - 예시 placeholder: "하나님의 창조 순서에서 무엇을 느꼈나요?\n오늘의 말씀이 나에게 주는 의미는?" - 컨트롤러에서 줄바꿈으로 split하여 배열로 저장 - 제출/취소 버튼 ### 3. 테스트 (신규) - `test/controllers/admin/qt_contents_controller_test.rb` - 테스트 케이스: - admin 로그인 후 콘텐츠 목록 조회 (GET /admin/qt_themes/:theme_id/qt_contents) - admin 로그인 후 콘텐츠 생성 (POST /admin/qt_themes/:theme_id/qt_contents) - admin 로그인 후 콘텐츠 수정 (PATCH /admin/qt_themes/:theme_id/qt_contents/:id) - admin 로그인 후 콘텐츠 삭제 (DELETE /admin/qt_themes/:theme_id/qt_contents/:id) - 일반 사용자 접근 차단 - Fixture: qt_themes(:default_theme), qt_contents(:day_one), users(:admin) - nested URL 패턴: admin_qt_theme_qt_contents_path(qt_themes(:default_theme)) ## 주의사항 - UUID PK (ApplicationRecord의 set_uuid) - ERB 멀티라인 주석 금지 - shared 파셜 strict locals 정확히 준수 - Tailwind CSS v4 의미 기반 색상 사용 - questions JSON 배열 처리: 줄바꿈 split → 배열 변환, 표시 시 배열 → 줄바꿈 join - SQLite JSON 컬럼 주의: JSON.parse 필요할 수 있음 - 전체 테스트 통과 확인: bin/rails test

A
admin-contents
9 days