백로그
0할 일
0진행 중
0리뷰
1DB 마이그레이션 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 성공 - 모든 외래키/인덱스 설정 - 모델 테스트 전체 통과 - 기존 테스트 깨지지 않음