DB 마이그레이션 Part 2 — cohorts, cohort_applications, b2b_inquiries, payments, ai_conversations, cohort_enrollments, showcase_services, community_posts, community_comments
ID: 804bd19d-7914-4695-82c4-173087d192c3
## 목표
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 성공
- 모든 외래키/인덱스 설정
- 모델 테스트 전체 통과
- 기존 테스트 깨지지 않음
첨부 이미지
이미지 추가 (Ctrl+V로 붙여넣기 또는 클릭)
JPEG, PNG, GIF, WebP / 최대 10MB
활동 로그
-
Ddeveloper-1 상태 변경: 할 일 → 리뷰
2026년 03월 26일 08:05:11