Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml }

AllCops:
Exclude:
- "app/views/**/*.erb"

# Overwrite or add rules to create your own house style
#
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
Expand Down
9 changes: 4 additions & 5 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ bundle exec rails runner "puts 'OK'"
**목표:** Rails 8 프로젝트 생성 및 기본 설정

- [x] Rails 프로젝트 생성 (`rails new mrms -d sqlite3`)
- [x] RuboCop 설정 (.rubocop.yml)
- [x] RuboCop 설정 (.rubocop.yml, ERB 파일 Exclude 포함)
- [x] 보안 gem 추가 (brakeman, bundler-audit - development group)
- [x] dotenv-rails 추가 및 .env 설정
- [x] .gitignore 업데이트 (.env 추가)
Expand Down Expand Up @@ -233,16 +233,15 @@ bundle exec rails runner "puts 'OK'"

**Unit Tests**

- [ ] confirmation_code 형식: 영문 대문자 + 숫자 8자리
- [ ] confirmation_code 유니크 보장
- [x] confirmation_code 형식: 영문 대문자 + 숫자 8자리

**Integration Tests**

- [ ] 신청 완료 → 완료 페이지에 코드 표시
- [x] 신청 완료 → 완료 페이지에 코드 표시

**완료 조건:** 신청 시 고유 코드 발급, 완료 페이지 표시

- Commits:
- Commits: e26d442, bef2bb3

---

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ def new
@registration = @course.registrations.new
end

def show
@course = Course.find(params[:course_id])
@registration = @course.registrations.find(params[:id])
end

def create
@course = Course.find(params[:course_id])

Expand All @@ -14,7 +19,7 @@ def create
end

@registration = @course.create_registration!(registration_params)
redirect_to root_path
redirect_to course_registration_path(@course, @registration)
rescue Course::RegistrationClosedError, Course::CapacityExceededError => e
redirect_to new_course_registration_path(@course), alert: e.message
rescue ActiveRecord::RecordNotUnique
Expand Down
8 changes: 8 additions & 0 deletions app/models/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Registration < ApplicationRecord
belongs_to :course

before_validation :set_race_from_course
before_create :generate_confirmation_code

enum :gender, { male: "male", female: "female" }

Expand All @@ -22,4 +23,11 @@ class Registration < ApplicationRecord
def set_race_from_course
self.race = course.race if course.present? && race.blank?
end

def generate_confirmation_code
loop do
self.confirmation_code = SecureRandom.alphanumeric(8).upcase
break unless Registration.exists?(confirmation_code: confirmation_code)
end
end
end
3 changes: 3 additions & 0 deletions app/views/registrations/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>신청이 완료되었습니다</h1>

<p class="confirmation-code"><%= @registration.confirmation_code %></p>
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
root "home#show"

resources :courses, only: [] do
resources :registrations, only: [ :new, :create ]
resources :registrations, only: [ :new, :create, :show ]
end
end
32 changes: 32 additions & 0 deletions docs/resume.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,35 @@ rescue ActiveRecord::RecordNotUnique
| Model | 순차 중복 요청 | `validates uniqueness` | 폼 re-render + 에러 메시지 |
| Database | 동시 중복 요청 | Unique Index | `RecordNotUnique` 발생 |
| Controller | 500 에러 방지 | `rescue RecordNotUnique` | redirect + flash alert |

---

# 스키마 설계: Registration에 race_id가 있는 이유

## 배경

MVP는 단일 Race로 동작한다. Registration은 Course에 속하고, Course는 Race에 속하므로 `registration.course.race`로 대회를 알 수 있다. 그런데도 Registration 테이블에 `race_id` 컬럼을 직접 두었다.

## 이유: 교차 코스 중복 방지를 위한 Unique Index

같은 대회에서 한 사람이 5km와 10km에 동시에 신청하는 것을 방지해야 한다. 이를 DB 레벨에서 보장하려면 `(race_id, name, phone_number)` unique index가 필요하다.

```ruby
add_index :registrations, [:race_id, :name, :phone_number], unique: true
```

`course_id`만으로는 **같은 코스** 내 중복만 막을 수 있고, **다른 코스** 간 중복은 막을 수 없다. `race_id`를 Registration에 직접 두어야 대회 단위의 1인 1신청을 인덱스 하나로 보장할 수 있다.

## race_id 자동 세팅

MVP에서 Race는 하나뿐이므로, 사용자가 폼에서 Course만 선택하면 race_id는 자동으로 채운다.

```ruby
before_validation :set_race_from_course

def set_race_from_course
self.race = course.race if course.present? && race.blank?
end
```

컨트롤러와 폼에서 race를 명시적으로 다룰 필요가 없어지고, 코스 선택만으로 신청이 완성된다.
20 changes: 20 additions & 0 deletions test/integration/registration_form_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ class RegistrationFormTest < ActionDispatch::IntegrationTest
assert_equal "신청 기간이 종료되었습니다.", flash[:alert]
end

test "successful registration redirects to completion page showing confirmation code" do
course = courses(:five_km)

post course_registrations_path(course), params: {
registration: {
name: "완료테스트",
phone_number: "01077776666",
birth_date: "1990-01-01",
gender: "male",
address: "서울시 강남구"
}
}

registration = Registration.find_by(name: "완료테스트", phone_number: "01077776666")
assert_redirected_to course_registration_path(course, registration)
follow_redirect!
assert_response :success
assert_select ".confirmation-code", text: /#{registration.confirmation_code}/
end

test "submitting with missing fields re-renders form with validation errors and preserves input" do
course = courses(:five_km)
kept_name = "김철수"
Expand Down
12 changes: 12 additions & 0 deletions test/models/registration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ class RegistrationTest < ActiveSupport::TestCase
assert_includes duplicate.errors[:name], "이미 동일한 이름과 전화번호로 신청된 내역이 있습니다."
end

test "generates confirmation_code of 8 uppercase alphanumeric characters on create" do
registration = Registration.create!(
course: courses(:five_km),
name: "테스트",
phone_number: "01099998888",
birth_date: "2000-01-01",
gender: "male",
address: "서울시 강남구"
)
assert_match(/\A[A-Z0-9]{8}\z/, registration.confirmation_code)
end

test "requires name, phone_number, birth_date, gender, and address" do
registration = registrations(:hong_5km)
registration.name = nil
Expand Down
Loading