백로그
0할 일
8Rails 8.1.2 프로젝트 생성 + Docker Compose + .env
## 작업 내용 1. `rails new valueit --database=postgresql --css=tailwind --asset-pipeline=propshaft` 프로젝트 생성 2. Docker Compose 개발 환경 설정: - PostgreSQL 16 서비스 - docker-compose.yml (개발용) - database.yml Docker 환경 호환 설정 3. `.env.example` 파일 생성 (PRD Section 5 환경변수 참조): - DATABASE_URL, RAILS_MASTER_KEY - TOSS_CLIENT_KEY, TOSS_SECRET_KEY, TOSS_WEBHOOK_SECRET - ANTHROPIC_API_KEY - VULTR_ACCESS_KEY, VULTR_SECRET_KEY, VULTR_BUCKET, VULTR_ENDPOINT, VULTR_REGION - NINEWAYS_API_KEY, NINEWAYS_API_URL - RESEND_API_KEY - SENTRY_DSN - DOMAIN 4. `.gitignore`에 `.env` 포함 확인 5. `bin/ci` 스크립트 생성 (테스트 + 린트) 6. git init + 첫 번째 커밋 ## 완료 기준 - [ ] `docker compose up`으로 PostgreSQL 기동 - [ ] `bin/rails server`로 Welcome 페이지 표시 - [ ] `.env.example` 파일 존재 (모든 키 포함) - [ ] `.gitignore`에 `.env` 포함 - [ ] `bin/rails test` 실행 가능 (0 tests, 0 failures) - [ ] bin/ci 스크립트 존재 및 실행 가능 ## 참조 - PRD: docs/valueit.md Section 3 (기술 스택), Section 5 (환경변수) - 스킬: `.claude/skills/rails-core/SKILL.md`, `.claude/skills/windows-docker/SKILL.md`
Gemfile 전체 gem 추가 + bundle install
## 작업 내용 PRD Section 4 (docs/valueit.md 155~217행)의 전체 Gemfile을 구성합니다. ### 추가할 gem 목록 **프로덕션:** - solid_queue, solid_cache, solid_cable, mission_control-jobs (Solid Stack) - bcrypt ~> 3.1.7 - ruby-anthropic (AI) - faraday ~> 2.0, faraday-retry (토스 HTTP) - resend (이메일) - aws-sdk-s3 (Vultr Object Storage, require: false) - pagy, ahoy_matey, meta-tags, sitemap_generator - sentry-ruby, sentry-rails - oj (JSON 최적화) - image_processing ~> 1.2 **⚠️ 중요: PRD에는 rspec-rails, factory_bot_rails가 있지만 이 프로젝트는 Minitest + Fixtures를 사용합니다.** - rspec-rails, factory_bot_rails, shoulda-matchers 제외 - 대신 기본 Minitest 유지 **development/test:** - debug - faker **development:** - web-console (이미 있을 수 있음) - annotate - letter_opener - rack-mini-profiler ### 완료 기준 - [ ] `bundle install` 성공 - [ ] `bin/rails test` 실행 가능 (0 tests, 0 failures) - [ ] 모든 gem 버전 호환 확인 - [ ] git commit ## 참조 - PRD: docs/valueit.md Section 4 (155~217행) - 스킬: `.claude/skills/rails-core/SKILL.md`
i18n 한국어 기본 설정 (ko.yml + application.rb)
## 작업 내용 PRD Section 14 (docs/valueit.md 1836~1876행) 기반 i18n 설정입니다. ### 1. config/application.rb 수정 ```ruby config.i18n.default_locale = :ko config.i18n.available_locales = [:ko, :en] config.i18n.fallbacks = [:en] ``` ### 2. config/locales/ko.yml 생성 PRD의 전체 ko.yml 내용: ```yaml ko: activerecord: errors: messages: blank: "을(를) 입력해주세요" taken: "이(가) 이미 사용 중입니다" too_short: "은(는) %{count}자 이상이어야 합니다" invalid: "형식이 올바르지 않습니다" models: user: attributes: email: { blank: "이메일을 입력해주세요", taken: "이미 사용 중인 이메일입니다" } password: { blank: "비밀번호를 입력해주세요" } name: { blank: "이름을 입력해주세요" } errors: messages: not_authenticated: "로그인이 필요합니다" not_authorized: "접근 권한이 없습니다" payments: success: "결제가 완료됐습니다" failed: "결제에 실패했습니다. 다시 시도해주세요" canceled: "결제가 취소됐습니다" plans: free: "무료" pro: "PRO" cohort: "코호트" ``` ⚠️ PRD에 이모지가 있지만 제거합니다 (taste-skill ANTI-EMOJI 정책). ### 완료 기준 - [ ] I18n.default_locale == :ko - [ ] ko.yml 파일 존재 - [ ] `bin/rails test` 통과 - [ ] git commit ## 참조 - PRD: docs/valueit.md Section 14 (1836~1876행)
User 모델 확장 — 마이그레이션 + enum + 메서드 + 테스트
## 목표 PRD Section 8.1 기반 users 테이블 확장 및 User 모델 구현. ## 현재 상태 - users 테이블: email_address, name, password_digest만 존재 - User 모델: has_secure_password, sessions 연관, email normalizes ## 마이그레이션 생성 (PRD Section 8.1) ``` add_column :users, :nickname, :string add_column :users, :avatar_url, :string add_column :users, :role, :integer, default: 0, null: false add_column :users, :nineways_uid, :string add_column :users, :nineways_strength_type, :string add_column :users, :nineways_dna_traits, :jsonb, default: {} add_column :users, :nineways_synced_at, :datetime add_column :users, :onboarding_goal, :integer, default: 0 add_column :users, :dev_level, :integer, default: 0 add_column :users, :onboarding_completed, :boolean, default: false add_column :users, :streak_days, :integer, default: 0 add_column :users, :last_active_at, :datetime add_column :users, :total_xp, :integer, default: 0 add_index :users, :nineways_uid add_index :users, :role ``` ## User 모델 확장 ```ruby enum :role, { free: 0, cohort: 1, admin: 2 } enum :onboarding_goal, { saas: 0, side_income: 1, portfolio: 2, validation: 3 } enum :dev_level, { none: 0, basic: 1, intermediate: 2, developer: 3 } # 연관 (현재 단계에서 추가 가능한 것만 — 다른 테이블은 아직 없음) has_many :cohort_applications, dependent: :destroy # 아직 테이블 없으나 선언만 def toss_customer_key = "valueit-user-#{id}" def can_access?(feature_level) case feature_level when :free then true when :cohort then role.in?(%w[cohort admin]) when :admin then admin? end end ``` ## ⚠️ 주의 - 기존 `email_address` 컬럼명 유지 (PRD의 `email`과 다름 — Rails 8 auth generator가 email_address로 생성) - 기존 normalizes, validates 유지 - 아직 없는 테이블(projects, cohort_applications 등)의 has_many는 추가하지 않거나, 존재하지 않는 테이블 참조 시 에러나지 않도록 주의 ## 테스트 (TDD) - role enum 동작 (free/cohort/admin) - onboarding_goal enum 동작 - dev_level enum 동작 - can_access?(:free) → 모든 유저 true - can_access?(:cohort) → cohort/admin만 true - can_access?(:admin) → admin만 true - toss_customer_key 포맷 확인 - fixture 업데이트 ## 완료 기준 - bin/rails db:migrate 성공 - 모델 테스트 전체 통과 - 기존 테스트 깨지지 않음
랜딩페이지 — PagesController + landing 뷰 + SEO + 테스트
## 목표 퍼블릭 랜딩페이지 구현. 다크 테마 + 라임 포인트. 프리미엄 에이전시 퀄리티. ## 현재 상태 - root는 sessions#new (→ pages#landing으로 변경 필요) - Tailwind 디자인 시스템 완료: bg #070707, accent #C8FF00, green #00E5A0 - 기존 Partial: _navbar, _button, _card, _input, _flash - Pretendard + DM Mono 폰트 설정 완료 ## 구현 사항 ### 1. PagesController ```ruby class PagesController < ApplicationController allow_unauthenticated_access def landing; end end ``` ### 2. 라우트 변경 ```ruby root "pages#landing" ``` 기존 `root "sessions#new"` 교체. ### 3. 랜딩페이지 뷰 (app/views/pages/landing.html.erb) PRD 기반 섹션: 1. **히어로** — 슬로건 "나만의 서비스를 세상에 내놓는 첫 번째 플랫폼", CTA "무료로 시작하기" 2. **3 Pain Points** — 아이디어만 있고 개발 못하는 사람, 코딩 배워도 뭘 만들지 모르는 사람, 시작했지만 완성 못하는 사람 3. **쇼케이스** — 런칭된 서비스 미리보기 (데이터 없으면 예시 카드) 4. **가격표** — Free/Cohort만 (Pro 제거). Free: 무료, AI 분석, 커뮤니티. Cohort: 299만원, 12주 과정, 1:1 코칭 5. **B2B 문의 CTA** — 기업 교육/대학 과정 도입 문의 6. **코호트 사전 신청 CTA** — "다음 기수 알림 받기" 7. **Footer** — 사업자 정보, 링크 ### 4. SEO meta-tags - title, description, OG tags, Twitter Card - application.html.erb의 head에서 content_for :meta_tags 지원 ### 5. 디자인 원칙 - `.claude/skills/ui-design/SKILL.md` 반드시 참조 - `.claude/skills/taste-skill/SKILL.md` (Supanova Design Engine) 반드시 참조 - 기존 shared partial 활용 (_button, _card) - 다크 테마 기본 (bg-bg, text-text-primary) - 라임 포인트 (text-accent, bg-accent) - 모바일 우선 반응형 - 한국어 word-break: keep-all 유지 ### 6. 테스트 - PagesController 테스트 (landing 접근 가능, 200 응답) - 미인증 상태에서 접근 가능 확인 - 주요 섹션 존재 확인 (히어로, 가격표 등) ## 완료 기준 - root → pages#landing 동작 - 모바일/데스크탑 반응형 - meta-tags 설정 - 가격표 Free/Cohort만 - B2B 문의 CTA, 코호트 사전 신청 CTA - CTA → 회원가입 연결 - 기존 테스트 깨지지 않음
BUILD→LAUNCH CTA + LAUNCH 접근 제어
## 작업 내용 1. 칸반 보드(dev/boards/show)에서 모든 티켓이 Done일 때 "출시 준비하기 →" CTA 표시 2. LAUNCH 페이지에서 빌드 미완료 프로젝트 필터링 (dev_board 있고 티켓이 있는 프로젝트만) 3. 대시보드 문구 "BUILD 탭" → 현재 네이밍에 맞게 업데이트 ## 배경 QA 리뷰에서 BUILD→LAUNCH 전환 CTA가 없고, LAUNCH가 빌드 완료 여부를 미확인하는 문제 발견
MCP 빌드킷에 설정 파일 포함 + CLAUDE.md 보드 메타데이터
## 작업 내용 1. 빌드킷 ZIP에 .claude/settings.local.json 포함 (MCP 서버 연결 설정, API 토큰은 빈값) 2. CLAUDE.md 생성 시 Dev Board 메타데이터 섹션 자동 append (board_id, URL) 3. 빌드킷 다운로드 페이지에 API 토큰 발급 안내 추가 ## 배경 P1 이슈 — 로컬에서 Claude Code로 작업 시작할 때 MCP 연결 정보가 빌드킷에 포함되지 않음
기존 todo/review 60개 티켓 정리
## 작업 내용 - review 60개 티켓: 이미 구현 완료된 초기 PRD 티켓들 → done 처리 또는 불필요 티켓 정리 - todo 5개 티켓: 초기 PRD 기반 티켓이지만 이미 구현됨 → done 처리 ## 배경 대시보드 보드에 오래된 티켓이 쌓여있어 현재 상태 파악이 어려움
진행 중
0리뷰
60[P1] DB 스키마 마이그레이션 Part 2 (결제/커뮤니티)
## 설명\nPRD Section 8.5~8.6, 8.10~8.15 마이그레이션.\n\n## 변경 사항 (PRD v2.1)\n- subscriptions 테이블 삭제 → cohort_applications 테이블로 대체\n- b2b_inquiries 테이블 추가\n- payments 테이블: subscription_id → cohort_id, plan 컬럼 제거\n\n## 포함 테이블\ncohort_applications, b2b_inquiries, payments, ai_conversations, cohorts, cohort_enrollments, showcase_services, community_posts, community_comments\n\n## 참조\n- PRD: Section 8.5~8.6, 8.10~8.15\n- 스킬: `database-migrations`\n\n## 완료 기준\n- [ ] `bin/rails db:migrate` 성공\n- [ ] cohort_applications 테이블 (status, motivation, current_job, expectation)\n- [ ] b2b_inquiries 테이블 (company_name, contact_name, contact_email, message)\n- [ ] payments 테이블 (cohort_id 참조, subscription_id 없음)\n- [ ] 모든 외래키/인덱스 설정\n- [ ] 모델 연관관계 설정 완료\n- [ ] fixture 파일 생성, 모델 테스트 통과\n\n## 의존성\n- [P1] DB 스키마 마이그레이션 Part 1
DB 마이그레이션 Part 2 — cohorts, cohort_applications, b2b_inquiries, payments, ai_conversations, cohort_enrollments, showcase_services, community_posts, community_comments
## 목표 PRD Section 8.5~8.6, 8.10~8.15 기반 9개 테이블 생성 + 모델 + 연관관계 + 테스트. ## 현재 DB 상태 이미 존재: users, sessions, projects, build_steps, curricula, lessons, user_lesson_progresses, ahoy_events, ahoy_visits ## 생성할 테이블 (PRD 그대로) ### 1. cohorts (Section 8.11) — 먼저 생성 (다른 테이블이 참조) ```ruby create_table :cohorts do |t| t.string :name, null: false t.integer :generation, null: false t.date :start_date, null: false t.date :end_date, null: false t.integer :max_capacity, default: 20 t.integer :price_cents, null: false # 2990000 t.string :slack_url t.boolean :accepting_applications, default: true t.text :description t.timestamps end ``` ### 2. cohort_applications (Section 8.5) ```ruby create_table :cohort_applications do |t| t.references :cohort, null: false, foreign_key: true t.references :user, null: false, foreign_key: true t.integer :status, default: 0, null: false # enum: { pending: 0, approved: 1, rejected: 2, converted: 3 } t.text :motivation t.string :current_job t.text :expectation t.datetime :approved_at t.datetime :rejected_at t.timestamps end add_index :cohort_applications, [:cohort_id, :user_id], unique: true add_index :cohort_applications, :status ``` ### 3. b2b_inquiries (Section 8.5.1) ```ruby create_table :b2b_inquiries do |t| t.string :company_name, null: false t.string :contact_name, null: false t.string :contact_email, null: false t.string :contact_phone t.integer :team_size t.text :message, null: false t.integer :status, default: 0, null: false # enum: { pending: 0, contacted: 1, closed: 2 } t.timestamps end add_index :b2b_inquiries, :status add_index :b2b_inquiries, :contact_email ``` ### 4. payments (Section 8.6) — cohort_id 참조 ```ruby create_table :payments do |t| t.references :user, null: false, foreign_key: true t.references :cohort, foreign_key: true t.string :toss_payment_key, null: false t.string :toss_order_id, null: false t.string :toss_order_name t.integer :amount, null: false t.string :payment_method t.string :card_company t.string :card_number t.integer :status, default: 0 # enum: { pending: 0, done: 1, canceled: 2, failed: 3 } t.datetime :paid_at t.datetime :canceled_at t.text :cancel_reason t.string :failure_code t.string :failure_message t.timestamps end add_index :payments, :toss_payment_key, unique: true add_index :payments, :toss_order_id add_index :payments, :user_id add_index :payments, :status ``` ### 5. ai_conversations (Section 8.10) ```ruby create_table :ai_conversations do |t| t.references :user, null: false, foreign_key: true t.references :project, foreign_key: true t.string :conversation_type, default: "coach" t.jsonb :messages, default: [] t.integer :total_tokens_used, default: 0 t.string :model_used t.timestamps end ``` ### 6. cohort_enrollments (Section 8.12) ```ruby create_table :cohort_enrollments do |t| t.references :cohort, null: false, foreign_key: true t.references :user, null: false, foreign_key: true t.integer :status, default: 0 # enum: { pending: 0, active: 1, completed: 2, dropped: 3 } t.string :toss_payment_key t.string :toss_order_id t.datetime :paid_at t.datetime :completed_at t.timestamps end add_index :cohort_enrollments, [:cohort_id, :user_id], unique: true ``` ### 7. showcase_services (Section 8.13) ```ruby create_table :showcase_services do |t| t.references :user, null: false, foreign_key: true t.references :project, foreign_key: true t.string :title, null: false t.text :description t.string :service_url, null: false t.string :thumbnail_url t.string :category t.integer :days_to_build t.integer :user_count, default: 0 t.boolean :published, default: false t.integer :likes_count, default: 0 t.timestamps end ``` ### 8. community_posts (Section 8.14) ```ruby create_table :community_posts do |t| t.references :user, null: false, foreign_key: true t.string :post_type, default: "general" t.string :title t.text :content, null: false t.integer :likes_count, default: 0 t.integer :comments_count, default: 0 t.boolean :pinned, default: false t.timestamps end ``` ### 9. community_comments (Section 8.15) ```ruby create_table :community_comments do |t| t.references :community_post, null: false, foreign_key: true t.references :user, null: false, foreign_key: true t.references :parent, foreign_key: { to_table: :community_comments } t.text :content, null: false t.integer :likes_count, default: 0 t.timestamps end ``` ## 모델 구현 (PRD Section 9 참조) ### Cohort ```ruby has_many :cohort_applications, dependent: :destroy has_many :cohort_enrollments, dependent: :destroy has_many :users, through: :cohort_enrollments has_many :payments validates :name, :generation, :start_date, :end_date, :price_cents, presence: true scope :accepting, -> { where(accepting_applications: true) } def full? = cohort_enrollments.active.count >= max_capacity ``` ### CohortApplication ```ruby belongs_to :cohort belongs_to :user enum :status, { pending: 0, approved: 1, rejected: 2, converted: 3 } validates :motivation, presence: true validates :user_id, uniqueness: { scope: :cohort_id } ``` ### B2bInquiry ```ruby enum :status, { pending: 0, contacted: 1, closed: 2 } validates :company_name, :contact_name, :contact_email, :message, presence: true validates :contact_email, format: { with: URI::MailTo::EMAIL_REGEXP } ``` ### Payment ```ruby belongs_to :user belongs_to :cohort, optional: true enum :status, { pending: 0, done: 1, canceled: 2, failed: 3 } validates :toss_payment_key, presence: true, uniqueness: true validates :toss_order_id, :amount, presence: true ``` ### AiConversation ```ruby belongs_to :user belongs_to :project, optional: true validates :conversation_type, inclusion: { in: %w[coach idea_analyzer blueprint copy_generator claude_md] } ``` ### CohortEnrollment ```ruby belongs_to :cohort belongs_to :user enum :status, { pending: 0, active: 1, completed: 2, dropped: 3 } validates :user_id, uniqueness: { scope: :cohort_id } ``` ### ShowcaseService ```ruby belongs_to :user belongs_to :project, optional: true validates :title, :service_url, presence: true scope :published, -> { where(published: true) } ``` ### CommunityPost ```ruby belongs_to :user has_many :community_comments, dependent: :destroy validates :content, presence: true scope :pinned, -> { where(pinned: true) } ``` ### CommunityComment ```ruby belongs_to :community_post belongs_to :user belongs_to :parent, class_name: "CommunityComment", optional: true has_many :replies, class_name: "CommunityComment", foreign_key: :parent_id, dependent: :destroy validates :content, presence: true counter_culture :community_post, column_name: :comments_count # 또는 after_create/destroy 콜백 ``` ### User 모델 업데이트 (추가 연관관계) ```ruby has_many :ai_conversations, dependent: :destroy has_many :cohort_enrollments, dependent: :destroy has_many :cohorts, through: :cohort_enrollments has_many :cohort_applications, dependent: :destroy has_many :showcase_services, dependent: :destroy has_many :community_posts, dependent: :destroy has_many :payments, dependent: :destroy ``` ## ⚠️ 주의 - 마이그레이션 1개 파일로 생성하거나, 테이블 의존성 순서대로 여러 파일로 생성 - cohorts 테이블을 먼저 만들어야 cohort_applications, payments, cohort_enrollments가 참조 가능 - User 모델에 기존 연관관계(sessions, projects, user_lesson_progresses, lessons) 유지 - `email_address` 컬럼명 유지 (PRD의 `email`과 다름) - community_comments의 parent self-reference 외래키 주의 ## 테스트 (TDD) - 각 모델: validates, enum, 연관관계 테스트 - Cohort#full? 테스트 - Payment toss_payment_key uniqueness 테스트 - CommunityComment 중첩(parent/replies) 테스트 - fixture 파일 생성 (각 모델 최소 2개) ## 완료 기준 - bin/rails db:migrate 성공 - 모든 외래키/인덱스 설정 - 모델 테스트 전체 통과 - 기존 테스트 깨지지 않음
Curriculum CRUD — Admin 컨트롤러 + 뷰 + position 정렬 + published 필터 + 테스트
## 목표 Admin 네임스페이스에서 Curriculum CRUD 완전 구현. position 기반 정렬, published 토글. ## 현재 상태 - Curriculum 모델 존재: title, description, week_start, week_end, position, published. has_many :lessons, validates, scope :published - Admin::CurriculaController 스텁 존재 (빈 액션) - Admin::BaseController 존재 (require_admin before_action) - 라우트: `namespace :admin { resources :curricula do resources :lessons end }` - 공용 Partial: _button, _card, _input, _flash, _navbar ## 구현 사항 ### 1. Admin::CurriculaController 완전 구현 ```ruby class Admin::CurriculaController < Admin::BaseController before_action :set_curriculum, only: [:show, :edit, :update, :destroy] def index @curricula = Curriculum.order(:position) # published 필터 파라미터 지원: ?filter=published / ?filter=unpublished end def show; end def new @curriculum = Curriculum.new end def create @curriculum = Curriculum.new(curriculum_params) if @curriculum.save redirect_to admin_curriculum_path(@curriculum), notice: "커리큘럼이 생성되었습니다." else render :new, status: :unprocessable_entity end end def edit; end def update if @curriculum.update(curriculum_params) redirect_to admin_curriculum_path(@curriculum), notice: "커리큘럼이 수정되었습니다." else render :edit, status: :unprocessable_entity end end def destroy @curriculum.destroy redirect_to admin_curricula_path, notice: "커리큘럼이 삭제되었습니다." end private def set_curriculum @curriculum = Curriculum.find(params[:id]) end def curriculum_params params.require(:curriculum).permit(:title, :description, :week_start, :week_end, :position, :published) end end ``` ### 2. 뷰 (app/views/admin/curricula/) - **다크 테마** (앱 내부 — bg-bg, text-text-primary, bg-surface) - 기존 Partial 활용 (_button, _card, _input) - index: 테이블 형태 목록 (position 순서), published 필터 탭, 새로 만들기 버튼 - show: 커리큘럼 상세 + 하위 레슨 목록 (있으면) - new/edit: 폼 (_form partial 공유) - _form: title, description, week_start, week_end, position, published 체크박스 ### 3. published 필터 - index에서 ?filter=published, ?filter=unpublished, 기본은 전체 - 탭 UI로 구현 ### 4. position 기반 정렬 - index에서 Curriculum.order(:position) - 폼에서 position 입력 가능 ### 5. Admin 레이아웃 - Admin 전용 네비게이션이 필요하면 간단히 추가 (사이드바 또는 상단 탭) - 최소한 Admin 제목 + 뒤로가기 링크 ### ⚠️ 주의 - developer-2가 동시에 Build::ProjectsController 작업 중 — admin/ 범위만 수정 - 모델(curriculum.rb)은 이미 완성됨 — 모델 수정 불필요 (Curriculum 모델에 추가 scope 필요하면 가능) - User 모델 건드리지 않기 - 한국어 Flash 메시지 ### 테스트 (TDD) - Admin::CurriculaController 테스트: - admin 유저만 접근 가능 (non-admin → 리다이렉트) - CRUD 전체 동작 (create, read, update, destroy) - published 필터 동작 - position 순서 정렬 확인 - 유효하지 않은 입력 시 에러 표시 - fixture: curricula.yml에 이미 데이터 있을 것 — 확인 후 사용 ### 완료 기준 - Admin 커리큘럼 CRUD 전체 동작 - published/unpublished 필터 - position 기반 정렬 - 컨트롤러 테스트 통과 - bin/rails test 전체 통과
Lesson CRUD — Admin 컨트롤러 (Curriculum 하위) + 뷰 + content_type 렌더링 + 테스트
## 목표 Admin 네임스페이스에서 Lesson CRUD 구현. Curriculum 하위 리소스. content_type별 렌더링. Markdown 지원. ## 현재 상태 - Lesson 모델: belongs_to :curriculum, has_many :user_lesson_progresses. title, content, content_type(default: "article"), video_url, read_time_minutes, position, published, required_role(default: 0). scope :published - Admin::LessonsController 스텁 존재 (빈 액션 7개) - Admin::BaseController 존재 (require_admin) - 라우트: `namespace :admin { resources :curricula do resources :lessons end }` - Admin::CurriculaController 이미 구현됨 (show 페이지에서 레슨 목록 표시) - 공용 Partial: _button, _card, _input ## 구현 사항 ### 1. Admin::LessonsController ```ruby class Admin::LessonsController < Admin::BaseController before_action :set_curriculum before_action :set_lesson, only: [:show, :edit, :update, :destroy] def index @lessons = @curriculum.lessons.order(:position) end def show; end def new @lesson = @curriculum.lessons.build end def create @lesson = @curriculum.lessons.build(lesson_params) if @lesson.save redirect_to admin_curriculum_lesson_path(@curriculum, @lesson), notice: "레슨이 생성되었습니다." else render :new, status: :unprocessable_entity end end def edit; end def update if @lesson.update(lesson_params) redirect_to admin_curriculum_lesson_path(@curriculum, @lesson), notice: "레슨이 수정되었습니다." else render :edit, status: :unprocessable_entity end end def destroy @lesson.destroy redirect_to admin_curriculum_lessons_path(@curriculum), notice: "레슨이 삭제되었습니다." end private def set_curriculum @curriculum = Curriculum.find(params[:curriculum_id]) end def set_lesson @lesson = @curriculum.lessons.find(params[:id]) end def lesson_params params.require(:lesson).permit(:title, :content, :content_type, :video_url, :read_time_minutes, :position, :published, :required_role) end end ``` ### 2. 뷰 (app/views/admin/lessons/) - 다크 테마 (bg-bg, text-text-primary, bg-surface) - 기존 Partial 활용 - index: 레슨 테이블 (position 순서), content_type 아이콘, published 상태 - show: 레슨 상세 — content_type에 따라: - article: Markdown 렌더링 (simple_format 또는 sanitize 사용) - video: video_url 임베드 - guide: Markdown 렌더링 - new/edit: 폼 (_form partial) - _form: title, content(textarea), content_type(select: article/video/guide), video_url(video 선택 시), read_time_minutes, position, published(체크박스), required_role(select: 0=free/1=cohort/2=admin) ### 3. Markdown 렌더링 - 간단하게 `simple_format` 또는 content를 그대로 표시 (redcarpet gem이 없으면 simple_format 사용) - Admin show 페이지에서 미리보기 ### 4. content_type별 처리 - article: content(Markdown) 표시 - video: video_url iframe 임베드 (YouTube/Vimeo 지원) - guide: content 표시 + 가이드 스타일 ### ⚠️ 주의 - admin/lessons/ 범위만 수정 - developer-2가 동시에 서비스 객체 작업 중 — 모델 수정 불필요 - required_role은 integer (0=free, 1=cohort, 2=admin) — PRD에서 pro가 있었지만 v2.1에서 제거됨. free/cohort/admin만 사용 - User 모델 건드리지 않기 - 한국어 Flash 메시지 ### 테스트 (TDD) - Admin::LessonsController 테스트: - admin만 접근 가능 - CRUD 전체 (curriculum 하위 리소스) - content_type별 생성 - 유효하지 않은 입력 시 에러 - fixture: lessons.yml 확인 후 사용 ### 완료 기준 - Admin 레슨 CRUD 동작 (Curriculum 하위) - content_type별 렌더링 - position 정렬 - 컨트롤러 테스트 통과 - bin/rails test 전체 통과
학습자 레슨 뷰 — Learn 네임스페이스 컨트롤러 + 뷰 + 진행 추적 + 완료 버튼 + 테스트
## 목표 Learn 네임스페이스에서 학습자용 커리큘럼/레슨 뷰 구현. UserLessonProgress 생성. 완료 버튼. ## 현재 상태 - Learn::CurriculaController 스텁 (index, show) - Learn::LessonsController 스텁 (show, complete) - Learn::HomeController 스텁 (index) - 라우트: `namespace :learn { get "/", to: "home#index"; resources :curricula, only: [:index, :show] do resources :lessons, only: [:show] do post :complete, on: :member end end }` - Curriculum 모델: has_many :lessons, scope :published - Lesson 모델: belongs_to :curriculum, has_many :user_lesson_progresses, scope :published - UserLessonProgress 모델: belongs_to :user, belongs_to :lesson, completed(boolean), completed_at, started_at, time_spent_seconds - User: has_many :user_lesson_progresses, has_many :lessons(through) ## 구현 사항 ### 1. Learn::HomeController ```ruby def index @curricula = Curriculum.published.order(:position).includes(:lessons) end ``` ### 2. Learn::CurriculaController ```ruby def index @curricula = Curriculum.published.order(:position).includes(:lessons) end def show @curriculum = Curriculum.published.find(params[:id]) @lessons = @curriculum.lessons.published.order(:position) # 현재 유저의 진행 상태 로드 @progresses = Current.user.user_lesson_progresses .where(lesson_id: @lessons.pluck(:id)) .index_by(&:lesson_id) end ``` ### 3. Learn::LessonsController ```ruby def show @curriculum = Curriculum.published.find(params[:curriculum_id]) @lesson = @curriculum.lessons.published.find(params[:id]) @progress = Current.user.user_lesson_progresses.find_or_initialize_by(lesson: @lesson) # started_at 기록 @progress.update(started_at: Time.current) if @progress.started_at.nil? end def complete @curriculum = Curriculum.published.find(params[:curriculum_id]) @lesson = @curriculum.lessons.published.find(params[:id]) progress = Current.user.user_lesson_progresses.find_or_initialize_by(lesson: @lesson) progress.update!(completed: true, completed_at: Time.current) redirect_to learn_curriculum_lesson_path(@curriculum, @lesson), notice: "레슨을 완료했습니다!" end ``` ### 4. 뷰 - **다크 테마** (bg-bg, bg-surface, text-text-primary, bg-accent) - 기존 Partial 활용 (_button, _card) **learn/home/index.html.erb**: 커리큘럼 카드 목록 + 각 커리큘럼의 진행률 **learn/curricula/index.html.erb**: 커리큘럼 목록 (published만), week 범위 표시 **learn/curricula/show.html.erb**: 커리큘럼 상세 + 레슨 목록 - 각 레슨에 완료 체크 표시 (UserLessonProgress 기반) - content_type 아이콘 (article/video/guide) - read_time_minutes 표시 **learn/lessons/show.html.erb**: 레슨 상세 - content_type별 렌더링 (article→simple_format, video→iframe, guide→simple_format) - "완료" 버튼 (POST complete) - 이미 완료된 경우 "완료됨" 표시 - 이전/다음 레슨 네비게이션 ### 5. role 기반 접근 제어 (선택사항) - Lesson의 required_role 확인: 0=free(모두 접근), 1=cohort(cohort/admin만), 2=admin - 접근 불가 시 Flash + 리다이렉트 ### ⚠️ 주의 - learn/ 범위만 수정 (developer-2는 build/ 작업 중) - 모델 수정 불필요 - N+1 방지: includes, index_by 활용 - 한국어 Flash 메시지 ### 테스트 (TDD) - Learn::CurriculaController: index(published만), show - Learn::LessonsController: show, complete(progress 생성), 이미 완료 시 - Learn::HomeController: index - 인증 필요 확인 - fixture 활용 ### 완료 기준 - 커리큘럼/레슨 목록 페이지 - 레슨 상세 컨텐츠 표시 - "완료" 버튼 → progress 레코드 생성 - 완료 레슨 체크 표시 - bin/rails test 전체 통과
학습 대시보드 — Learn 홈 진행률 바 + 커리큘럼별 진행률 + Turbo Frames + 테스트
## 목표 Learn 홈(learn/home#index)을 학습 대시보드로 업그레이드. 커리큘럼별 진행률 바, 전체 진행률 표시. ## 현재 상태 - Learn::HomeController 구현됨: `@curricula = Curriculum.published.order(:position).includes(:lessons)` - learn/home/index.html.erb 존재 (커리큘럼 카드 목록) - UserLessonProgress 모델로 진행 추적 가능 - User has_many :user_lesson_progresses, has_many :lessons(through) ## 구현 사항 ### 1. Learn::HomeController 업그레이드 ```ruby def index @curricula = Curriculum.published.order(:position).includes(:lessons) # 유저의 전체 진행 상태 @total_lessons = Lesson.published.count @completed_lessons = Current.user.user_lesson_progresses.where(completed: true).count @overall_progress = @total_lessons.zero? ? 0 : (@completed_lessons.to_f / @total_lessons * 100).round # 커리큘럼별 진행률 계산 lesson_ids = Lesson.published.pluck(:id) completed_by_curriculum = Current.user.user_lesson_progresses .where(completed: true, lesson_id: lesson_ids) .joins(:lesson) .group("lessons.curriculum_id") .count @progress_by_curriculum = completed_by_curriculum end ``` ### 2. 뷰 업그레이드 (learn/home/index.html.erb) - **전체 진행률 섹션**: 큰 진행률 바 + "N / M 레슨 완료" 텍스트 - **커리큘럼별 카드**: 각 카드에 진행률 바 (완료 레슨 수 / 전체 레슨 수) - **진행률 바**: Tailwind CSS로 구현 (bg-surface 배경, bg-accent 채움) - **Turbo Frame**: 각 커리큘럼 진행률을 turbo_frame_tag로 감싸서 부분 갱신 가능 - **다크 테마** (bg-bg, bg-surface, text-text-primary, bg-accent) - 기존 Partial 활용 (_card) ### 3. 커리큘럼 진행률 partial (선택) - `learn/home/_curriculum_progress.html.erb` — 커리큘럼 카드 + 진행률 바 - turbo_frame_tag "curriculum_progress_#{curriculum.id}" ### ⚠️ 주의 - learn/ 범위만 수정 (developer-2는 services/ 작업 중) - 모델 수정 불필요 - N+1 방지: joins, group, count 사용 - 한국어 텍스트 ### 테스트 - Learn::HomeController: index(진행률 계산 확인) - 레슨 완료 후 진행률 변경 확인 - 인증 필요 확인 ### 완료 기준 - 커리큘럼별 진행률 퍼센트 표시 - 전체 진행률 바 - 다크 테마 디자인 - bin/rails test 전체 통과
메인 대시보드 — DashboardController + LEARN/BUILD 요약 위젯 + 스트릭/XP + 테스트
## 목표 로그인 후 메인 대시보드 (/dashboard). LEARN 진행률 요약, BUILD 프로젝트 요약, 스트릭/XP 표시. ## 현재 상태 - DashboardController 스텁 존재 (빈 index 액션) - 라우트: `get "/dashboard", to: "dashboard#index"` - User: streak_days, total_xp 필드 존재 - 학습 진행 데이터: UserLessonProgress (completed, completed_at) - 프로젝트 데이터: Current.user.projects ## 구현 사항 ### 1. DashboardController ```ruby class DashboardController < ApplicationController def index # LEARN 요약 @total_lessons = Lesson.published.count @completed_lessons = Current.user.user_lesson_progresses.where(completed: true).count @learn_progress = @total_lessons.zero? ? 0 : (@completed_lessons.to_f / @total_lessons * 100).round # BUILD 요약 @projects = Current.user.projects.order(updated_at: :desc).limit(3) @project_count = Current.user.projects.count # 스트릭/XP @streak_days = Current.user.streak_days @total_xp = Current.user.total_xp end end ``` ### 2. 뷰 (app/views/dashboard/index.html.erb) - **다크 테마** (bg-bg, bg-surface, text-text-primary, bg-accent) - 기존 Partial 활용 (_card, _button) **레이아웃**: 그리드 (모바일 1열, md 2열) - **환영 메시지**: "안녕하세요, {이름}님!" + 날짜 - **스트릭/XP 카드**: streak_days 일 연속, total_xp XP - **LEARN 위젯**: 진행률 바 + "N/M 레슨 완료" + "학습 계속하기" 링크 (→ /learn) - **BUILD 위젯**: 최근 프로젝트 카드 (최대 3개) + "새 프로젝트 시작" 버튼 (→ /build/projects/new) - 프로젝트 없으면 "첫 프로젝트를 시작해보세요" CTA - 각 프로젝트: title, status badge, completion_percentage ### 3. 온보딩 후 리다이렉트 - ApplicationController의 check_onboarding에서 온보딩 완료 시 /dashboard로 이동하는 로직이 이미 있는지 확인 - 없으면 온보딩 complete 후 redirect_to dashboard_path 추가 (Onboarding::StepsController의 complete 액션에서) ### ⚠️ 주의 - dashboard/ 범위만 수정 (developer-2는 build/blueprints/ 작업 중) - 모델 수정 불필요 - N+1 방지 - 한국어 텍스트 ### 테스트 - DashboardController: index 접근, 인증 필요, LEARN/BUILD 데이터 표시 - 프로젝트 없을 때 빈 상태 처리 ### 완료 기준 - /dashboard LEARN 진행률 위젯 - BUILD 프로젝트 카드 - 스트릭/XP 표시 - bin/rails test 전체 통과
ClaudeMdGeneratorService + Build::ClaudeMdsController + 미리보기/다운로드 + 테스트
## 목표 PRD Section 13.3 기반 Ai::ClaudeMdGeneratorService + Build::ClaudeMdsController 구현. CLAUDE.md 생성, 미리보기, 파일 다운로드. ## 현재 상태 - Build::ClaudeMdsController 스텁 (show, create + download member route) - 라우트: `resource :claude_md, only: [:show, :create] do get :download, on: :member end` - Project: claude_md_content(text), claude_md_generated_at(datetime) - BlueprintGeneratorService 패턴 참고 가능 ## PRD 코드 (Section 13.3) — API 호출 없이 프로젝트 데이터로 마크다운 생성 ```ruby class Ai::ClaudeMdGeneratorService def initialize(project:) @project = project @user = project.user end def call content = generate @project.update!(claude_md_content: content, claude_md_generated_at: Time.current) content end private def generate # 프로젝트 정보를 CLAUDE.md 마크다운으로 변환 # title, one_line_definition, target_customer, revenue_model, tech_stack, # core_features, db_schema_draft, pages_list, dev_priority 포함 # 개발 원칙, 코딩 컨벤션, 디자인 시스템 등 고정 섹션 추가 end def format_db_schema return "아직 생성되지 않음" unless @project.db_schema_draft.present? @project.db_schema_draft.map { |table, cols| "### #{table}\n#{cols.map { |c| "- #{c}" }.join("\n")}" }.join("\n\n") end end ``` ## 구현 사항 ### 1. app/services/ai/claude_md_generator_service.rb - PRD 코드 기반 (API 호출 없음, 순수 마크다운 생성) - project 데이터를 CLAUDE.md 형식으로 변환 - format_db_schema 헬퍼 ### 2. Build::ClaudeMdsController ```ruby class Build::ClaudeMdsController < ApplicationController before_action :set_project before_action :authorize_owner! def show # claude_md_content가 있으면 미리보기, 없으면 생성 버튼 end def create Ai::ClaudeMdGeneratorService.new(project: @project).call redirect_to build_project_claude_md_path(@project), notice: "CLAUDE.md가 생성되었습니다." end def download content = @project.claude_md_content || Ai::ClaudeMdGeneratorService.new(project: @project).call send_data content, filename: "CLAUDE.md", type: "text/markdown" end private def set_project = @project = Project.find(params[:project_id]) def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end end ``` ### 3. 뷰 (build/claude_mds/show.html.erb) - 미리보기: claude_md_content를 simple_format 또는 pre 태그로 표시 - "다운로드" 버튼 (→ download 액션) - "재생성" 버튼 (→ create 액션) - 미생성 시 "CLAUDE.md 생성하기" 버튼 - claude_md_generated_at 타임스탬프 표시 - 다크 테마, 기존 Partial ### ⚠️ 주의 - services/ai/ + build/claude_mds/ 범위만 (developer-2는 build/build_steps/ 작업 중) - 이 서비스는 API 호출 없음 (동기 실행) ### 테스트 - 서비스: CLAUDE.md 콘텐츠 생성 확인, project 필드 업데이트 - 컨트롤러: show, create, download, 인증/소유자 ### 완료 기준 - CLAUDE.md 생성 + 미리보기 - .md 파일 다운로드 - 재생성 가능 - bin/rails test 전체 통과
Spec 편집 UI — Build::SpecsController + Turbo Frame 인라인 편집 + core_features 편집 + 테스트
## 목표 AI 생성 spec(one_line_definition, core_features, target_customer, revenue_model) 수정 가능 폼. Turbo Frame 인라인 편집. ## 현재 상태 - Build::SpecsController 스텁 (show, update) - 라우트: `resources :projects do resource :spec, only: [:show, :update] end` - Project: one_line_definition, core_features(jsonb array), target_customer, revenue_model, competitor_analysis(jsonb) ## 구현 사항 ### 1. Build::SpecsController ```ruby class Build::SpecsController < ApplicationController before_action :set_project before_action :authorize_owner! def show; end def update if @project.update(spec_params) redirect_to build_project_spec_path(@project), notice: "스펙이 수정되었습니다." else render :show, status: :unprocessable_entity end end private def set_project = @project = Project.find(params[:project_id]) def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end def spec_params params.require(:project).permit(:one_line_definition, :target_customer, :revenue_model, core_features: []) end end ``` ### 2. 뷰 (build/specs/show.html.erb) - **Turbo Frame 인라인 편집**: 각 필드를 turbo_frame_tag로 감싸기 - 읽기 모드: 값 표시 + "편집" 버튼 - 편집 모드: 폼 입력 + "저장"/"취소" 버튼 - 또는 전체 편집 폼 방식 (더 간단) **표시 필드:** - one_line_definition: 텍스트 입력 - core_features: 리스트 — 각 항목 편집/삭제 가능, "항목 추가" 버튼 - target_customer: 텍스트 입력 - revenue_model: 텍스트 입력 - competitor_analysis: 읽기 전용 표시 (jsonb 구조) **core_features 편집:** - jsonb array이므로 params에서 core_features: [] 형태로 받기 - 각 항목에 text_field + 삭제 버튼 - "항목 추가" 버튼 (Stimulus controller 또는 간단한 JS) - Stimulus nested_form_controller 패턴 ### 3. 다크 테마 - bg-bg, bg-surface, text-text-primary, bg-accent - 기존 Partial 활용 ### ⚠️ 주의 - build/specs/ 범위만 (developer-2는 services/toss/ 작업 중) - Project 모델 수정 불필요 - core_features가 jsonb array — strong params에서 array 허용 ### 테스트 - Build::SpecsController: show, update(성공/실패), 인증/소유자 - core_features 업데이트 확인 ### 완료 기준 - Spec show 페이지 - 인라인 편집 + 저장 - core_features 항목 추가/삭제 - bin/rails test 전체 통과
코호트 일반결제 — Toss::PaymentService + checkout/confirm + Payment/CohortEnrollment 생성 + 테스트
## 목표 PRD Section 11.2 기반 Toss::PaymentService 구현. 코호트 1회성 결제. checkout 파라미터 생성, confirm 처리, Payment + CohortEnrollment 생성. ## 현재 상태 - Toss::Client 구현 완료 (confirm_payment, cancel_payment, get_payment) - Payment 모델: belongs_to :user, belongs_to :cohort(optional). toss_payment_key, toss_order_id, amount, status enum - CohortEnrollment 모델: belongs_to :cohort, belongs_to :user. status enum - Cohort 모델: name, price_cents 등 ## PRD 코드 (Section 11.2) ```ruby class Toss::PaymentService def initialize(user:, plan:, cohort: nil) @user = user @plan = plan @cohort = cohort @client = Toss::Client.new end def checkout_params { amount: amount, orderId: generate_order_id, orderName: order_name, customerKey: @user.toss_customer_key, customerName: @user.name, customerEmail: @user.email_address, successUrl: "#{ENV["APP_URL"]}/payments/success", failUrl: "#{ENV["APP_URL"]}/payments/fail" } end def confirm(payment_key:, order_id:) result = @client.confirm_payment(payment_key: payment_key, order_id: order_id, amount: amount) return Result.failure(result.error_message) unless result.success? save_payment(result.data, payment_key, order_id) activate_plan Result.success end private def amount case @plan when "cohort" then @cohort&.price_cents || 2_990_000 else raise "일반결제는 cohort만 지원" end end def order_name = "VALUEIT #{@cohort&.name || @plan.upcase}" def generate_order_id = "valueit-#{@plan}-#{@user.id}-#{Time.current.to_i}" def save_payment(data, payment_key, order_id) @user.payments.create!( cohort: @cohort, toss_payment_key: payment_key, toss_order_id: order_id, toss_order_name: order_name, amount: amount, payment_method: data["method"], card_company: data.dig("card", "company"), card_number: data.dig("card", "number"), status: :done, paid_at: Time.current ) end def activate_plan if @plan == "cohort" && @cohort @user.cohort_enrollments.create!(cohort: @cohort, status: :active, paid_at: Time.current) @user.update!(role: :cohort) end end end ``` ## 구현 사항 1. **app/services/toss/payment_service.rb** — PRD 코드 기반 2. **Result 패턴**: 간단한 Result struct (success/failure) — 또는 OpenStruct 사용 3. **⚠️ PRD의 `@user.email`을 `@user.email_address`로 변경** (Rails 8 auth generator가 email_address 컬럼 사용) 4. **⚠️ PRD의 `plan:` 컬럼은 payments 테이블에 없음** — save_payment에서 plan 컬럼 제거 5. **의존성 주입**: client: 파라미터로 테스트 용이성 6. **테스트**: checkout_params 생성, confirm 성공(Payment+CohortEnrollment 생성, User role 변경), confirm 실패 ### ⚠️ 주의 - services/toss/ 범위만 (developer-2는 launch/ 작업 중) - Payment, CohortEnrollment, User, Cohort 모델 건드리지 않기 - email_address 컬럼명 주의 (PRD의 email과 다름) ### 완료 기준 - checkout 파라미터 생성 - confirm → Payment + CohortEnrollment 생성 + User role :cohort - 실패 시 Result.failure - bin/rails test 전체 통과
토스 웹훅 — Payments::WebhooksController + HMAC 서명 검증 + PAYMENT_STATUS_CHANGED 처리 + 테스트
## 목표 PRD Section 11.4 기반 토스 웹훅 처리. CSRF skip, HMAC SHA-256 서명 검증, PAYMENT_STATUS_CHANGED 이벤트 처리. ## 현재 상태 - Payments::WebhooksController 스텁 존재 (receive 액션, allow_unauthenticated_access + skip_forgery_protection 설정됨) - 라우트: `post "/payments/webhook", to: "payments/webhooks#receive"` - Payment 모델: toss_payment_key, status enum {pending:0, done:1, canceled:2, failed:3}, paid_at, canceled_at ## PRD 코드 (Section 11.4) ```ruby class Payments::WebhooksController < ApplicationController skip_before_action :verify_authenticity_token allow_unauthenticated_access def receive unless valid_signature? return render json: { error: "Invalid signature" }, status: :unauthorized end payload = JSON.parse(request.body.read) case payload["eventType"] when "PAYMENT_STATUS_CHANGED" handle_payment_status_changed(payload["data"]) end render json: { received: true } end private def valid_signature? secret = ENV["TOSS_WEBHOOK_SECRET"] signature = request.headers["TossPayments-Signature"] body = request.body.read request.body.rewind expected = OpenSSL::HMAC.hexdigest("sha256", secret, body) ActiveSupport::SecurityUtils.secure_compare(expected, signature.to_s) end def handle_payment_status_changed(data) payment = Payment.find_by(toss_payment_key: data["paymentKey"]) return unless payment case data["status"] when "DONE" then payment.update!(status: :done, paid_at: Time.current) when "CANCELED" then payment.update!(status: :canceled, canceled_at: Time.current) when "ABORTED", "EXPIRED" then payment.update!(status: :failed) end end end ``` ## 구현 사항 1. **Payments::WebhooksController** — PRD 코드 기반 2. **⚠️ request.body.read 주의**: body를 두 번 읽으므로 rewind 필요 3. **HMAC SHA-256 서명 검증**: TOSS_WEBHOOK_SECRET + secure_compare 4. **PAYMENT_STATUS_CHANGED만 처리** (v2.1: BILLING_SKIPPED 삭제) 5. **allow_unauthenticated_access** 이미 설정되어 있을 수 있음 — 확인 후 유지 ### 테스트 - 유효한 서명 → 200 + Payment 상태 변경 - 무효한 서명 → 401 - DONE → payment.done, CANCELED → payment.canceled, ABORTED → payment.failed - 없는 paymentKey → 무시 (200 반환) - 알 수 없는 eventType → 무시 ### ⚠️ payments/webhooks 범위만 (developer-2는 payments/checkout, completions 작업 중) ## 대시보드 기록 (MCP) - 시작: `AddActivityLogTool` (ticket_id: "c547c6c5-b25a-4c42-a7fe-850261c83e5a", message: "토스 웹훅 시작") - 완료: `AddActivityLogTool` (message: "토스 웹훅 완료", details 필수) ## 완료 절차 1. `UpdateTicketStatusTool`로 서브 티켓 → `review` 2. `AddActivityLogTool`로 완료 기록 3. `SendMessage`로 팀리드에게 보고 (summary: "토스 웹훅 구현 완료") ⚠️ SendMessage 필수!
커뮤니티 피드 — CommunityPostsController CRUD + post_type 필터 + 핀 고정 + 페이지네이션 + 테스트
## 목표 CommunityPosts CRUD. post_type별 필터(general/question/launch/share). 핀 게시글 상단 고정. 페이지네이션(Pagy 또는 간단 구현). ## 현재 상태 - CommunityPostsController 스텁 (7개 CRUD 액션) - 라우트: `resources :community_posts do resources :community_comments, only: [:create, :destroy]; post :like, on: :member end` - CommunityPost 모델: belongs_to :user, has_many :community_comments. post_type(default: "general"), title, content(not null), likes_count, comments_count, pinned. scope :pinned - 공용 Partial: _button, _card, _input ## 구현 사항 ### 1. CommunityPostsController - index: post_type 필터(?type=general/question/launch/share), 핀 상단 고정, 최신순 정렬 - show: 게시글 + 댓글 목록 (댓글은 다음 티켓에서 구현 — 지금은 빈 영역) - new/create: Current.user.community_posts.build, post_type 선택 - edit/update: 소유자만 - destroy: 소유자만 - like: likes_count 증가 (간단 구현) - strong params: title, content, post_type ### 2. 뷰 - **index**: post_type 필터 탭 (전체/일반/질문/출시/공유), 핀 게시글 상단, 게시글 카드 리스트, "글쓰기" 버튼 - **show**: 게시글 상세, 좋아요 버튼, 수정/삭제(소유자), 댓글 영역(placeholder) - **new/edit**: 폼 (_form partial — title, content textarea, post_type select) - 다크 테마, 기존 Partial ### 3. 페이지네이션 - Pagy gem이 있으면 사용, 없으면 간단한 offset/limit 또는 Kaminari 없이 수동 구현 - 기본 20개씩 ### 4. 핀 게시글 - pinned: true인 게시글을 상단에 고정 표시 - 일반 사용자는 pin 불가 (admin만 — 지금은 구현 안 해도 됨) ### ⚠️ 주의 - community_posts/ 범위만 (developer-2는 showcase_services/ 작업 중) - CommunityComment는 다음 티켓 — 지금은 댓글 영역 placeholder만 - 소유자 접근제어 (edit/update/destroy) - 한국어 Flash ### 테스트 - CRUD 전체, 인증 필요, 소유자 접근제어, post_type 필터, like 동작 ### 완료 기준 - 게시글 CRUD + post_type 필터 + 핀 고정 - bin/rails test 전체 통과
커뮤니티 댓글 — CommunityCommentsController + 중첩 댓글 + 좋아요 토글 + Turbo Stream + 테스트
## 목표 CommunityComments CRUD (중첩 댓글 parent_id). 좋아요 토글. Turbo Stream 실시간 갱신. ## 현재 상태 - CommunityCommentsController 스텁 (create, destroy) - 라우트: `resources :community_posts do resources :community_comments, only: [:create, :destroy]; post :like, on: :member end` - CommunityComment 모델: belongs_to :community_post, :user, :parent(optional, self-ref). has_many :replies. validates :content. likes_count - CommunityPost#show에 댓글 placeholder 있음 ## 구현 사항 ### 1. CommunityCommentsController - create: 댓글 생성 (parent_id로 대댓글 지원). Turbo Stream 응답 - destroy: 소유자만 삭제. Turbo Stream 응답 - strong params: content, parent_id ### 2. 게시글 show 페이지에 댓글 영역 추가 - community_posts/show에 댓글 목록 + 댓글 작성 폼 - 대댓글: parent_id가 있으면 들여쓰기 - 각 댓글: 내용, 작성자, 시간, 삭제(소유자), 답글 버튼 - Turbo Frame으로 각 댓글 감싸기 ### 3. Turbo Stream - 댓글 생성 시: Turbo Stream으로 목록에 추가 (prepend/append) - 댓글 삭제 시: Turbo Stream으로 제거 ### 4. 좋아요 토글 (게시글) - CommunityPostsController#like 이미 구현됨 — 확인 후 필요시 개선 - 토글 방식: 한 번 누르면 +1, 다시 누르면 -1 (간단 구현 또는 현재 상태 유지) ### 5. comments_count 카운터 - 댓글 생성/삭제 시 community_post.comments_count 업데이트 (콜백 또는 counter_cache) ### ⚠️ 주의 - community_comments/ + community_posts/show 범위만 (developer-2는 cohorts/ 작업 중) - 1depth 대댓글만 (parent → replies, replies에는 답글 불가) - 다크 테마, 한국어 ### 테스트 - create (댓글 + 대댓글), destroy (소유자만), Turbo Stream 응답 - comments_count 업데이트 ### 완료 기준 - 댓글 작성/삭제, 대댓글 1depth, Turbo Stream 갱신 - bin/rails test 전체 통과
AI 코치 채팅 — Ai::CoachService + AiConversationsController + Turbo Stream 실시간 채팅 + 테스트
## 목표 AI 코치 채팅. AiConversation CRUD + Ai::CoachService (Claude API) + Turbo Stream 실시간 메시지. ## 현재 상태 - AiConversationsController 스텁 (index, create, show + message member) - 라우트: `resources :ai_conversations, only: [:index, :create, :show] do post :message, on: :member end` - AiConversation 모델: belongs_to :user, :project(optional). conversation_type, messages(jsonb, default: []), total_tokens_used, model_used ## 구현 사항 ### 1. Ai::CoachService ```ruby class Ai::CoachService def initialize(conversation:) @conversation = conversation @user = conversation.user @client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) end def call(user_message) # messages jsonb에 유저 메시지 추가 @conversation.messages << { role: "user", content: user_message, created_at: Time.current.iso8601 } # Claude API 호출 response = @client.messages( model: "claude-sonnet-4-6", max_tokens: 2000, system: system_prompt, messages: @conversation.messages.map { |m| { role: m["role"], content: m["content"] } } ) assistant_message = response.content.first.text @conversation.messages << { role: "assistant", content: assistant_message, created_at: Time.current.iso8601 } @conversation.save! assistant_message end private def system_prompt project_context = @conversation.project ? "프로젝트: #{@conversation.project.title} - #{@conversation.project.one_line_definition}" : "" "VALUEIT AI 코치. 20대가 서비스를 만들도록 돕는 멘토. 한국어로 답변. #{project_context}" end end ``` ### 2. AiConversationsController - index: Current.user.ai_conversations 목록 - create: 새 대화 생성 (project_id optional) - show: 대화 상세 + 메시지 목록 - message: 메시지 전송 → CoachService 호출 → Turbo Stream 응답 ### 3. 뷰 - index: 대화 목록 (최근순), "새 대화" 버튼 - show: 채팅 UI (메시지 버블, 입력 폼, turbo_stream_from) - _message partial: 유저/AI 메시지 스타일 분리 - Turbo Stream: 메시지 전송 시 append ### 4. DI 패턴 (client: 파라미터), 에러 처리, 한국어, 다크 테마 ### ⚠️ ai_conversations/ + services/ai/ 범위만 (developer-2는 mailers/ 작업 중) ### 테스트 - CoachService: 메시지 추가, API mock, 프로젝트 컨텍스트 - AiConversationsController: index, create, show, message(인증/소유자) ### 완료 기준 - 새 대화 생성 + 메시지 전송 + AI 응답 + Turbo Stream - bin/rails test 전체 통과
9WAY 연동 + Sentry 모니터링 — OAuth + SyncService + Sentry initializer + 테스트
## 목표 2개 티켓 동시 구현: 9WAY 연동 API + Sentry 에러 모니터링. ## 작업 1: 9WAY 연동 API + 동기화 ### 현재 상태 - Auth::NinewaysController 스텁 (redirect, callback) — allow_unauthenticated_access - My::NinewaysSyncsController 스텁 (create) - Api::V1::NinewaysController 스텁 (webhook) — allow_unauthenticated_access + skip_forgery_protection - User: nineways_uid, nineways_strength_type, nineways_dna_traits(jsonb), nineways_synced_at ### 구현 사항 1. **Nineways::SyncService** (app/services/nineways/sync_service.rb) - 9WAY API 호출 → User 필드 업데이트 (strength_type, dna_traits, synced_at) - DI 패턴, 에러 처리 2. **Auth::NinewaysController** - redirect: 9WAY OAuth URL로 리다이렉트 - callback: code → token → user info → User 업데이트 3. **My::NinewaysSyncsController#create** - 수동 동기화 버튼 → SyncService 호출 4. **Api::V1::NinewaysController#webhook** - 9WAY 웹훅 수신 → User 업데이트 5. **NinewaysSyncAllJob** (app/jobs/nineways_sync_all_job.rb) - nineways_uid가 있는 모든 유저 동기화 6. **테스트**: 서비스 + 컨트롤러 (API mock) ## 작업 2: Sentry 에러 모니터링 ### 구현 사항 1. **config/initializers/sentry.rb** ```ruby Sentry.init do |config| config.dsn = ENV["SENTRY_DSN"] config.breadcrumbs_logger = [:active_support_logger, :http_logger] config.traces_sample_rate = 0.1 config.environment = Rails.env config.enabled_environments = %w[production staging] end ``` 2. **ApplicationController에 Sentry 유저 컨텍스트** ```ruby before_action :set_sentry_context def set_sentry_context Sentry.set_user(id: Current.user&.id, email: Current.user&.email_address) if Current.user end ``` 3. **테스트**: initializer 로드 확인 ### ⚠️ 주의 - services/nineways/ + auth/ + api/ + config/initializers/ 범위 (developer-2는 ahoy 작업 중) - 9WAY API URL: ENV["NINEWAYS_API_URL"], ENV["NINEWAYS_API_KEY"] - sentry-ruby, sentry-rails gem은 Gemfile에 이미 포함 ## 대시보드 기록 (MCP) - 시작: `AddActivityLogTool` (ticket_id: "c20ae5ba-7f13-4d5e-8547-dd5d4dee521e", message: "9WAY + Sentry 시작") - 완료: `AddActivityLogTool` (message: "9WAY + Sentry 완료", details 필수) ## 완료 절차 1. `UpdateTicketStatusTool`로 두 서브 티켓 모두 → `review` 2. `AddActivityLogTool`로 완료 기록 3. `SendMessage`로 팀리드에게 보고 (summary: "9WAY + Sentry 구현 완료") ⚠️ SendMessage 필수!
Solid Cache + CSP 헤더 + Rate Limiting + 보안 설정 + 테스트
## 목표 Solid Cache 설정, CSP 헤더, Rate Limiting, 보안 마무리. ## 구현 사항 ### 1. Solid Cache 설정 - config/environments/production.rb에서 cache_store 설정 - `config.cache_store = :solid_cache_store` - development에서는 기본 memory_store 유지 ### 2. CSP 헤더 (PRD Section 16) - config/initializers/content_security_policy.rb ```ruby Rails.application.configure do config.content_security_policy do |policy| policy.default_src :self policy.font_src :self, "https://cdn.jsdelivr.net" policy.img_src :self, :data, "https:" policy.object_src :none policy.script_src :self, "https://js.tosspayments.com" policy.style_src :self, :unsafe_inline, "https://cdn.jsdelivr.net" policy.connect_src :self, "wss:", "https://api.tosspayments.com" policy.frame_src "https://js.tosspayments.com" end config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } config.content_security_policy_nonce_directives = %w[script-src] end ``` ### 3. Rate Limiting (Rails 8) - ApplicationController에 rate_limit 설정 (있으면) - 또는 Rack::Attack 없이 간단한 Rails 8 내장 rate limiting ### 4. 보안 점검 - CSRF 토큰 확인 (웹훅 제외 모든 컨트롤러) - Strong Parameters 올바른 사용 확인 - 인증/인가 누락 여부 확인 ### ⚠️ config/ + initializers/ 범위 (developer-2는 db/seeds 작업 중) ### 테스트 - CSP 헤더 존재 확인 - Solid Cache 설정 로드 확인 ### 완료 기준 - Solid Cache 설정 완료 - CSP 헤더 설정 - bin/rails test 전체 통과
라우트 설정 — PRD Section 10 전체 라우트 + 스텁 컨트롤러 + 테스트
## 목표 PRD Section 10 전체 라우트를 config/routes.rb에 설정. 필요한 스텁 컨트롤러 생성. 라우트 테스트 작성. ## 현재 routes.rb 상태 ```ruby Rails.application.routes.draw do resource :session resources :passwords, param: :token resources :registrations, only: %i[new create] namespace :onboarding do # ... (4단계 온보딩 — 이미 구현됨) end root "pages#landing" get "up" => "rails/health#show", as: :rails_health_check end ``` ## PRD Section 10 전체 라우트 (목표 상태) ```ruby Rails.application.routes.draw do # 공개 root "pages#landing" get "/about", to: "pages#about" get "/showcase", to: "pages#showcase" get "/pricing", to: "pages#pricing" # 인증 (Rails 8 내장) resource :session resources :passwords, param: :token get "/signup", to: "registrations#new" post "/signup", to: "registrations#create" # 9WAY OAuth get "/auth/nineways", to: "auth/nineways#redirect" get "/auth/nineways/callback", to: "auth/nineways#callback" # 온보딩 (이미 구현됨) namespace :onboarding do get "/", to: "steps#index" get "nineways", to: "steps#nineways" post "nineways", to: "steps#connect_nineways" get "goal", to: "steps#goal" post "goal", to: "steps#save_goal" get "level", to: "steps#level" post "level", to: "steps#save_level" get "idea", to: "steps#idea" post "idea", to: "steps#save_idea" get "complete", to: "steps#complete" end # 인증 필요 구간 — before_action :require_authentication 사용 # PRD의 `authenticated :user do` 블록 대신 컨트롤러에서 인증 체크 get "/dashboard", to: "dashboard#index" # LEARN namespace :learn do get "/" , to: "home#index" resources :curricula, only: [:index, :show] do resources :lessons, only: [:show] do post :complete, on: :member end end end # BUILD namespace :build do get "/", to: "home#index" resources :projects do resource :idea_analysis, only: [:show, :create] resource :spec, only: [:show, :update] resource :blueprint, only: [:show, :create] resource :claude_md, only: [:show, :create] do get :download, on: :member end resources :build_steps, only: [:index, :show] do post :complete, on: :member end end end # LAUNCH namespace :launch do get "/", to: "home#index" resources :projects, only: [] do resource :checklist, only: [:show, :update] resource :copy_generator, only: [:show, :create] end end # AI 코치 resources :ai_conversations, only: [:index, :create, :show] do post :message, on: :member end # 커뮤니티 resources :community_posts do resources :community_comments, only: [:create, :destroy] post :like, on: :member end # 코호트 resources :cohorts, only: [:index, :show] do resources :cohort_applications, only: [:new, :create] resources :cohort_enrollments, only: [:new, :create] end # B2B 문의 resources :b2b_inquiries, only: [:new, :create] # 쇼케이스 resources :showcase_services, only: [:index, :show, :new, :create, :edit, :update] # MY namespace :my do get "/", to: "dashboard#index" resource :profile, only: [:show, :edit, :update] resource :nineways_sync, only: [:create] end # 결제 (토스페이먼츠) namespace :payments do get "/checkout/:plan", to: "checkout#show", as: :checkout post "/checkout/:plan", to: "checkout#create" get "/success", to: "completions#success" get "/fail", to: "completions#fail" end # 토스 웹훅 post "/payments/webhook", to: "payments/webhooks#receive" # 관리자 namespace :admin do root to: "dashboard#index" resources :users resources :cohorts resources :curricula do resources :lessons end resources :cohort_applications resources :b2b_inquiries resources :payments end # 9WAY 웹훅 namespace :api do namespace :v1 do post "/nineways/webhook", to: "nineways#webhook" end end get "up" => "rails/health#show", as: :rails_health_check end ``` ## 스텁 컨트롤러 생성 규칙 - 각 라우트에 대응하는 컨트롤러가 없으면 **빈 스텁 컨트롤러**를 생성 - 이미 존재하는 컨트롤러: PagesController, SessionsController, PasswordsController, RegistrationsController, Onboarding::StepsController - 스텁 컨트롤러 패턴: ```ruby class DashboardController < ApplicationController def index # TODO: 구현 예정 end end ``` - 인증 필요 컨트롤러는 `before_action :require_authentication` 제거하지 않음 (ApplicationController에서 상속) - Admin 네임스페이스 컨트롤러는 `Admin::BaseController`를 만들고 admin 권한 체크: ```ruby class Admin::BaseController < ApplicationController before_action :require_admin private def require_admin redirect_to root_path, alert: I18n.t("errors.messages.not_authorized") unless Current.user&.admin? end end ``` - Admin 하위 컨트롤러는 `Admin::BaseController` 상속 - 각 네임스페이스(learn, build, launch, my, payments, admin)의 컨트롤러 생성 - `allow_unauthenticated_access` 필요한 컨트롤러: PagesController, 웹훅 컨트롤러 ## ⚠️ 주의 - PRD의 `authenticated :user do`는 Devise 패턴 — Rails 8 Authentication은 이를 지원하지 않음 → 대신 ApplicationController의 `before_action :require_authentication`으로 처리 (이미 설정됨) → `allow_unauthenticated_access`로 공개 라우트 해제 - `authenticate :user, ->(u) { u.admin? }` 대신 Admin::BaseController에서 before_action으로 처리 - MissionControl::Jobs::Engine 마운트는 gem이 설치되어 있으면 추가, 없으면 주석 처리 - 기존 `resources :registrations, only: %i[new create]`를 PRD의 `get/post "/signup"` 패턴으로 변경 - 기존 온보딩 라우트 유지 - 토스 웹훅은 `skip_forgery_protection` 필요 - developer-1이 동시에 DB 마이그레이션 작업 중 — routes.rb만 수정, 모델 파일은 건드리지 않기 ## 테스트 - 라우트 테스트: 주요 경로가 올바른 컨트롤러#액션으로 연결되는지 - root → pages#landing - /dashboard → dashboard#index - /learn → learn/home#index - /build → build/home#index - /admin → admin/dashboard#index - 인증 테스트: - 공개 페이지 접근 가능 (root, /about, /pricing, /signup) - 보호 페이지 미인증 시 로그인 리다이렉트 (/dashboard, /learn, /build) - Admin 페이지 비관리자 접근 시 리다이렉트 - `bin/rails routes` 에러 없이 출력 ## 완료 기준 - config/routes.rb PRD Section 10과 일치 - 모든 라우트에 대응하는 컨트롤러 존재 (스텁이라도) - 미인증 사용자 보호 라우트 → 로그인 리다이렉트 - admin 역할만 관리자 라우트 접근 - bin/rails test 전체 통과
Project CRUD — Build 네임스페이스 컨트롤러 + 뷰 + 소유자 접근제어 + 테스트
## 목표 Build 네임스페이스에서 Project CRUD 완전 구현. raw_idea 입력, status 필터, 소유자 접근 제어. ## 현재 상태 - Project 모델 존재: belongs_to :user, has_many :build_steps, has_many :ai_conversations, has_many :showcase_services. enum :status { ideating: 0, building: 1, launched: 2, abandoned: 3 }. validates :title. completion_percentage 메서드. - Build::ProjectsController 스텁 존재 (빈 액션) - Build::HomeController 스텁 존재 - 라우트: `namespace :build { get "/", to: "home#index"; resources :projects do ... end }` - 공용 Partial: _button, _card, _input, _flash, _navbar ## 구현 사항 ### 1. Build::ProjectsController 완전 구현 ```ruby class Build::ProjectsController < ApplicationController before_action :set_project, only: [:show, :edit, :update, :destroy] before_action :authorize_owner!, only: [:show, :edit, :update, :destroy] def index @projects = Current.user.projects.order(updated_at: :desc) # status 필터: ?status=ideating / building / launched / abandoned @projects = @projects.where(status: params[:status]) if params[:status].present? end def show; end def new @project = Current.user.projects.build end def create @project = Current.user.projects.build(project_params) if @project.save redirect_to build_project_path(@project), notice: "프로젝트가 생성되었습니다." else render :new, status: :unprocessable_entity end end def edit; end def update if @project.update(project_params) redirect_to build_project_path(@project), notice: "프로젝트가 수정되었습니다." else render :edit, status: :unprocessable_entity end end def destroy @project.destroy redirect_to build_projects_path, notice: "프로젝트가 삭제되었습니다." end private def set_project @project = Project.find(params[:id]) end def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end def project_params params.require(:project).permit(:title, :description, :raw_idea, :status) end end ``` ### 2. Build::HomeController ```ruby class Build::HomeController < ApplicationController def index @projects = Current.user.projects.order(updated_at: :desc).limit(10) end end ``` ### 3. 뷰 (app/views/build/projects/) - **다크 테마** (앱 내부 — bg-bg, text-text-primary, bg-surface, bg-accent) - 기존 Partial 활용 (_button, _card, _input) - index: 프로젝트 카드 그리드, status 필터 탭 (전체/기획중/개발중/출시/포기), 새 프로젝트 버튼 - show: 프로젝트 상세 (title, description, raw_idea, status, completion_percentage). build_steps 목록 (있으면). 편집/삭제 버튼 - new/edit: 폼 (_form partial) - title (필수), description (textarea), raw_idea (textarea — "아이디어를 자유롭게 적어주세요") - status는 create 시 자동 ideating, edit 시만 변경 가능 - _form: title, description, raw_idea 입력. status 선택 (edit 시만) ### 4. 소유자 접근 제어 - show/edit/update/destroy: 소유자(Current.user == project.user)만 접근 - 다른 사용자 접근 시 리다이렉트 + alert ### 5. Build 홈 뷰 (app/views/build/home/index.html.erb) - 최근 프로젝트 카드 (없으면 "첫 프로젝트를 시작해보세요" CTA) - 새 프로젝트 만들기 버튼 ### ⚠️ 주의 - developer-1이 동시에 Admin::CurriculaController 작업 중 — build/ 범위만 수정 - Project 모델(project.rb) 수정 불필요 (이미 완성됨) - User 모델 건드리지 않기 - 한국어 Flash 메시지 - status 필터 쿼리에서 SQL injection 주의 (enum 값 검증) ### 테스트 (TDD) - Build::ProjectsController 테스트: - 인증 필요 (미인증 → 리다이렉트) - CRUD 전체 동작 - 소유자만 접근 가능 (타인 프로젝트 → 리다이렉트) - status 필터 동작 - raw_idea 입력 저장 - 유효하지 않은 입력 시 에러 - Build::HomeController 테스트: - 프로젝트 목록 표시 - fixture: projects.yml에 이미 데이터 있을 것 — user 연결 확인 ### 완료 기준 - Build 프로젝트 CRUD 전체 동작 - raw_idea 입력 + 저장 - status 필터 동작 - 소유자 외 접근 시 리다이렉트 - 컨트롤러 테스트 통과 - bin/rails test 전체 통과
Ai::IdeaAnalyzerService — Claude API + JSON 파싱 + 에러 처리 + 테스트
## 목표 PRD Section 13.1 기반 Ai::IdeaAnalyzerService 구현. ruby-anthropic gem으로 Claude API 호출. raw_idea → 프로젝트 필드 업데이트. ## 현재 상태 - Project 모델: raw_idea, one_line_definition, core_features(jsonb), target_customer, revenue_model, competitor_analysis(jsonb) 필드 존재 - User 모델: nineways_strength_type, dev_level, onboarding_goal 필드 존재 - ruby-anthropic gem: Gemfile에 포함 여부 확인 필요 (없으면 추가) - app/services/ 디렉토리 없으면 생성 ## PRD 코드 (Section 13.1) ```ruby class Ai::IdeaAnalyzerService def initialize(project:, user:) @project = project @user = user @client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) end def call response = @client.messages( model: "claude-sonnet-4-6", max_tokens: 2000, system: system_prompt, messages: [{ role: "user", content: user_prompt }] ) parse_and_save(response.content.first.text) end private def system_prompt <<~PROMPT 당신은 VALUEIT의 서비스 기획 전문가입니다. 비개발자도 Claude Code로 만들 수 있는 수준의 MVP 서비스를 기획합니다. 반드시 JSON만 응답하세요. 마크다운 코드블록 없이. 언어: 한국어 PROMPT end def user_prompt <<~PROMPT 사용자 정보: - 9WAY 강점: #{@user.nineways_strength_type || "미연동"} - 개발 경험: #{@user.dev_level} - 목표: #{@user.onboarding_goal} 아이디어: #{@project.raw_idea} JSON 형식으로 분석: { "one_line_definition": "서비스 한 줄 정의 (20자 이내)", "core_features": ["기능1", "기능2", "기능3", "기능4", "기능5"], "target_customer": "타겟 고객 구체적 설명", "revenue_model": "추천 수익 모델과 이유", "competitor_analysis": { "competitors": ["경쟁사1", "경쟁사2"], "differentiation": "차별화 포인트" }, "viability_score": 7, "viability_reason": "실현 가능성 평가 이유" } PROMPT end def parse_and_save(content) data = JSON.parse(content.strip) @project.update!( one_line_definition: data["one_line_definition"], core_features: data["core_features"], target_customer: data["target_customer"], revenue_model: data["revenue_model"], competitor_analysis: data["competitor_analysis"] ) rescue JSON::ParserError => e raise "AI 응답 파싱 실패: #{e.message}" end end ``` ## 구현 사항 ### 1. app/services/ai/idea_analyzer_service.rb - PRD 코드 그대로 구현 - Sentry가 없으면 Sentry.capture_exception 제거하고 Rails.logger.error 사용 - API 키 미설정 시 적절한 에러 메시지 ### 2. 에러 처리 - JSON::ParserError: "AI 응답 파싱 실패" 에러 raise - Anthropic::Error (API 에러): 적절한 에러 메시지 - ENV["ANTHROPIC_API_KEY"] 미설정: 서비스 초기화 시 에러 - content가 JSON 코드블록으로 감싸진 경우 (```json ... ```) 처리 ### 3. 테스트 (API mock) - WebMock 또는 stub으로 Anthropic API 호출 mock - 성공 케이스: 프로젝트 필드 업데이트 확인 - JSON 파싱 실패: 에러 raise 확인 - API 키 미설정: 에러 확인 - 잘못된 JSON 구조: 에러 처리 ### ⚠️ 주의 - app/services/ai/ 디렉토리 생성 - developer-1이 동시에 Admin::LessonsController 작업 중 — services/ 범위만 수정 - Project 모델, User 모델 건드리지 않기 - ruby-anthropic gem 확인 (Gemfile에 없으면 추가 + bundle install) ### 완료 기준 - 서비스 호출 시 프로젝트 필드 업데이트 - JSON 파싱 실패 에러 핸들링 - API 키 미설정 시 적절한 에러 - 서비스 테스트 통과 (API mock) - bin/rails test 전체 통과
Idea Analysis 비동기 처리 — Ai::AnalyzeIdeaJob + Build::IdeaAnalysesController + Turbo Stream + 테스트
## 목표 PRD Section 12.2 기반 Ai::AnalyzeIdeaJob + Build::IdeaAnalysesController 구현. 비동기 AI 분석 + Turbo Stream 실시간 갱신. ## 현재 상태 - Ai::IdeaAnalyzerService 구현 완료 (app/services/ai/idea_analyzer_service.rb) - Build::IdeaAnalysesController 스텁 존재 (show, create) - Build::ProjectsController 구현 완료 - 라우트: `resources :projects do resource :idea_analysis, only: [:show, :create] end` - Solid Queue 설정 완료 (Gemfile에 solid_queue 포함) ## PRD 코드 (Section 12.2) ```ruby class Ai::AnalyzeIdeaJob < ApplicationJob queue_as :ai_processing def perform(project_id) project = Project.find(project_id) Ai::IdeaAnalyzerService.new( project: project, user: project.user ).call Turbo::StreamsChannel.broadcast_update_to( "project_#{project_id}", target: "idea_analysis_result", partial: "build/idea_analyses/result", locals: { project: project } ) rescue => e Rails.logger.error("AnalyzeIdeaJob failed: #{e.message}") raise end end ``` ## 구현 사항 ### 1. app/jobs/ai/analyze_idea_job.rb - PRD 코드 기반 (Sentry 대신 Rails.logger.error) - queue_as :ai_processing - 성공 시 Turbo Stream broadcast ### 2. Build::IdeaAnalysesController ```ruby class Build::IdeaAnalysesController < ApplicationController before_action :set_project before_action :authorize_owner! def show # 분석 결과 표시 (또는 분석 전이면 "분석하기" 버튼) end def create # Job 큐잉 Ai::AnalyzeIdeaJob.perform_later(@project.id) redirect_to build_project_idea_analysis_path(@project), notice: "AI 분석을 시작했습니다. 잠시 후 결과가 표시됩니다." # 또는 Turbo Stream으로 로딩 UI 표시 end private def set_project @project = Project.find(params[:project_id]) end def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end end ``` ### 3. 뷰 **build/idea_analyses/show.html.erb**: - 분석 결과가 있으면: one_line_definition, core_features, target_customer, revenue_model, competitor_analysis 표시 - 분석 전이면: "아이디어 분석하기" 버튼 - turbo_stream_from "project_#{@project.id}" — 실시간 갱신 수신 - id="idea_analysis_result" div — Turbo Stream 업데이트 타겟 **build/idea_analyses/_result.html.erb** (partial): - 분석 결과 카드 (one_line_definition, core_features 리스트, target_customer, revenue_model, competitor_analysis) - "재분석" 버튼 **로딩 UI**: - 분석 중일 때 스피너/로딩 상태 표시 - Turbo Stream이 result를 업데이트하면 로딩 → 결과로 전환 ### 4. 다크 테마 - bg-bg, bg-surface, text-text-primary, bg-accent 토큰 사용 - 기존 Partial 활용 (_button, _card) ### ⚠️ 주의 - build/idea_analyses/ 범위만 수정 (developer-1은 learn/ 작업 중) - Project, User 모델 건드리지 않기 - Turbo Stream broadcast: `turbo_stream_from` 헬퍼 사용 - Solid Queue가 development에서 inline 실행될 수 있음 — perform_later 사용 - 한국어 Flash/UI 텍스트 ### 테스트 (TDD) - Build::IdeaAnalysesController: show(인증, 소유자 확인), create(Job 큐잉 확인) - Ai::AnalyzeIdeaJob: perform 시 서비스 호출 + broadcast 확인 (mock) - 인증/소유자 접근 제어 ### 완료 기준 - "분석하기" 버튼 → Job 큐잉 - 분석 결과 표시 - Turbo Stream 실시간 갱신 - 재분석 가능 - bin/rails test 전체 통과
Ai::BlueprintGeneratorService — Claude API + build_steps 자동 생성 + dev_level 스택 분기 + 테스트
## 목표 PRD Section 13.2 기반 Ai::BlueprintGeneratorService 구현. 개발 수준별 스택 추천, blueprint 필드 업데이트, build_steps 6단계 자동 생성. ## 현재 상태 - Ai::IdeaAnalyzerService 패턴 참고 가능 (app/services/ai/idea_analyzer_service.rb) - Project 모델: tech_stack, tech_stack_reason, db_schema_draft(jsonb), pages_list(jsonb), dev_priority(jsonb) 필드 존재 - BuildStep 모델: belongs_to :project, step_number, title, description, prompt_template, completed - User: dev_level enum (none: 0, basic: 1, intermediate: 2, developer: 3) ## PRD 코드 (Section 13.2) ```ruby class Ai::BlueprintGeneratorService TECH_STACKS = { "none" => "Rails 8.1 + PostgreSQL + Hotwire + Tailwind CSS", "basic" => "Rails 8.1 + PostgreSQL + Hotwire + Tailwind CSS", "intermediate" => "Rails 8.1 + PostgreSQL + Hotwire + Tailwind CSS", "developer" => "Rails 8.1 API + Next.js 15 + PostgreSQL + Tailwind CSS" }.freeze def initialize(project:, user:) @project = project @user = user @client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) end def call response = @client.messages( model: "claude-sonnet-4-6", max_tokens: 3000, system: system_prompt, messages: [{ role: "user", content: user_prompt }] ) data = JSON.parse(response.content.first.text.strip) save_blueprint(data) create_default_build_steps end private # system_prompt, user_prompt, save_blueprint, create_default_build_steps # (PRD 코드 그대로 구현) def save_blueprint(data) @project.update!( tech_stack: data["tech_stack"], tech_stack_reason: data["tech_stack_reason"], db_schema_draft: data["db_schema"], pages_list: data["pages"], dev_priority: data["dev_priority"] ) end def create_default_build_steps steps = [ { step_number: 1, title: "프로젝트 초기화", prompt_template: "Rails 8.1.2 프로젝트를 생성하고 CLAUDE.md를 프로젝트에 적용해줘" }, { step_number: 2, title: "DB 구조 생성", prompt_template: "CLAUDE.md의 DB 스키마대로 모델과 마이그레이션을 생성해줘" }, { step_number: 3, title: "인증 구현", prompt_template: "Rails 8 내장 Authentication Generator로 이메일 로그인을 구현해줘" }, { step_number: 4, title: "핵심 기능 구현", prompt_template: "가장 중요한 핵심 기능 1개를 먼저 구현해줘" }, { step_number: 5, title: "랜딩페이지", prompt_template: "다크 테마, 라임 포인트 컬러로 랜딩페이지를 만들어줘" }, { step_number: 6, title: "배포", prompt_template: "Kamal 2 + Vultr으로 배포 설정을 완료해줘" }, ] steps.each do |step_attrs| @project.build_steps.find_or_create_by(step_number: step_attrs[:step_number]) do |step| step.assign_attributes(step_attrs) end end end end ``` ### 구현 사항 1. **app/services/ai/blueprint_generator_service.rb** — PRD 코드 기반 2. **IdeaAnalyzerService와 동일한 패턴**: 의존성 주입(client:), 에러 처리, JSON 코드블록 strip 3. **에러 처리**: JSON::ParserError, API 에러, API 키 미설정 4. **테스트** (API mock): - 성공: project blueprint 필드 업데이트 + build_steps 6개 생성 - TECH_STACKS 상수 확인 - dev_level별 분기 - JSON 파싱 실패 - 기존 build_steps 있을 때 find_or_create_by 동작 (중복 방지) ### ⚠️ 주의 - app/services/ai/ 범위만 수정 (developer-1은 learn/ 작업 중) - Project, User, BuildStep 모델 건드리지 않기 - IdeaAnalyzerService 패턴과 일관성 유지 ### 완료 기준 - 서비스 호출 시 blueprint 필드 업데이트 - build_steps 6개 자동 생성 - 개발 수준별 스택 분기 - 서비스 테스트 통과 (API mock) - bin/rails test 전체 통과
Blueprint UI — Build::BlueprintsController + 결과 뷰 + DB 스키마 시각화 + 비동기 Job + Turbo Stream + 테스트
## 목표 Blueprint 결과 뷰 + 비동기 생성(Job + Turbo Stream). 기술 스택, DB 스키마 시각화, 페이지 목록, 개발 우선순위 표시. ## 현재 상태 - Build::BlueprintsController 스텁 (show, create) - Ai::BlueprintGeneratorService 구현 완료 - Ai::AnalyzeIdeaJob 패턴 참고 가능 - 라우트: `resources :projects do resource :blueprint, only: [:show, :create] end` - Project: tech_stack, tech_stack_reason, db_schema_draft(jsonb), pages_list(jsonb), dev_priority(jsonb) ## 구현 사항 ### 1. Ai::GenerateBlueprintJob (IdeaAnalyzeJob과 동일 패턴) ```ruby class Ai::GenerateBlueprintJob < ApplicationJob queue_as :ai_processing def perform(project_id) project = Project.find(project_id) Ai::BlueprintGeneratorService.new(project: project, user: project.user).call Turbo::StreamsChannel.broadcast_update_to( "project_#{project_id}", target: "blueprint_result", partial: "build/blueprints/result", locals: { project: project } ) rescue => e Rails.logger.error("GenerateBlueprintJob failed: #{e.message}") raise end end ``` ### 2. Build::BlueprintsController ```ruby class Build::BlueprintsController < ApplicationController before_action :set_project before_action :authorize_owner! def show; end def create Ai::GenerateBlueprintJob.perform_later(@project.id) redirect_to build_project_blueprint_path(@project), notice: "설계도 생성을 시작했습니다." end private def set_project = @project = Project.find(params[:project_id]) def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end end ``` ### 3. 뷰 **build/blueprints/show.html.erb**: - turbo_stream_from "project_#{@project.id}" - 결과 있으면 _result 렌더, 없으면 "설계도 생성하기" 버튼 - id="blueprint_result" div **build/blueprints/_result.html.erb**: - **기술 스택 카드**: tech_stack + tech_stack_reason - **DB 스키마 시각화**: db_schema_draft(jsonb) 테이블별 컬럼 표시 (테이블 형태) - **페이지 목록**: pages_list 리스트 - **개발 우선순위**: dev_priority 순서 리스트 - **재생성 버튼** - 다크 테마 (bg-bg, bg-surface, bg-accent) - 기존 Partial 활용 ### ⚠️ 주의 - build/blueprints/ + jobs/ 범위만 수정 (developer-1은 dashboard/ 작업 중) - IdeaAnalysis 패턴과 일관성 유지 - Project 모델 건드리지 않기 ### 테스트 - Build::BlueprintsController: show(인증, 소유자), create(Job 큐잉) - Ai::GenerateBlueprintJob: perform(서비스 호출 mock) ### 완료 기준 - Blueprint 결과 페이지 - DB 스키마 시각적 표시 - 비동기 생성 + Turbo Stream 실시간 갱신 - bin/rails test 전체 통과
BuildSteps 단계별 진행 UI — Build::BuildStepsController + 목록/상세 + 완료 토글 + 프롬프트 복사 + 테스트
## 목표 BuildSteps 목록/상세 뷰. prompt_template 표시 + 복사 버튼. 완료 체크 토글. 전체 진행률. ## 현재 상태 - Build::BuildStepsController 스텁 (index, show + complete member route) - 라우트: `resources :build_steps, only: [:index, :show] do post :complete, on: :member end` - BuildStep 모델: belongs_to :project, step_number, title, description, prompt_template, completed, completed_at - Project#completion_percentage 메서드 존재 ## 구현 사항 ### 1. Build::BuildStepsController ```ruby class Build::BuildStepsController < ApplicationController before_action :set_project before_action :authorize_owner! before_action :set_build_step, only: [:show, :complete] def index @build_steps = @project.build_steps.order(:step_number) end def show; end def complete @build_step.update!(completed: !@build_step.completed, completed_at: @build_step.completed ? nil : Time.current) redirect_to build_project_build_step_path(@project, @build_step), notice: @build_step.completed? ? "완료했습니다!" : "완료 취소했습니다." end private def set_project = @project = Project.find(params[:project_id]) def set_build_step = @build_step = @project.build_steps.find(params[:id]) def authorize_owner! redirect_to build_projects_path, alert: I18n.t("errors.messages.not_authorized") unless @project.user == Current.user end end ``` ### 2. 뷰 **build/build_steps/index.html.erb**: - 전체 진행률 바 (Project#completion_percentage) - 6단계 리스트: step_number, title, 완료 체크마크 - 각 step 클릭 → show 페이지 - 다크 테마 **build/build_steps/show.html.erb**: - step_number + title - description (있으면) - **prompt_template 표시**: 코드 블록 스타일 (bg-surface, monospace 폰트) - **"프롬프트 복사" 버튼**: Stimulus controller로 clipboard 복사 (또는 간단한 JS) - **완료 토글 버튼**: POST complete (완료/완료 취소) - 이전/다음 step 네비게이션 - 다크 테마, 기존 Partial ### 3. 프롬프트 복사 (Stimulus) - `app/javascript/controllers/clipboard_controller.js` (간단한 Stimulus controller) - data-action="click->clipboard#copy" data-clipboard-text-value="..." - 복사 후 버튼 텍스트 "복사됨!" 피드백 ### ⚠️ 주의 - build/build_steps/ 범위만 (developer-1은 services/ai/ + build/claude_mds/ 작업 중) - BuildStep, Project 모델 수정 불필요 - Stimulus controller는 clipboard_controller.js 1개만 추가 ### 테스트 - Build::BuildStepsController: index, show, complete(토글), 인증/소유자 - completion_percentage 반영 확인 - fixture 활용 ### 완료 기준 - BuildSteps 목록 (체크박스 + 진행률) - prompt_template 표시 + 복사 버튼 - 완료 토글 - bin/rails test 전체 통과
Toss::Client — Faraday HTTP 클라이언트 + confirm/cancel/get_payment + Basic Auth + 에러 처리 + 테스트
## 목표 PRD Section 11.1 기반 Toss::Client 서비스. Faraday HTTP 클라이언트. confirm_payment, cancel_payment, get_payment. ## PRD 코드 (Section 11.1) ```ruby class Toss::Client BASE_URL = "https://api.tosspayments.com/v1".freeze def initialize @secret_key = ENV["TOSS_SECRET_KEY"] @conn = build_connection end def confirm_payment(payment_key:, order_id:, amount:) post("/payments/confirm", { paymentKey: payment_key, orderId: order_id, amount: amount }) end def cancel_payment(payment_key:, cancel_reason:) post("/payments/#{payment_key}/cancel", { cancelReason: cancel_reason }) end def get_payment(payment_key) get("/payments/#{payment_key}") end private def post(path, body) response = @conn.post(path) { |req| req.body = body.to_json } handle_response(response) end def get(path) response = @conn.get(path) handle_response(response) end def build_connection Faraday.new(url: BASE_URL) do |f| f.request :json f.response :json f.request :retry, max: 3, interval: 0.5 f.headers["Authorization"] = "Basic #{encoded_key}" f.headers["Content-Type"] = "application/json" end end def encoded_key Base64.strict_encode64("#{@secret_key}:") end def handle_response(response) body = response.body if response.status == 200 OpenStruct.new(success?: true, data: body) else Rails.logger.error("Toss API Error: #{body}") OpenStruct.new(success?: false, error_code: body["code"], error_message: body["message"]) end end end ``` ## 구현 사항 ### 1. app/services/toss/client.rb - PRD 코드 그대로 구현 - Faraday + JSON + retry 미들웨어 - Basic Auth: Base64(secret_key + ":") - handle_response: 200 → success, else → error with code/message ### 2. 에러 처리 - ENV["TOSS_SECRET_KEY"] 미설정 시 에러 - Faraday::ConnectionFailed 등 네트워크 에러 처리 - 응답 body 파싱 실패 처리 ### 3. 테스트 (WebMock/stub) - confirm_payment 성공/실패 - cancel_payment 성공/실패 - get_payment 성공/실패 - Basic Auth 헤더 확인 - retry 미들웨어 설정 확인 - 비밀키 미설정 에러 ### ⚠️ 주의 - services/toss/ 범위만 (developer-1은 build/specs/ 작업 중) - Payment 모델 건드리지 않기 - Faraday gem은 이미 Gemfile에 포함 - faraday-retry gem 확인 (없으면 추가) ### 완료 기준 - confirm_payment, cancel_payment, get_payment 구현 - Basic Auth 헤더 설정 - 에러 응답 파싱 - Faraday retry 미들웨어 - 서비스 테스트 통과 (HTTP stub) - bin/rails test 전체 통과
LAUNCH 체크리스트 + Ai::CopyGeneratorService — Launch 컨트롤러 + 카피 생성 + 테스트
## 목표 Launch 네임스페이스. 출시 준비 체크리스트 UI. AI 카피 생성기 (landing_headline, landing_subcopy, landing_cta). ## 현재 상태 - Launch::HomeController 스텁 (index) - Launch::ChecklistsController 스텁 (show, update) - Launch::CopyGeneratorsController 스텁 (show, create) - 라우트: `namespace :launch { get "/", to: "home#index"; resources :projects, only: [] do resource :checklist, only: [:show, :update]; resource :copy_generator, only: [:show, :create] end }` - Project: landing_headline, landing_subcopy, landing_cta 필드 존재 - Ai::IdeaAnalyzerService/BlueprintGeneratorService 패턴 참고 ## 구현 사항 ### 1. Launch::ChecklistsController ```ruby def show @project = Current.user.projects.find(params[:project_id]) @checklist = build_checklist(@project) end def update # 체크리스트 상태는 프로젝트 필드 기반으로 계산 (별도 DB 불필요) redirect_to launch_project_checklist_path(@project) end ``` **체크리스트 항목** (프로젝트 데이터 기반 자동 판단): - [ ] 아이디어 분석 완료 (one_line_definition 존재) - [ ] 설계도 생성 완료 (tech_stack 존재) - [ ] CLAUDE.md 생성 완료 (claude_md_content 존재) - [ ] 빌드 단계 50% 이상 완료 (completion_percentage >= 50) - [ ] 랜딩 카피 생성 완료 (landing_headline 존재) - [ ] 서비스 URL 등록 (service_url 존재) ### 2. Ai::CopyGeneratorService ```ruby class Ai::CopyGeneratorService def initialize(project:, user:) @project = project @user = user @client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) end def call response = @client.messages(model: "claude-sonnet-4-6", max_tokens: 1000, system: system_prompt, messages: [{ role: "user", content: user_prompt }]) data = JSON.parse(response.content.first.text.strip) @project.update!(landing_headline: data["headline"], landing_subcopy: data["subcopy"], landing_cta: data["cta"]) end private def system_prompt = "VALUEIT 랜딩 카피 전문가. JSON만 응답. 한국어." def user_prompt "서비스: #{@project.one_line_definition}\n타겟: #{@project.target_customer}\n수익모델: #{@project.revenue_model}\n\nJSON: {\"headline\": \"...\", \"subcopy\": \"...\", \"cta\": \"...\"}" end end ``` ### 3. Launch::CopyGeneratorsController - show: 카피 결과 표시 (있으면) / 생성 버튼 - create: CopyGeneratorService 호출 (동기 또는 Job) → redirect ### 4. Launch::HomeController - index: 프로젝트 목록 + 각 프로젝트의 체크리스트 진행률 ### 5. 뷰 - checklist/show: 체크리스트 항목 (자동 판단, 읽기 전용 체크마크) - copy_generator/show: headline, subcopy, cta 표시 + 재생성 버튼 - home/index: 프로젝트별 출시 준비 상태 - 다크 테마, 기존 Partial ### ⚠️ 주의 - launch/ + services/ai/ 범위만 (developer-1은 services/toss/ 작업 중) - IdeaAnalyzerService 패턴과 일관성 (DI, JSON strip, 에러 처리) - 소유자 접근제어 ### 테스트 - ChecklistsController: show (체크리스트 항목 표시) - CopyGeneratorsController: show, create (API mock) - CopyGeneratorService: 카피 생성 + 프로젝트 업데이트 ### 완료 기준 - 체크리스트 항목 자동 판단 표시 - AI 카피 생성 동작 - 생성 카피 프로젝트 저장 - bin/rails test 전체 통과
결제 UI — Payments::CheckoutController + CompletionsController + 토스 결제위젯 + 성공/실패 페이지 + 테스트
## 목표 코호트 결제 UI. checkout 페이지(토스 결제위젯), 성공/실패 결과 페이지. ## 현재 상태 - Payments::CheckoutController 스텁 (show, create) - Payments::CompletionsController 스텁 (success, fail) - 라우트: `namespace :payments { get "/checkout/:plan", to: "checkout#show", as: :checkout; post "/checkout/:plan", to: "checkout#create"; get "/success", to: "completions#success"; get "/fail", to: "completions#fail" }` - Toss::PaymentService 구현 완료 (checkout_params, confirm) - Cohort 모델: name, price_cents, accepting_applications ## 구현 사항 ### 1. Payments::CheckoutController ```ruby class Payments::CheckoutController < ApplicationController def show @plan = params[:plan] @cohort = Cohort.find(params[:cohort_id]) if params[:cohort_id] service = Toss::PaymentService.new(user: Current.user, plan: @plan, cohort: @cohort) @checkout_params = service.checkout_params end def create # checkout → 토스 결제창으로 리다이렉트 (JS에서 처리) redirect_to payments_checkout_path(params[:plan]) end end ``` ### 2. Payments::CompletionsController ```ruby class Payments::CompletionsController < ApplicationController def success # 토스 콜백: paymentKey, orderId, amount service = Toss::PaymentService.new(user: Current.user, plan: extract_plan, cohort: find_cohort) result = service.confirm(payment_key: params[:paymentKey], order_id: params[:orderId]) if result.success? redirect_to dashboard_path, notice: I18n.t("payments.success") else redirect_to dashboard_path, alert: result.error_message end end def fail @error_code = params[:code] @error_message = params[:message] end private def extract_plan params[:orderId]&.split("-")&.[](1) || "cohort" end def find_cohort # orderId에서 cohort 정보 추출 또는 세션에서 Cohort.accepting.order(:created_at).last end end ``` ### 3. 뷰 **payments/checkout/show.html.erb**: - 결제 정보 요약 (코호트명, 금액) - 토스 결제위젯 JS 스크립트 로드 (CDN) - "결제하기" 버튼 → 토스 결제창 호출 - 다크 테마 **payments/completions/success.html.erb** (리다이렉트이므로 뷰 불필요할 수 있음) **payments/completions/fail.html.erb**: - 에러 코드, 메시지 표시 - "다시 시도" 버튼 → checkout - 다크 테마 ### 4. 토스 결제위젯 Stimulus Controller - `app/javascript/controllers/toss_payment_controller.js` - data에서 checkout_params 읽어 토스 SDK 호출 - 또는 인라인 스크립트로 간단 구현 ### ⚠️ 주의 - payments/checkout, completions 범위만 (developer-1은 payments/webhooks 작업 중) - Pro 관련 UI 없음 (코호트만) - 토스 JS SDK URL: https://js.tosspayments.com/v2/standard - 한국어 Flash 메시지 ### 테스트 - CheckoutController: show (checkout_params 존재), 인증 필요 - CompletionsController: success (confirm 호출 mock), fail (에러 표시) ### 완료 기준 - checkout 페이지 (결제 정보 + 토스 위젯) - 성공/실패 결과 페이지 - bin/rails test 전체 통과
쇼케이스 페이지 — ShowcaseServicesController CRUD + 카테고리 필터 + 퍼블릭 접근 + 테스트
## 목표 ShowcaseServices CRUD. 카테고리별 필터. 카드 그리드. 퍼블릭(/showcase) + 인증(CRUD) 접근. ## 현재 상태 - ShowcaseServicesController 스텁 (index, show, new, create, edit, update) - 라우트: `resources :showcase_services, only: [:index, :show, :new, :create, :edit, :update]` - PagesController에 `get "/showcase", to: "pages#showcase"` 퍼블릭 경로도 있음 - ShowcaseService 모델: belongs_to :user, belongs_to :project(optional). title, description, service_url, thumbnail_url, category, days_to_build, user_count, published, likes_count. scope :published ## 구현 사항 ### 1. ShowcaseServicesController - index: published만 표시, 카테고리 필터(?category=...), 카드 그리드 - show: 상세 (service_url 외부 링크, thumbnail, 설명) - new/create: Current.user.showcase_services.build, project 연결(optional) - edit/update: 소유자만 - strong params: title, description, service_url, thumbnail_url, category, days_to_build, project_id, published ### 2. PagesController#showcase (퍼블릭) - 이미 존재하는 pages#showcase → published 쇼케이스 목록 표시 - allow_unauthenticated_access 이미 설정됨 ### 3. 뷰 - **index**: 카테고리 필터 탭, 카드 그리드(title, thumbnail, category, user_count), "등록하기" 버튼 - **show**: 상세 (service_url 링크, description, days_to_build, user_count), 수정(소유자) - **new/edit**: 폼 (_form partial) - 다크 테마, 기존 Partial (_card, _button) ### 4. 퍼블릭 접근 - /showcase (pages#showcase) → 비인증 접근 가능 - /showcase_services (인증 필요 — CRUD) ### ⚠️ 주의 - showcase_services/ + pages/ 범위만 (developer-1은 community_posts/ 작업 중) - 소유자 접근제어 (edit/update) - published만 index에 표시 - 한국어 Flash ### 테스트 - CRUD 전체, 인증/소유자, 카테고리 필터, published 필터 - pages#showcase 퍼블릭 접근 ### 완료 기준 - 쇼케이스 카드 그리드 + 카테고리 필터 - 소유자 CRUD + 퍼블릭 조회 - bin/rails test 전체 통과
코호트 신청 — CohortsController + CohortApplicationsController + B2bInquiriesController + Admin 관리 + 테스트
## 목표 코호트 목록/상세. 사전 신청 폼(CohortApplication). Admin 승인/거절. B2B 문의 폼. 정원 체크. ## 현재 상태 - CohortsController 스텁 (index, show) - CohortApplicationsController 스텁 (new, create) - CohortEnrollmentsController 스텁 (new, create) - B2bInquiriesController 스텁 (new, create) - Admin::CohortsController, Admin::CohortApplicationsController, Admin::B2bInquiriesController 스텁 - 라우트: `resources :cohorts, only: [:index, :show] do resources :cohort_applications, only: [:new, :create]; resources :cohort_enrollments, only: [:new, :create] end; resources :b2b_inquiries, only: [:new, :create]` - 모델: Cohort(accepting_applications, max_capacity, price_cents), CohortApplication(status enum, motivation, current_job, expectation), B2bInquiry(status enum, company_name, contact_name, contact_email, message) ## 구현 사항 ### 1. CohortsController - index: accepting 코호트 목록 - show: 코호트 상세 (이름, 기간, 가격, 정원, 설명) + 신청 버튼 ### 2. CohortApplicationsController - new: 신청 폼 (motivation, current_job, expectation) - create: 신청 생성, 정원 체크(Cohort#full?), accepting_applications 체크 ### 3. B2bInquiriesController - new: B2B 문의 폼 (company_name, contact_name, contact_email, contact_phone, team_size, message) - create: 문의 생성 ### 4. Admin 컨트롤러 - Admin::CohortsController: CRUD - Admin::CohortApplicationsController: index(상태별 필터), show, 승인/거절 액션 - 승인: status → approved, approved_at 설정 - 거절: status → rejected, rejected_at 설정 - Admin::B2bInquiriesController: index(상태별 필터), show, 상태 변경 ### 5. 뷰 - cohorts/index: 코호트 카드 목록 - cohorts/show: 상세 + 신청 버튼/폼 링크 - cohort_applications/new: 신청 폼 - b2b_inquiries/new: B2B 문의 폼 - admin/cohorts/, admin/cohort_applications/, admin/b2b_inquiries/ (CRUD 뷰) - 다크 테마, 기존 Partial ### ⚠️ 주의 - cohorts/ + cohort_applications/ + b2b_inquiries/ + admin/ 범위 (developer-1은 community_comments/ 작업 중) - 모델 수정 불필요 - 한국어 Flash - 정원 초과 시 신청 불가 처리 ### 테스트 - CohortsController: index, show - CohortApplicationsController: new, create(성공/정원초과/비수락) - B2bInquiriesController: new, create - Admin 컨트롤러: CRUD + 승인/거절 - 인증/admin 접근제어 ### 완료 기준 - 코호트 목록/상세 + 신청 폼 + Admin 관리 - B2B 문의 폼 + Admin 관리 - bin/rails test 전체 통과
메일러 — UserMailer 8개 메일 + WeeklyNudgeEmailJob + Resend 설정 + letter_opener + 테스트
## 목표 PRD Section 15 기반 UserMailer. 8개 메일 템플릿. Resend API. letter_opener 개발환경. WeeklyNudgeEmailJob. ## 현재 상태 - PasswordsMailer 존재 (Rails 8 auth generator) - resend gem: Gemfile 확인 필요 - letter_opener gem: Gemfile 확인 필요 - Solid Queue 설정 완료 ## 구현 사항 ### 1. UserMailer (app/mailers/user_mailer.rb) 8개 메일: - welcome(user): 회원가입 환영 - onboarding_complete(user): 온보딩 완료 - payment_success(user, payment): 결제 성공 - payment_failed(user, payment): 결제 실패 - cohort_application_received(user, application): 사전 신청 접수 - cohort_application_approved(user, application): 승인 + 결제 안내 - cohort_enrollment_confirmation(user, enrollment): 코호트 등록 확인 - weekly_nudge(user): 주간 학습 독려 ### 2. 메일 뷰 (app/views/user_mailer/) - 각 메일별 HTML 템플릿 (간단한 레이아웃) - VALUEIT 브랜딩 (다크 테마 컬러 참조하되 메일은 라이트) - 한국어 ### 3. WeeklyNudgeEmailJob (app/jobs/weekly_nudge_email_job.rb) ```ruby class WeeklyNudgeEmailJob < ApplicationJob queue_as :mailers def perform User.where(onboarding_completed: true).find_each do |user| UserMailer.weekly_nudge(user).deliver_later end end end ``` ### 4. Resend 설정 (프로덕션) - config/environments/production.rb에 Action Mailer 설정 - RESEND_API_KEY 환경변수 ### 5. letter_opener (개발환경) - config/environments/development.rb에 letter_opener 설정 - Gemfile에 letter_opener 확인 (없으면 추가) ### ⚠️ 주의 - mailers/ + jobs/ + views/user_mailer/ 범위만 (developer-1은 ai_conversations/ 작업 중) - User, Payment, CohortApplication, CohortEnrollment 모델 건드리지 않기 - resend, letter_opener gem 확인 ### 테스트 - UserMailer 각 메일 테스트 (수신자, 제목, 본문 확인) - WeeklyNudgeEmailJob 테스트 (대상 사용자 필터) ### 완료 기준 - 8개 메일 동작 - letter_opener 개발환경 설정 - WeeklyNudgeEmailJob - bin/rails test 전체 통과
Ahoy 이벤트 추적 — 13개 핵심 이벤트 + Admin 통계 뷰 + 테스트
## 목표 PRD Section 17 기반 Ahoy 이벤트 추적. 13개 핵심 이벤트. Admin 이벤트 통계. ## 현재 상태 - ahoy_matey gem 설정 완료 (ahoy_visits, ahoy_events 테이블 존재) - config/initializers/ahoy.rb 존재 - Admin 네임스페이스 설정 완료 ## 구현 사항 ### 1. 13개 핵심 이벤트 추적 (컨트롤러에 ahoy.track 추가) 주요 이벤트: - user_signed_up (registrations#create) - user_signed_in (sessions#create) - onboarding_completed (onboarding/steps#save_idea) - project_created (build/projects#create) - idea_analyzed (build/idea_analyses#create) - blueprint_generated (build/blueprints#create) - claude_md_downloaded (build/claude_mds#download) - build_step_completed (build/build_steps#complete) - lesson_completed (learn/lessons#complete) - payment_completed (payments/completions#success) - cohort_applied (cohort_applications#create) - community_post_created (community_posts#create) - ai_coach_message_sent (ai_conversations#message) ### 2. ahoy.track 호출 패턴 ```ruby ahoy.track "project_created", project_id: @project.id, title: @project.title ``` ### 3. Admin 이벤트 통계 (선택) - Admin 대시보드 또는 별도 페이지에서 이벤트 카운트 표시 - 최근 7일/30일 이벤트 집계 ### ⚠️ 주의 - 기존 컨트롤러에 ahoy.track 1줄씩 추가하는 작업 - 각 컨트롤러 파일에 최소한의 변경만 - developer-1은 9WAY/Sentry 작업 중 — 겹치는 파일 주의 - ahoy.track은 인증된 사용자에서만 동작 (Current.user 있을 때) ### 테스트 - 이벤트 추적 확인 (ahoy.track 호출 검증) - 주요 액션에서 이벤트 생성 확인 ### 완료 기준 - 13개 이벤트 추적 코드 삽입 - bin/rails test 전체 통과
시드 데이터 + N+1 쿼리 점검 + 이미지 처리 설정 + 테스트
## 목표 시드 데이터 생성, N+1 쿼리 점검/수정, 이미지 처리 설정. ## 구현 사항 ### 1. 시드 데이터 (db/seeds.rb) 데모 환경용 시드: - Admin 유저 1명 (admin@valueit.kr, role: admin) - 일반 유저 2명 (user1@test.com, user2@test.com) - 커리큘럼 3개 + 레슨 9개 (각 3개씩) - 코호트 1개 (1기, accepting: true) - 프로젝트 2개 (ideating, building) - 커뮤니티 게시글 5개 - 쇼케이스 서비스 3개 - 각 데이터에 적절한 한국어 콘텐츠 ### 2. N+1 쿼리 점검 - 주요 index 액션에서 includes/eager_load 확인: - CommunityPostsController#index → includes(:user) - ShowcaseServicesController#index → includes(:user, :project) - CohortsController#index → includes(:cohort_enrollments) - Admin 컨트롤러들 확인 - 이미 includes가 있으면 확인만, 없으면 추가 ### 3. 이미지 처리 설정 - config/environments/production.rb에 image_processing 설정 - Active Storage variant 설정 (있으면) - 간단한 설정만 — 실제 이미지 업로드는 별도 티켓 ### ⚠️ db/seeds.rb + 컨트롤러 N+1 수정 범위 (developer-1은 config/ 작업 중) ### 테스트 - seeds.rb 실행 가능 확인 (bin/rails db:seed) - N+1 수정 후 기존 테스트 통과 ### 완료 기준 - 시드 데이터로 데모 환경 구성 가능 - N+1 쿼리 주요 해소 - bin/rails test 전체 통과
[P1] 라우트 설정 (전체)
## 설명 PRD Section 10 전체 라우트. 퍼블릭/인증/관리자 구간 분리. 네임스페이스별 정리. Rails 8 Authentication concern 기반 인증 체크. ## 참조 - PRD: Section 10 (routes.rb 전체 코드) - 스킬: `rails-core` ## 완료 기준 - [ ] `bin/rails routes` 출력이 PRD Section 10과 일치 - [ ] 미인증 사용자 보호 라우트 → 로그인 리다이렉트 - [ ] admin 역할만 관리자 라우트 접근 - [ ] 라우트 테스트 통과 ## 의존성 - [P1] Rails 8 Authentication 설정 - [P1] DB 스키마 마이그레이션 Part 1, Part 2
[P2] Curriculum CRUD + Admin 관리
## 설명 Curriculum 모델 CRUD. Admin 네임스페이스에서 커리큘럼 관리. position 기반 정렬. published 토글. ## 참조 - PRD: Section 8.7, Section 10 (admin/curricula) - 스킬: `rails-core`, `rails-testing` - 워크플로우: Analyzer → Rails Dev (TDD) ## 완료 기준 - [ ] Admin 커리큘럼 CRUD 동작 - [ ] position 기반 순서 변경 - [ ] published/unpublished 필터링 - [ ] 컨트롤러 + 모델 테스트 통과 ## 의존성 - [P1] DB 스키마 마이그레이션 Part 1 - [P1] 라우트 설정
[P2] Lesson CRUD + Admin 관리
## 설명 Lesson 모델 CRUD (Curriculum 하위). content_type별 렌더링 (article/video/guide). Markdown 렌더링. required_role별 접근 제어. ## 참조 - PRD: Section 8.8, Section 10 ## 완료 기준 - [ ] Admin 레슨 CRUD 동작 (커리큘럼 하위) - [ ] Markdown 컨텐츠 렌더링 - [ ] video_url 비디오 임베드 - [ ] role 기반 접근 제어 (free/pro/cohort) - [ ] 테스트 통과 ## 의존성 - [P2] Curriculum CRUD
[P2] 학습자 레슨 뷰 + 진행 추적
## 설명 Learn 네임스페이스 사용자 뷰. 커리큘럼/레슨 목록 → 레슨 상세. UserLessonProgress 생성/업데이트. 완료 버튼. ## 참조 - PRD: Section 8.9, Section 10 (learn 네임스페이스) - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] 커리큘럼/레슨 목록 페이지 - [ ] 레슨 상세 컨텐츠 표시 - [ ] "완료" 버튼 → progress 레코드 생성 - [ ] 완료 레슨 체크 표시 - [ ] 테스트 통과 ## 의존성 - [P2] Lesson CRUD
[P2] 학습 대시보드 + 진행률
## 설명 Learn 홈 (learn/home#index). Turbo Frames로 커리큘럼별 진행률 바. 전체 진행률. 주간 학습 통계. ## 참조 - PRD: Section 10 (learn 네임스페이스) - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] 커리큘럼별 진행률 퍼센트 표시 - [ ] 전체 진행률 바 - [ ] Turbo Frame 부분 갱신 ## 의존성 - [P2] 학습자 레슨 뷰 + 진행 추적
[P2] 메인 대시보드 (dashboard#index)
## 설명 로그인 후 메인 대시보드. LEARN 진행 요약, BUILD 프로젝트 요약, 스트릭, XP. Turbo Frames 분리. ## 참조 - PRD: Section 10 (dashboard) - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] 로그인 후 /dashboard 리다이렉트 - [ ] LEARN 진행률 위젯 - [ ] BUILD 프로젝트 카드 (없으면 "시작하기" CTA) - [ ] 스트릭/XP 표시 - [ ] 테스트 통과 ## 의존성 - [P1] 온보딩 플로우 - [P2] 학습자 레슨 뷰
[P3] Project CRUD + Build 네임스페이스
## 설명 Build 네임스페이스 프로젝트 CRUD. raw_idea 입력, status enum 관리. 소유자 접근 제어. ## 참조 - PRD: Section 8.3, 9 (Project 모델), Section 10 (build 네임스페이스) - 스킬: `rails-core`, `rails-testing` ## 완료 기준 - [ ] 프로젝트 생성 시 raw_idea 입력 - [ ] 목록 status별 필터 - [ ] 소유자 외 접근 시 403 - [ ] CRUD 전체 동작 - [ ] 컨트롤러 + 모델 테스트 통과 ## 의존성 - [P1] DB 스키마 마이그레이션 Part 1 - [P1] 라우트 설정
[P3] Ai::IdeaAnalyzerService
## 설명 PRD Section 13.1 구현. ruby-anthropic gem Claude API 호출. raw_idea → one_line_definition, core_features, target_customer, revenue_model, competitor_analysis. JSON 파싱 + 에러 처리. ## 참조 - PRD: Section 13.1 (전체 코드) - 스킬: `service-objects`, `rails-testing` ## 완료 기준 - [ ] 서비스 호출 시 프로젝트 필드 업데이트 - [ ] JSON 파싱 실패 에러 핸들링 - [ ] API 키 미설정 시 적절한 에러 - [ ] 서비스 테스트 통과 (API mock) ## 의존성 - [P3] Project CRUD
[P3] Idea Analysis 비동기 처리 (Solid Queue + Turbo Stream)
## 설명 Ai::AnalyzeIdeaJob 비동기 실행. 로딩 UI. 완료 시 Turbo Stream broadcast 실시간 갱신. idea_analyses 컨트롤러. ## 참조 - PRD: Section 12.2 (AnalyzeIdeaJob 코드) - 스킬: `hotwire-patterns`, `service-objects` ## 완료 기준 - [ ] "분석하기" 버튼 → Job 큐잉 - [ ] 로딩 스피너 표시 - [ ] 분석 완료 시 Turbo Stream 자동 갱신 - [ ] 재분석 가능 - [ ] 테스트 통과 ## 의존성 - [P3] Ai::IdeaAnalyzerService
[P3] Ai::BlueprintGeneratorService
## 설명 PRD Section 13.2 구현. 개발 수준별 스택 추천. tech_stack, db_schema_draft, pages_list, dev_priority 생성. build_steps 6단계 자동 생성. ## 참조 - PRD: Section 13.2 (전체 코드) - 스킬: `service-objects` ## 완료 기준 - [ ] 서비스 호출 시 blueprint 필드 업데이트 - [ ] build_steps 6개 자동 생성 - [ ] 개발 수준별 스택 분기 - [ ] 서비스 테스트 통과 ## 의존성 - [P3] Ai::IdeaAnalyzerService
[P3] Blueprint UI + 비동기 처리
## 설명 Blueprint 결과 뷰. 기술 스택, DB 스키마 시각화, 페이지 목록, 개발 우선순위. Turbo Stream 비동기 갱신. ## 참조 - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] Blueprint 결과 페이지 - [ ] DB 스키마 테이블/컬럼 시각적 표시 - [ ] 비동기 생성 + 실시간 갱신 ## 의존성 - [P3] Ai::BlueprintGeneratorService
[P3] Ai::ClaudeMdGeneratorService + 다운로드
## 설명 PRD Section 13.3 구현. 프로젝트 정보 → CLAUDE.md 마크다운 변환. 파일 다운로드 (send_data). claude_md_generated_at 타임스탬프. ## 참조 - PRD: Section 13.3 (전체 코드) ## 완료 기준 - [ ] CLAUDE.md 생성 + 미리보기 - [ ] .md 파일 다운로드 - [ ] 재생성 가능 - [ ] 서비스 + 컨트롤러 테스트 통과 ## 의존성 - [P3] Ai::BlueprintGeneratorService
[P3] BuildSteps 단계별 진행 UI
## 설명 build_steps 목록 + 상세. prompt_template 표시 + 복사 버튼. 완료 체크. 전체 진행률. ## 참조 - PRD: Section 8.4, Section 10 (build_steps) - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] BuildSteps 목록 (체크박스 + 진행률) - [ ] prompt_template 표시 + "프롬프트 복사" 버튼 - [ ] 완료 토글 - [ ] completion_percentage 반영 ## 의존성 - [P3] Ai::BlueprintGeneratorService
[P3] Spec 편집 UI
## 설명 AI 생성 spec(one_line_definition, core_features 등) 수정 가능 폼. Turbo Frame 인라인 편집. ## 참조 - 스킬: `hotwire-patterns`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] Spec show 페이지 - [ ] 인라인 편집 + 저장 - [ ] core_features 항목 추가/삭제 - [ ] 테스트 통과 ## 의존성 - [P3] Idea Analysis 비동기 처리
[P4] Toss::Client 서비스 구현
## 설명\nPRD Section 11.1 기반. Faraday HTTP 클라이언트.\n\n## 변경 사항 (PRD v2.1)\n- issue_billing_key, charge_billing 메서드 삭제\n- 남은 메서드: confirm_payment, cancel_payment, get_payment\n- find_user_email, find_user_name 헬퍼 삭제\n\n## 참조\n- PRD: Section 11.1\n- 스킬: `service-objects`, `rails-testing`\n\n## 완료 기준\n- [ ] confirm_payment, cancel_payment, get_payment 구현\n- [ ] Basic Auth 헤더 설정\n- [ ] 에러 응답 파싱\n- [ ] Faraday retry 미들웨어\n- [ ] 서비스 테스트 통과 (HTTP stub)\n\n## 의존성\n- [P1] DB 스키마 마이그레이션 Part 2
[P4] 코호트 일반결제 구현
## 설명\nPRD Section 11.2 Toss::PaymentService. 코호트 1회성 결제. checkout 파라미터 → 토스 결제창 → confirm → CohortEnrollment 생성.\n\n## 변경 사항 (PRD v2.1)\n- 유일한 결제 방식 (빌링키 없음)\n- 토스 빌링 추가 계약 불필요, 일반결제만 사용\n- payments 테이블에서 cohort_id 참조 (subscription_id 없음)\n\n## 참조\n- PRD: Section 11.2\n- 스킬: `service-objects`\n\n## 완료 기준\n- [ ] checkout 파라미터 생성\n- [ ] success/fail 콜백 처리\n- [ ] Payment 레코드 생성 (cohort_id 참조)\n- [ ] CohortEnrollment + User role :cohort\n- [ ] 테스트 통과\n\n## 의존성\n- [P4] Toss::Client 서비스
[P4] 토스 웹훅 처리
## 설명\nPRD Section 11.4. payments/webhooks 컨트롤러. CSRF skip. HMAC SHA-256 서명 검증.\n\n## 변경 사항 (PRD v2.1)\n- BILLING_SKIPPED 이벤트 핸들러 삭제\n- handle_billing_skipped 메서드 삭제\n- PAYMENT_STATUS_CHANGED만 처리\n\n## 참조\n- PRD: Section 11.4\n- 스킬: `security-pipeline`\n\n## 완료 기준\n- [ ] 웹훅 엔드포인트 동작\n- [ ] 서명 검증 (유효/무효)\n- [ ] 결제 상태 변경 → Payment 업데이트\n- [ ] 테스트 통과\n\n## 의존성\n- [P4] 코호트 일반결제
[P4] 결제 UI (코호트 1회성만)
## 설명\n코호트 결제 플로우 UI. 성공/실패 페이지.\n\n## 변경 사항 (PRD v2.1)\n- Pro 구독 관리 페이지 삭제 (my/subscription 제거)\n- 빌링키 등록 UI 삭제\n- 가격표는 Free/Cohort만 표시\n- 코호트 299만원 1회 결제만\n\n## 참조\n- PRD: Section 10 (payments)\n- 스킬: `ui-design`, `taste-skill`, `hotwire-patterns`\n\n## 완료 기준\n- [ ] 코호트 결제 checkout 페이지\n- [ ] 토스 결제위젯 JS 로드 + 결제창 호출\n- [ ] 성공/실패 결과 페이지\n- [ ] Pro 관련 UI 없음 확인\n\n## 의존성\n- [P4] 코호트 일반결제
[P5] 커뮤니티 피드 (게시글 CRUD)
## 설명 community_posts CRUD. post_type별 필터 (general/question/launch/share). Pagy 페이지네이션. ## 참조 - PRD: Section 8.14, Section 10 - 스킬: `rails-core`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] 게시글 작성/수정/삭제 - [ ] post_type별 필터 탭 - [ ] 페이지네이션 - [ ] 핀 게시글 상단 고정 - [ ] 테스트 통과 ## 의존성 - [P1] DB 스키마 마이그레이션 Part 2 - [P1] 라우트 설정
[P5] 커뮤니티 댓글 + 좋아요
## 설명 community_comments CRUD (중첩 댓글: parent_id). 좋아요 (post :like). likes_count 카운터 캐시. Turbo Stream 실시간. ## 참조 - PRD: Section 8.15, Section 10 - 스킬: `hotwire-patterns` ## 완료 기준 - [ ] 댓글 작성/삭제 - [ ] 대댓글 (1depth) - [ ] 좋아요 토글 - [ ] Turbo Stream 갱신 - [ ] 테스트 통과 ## 의존성 - [P5] 커뮤니티 피드
[P5] 쇼케이스 페이지
## 설명 showcase_services CRUD. 카테고리별 필터. 카드 그리드. 퍼블릭 + 인증 접근. ## 참조 - PRD: Section 8.13, Section 10 - 스킬: `ui-design`, `taste-skill` ## 완료 기준 - [ ] 쇼케이스 목록 (카드 그리드) - [ ] 카테고리 필터 - [ ] 상세 페이지 + 외부 링크 - [ ] 소유자만 수정 - [ ] 퍼블릭 /showcase 접근 - [ ] 테스트 통과 ## 의존성 - [P3] Project CRUD - [P1] 라우트 설정
[P5] LAUNCH 체크리스트 + 카피 생성기
## 설명 Launch 네임스페이스. 출시 준비 체크리스트. AI 카피 생성기 (landing_headline, landing_subcopy, landing_cta). Ai::CopyGeneratorService. ## 참조 - PRD: Section 10 (launch 네임스페이스) - 스킬: `service-objects` ## 완료 기준 - [ ] 체크리스트 항목 토글 - [ ] AI 카피 생성 동작 - [ ] 생성 카피 프로젝트 저장 - [ ] 테스트 통과 ## 의존성 - [P3] Ai::ClaudeMdGeneratorService
[P5] 코호트 신청 페이지 + 관리
## 설명\nCohort 목록/상세. 사전 신청 폼 + 승인 후 결제 연동. Admin 코호트 CRUD. CohortEnrollment 상태 관리.\n\n## 변경 사항 (PRD v2.1)\n- cohort_applications (사전 신청) 추가\n - 지원 동기, 현재 직업, 기대하는 점 입력 폼\n - Admin 승인/거절 관리\n - 승인 후 결제 안내 메일 발송\n - 결제 완료 → converted → cohort_enrollment 생성\n- B2B 문의 폼 추가 (별도 페이지)\n\n## 참조\n- PRD: Section 8.5 (cohort_applications), 8.5.1 (b2b_inquiries), Section 10\n- 스킬: `ui-design`, `taste-skill`\n\n## 완료 기준\n- [ ] 코호트 목록/상세\n- [ ] 사전 신청 폼 (motivation, current_job, expectation)\n- [ ] Admin 신청 관리 (승인/거절)\n- [ ] 승인 → 결제 플로우 연결\n- [ ] B2B 문의 폼 (company_name, contact_name, contact_email, message)\n- [ ] Admin B2B 문의 관리\n- [ ] 정원 체크 (max_capacity)\n- [ ] accepting_applications 토글\n- [ ] 테스트 통과\n\n## 의존성\n- [P4] 코호트 일반결제
[P6] AI 코치 채팅
## 설명 ai_conversations CRUD. 실시간 채팅 UI. Turbo Stream 메시지 스트리밍. Ai::CoachService (claude-opus-4-6). messages jsonb. ## 참조 - PRD: Section 8.10, Section 10 - 스킬: `hotwire-patterns`, `service-objects`, `ui-design`, `taste-skill` ## 완료 기준 - [ ] 새 대화 생성 - [ ] 메시지 전송 + AI 응답 - [ ] Turbo Stream 실시간 표시 - [ ] 대화 내역 보기 - [ ] 프로젝트 컨텍스트 연동 - [ ] 테스트 통과 ## 의존성 - [P1] DB 스키마 마이그레이션 Part 2 - [P1] 라우트 설정
[P6] 9WAY 연동 API + 동기화
## 설명 Nineways::SyncService. 9WAY API 강점 유형/DNA traits 조회. OAuth 리다이렉트/콜백. NinewaysSyncAllJob 정기 동기화. ## 참조 - PRD: Section 10 (auth/nineways, api/v1/nineways) ## 완료 기준 - [ ] 9WAY OAuth 플로우 - [ ] 강점 유형/DNA traits User에 저장 - [ ] 수동 동기화 버튼 - [ ] 배치 동기화 Job - [ ] API 웹훅 수신 - [ ] 테스트 통과 ## 의존성 - [P1] User 모델 확장 - [P1] 라우트 설정
[P6] Ahoy 이벤트 추적
## 설명 PRD Section 17 기반. 핵심 이벤트 13개 추적. ahoy.track 호출. Admin 이벤트 통계. ## 참조 - PRD: Section 17 ## 완료 기준 - [ ] Ahoy 설정 (visits + events 테이블) - [ ] 13개 핵심 이벤트 추적 - [ ] Admin 이벤트 조회 ## 의존성 - [P1] 라우트 설정
[P6] Sentry 에러 모니터링
## 설명 sentry-ruby, sentry-rails 초기화. SENTRY_DSN. Current.user 컨텍스트. 서비스 객체 에러 캡처. ## 완료 기준 - [ ] Sentry initializer 설정 - [ ] 에러 발생 시 Sentry 보고 - [ ] 유저 컨텍스트 포함 ## 의존성 - [P1] Rails 프로젝트 초기화
[P6] 메일러 구현 (UserMailer)
## 설명\nPRD Section 15 기반. 메일 템플릿. Resend API (Action Mailer). letter_opener 개발. deliver_later (Solid Queue).\n\n## 변경 사항 (PRD v2.1)\n- subscription_canceled 메일 삭제\n- cohort_application_received 메일 추가 (사전 신청 접수)\n- cohort_application_approved 메일 추가 (승인 후 결제 안내)\n\n## 참조\n- PRD: Section 15\n\n## 완료 기준\n- [ ] 메일 동작: welcome, onboarding, payment_success, payment_failed, cohort_application_received, cohort_application_approved, cohort_enrollment_confirmation, weekly_nudge\n- [ ] 개발환경 letter_opener\n- [ ] 프로덕션 Resend 설정\n- [ ] WeeklyNudgeEmailJob\n- [ ] 메일러 테스트 통과\n\n## 의존성\n- [P1] Gemfile 구성
[P6] 성능 최적화 + 보안 마무리
## 설명 Solid Cache 설정. N+1 쿼리 제거. CSP 설정 (PRD Section 16). Rate limiting. 이미지 처리. 시드 데이터. ## 참조 - PRD: Section 16 (보안 설정) - 스킬: `rails-best-practices`, `security-pipeline` ## 완료 기준 - [ ] Solid Cache 동작 - [ ] 주요 쿼리 N+1 해소 - [ ] CSP 헤더 설정 - [ ] 시드 데이터 데모 환경 - [ ] bin/ci 전체 테스트 통과 ## 의존성 - 모든 이전 작업
완료 (전체)
14[P1] Rails 프로젝트 초기화
## 설명 `rails new valueit --database=postgresql --css=tailwind --asset-pipeline=propshaft`로 프로젝트 생성. Docker Compose 개발 환경 설정 (PostgreSQL 16). `.env` 파일 템플릿 생성. ## 참조 - PRD: Section 3 (기술 스택), Section 18 Phase 1 Step 1 - 스킬: `rails-core`, `windows-docker` - 워크플로우: Analyzer → Rails Dev (TDD) ## 완료 기준 - [ ] `docker compose up`으로 Rails 서버 기동 - [ ] `bin/rails server`로 Welcome 페이지 표시 - [ ] `.env.example` 파일 존재 - [ ] `.gitignore`에 `.env` 포함 ## 의존성 없음
[P1] Gemfile 구성 + Bundle Install
## 설명 PRD Section 4의 전체 gem 추가. Solid Queue, Solid Cache, Solid Cable, Faraday, ruby-anthropic, pagy, ahoy_matey, meta-tags, sentry-ruby, resend 등. 테스트: Minitest + Fixtures 사용. ## 참조 - PRD: Section 4 (Gemfile 전체) - 스킬: `rails-core` ## 완료 기준 - [ ] `bundle install` 성공 - [ ] `bin/rails test` 실행 가능 - [ ] 모든 gem 버전 호환 확인 ## 의존성 - [P1] Rails 프로젝트 초기화
[P1] Rails 8 Authentication 설정
## 설명 `rails generate authentication` 실행. Session 모델 자동 생성. Authentication concern + Current.user 패턴. 회원가입 (registrations#new, #create) 컨트롤러 추가. ## 참조 - PRD: Section 8.2 (sessions), Section 9 (User 모델) - 스킬: `rails-core`, `rails-testing` - 워크플로우: Analyzer → Rails Dev (TDD) ## 완료 기준 - [ ] 로그인/로그아웃 동작 - [ ] 회원가입 동작 - [ ] `Current.user` 접근 가능 - [ ] 비인증 사용자 리다이렉트 - [ ] Minitest 통과 ## 의존성 - [P1] Gemfile 구성
[P1] User 모델 확장
## 설명\nPRD Section 8.1 추가 필드 마이그레이션. role/onboarding_goal/dev_level enum. 9WAY 연동 필드. streak/XP 필드.\n`can_access?`, `toss_customer_key` 메서드 구현.\n\n## 변경 사항 (PRD v2.1)\n- role enum 변경: `{ free: 0, cohort: 1, admin: 2 }` (Pro 제거)\n- `has_one :subscription` 제거\n- `active_subscription?` 메서드 제거\n- `can_access?`에서 `:pro` 레벨 제거\n- `has_many :cohort_applications` 추가\n\n## 참조\n- PRD: Section 8.1, Section 9 (User 모델 코드)\n- 스킬: `rails-testing`, `database-migrations`\n\n## 완료 기준\n- [ ] role enum: { free: 0, cohort: 1, admin: 2 } 동작\n- [ ] onboarding_goal, dev_level enum 동작\n- [ ] `can_access?(:free/:cohort/:admin)` 정상 동작\n- [ ] `toss_customer_key` 메서드 동작\n- [ ] 모델 테스트 통과\n\n## 의존성\n- [P1] Rails 8 Authentication 설정
[P1] DB 스키마 마이그레이션 Part 1 (학습/프로젝트)
## 설명 PRD Section 8.3~8.4, 8.7~8.9 마이그레이션. projects, build_steps, curricula, lessons, user_lesson_progresses. ## 참조 - PRD: Section 8.3~8.4, 8.7~8.9 - 스킬: `database-migrations` ## 완료 기준 - [ ] `bin/rails db:migrate` 성공 - [ ] 모든 인덱스 생성 확인 - [ ] 모델 연관관계 설정 (belongs_to, has_many 등) - [ ] fixture 파일 생성 - [ ] 모델 테스트 통과 ## 의존성 - [P1] User 모델 확장
DB 마이그레이션 Part 1 — projects, build_steps, curricula, lessons, user_lesson_progresses
## 목표 PRD Section 8.3~8.4, 8.7~8.9 기반 5개 테이블 생성 + 모델 + 연관관계 + 테스트. ## 생성할 테이블 (PRD 그대로) ### 1. projects (Section 8.3) ```ruby create_table :projects do |t| t.references :user, null: false, foreign_key: true t.string :title, null: false t.text :description t.integer :status, default: 0, null: false t.text :raw_idea t.string :one_line_definition t.jsonb :core_features, default: [] t.string :target_customer t.string :revenue_model t.jsonb :competitor_analysis, default: {} t.string :tech_stack t.string :tech_stack_reason t.jsonb :db_schema_draft, default: {} t.jsonb :pages_list, default: [] t.jsonb :dev_priority, default: [] t.text :claude_md_content t.datetime :claude_md_generated_at t.string :service_url t.datetime :launched_at t.integer :user_count, default: 0 t.string :landing_headline t.string :landing_subcopy t.string :landing_cta t.string :category t.timestamps end add_index :projects, :user_id # references가 자동 생성하지만 확인 add_index :projects, :status ``` ### 2. build_steps (Section 8.4) ```ruby create_table :build_steps do |t| t.references :project, null: false, foreign_key: true t.integer :step_number, null: false t.string :title, null: false t.text :description t.text :prompt_template t.boolean :completed, default: false t.datetime :completed_at t.timestamps end add_index :build_steps, [:project_id, :step_number], unique: true ``` ### 3. curricula (Section 8.7) ```ruby create_table :curricula do |t| t.string :title, null: false t.text :description t.integer :week_start, null: false t.integer :week_end, null: false t.integer :position, null: false t.boolean :published, default: false t.timestamps end ``` ### 4. lessons (Section 8.8) ```ruby create_table :lessons do |t| t.references :curriculum, null: false, foreign_key: true t.string :title, null: false t.text :content t.string :content_type, default: "article" t.string :video_url t.integer :read_time_minutes t.integer :position t.boolean :published, default: false t.integer :required_role, default: 0 t.timestamps end ``` ### 5. user_lesson_progresses (Section 8.9) ```ruby create_table :user_lesson_progresses do |t| t.references :user, null: false, foreign_key: true t.references :lesson, null: false, foreign_key: true t.boolean :completed, default: false t.datetime :completed_at t.datetime :started_at t.integer :time_spent_seconds, default: 0 t.timestamps end add_index :user_lesson_progresses, [:user_id, :lesson_id], unique: true ``` ## 모델 (PRD Section 9 참조) ### Project ```ruby belongs_to :user has_many :build_steps, -> { order(:step_number) }, dependent: :destroy has_many :ai_conversations, dependent: :destroy # 아직 테이블 없음 — 선언만 has_one :showcase_service, dependent: :destroy # 아직 테이블 없음 — 선언만 enum :status, { ideating: 0, building: 1, launched: 2, abandoned: 3 } validates :title, presence: true def completion_percentage total = build_steps.count return 0 if total.zero? (build_steps.where(completed: true).count.to_f / total * 100).round end ``` ### BuildStep ```ruby belongs_to :project validates :step_number, presence: true, uniqueness: { scope: :project_id } validates :title, presence: true ``` ### Curriculum ```ruby has_many :lessons, -> { order(:position) }, dependent: :destroy validates :title, presence: true scope :published, -> { where(published: true) } ``` ### Lesson ```ruby belongs_to :curriculum has_many :user_lesson_progresses, dependent: :destroy validates :title, presence: true scope :published, -> { where(published: true) } ``` ### UserLessonProgress ```ruby belongs_to :user belongs_to :lesson validates :lesson_id, uniqueness: { scope: :user_id } ``` ### User 모델 업데이트 (추가 연관관계) ```ruby has_many :projects, dependent: :destroy has_many :user_lesson_progresses, dependent: :destroy has_many :lessons, through: :user_lesson_progresses ``` ## ⚠️ 주의 - ai_conversations, showcase_services 테이블은 아직 없음 → has_many 선언은 하되, 없는 테이블 참조 시 에러나지 않도록 주의 - `dev_level` enum은 `no_experience: 0`으로 변경됨 (이전 세션에서 수정) - 기존 User 모델의 normalizes, validates 유지 ## 테스트 (TDD) - 각 모델 테스트: validates, enum, 연관관계 - Project#completion_percentage 테스트 - fixture 파일 생성 (각 모델 최소 2개) - User has_many :projects 테스트 ## 완료 기준 - bin/rails db:migrate 성공 - 모든 인덱스 생성 확인 - 모델 테스트 전체 통과 - 기존 테스트 깨지지 않음 (bin/rails test 전체 통과)
[P1] Tailwind 디자인 시스템 설정
## 설명 Tailwind CSS 4.x 커스텀 테마. 색상: bg #070707, accent #C8FF00, green #00E5A0, text #f2f2f2. 폰트: Pretendard(한국어), DM Mono(코드). 공용 레이아웃, 네비게이션 Partial. ## 참조 - PRD: Section 3.2, 디자인 시스템 섹션 - 스킬: `ui-design`, `taste-skill(Supanova Design Engine)` - 디자인 원칙: `.claude/skills/taste-skill/SKILL.md` 반드시 참조 - Pretendard 필수, Inter/Noto Sans KR 금지 - 다크 모드 기본, ANTI-EMOJI 정책 - Korean First 타이포그래피 (break-keep-all, leading-tight) ## 완료 기준 - [ ] 커스텀 색상 Tailwind 클래스 사용 가능 - [ ] Pretendard, DM Mono 폰트 로드 - [ ] 다크 테마 기본 레이아웃 (application.html.erb) - [ ] 반응형 네비게이션 바 (모바일 우선) - [ ] 공용 Partial: _button, _card, _input 등 ## 의존성 - [P1] Gemfile 구성
온보딩 플로우 — 4단계 컨트롤러 + 뷰 + 테스트
## 목표 온보딩 4단계 구현. Onboarding::StepsController + 뷰 + 테스트. ## PRD 라우트 (Section 10) ```ruby namespace :onboarding do get "/", to: "steps#index" # → nineways로 리다이렉트 get "/nineways", to: "steps#nineways" # Step 1 post "/nineways", to: "steps#connect_nineways" get "/goal", to: "steps#goal" # Step 2 post "/goal", to: "steps#save_goal" get "/level", to: "steps#level" # Step 3 post "/level", to: "steps#save_level" get "/idea", to: "steps#idea" # Step 4 post "/idea", to: "steps#save_idea" get "/complete", to: "steps#complete" # 완료 페이지 end ``` ## 현재 상태 - User 모델에 onboarding_goal(enum), dev_level(enum), onboarding_completed(boolean) 존재 - 라이트 테마 랜딩페이지 구현됨 - ⚠️ 랜딩페이지가 라이트 테마 (bg-[#FAFAF7])로 되어있지만, 온보딩은 앱 내부이므로 기존 다크 테마 토큰 사용 (bg-bg, text-text-primary, bg-accent 등) ## 구현 사항 ### 1. config/routes.rb에 온보딩 네임스페이스 추가 기존 라우트 유지하면서 추가. `resource :session` 바로 아래에: ```ruby namespace :onboarding do get "/", to: "steps#index" get "nineways", to: "steps#nineways" post "nineways", to: "steps#connect_nineways" get "goal", to: "steps#goal" post "goal", to: "steps#save_goal" get "level", to: "steps#level" post "level", to: "steps#save_level" get "idea", to: "steps#idea" post "idea", to: "steps#save_idea" get "complete", to: "steps#complete" end ``` ### 2. Onboarding::StepsController ```ruby class Onboarding::StepsController < ApplicationController before_action :redirect_if_completed def index redirect_to onboarding_nineways_path end # Step 1: 9WAY 연동 (선택) def nineways; end def connect_nineways # 9WAY 연동은 나중 구현 — 지금은 건너뛰기만 redirect_to onboarding_goal_path end # Step 2: 목표 선택 def goal; end def save_goal Current.user.update!(onboarding_goal: params[:onboarding_goal]) redirect_to onboarding_level_path end # Step 3: 개발 수준 def level; end def save_level Current.user.update!(dev_level: params[:dev_level]) redirect_to onboarding_idea_path end # Step 4: 첫 아이디어 def idea; end def save_idea # 아이디어는 나중에 Project로 연결 — 지금은 온보딩 완료만 Current.user.update!(onboarding_completed: true) redirect_to onboarding_complete_path end def complete # 온보딩 완료 축하 페이지 end private def redirect_if_completed redirect_to root_path if Current.user&.onboarding_completed? end end ``` ### 3. 뷰 (app/views/onboarding/steps/) - 다크 테마 (기존 디자인 토큰 사용: bg-bg, bg-surface, text-accent 등) - 진행 인디케이터: 상단에 4단계 표시 (Step 1/4, 2/4, 3/4, 4/4) - 각 단계별: - **nineways.html.erb**: 9WAY 연동 안내 + "건너뛰기" 버튼 - **goal.html.erb**: 4가지 목표 라디오 버튼 (saas/side_income/portfolio/validation) - **level.html.erb**: 4가지 레벨 라디오 버튼 (no_experience/basic/intermediate/developer) - **idea.html.erb**: textarea로 아이디어 입력 - **complete.html.erb**: 축하 메시지 + 대시보드 이동 CTA - 기존 shared partial 활용 (_button, _card, _input) - 한국어 텍스트 ### 4. ApplicationController에 온보딩 체크 이미 인증된 사용자가 온보딩 미완료 시 리다이렉트: ```ruby # app/controllers/application_controller.rb before_action :check_onboarding def check_onboarding return unless authenticated? return if Current.user&.onboarding_completed? return if controller_path.start_with?("onboarding") return if controller_path.in?(["sessions", "passwords", "registrations", "pages"]) redirect_to onboarding_path end ``` ### 5. 테스트 - 각 단계 접근 가능 (인증 필요) - save_goal → User.onboarding_goal 업데이트 - save_level → User.dev_level 업데이트 - save_idea → User.onboarding_completed = true - 온보딩 완료 시 redirect_to root_path - 미인증 사용자 → 로그인 리다이렉트 - 온보딩 완료 후 재방문 시 root 리다이렉트 ## ⚠️ 주의 - developer-1이 동시에 DB 마이그레이션 작업 중 — 충돌 방지 위해 user.rb의 has_many 추가는 developer-1 담당 - routes.rb 변경 시 기존 라우트 유지 (root "pages#landing", sessions, passwords, registrations) - 온보딩 뷰는 다크 테마 (앱 내부) — 랜딩페이지의 라이트 테마와 다름 - 기존 PagesController 테스트, Sessions/Passwords/Registrations 테스트 깨지지 않게 주의 ## 완료 기준 - 4단계 순차 진행 가능 - 각 단계에서 User 필드 업데이트 - 온보딩 미완료 시 리다이렉트 - 진행 인디케이터 UI - bin/rails test 전체 통과
[P1] 랜딩페이지 (pages#landing)
## 설명\n퍼블릭 랜딩페이지. 히어로 + 3 Pain Points + 쇼케이스 + 가격표 + CTA. 다크 테마 + 라임 포인트. SEO meta-tags.\n\n## 변경 사항 (PRD v2.1)\n- 가격표: Free/Cohort만 (Pro 플랜 제거)\n- B2B 문의 CTA 추가\n- 코호트 사전 신청 CTA 추가\n\n## 참조\n- PRD: Section 18 Phase 1 Step 7\n- 스킬: `ui-design`, `taste-skill(Supanova Design Engine)` 반드시 참조\n\n## 완료 기준\n- [ ] `root \"pages#landing\"` 라우트 동작\n- [ ] 모바일/데스크탑 반응형\n- [ ] meta-tags (OG, Twitter Card)\n- [ ] 가격표 섹션 (Free/Cohort만)\n- [ ] B2B 문의 CTA\n- [ ] 코호트 사전 신청 CTA\n- [ ] CTA → 회원가입 연결\n\n## 의존성\n- [P1] Tailwind 디자인 시스템 설정
[P1] 온보딩 플로우 (4단계)
## 설명 온보딩 네임스페이스 컨트롤러/뷰. Step 1: 9WAY 연동(선택), Step 2: 목표 선택, Step 3: 개발 수준, Step 4: 첫 아이디어 입력. 완료 시 onboarding_completed = true. ## 참조 - PRD: Section 10 (onboarding 라우트), Section 18 Step 8 - 스킬: `hotwire-patterns` (Turbo Frames 멀티스텝), `ui-design`, `taste-skill` ## 완료 기준 - [ ] 4단계 순차 진행 가능 - [ ] 각 단계에서 User 모델 필드 업데이트 - [ ] 온보딩 미완료 시 대시보드 → 온보딩 리다이렉트 - [ ] 진행 인디케이터 UI - [ ] 컨트롤러 테스트 통과 ## 의존성 - [P1] User 모델 확장 - [P1] Tailwind 디자인 시스템 설정
[P1] Kamal 2 배포 설정
## 설명 PRD Section 6.1 기반 config/deploy.yml. .kamal/secrets 템플릿. Dockerfile 최적화. Active Storage Vultr Object Storage 설정. ## 참조 - PRD: Section 6 (Kamal 2 전체), Section 7 (Active Storage) ## 완료 기준 - [ ] config/deploy.yml PRD 구조 일치 - [ ] .kamal/secrets 템플릿 존재 - [ ] config/storage.yml vultr 서비스 설정 - [ ] Dockerfile 빌드 성공 - [ ] 배포 체크리스트 문서 ## 의존성 - [P1] Gemfile 구성
[P1] i18n 한국어 기본 설정
## 설명 PRD Section 14 기반. default_locale :ko. config/locales/ko.yml 기본 번역. ActiveRecord 에러 메시지 한국어화. ## 참조 - PRD: Section 14 (i18n 전체 코드) ## 완료 기준 - [ ] I18n.locale == :ko 기본값 - [ ] 유효성 검사 에러 한국어 표시 - [ ] 결제/플랜 관련 번역 키 존재 ## 의존성 - [P1] Rails 프로젝트 초기화
[삭제] Pro 정기결제 (빌링키) — 스코프 제외
## 삭제 사유\nPro 플랜 및 빌링키 정기결제 로직 전체 삭제. 일반결제(코호트 1회성)만 사용.\n\n## 영향\n- Toss::BillingService 삭제\n- subscriptions 테이블 삭제\n- 빌링키 관련 API 메서드 삭제
[삭제] Solid Queue 정기결제 스케줄링 — 스코프 제외
## 삭제 사유\nPro 정기결제 삭제로 인해 관련 잡(MonthlyBillingJob, ChargeBillingJob, RetryBillingJob) 전체 불필요.\nbilling 큐, recurring.yml monthly_billing 설정 삭제.