├── .gitignore ├── image ├── Chapter5 │ ├── kitten.jpg │ ├── kitten_on_home.png │ ├── layout_production.png │ ├── about_page_styled_3rd_edition.png │ ├── home_page_mockup_3rd_edition.png │ ├── new_signup_page_4th_edition.png │ ├── sample_app_logo_3rd_edition.png │ ├── sample_app_typography_3rd_edition.png │ ├── sample_app_universal_3rd_edition.png │ ├── sample_app_only_bootstrap_3rd_edition.png │ ├── site_with_footer_bootstrap_3rd_edition.png │ └── layout_no_logo_or_custom_css_bootstrap_3rd_edition.png ├── Chapter3 │ ├── about_us.png │ ├── gitignore.png │ ├── raw_home_view.png │ ├── red_to_green.png │ ├── config_directory.png │ ├── custom_help_page.png │ ├── custom_home_page.png │ ├── home_root_route.png │ ├── home_view_full_html.png │ ├── show_hidden_files.png │ ├── file_navigator_gear_icon.png │ └── heroku_app_hello_world.png ├── Chapter6 │ ├── rubular.png │ ├── users_table.png │ ├── sqlite_download.png │ ├── user_model_sketch.png │ ├── signup_mockup_bootstrap.png │ ├── user_model_initial_3rd_edition.png │ ├── sqlite_database_browser_3rd_edition.png │ ├── user_model_password_digest_3rd_edition.png │ └── sqlite_user_row_with_password_4th_edition.png ├── Chapter14 │ ├── user_feed.png │ ├── followed_user.png │ ├── unfollowed_user.png │ ├── live_status_feed.png │ ├── relationship_model.png │ ├── stats_partial_mockup.png │ ├── followers_mockup_bootstrap.png │ ├── following_mockup_bootstrap.png │ ├── user_followers_3rd_edition.png │ ├── user_following_3rd_edition.png │ ├── naive_user_has_many_following.png │ ├── home_page_with_feed_3rd_edition.png │ ├── profile_follow_button_3rd_edition.png │ ├── diferent_user_followers_3rd_edition.png │ ├── home_page_follow_stats_3rd_edition.png │ ├── profile_unfollow_button_3rd_edition.png │ ├── user_has_many_followers_3rd_edition.png │ ├── user_has_many_following_3rd_edition.png │ ├── page_flow_profile_mockup_3rd_edition.png │ ├── page_flow_user_index_mockup_bootstrap.png │ ├── page_flow_home_page_feed_mockup_3rd_edition.png │ ├── page_flow_home_page_feed_mockup_bootstrap.png │ ├── page_flow_other_profile_follow_button_mockup_3rd_edition.png │ └── page_flow_other_profile_unfollow_button_mockup_3rd_edition.png ├── Chapter8 │ ├── login_form.png │ ├── login_mockup.png │ ├── login_failure_mockup.png │ ├── login_success_mockup.png │ ├── flash_persistence_3rd_edition.png │ ├── failed_login_flash_3rd_edition.png │ ├── initial_failed_login_3rd_edition.png │ └── profile_with_logout_link_3rd_edition.png ├── Chapter1 │ ├── add_public_key.png │ ├── add_repository.png │ ├── cloud9_ide_aws.png │ ├── goodbye_world.png │ ├── mvc_schematic.png │ ├── cloud9_page_aws.png │ ├── ide_anatomy_aws.png │ ├── cloud9_gemfile_aws.png │ ├── share_workspace_aws.png │ ├── cloud9_two_spaces_aws.png │ ├── hello_world_hello_app.png │ ├── new_terminal_tab_aws.png │ ├── bitbucket_default_readme.png │ ├── cloud9_name_environment.png │ ├── full_browser_window_aws.png │ ├── heroku_app_hello_world.png │ ├── rails_server_new_tab_aws.png │ ├── new_readme_bitbucket_4th_ed.png │ ├── riding_rails_4th_edition_aws.png │ ├── bitbucket_repository_page_4th_ed.png │ ├── directory_structure_rails_4th_edition.png │ └── create_first_repository_bitbucket_4th_ed.png ├── Chapter2 │ ├── mvc_detailed.png │ ├── demo_user_model.png │ ├── demo_micropost_model.png │ ├── demo_micropost_model 2.png │ ├── heroku_app_hello_world.png │ ├── create_demo_repo_bitbucket.png │ ├── demo_edit_user_3rd_edition.png │ ├── demo_new_user_3rd_edition.png │ ├── demo_show_user_3rd_edition.png │ ├── micropost_user_association.png │ ├── user_presence_validations.png │ ├── demo_controller_inheritance.png │ ├── demo_destroy_user_3rd_edition.png │ ├── demo_update_user_3rd_edition.png │ ├── demo_new_micropost_3rd_edition.png │ ├── demo_user_index_two_3rd_edition.png │ ├── micropost_content_cant_be_blank.png │ ├── demo_blank_user_index_3rd_edition.png │ ├── demo_micropost_index_3rd_edition.png │ └── micropost_length_error_3rd_edition.png ├── Chapter7 │ ├── first_signup.png │ ├── user_show_3rd_edition.png │ ├── no_create_template_error.png │ ├── profile_mockup_bootstrap.png │ ├── signup_flash_3rd_edition.png │ ├── signup_form_3rd_edition.png │ ├── signup_mockup_bootstrap.png │ ├── signup_failure_4th_edition.png │ ├── new_signup_page_3rd_edition.png │ ├── valid_submission_error_4th_ed.png │ ├── signup_failure_mockup_bootstrap.png │ ├── signup_success_mockup_bootstrap.png │ ├── home_page_with_debug_3rd_edition.png │ ├── profile_routing_error_4th_edition.png │ ├── profile_with_gravatar_3rd_edition.png │ ├── signup_error_messages_3rd_edition.png │ ├── signup_failure_debug_4th_edition.png │ ├── signup_flash_reloaded_3rd_edition.png │ ├── signup_in_production_4th_edition.png │ ├── user_show_sidebar_css_3rd_edition.png │ ├── filled_in_form_bootstrap_3rd_edition.png │ ├── profile_custom_gravatar_3rd_edition.png │ ├── user_show_unknown_action_3rd_edition.png │ ├── invalid_submission_no_feedback_4th_ed.png │ └── profile_mockup_profile_name_bootstrap.png ├── Chapter10 │ ├── gravatar_cropper_new.png │ ├── heroku_sample_users.png │ ├── edit_form_working_new.png │ ├── edit_page_3rd_edition.png │ ├── edit_user_mockup_bootstrap.png │ ├── login_page_protected_mockup.png │ ├── protected_log_in_3rd_edition.png │ ├── user_index_all_3rd_edition.png │ ├── user_index_mockup_bootstrap.png │ ├── user_model_admin_3rd_edition.png │ ├── index_delete_links_3rd_edition.png │ ├── user_index_only_one_3rd_edition.png │ ├── user_index_page_two_3rd_edition.png │ ├── user_index_pagination_3rd_edition.png │ ├── user_index_delete_links_mockup_bootstrap.png │ └── edit_with_invalid_information_3rd_edition.png ├── Chapter12 │ ├── forgot_password_form.png │ ├── forgot_password_link.png │ ├── password_reset_form.png │ ├── user_model_password_reset.png │ ├── forgot_password_form_mockup.png │ ├── invalid_email_password_reset.png │ ├── login_forgot_password_mockup.png │ ├── reset_password_form_mockup.png │ ├── valid_email_password_reset.png │ ├── password_reset_failure_4th_ed.png │ ├── password_reset_success_4th_ed.png │ ├── reset_email_production_4th_ed.png │ ├── password_reset_html_preview_4th_ed.png │ └── password_reset_text_preview_4th_ed.png ├── Chapter13 │ ├── resized_image_4th_ed.png │ ├── micropost_image_mockup.png │ ├── micropost_model_picture.png │ ├── micropost_belongs_to_user.png │ ├── user_has_many_microposts.png │ ├── home_form_errors_3rd_edition.png │ ├── home_post_delete_3rd_edition.png │ ├── home_with_form_3rd_edition.png │ ├── large_uploaded_image_4th_ed.png │ ├── micropost_model_3rd_edition.png │ ├── microposts_with_image_4th_ed.png │ ├── image_upload_production_4th_ed.png │ ├── micropost_created_3rd_edition.png │ ├── proto_feed_mockup_3rd_edition.png │ ├── home_with_proto_feed_3rd_edition.png │ ├── user_microposts_mockup_3rd_edition.png │ ├── user_profile_no_microposts_3rd_edition.png │ ├── user_profile_with_microposts_3rd_edition.png │ ├── micropost_delete_links_mockup_3rd_edition.png │ ├── other_profile_with_microposts_3rd_edition.png │ ├── user_profile_microposts_page_2_3rd_edition.png │ ├── home_page_with_micropost_form_mockup_bootstrap.png │ └── user_profile_microposts_no_styling_3rd_edition.png ├── Chapter11 │ ├── activated_in_production.png │ ├── activated_user_4th_ed.png │ ├── not_activated_warning.png │ ├── redirected_not_activated.png │ ├── user_model_account_activation.png │ ├── activation_email_production_4th_ed.png │ ├── account_activation_html_preview_4th_ed.png │ └── account_activation_text_preview_4th_ed.png ├── Chapter9 │ ├── cookie_in_browser_chrome.png │ ├── login_form_remember_me.png │ ├── login_remember_me_mockup.png │ └── user_model_remember_digest.png └── Chapter4 │ ├── word_inheritance_ruby_1_9.png │ ├── string_inheritance_ruby_1_9.png │ └── static_pages_controller_inheritance.png ├── Documentation ├── author.md ├── license.md ├── Chapter2.md ├── Chapter12.md └── Chapter8.md ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /image/Chapter5/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/kitten.jpg -------------------------------------------------------------------------------- /image/Chapter3/about_us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/about_us.png -------------------------------------------------------------------------------- /image/Chapter3/gitignore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/gitignore.png -------------------------------------------------------------------------------- /image/Chapter6/rubular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/rubular.png -------------------------------------------------------------------------------- /image/Chapter14/user_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/user_feed.png -------------------------------------------------------------------------------- /image/Chapter6/users_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/users_table.png -------------------------------------------------------------------------------- /image/Chapter8/login_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/login_form.png -------------------------------------------------------------------------------- /image/Chapter1/add_public_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/add_public_key.png -------------------------------------------------------------------------------- /image/Chapter1/add_repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/add_repository.png -------------------------------------------------------------------------------- /image/Chapter1/cloud9_ide_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/cloud9_ide_aws.png -------------------------------------------------------------------------------- /image/Chapter1/goodbye_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/goodbye_world.png -------------------------------------------------------------------------------- /image/Chapter1/mvc_schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/mvc_schematic.png -------------------------------------------------------------------------------- /image/Chapter14/followed_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/followed_user.png -------------------------------------------------------------------------------- /image/Chapter2/mvc_detailed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/mvc_detailed.png -------------------------------------------------------------------------------- /image/Chapter3/raw_home_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/raw_home_view.png -------------------------------------------------------------------------------- /image/Chapter3/red_to_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/red_to_green.png -------------------------------------------------------------------------------- /image/Chapter5/kitten_on_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/kitten_on_home.png -------------------------------------------------------------------------------- /image/Chapter7/first_signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/first_signup.png -------------------------------------------------------------------------------- /image/Chapter8/login_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/login_mockup.png -------------------------------------------------------------------------------- /image/Chapter1/cloud9_page_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/cloud9_page_aws.png -------------------------------------------------------------------------------- /image/Chapter1/ide_anatomy_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/ide_anatomy_aws.png -------------------------------------------------------------------------------- /image/Chapter14/unfollowed_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/unfollowed_user.png -------------------------------------------------------------------------------- /image/Chapter2/demo_user_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_user_model.png -------------------------------------------------------------------------------- /image/Chapter3/config_directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/config_directory.png -------------------------------------------------------------------------------- /image/Chapter3/custom_help_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/custom_help_page.png -------------------------------------------------------------------------------- /image/Chapter3/custom_home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/custom_home_page.png -------------------------------------------------------------------------------- /image/Chapter3/home_root_route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/home_root_route.png -------------------------------------------------------------------------------- /image/Chapter6/sqlite_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/sqlite_download.png -------------------------------------------------------------------------------- /image/Chapter1/cloud9_gemfile_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/cloud9_gemfile_aws.png -------------------------------------------------------------------------------- /image/Chapter1/share_workspace_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/share_workspace_aws.png -------------------------------------------------------------------------------- /image/Chapter14/live_status_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/live_status_feed.png -------------------------------------------------------------------------------- /image/Chapter14/relationship_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/relationship_model.png -------------------------------------------------------------------------------- /image/Chapter3/home_view_full_html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/home_view_full_html.png -------------------------------------------------------------------------------- /image/Chapter3/show_hidden_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/show_hidden_files.png -------------------------------------------------------------------------------- /image/Chapter5/layout_production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/layout_production.png -------------------------------------------------------------------------------- /image/Chapter6/user_model_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/user_model_sketch.png -------------------------------------------------------------------------------- /image/Chapter1/cloud9_two_spaces_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/cloud9_two_spaces_aws.png -------------------------------------------------------------------------------- /image/Chapter1/hello_world_hello_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/hello_world_hello_app.png -------------------------------------------------------------------------------- /image/Chapter1/new_terminal_tab_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/new_terminal_tab_aws.png -------------------------------------------------------------------------------- /image/Chapter10/gravatar_cropper_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/gravatar_cropper_new.png -------------------------------------------------------------------------------- /image/Chapter10/heroku_sample_users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/heroku_sample_users.png -------------------------------------------------------------------------------- /image/Chapter12/forgot_password_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/forgot_password_form.png -------------------------------------------------------------------------------- /image/Chapter12/forgot_password_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/forgot_password_link.png -------------------------------------------------------------------------------- /image/Chapter12/password_reset_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/password_reset_form.png -------------------------------------------------------------------------------- /image/Chapter13/resized_image_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/resized_image_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter14/stats_partial_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/stats_partial_mockup.png -------------------------------------------------------------------------------- /image/Chapter2/demo_micropost_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_micropost_model.png -------------------------------------------------------------------------------- /image/Chapter7/user_show_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/user_show_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter8/login_failure_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/login_failure_mockup.png -------------------------------------------------------------------------------- /image/Chapter8/login_success_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/login_success_mockup.png -------------------------------------------------------------------------------- /image/Chapter1/bitbucket_default_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/bitbucket_default_readme.png -------------------------------------------------------------------------------- /image/Chapter1/cloud9_name_environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/cloud9_name_environment.png -------------------------------------------------------------------------------- /image/Chapter1/full_browser_window_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/full_browser_window_aws.png -------------------------------------------------------------------------------- /image/Chapter1/heroku_app_hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/heroku_app_hello_world.png -------------------------------------------------------------------------------- /image/Chapter1/rails_server_new_tab_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/rails_server_new_tab_aws.png -------------------------------------------------------------------------------- /image/Chapter10/edit_form_working_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/edit_form_working_new.png -------------------------------------------------------------------------------- /image/Chapter10/edit_page_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/edit_page_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter11/activated_in_production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/activated_in_production.png -------------------------------------------------------------------------------- /image/Chapter11/activated_user_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/activated_user_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter11/not_activated_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/not_activated_warning.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_image_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_image_mockup.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_model_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_model_picture.png -------------------------------------------------------------------------------- /image/Chapter2/demo_micropost_model 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_micropost_model 2.png -------------------------------------------------------------------------------- /image/Chapter2/heroku_app_hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/heroku_app_hello_world.png -------------------------------------------------------------------------------- /image/Chapter3/file_navigator_gear_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/file_navigator_gear_icon.png -------------------------------------------------------------------------------- /image/Chapter3/heroku_app_hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter3/heroku_app_hello_world.png -------------------------------------------------------------------------------- /image/Chapter6/signup_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/signup_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter7/no_create_template_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/no_create_template_error.png -------------------------------------------------------------------------------- /image/Chapter7/profile_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/profile_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter7/signup_flash_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_flash_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_form_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_form_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter9/cookie_in_browser_chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter9/cookie_in_browser_chrome.png -------------------------------------------------------------------------------- /image/Chapter9/login_form_remember_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter9/login_form_remember_me.png -------------------------------------------------------------------------------- /image/Chapter9/login_remember_me_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter9/login_remember_me_mockup.png -------------------------------------------------------------------------------- /image/Chapter11/redirected_not_activated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/redirected_not_activated.png -------------------------------------------------------------------------------- /image/Chapter12/user_model_password_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/user_model_password_reset.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_belongs_to_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_belongs_to_user.png -------------------------------------------------------------------------------- /image/Chapter13/user_has_many_microposts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_has_many_microposts.png -------------------------------------------------------------------------------- /image/Chapter2/create_demo_repo_bitbucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/create_demo_repo_bitbucket.png -------------------------------------------------------------------------------- /image/Chapter2/demo_edit_user_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_edit_user_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_new_user_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_new_user_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_show_user_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_show_user_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/micropost_user_association.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/micropost_user_association.png -------------------------------------------------------------------------------- /image/Chapter2/user_presence_validations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/user_presence_validations.png -------------------------------------------------------------------------------- /image/Chapter4/word_inheritance_ruby_1_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter4/word_inheritance_ruby_1_9.png -------------------------------------------------------------------------------- /image/Chapter7/signup_failure_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_failure_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter9/user_model_remember_digest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter9/user_model_remember_digest.png -------------------------------------------------------------------------------- /image/Chapter1/new_readme_bitbucket_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/new_readme_bitbucket_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter1/riding_rails_4th_edition_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/riding_rails_4th_edition_aws.png -------------------------------------------------------------------------------- /image/Chapter10/edit_user_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/edit_user_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter10/login_page_protected_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/login_page_protected_mockup.png -------------------------------------------------------------------------------- /image/Chapter10/protected_log_in_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/protected_log_in_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_all_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_all_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter10/user_model_admin_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_model_admin_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter12/forgot_password_form_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/forgot_password_form_mockup.png -------------------------------------------------------------------------------- /image/Chapter12/invalid_email_password_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/invalid_email_password_reset.png -------------------------------------------------------------------------------- /image/Chapter12/login_forgot_password_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/login_forgot_password_mockup.png -------------------------------------------------------------------------------- /image/Chapter12/reset_password_form_mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/reset_password_form_mockup.png -------------------------------------------------------------------------------- /image/Chapter12/valid_email_password_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/valid_email_password_reset.png -------------------------------------------------------------------------------- /image/Chapter13/home_form_errors_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/home_form_errors_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/home_post_delete_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/home_post_delete_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/home_with_form_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/home_with_form_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/large_uploaded_image_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/large_uploaded_image_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_model_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_model_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/microposts_with_image_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/microposts_with_image_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter14/followers_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/followers_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter14/following_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/following_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter14/user_followers_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/user_followers_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/user_following_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/user_following_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_controller_inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_controller_inheritance.png -------------------------------------------------------------------------------- /image/Chapter2/demo_destroy_user_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_destroy_user_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_update_user_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_update_user_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter4/string_inheritance_ruby_1_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter4/string_inheritance_ruby_1_9.png -------------------------------------------------------------------------------- /image/Chapter5/about_page_styled_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/about_page_styled_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/home_page_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/home_page_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/new_signup_page_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/new_signup_page_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter5/sample_app_logo_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/sample_app_logo_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/new_signup_page_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/new_signup_page_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/valid_submission_error_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/valid_submission_error_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter8/flash_persistence_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/flash_persistence_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter10/index_delete_links_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/index_delete_links_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter11/user_model_account_activation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/user_model_account_activation.png -------------------------------------------------------------------------------- /image/Chapter12/password_reset_failure_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/password_reset_failure_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter12/password_reset_success_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/password_reset_success_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter12/reset_email_production_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/reset_email_production_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter13/image_upload_production_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/image_upload_production_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_created_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_created_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/proto_feed_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/proto_feed_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/naive_user_has_many_following.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/naive_user_has_many_following.png -------------------------------------------------------------------------------- /image/Chapter2/demo_new_micropost_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_new_micropost_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_user_index_two_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_user_index_two_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/micropost_content_cant_be_blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/micropost_content_cant_be_blank.png -------------------------------------------------------------------------------- /image/Chapter6/user_model_initial_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/user_model_initial_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_failure_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_failure_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter7/signup_success_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_success_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter8/failed_login_flash_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/failed_login_flash_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter1/bitbucket_repository_page_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/bitbucket_repository_page_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_only_one_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_only_one_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_page_two_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_page_two_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_pagination_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_pagination_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/home_with_proto_feed_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/home_with_proto_feed_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/home_page_with_feed_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/home_page_with_feed_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/profile_follow_button_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/profile_follow_button_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_blank_user_index_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_blank_user_index_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/demo_micropost_index_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/demo_micropost_index_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter2/micropost_length_error_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter2/micropost_length_error_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/sample_app_typography_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/sample_app_typography_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/sample_app_universal_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/sample_app_universal_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/home_page_with_debug_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/home_page_with_debug_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/profile_routing_error_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/profile_routing_error_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter7/profile_with_gravatar_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/profile_with_gravatar_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_error_messages_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_error_messages_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_failure_debug_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_failure_debug_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_flash_reloaded_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_flash_reloaded_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/signup_in_production_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/signup_in_production_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter7/user_show_sidebar_css_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/user_show_sidebar_css_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter8/initial_failed_login_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/initial_failed_login_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter11/activation_email_production_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/activation_email_production_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter12/password_reset_html_preview_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/password_reset_html_preview_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter12/password_reset_text_preview_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter12/password_reset_text_preview_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter13/user_microposts_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_microposts_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/diferent_user_followers_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/diferent_user_followers_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/home_page_follow_stats_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/home_page_follow_stats_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/profile_unfollow_button_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/profile_unfollow_button_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/user_has_many_followers_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/user_has_many_followers_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/user_has_many_following_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/user_has_many_following_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter4/static_pages_controller_inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter4/static_pages_controller_inheritance.png -------------------------------------------------------------------------------- /image/Chapter6/sqlite_database_browser_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/sqlite_database_browser_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/filled_in_form_bootstrap_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/filled_in_form_bootstrap_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/profile_custom_gravatar_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/profile_custom_gravatar_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/user_show_unknown_action_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/user_show_unknown_action_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter8/profile_with_logout_link_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter8/profile_with_logout_link_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter1/directory_structure_rails_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/directory_structure_rails_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter11/account_activation_html_preview_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/account_activation_html_preview_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter11/account_activation_text_preview_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter11/account_activation_text_preview_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter13/user_profile_no_microposts_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_profile_no_microposts_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_profile_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_profile_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_user_index_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_user_index_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter5/sample_app_only_bootstrap_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/sample_app_only_bootstrap_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/site_with_footer_bootstrap_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/site_with_footer_bootstrap_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter6/user_model_password_digest_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/user_model_password_digest_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter7/invalid_submission_no_feedback_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/invalid_submission_no_feedback_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter7/profile_mockup_profile_name_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter7/profile_mockup_profile_name_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter1/create_first_repository_bitbucket_4th_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter1/create_first_repository_bitbucket_4th_ed.png -------------------------------------------------------------------------------- /image/Chapter10/user_index_delete_links_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/user_index_delete_links_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter13/user_profile_with_microposts_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_profile_with_microposts_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter6/sqlite_user_row_with_password_4th_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter6/sqlite_user_row_with_password_4th_edition.png -------------------------------------------------------------------------------- /image/Chapter10/edit_with_invalid_information_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter10/edit_with_invalid_information_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/micropost_delete_links_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/micropost_delete_links_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/other_profile_with_microposts_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/other_profile_with_microposts_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter13/user_profile_microposts_page_2_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_profile_microposts_page_2_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_home_page_feed_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_home_page_feed_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_home_page_feed_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_home_page_feed_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter13/home_page_with_micropost_form_mockup_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/home_page_with_micropost_form_mockup_bootstrap.png -------------------------------------------------------------------------------- /image/Chapter13/user_profile_microposts_no_styling_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter13/user_profile_microposts_no_styling_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter5/layout_no_logo_or_custom_css_bootstrap_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter5/layout_no_logo_or_custom_css_bootstrap_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_other_profile_follow_button_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_other_profile_follow_button_mockup_3rd_edition.png -------------------------------------------------------------------------------- /image/Chapter14/page_flow_other_profile_unfollow_button_mockup_3rd_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoodahun/Rails_Tutorials_Translation/HEAD/image/Chapter14/page_flow_other_profile_unfollow_button_mockup_3rd_edition.png -------------------------------------------------------------------------------- /Documentation/author.md: -------------------------------------------------------------------------------- 1 | # 저자 2 | [마이클 허틀 (Michael Hartl)](http://www.michaelhartl.com/) 은 [Ruby on Rails Tutorial](http://www.railstutorial.org/) 이라고 하는 웹 개발을 배울 때에 자주 참고하는 책의 저자입니다. 또한, [Learn Enough to Be Dangerous](http://learnenough.com/) ( [learnenough.com](http://learnenough.com/) ) 교육 사이트의 창업자이기도 합니다. 3 | 전에는 (지금은 엄청 오래전 얘기입니다만) 「*RailsSpace*」 라고 하는 책의 집필 및 개발에 참여하기도 하였으며, 한 때 인기 있었던 Ruby on Rails 기반의 SNS플랫폼 「Insoshi」 의 개발에도 참여하였습니다. 4 | 2011년에는 Rails 커뮤니티에 높은 공헌을 인정받아 [Ruby Hero Award](https://rubyheroes.com/heroes/2011) 을 수상하였습니다. 5 | 6 | [하버드 대학](http://college.harvard.edu/) 졸업 후, [캘리포니아 공과대학](http://www.caltech.edu/) 에서 [물리학 석사](http://resolver.caltech.edu/CaltechETD:etd-05222003-161626) 학위를 받았으며, 실리콘 밸리의 유명한 창업 프로그램[YCombinator](http://ycombinator.com/) 의 졸업생이기도 합니다. 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dahun Yoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Documentation/license.md: -------------------------------------------------------------------------------- 1 | # 저작권과 라이센스 2 | > The MIT License 3 | > 4 | > Copyright (c) 2016 Michael Hartl 5 | > 6 | > Permission is hereby granted, free of charge, to any person 7 | > obtaining a copy of this software and associated documentation 8 | > files (the “Software”), to deal in the Software without restriction, 9 | > including without limitation the rights to use, copy, modify, merge, 10 | > publish, distribute, sublicense, and/or sell copies of the Software, 11 | > and to permit persons to whom the Software is furnished to do so, 12 | > subject to the following conditions: 13 | > 14 | > The above copyright notice and this permission notice shall be 15 | > included in all copies or substantial portions of the Software. 16 | > 17 | > THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF 18 | > ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | > TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR 20 | > A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | > SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | > ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 23 | > ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 25 | > OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | > THE BEERWARE LICENSE (Revision 42) 28 | > 29 | > Michael Hartl wrote this code. As long as you retain 30 | > this notice you can do whatever you want with this stuff. 31 | > If we meet some day, and you think this stuff is worth it, 32 | > you can buy me a beer in return. 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby on Rails Tutorial:실제 예를 통해 배워보자. 2 | 3 |
원작자 Michael Hartl (마이클 허틀)
4 | ※본 자료는 Ruby on Rails Tutorial의 원본과 일어판을 번역한 것으로, 개인적인 공부를 위해 번역을 한 것입니다. 5 | 6 | - 원 본 [Learn Web Development with Rails: Michael Hartl’s Ruby on Rails Tutorial | Softcover.io](https://www.railstutorial.org/) 7 | - 일어판 [https://railstutorial.jp/](https://railstutorial.jp/) 8 | 9 | --- 10 | 11 | 본 튜토리얼에 대한 과제물의 Repository는 아래에 링크를 해놓습니다. 12 | 13 | 또한 본 튜토리얼에서는 AWS C9을 이용한 개발을 추천하고 있으나, 본인은 로컬환경에서 [RubyMine](https://www.jetbrains.com/ruby/)을 설치하여 진행하였습니다. 14 | 15 | - 1장의 결과물 [Repository](https://github.com/Yoodahun/HelloWorldRubyMine) 16 | - 2장의 결과물 [Repository](https://github.com/Yoodahun/ToyApplication_RailsTutorial) 17 | - 3장 이후의 결과물 Sample Application [Repository](https://github.com/Yoodahun/sample_application) 18 | 19 | 20 | 21 | 22 | 23 | --- 24 | 25 | ## Rails Tutorial이란 ?? 26 | 27 | Rails Tutorial은, 실제 Web Application의 개발에서 공개까지, 실제로 개발해가며 배워보는 대형 튜토리얼입니다.📕✨ 28 | 29 | **※ 본서는 개인적인 학습을 위해 번역한 것이며 원작자 Michael Hertl에게 허가를 받았습니다. ** 30 | 31 | - - - - 32 | - [ ] 제 4판 목차 33 | - [ ] 추천의 말씀 (생략) 34 | - [ ] 감사의 말씀 (생략) 35 | - [ ] [저자](Documentation/auther.md) 36 | - [ ] [저작권과 라이센스](Documentation/license.md) 37 | 38 | - [ ] [제 1장 제로부터 배포까지](Documentation/Chapter1.md) 39 | - [ ] [1.1 처음에는](Documentation/Chapter1.md#11-처음에는) 40 | - [ ] [1.1.1 전제 조건](Documentation/Chapter1.md#111-전제-조건) 41 | - [ ] [1.1.2 본 튜토리얼에서의 약속](Documentation/Chapter1.md#112-본-튜토리얼에서의-약속) 42 | - [ ] [1.2 일단 구동시켜보자](Documentation/Chapter1.md#12-일단-구동시켜보자) 43 | - [ ] [1.2.1 개발 환경](Documentation/Chapter1.md#121-개발-환경) 44 | - [ ] [1.2.2 Rails를 설치해보자.](Documentation/Chapter1.md#122-Rails를-설치해보자.) 45 | - [ ] [1.3 첫 Application](Documentation/Chapter1.md#13-첫-Application) 46 | - [ ] [1.3.1 Bundler](Documentation/Chapter1.md#131-bundler) 47 | - [ ] [1.3.2 rails server](Documentation/Chapter1.md#132-rails-server) 48 | - [ ] [1.3.3 Model-View-Controller (MVC)](Documentation/Chapter1.md#133-model—view—controller-(mvc)) 49 | - [ ] [1.3.4 Hello, world!](Documentation/Chapter1.md#134-hello,-world!) 50 | - [ ] [1.4 Git을 이용한 버전 관리](Documentation/Chapter1.md#14-git을-이용한-버전-관리) 51 | - [ ] [1.4.1 설치](Documentation/Chapter1.md#141-설치) 52 | - [ ] [1.4.2 Git을 쓰면 좋은 점](Documentation/Chapter1.md#142-git을-쓰면-좋은-점) 53 | - [ ] [1.4.3 Bitbucket](Documentation/Chapter1.md#143-bitbucket) 54 | - [ ] [1.4.4 Branch, Edit, Commit, Merge](Documentation/Chapter1.md#144-branch,-edit,-commit,-merge) 55 | - [ ] [1.5 배포해보자](Documentation/Chapter1.md#15-배포해보자) 56 | - [ ] [1.5.1 Heroku 설치](Documentation/Chapter1.md#151-heroku의-설치) 57 | - [ ] [1.5.2 Heroku에 배포해보자 (1)](Documentation/Chapter1.md#152-heroku에-배포해보자-(1)) 58 | - [ ] [1.5.3 Heroku에 배포해보자 (2)](Documentation/Chapter1.md#153-heroku에-배포해보자-(2)) 59 | - [ ] [1.5.4 Heroku 명령어](Documentation/Chapter1.md#154-heroku-명령어) 60 | - [ ] [1.6 마지막으로](Documentation/Chapter1.md#16-마지막으로) 61 | - [ ] [1.6.1 1장의 정리](Documentation/Chapter1.md#161-1장의-정리) 62 | 63 | - [ ] [제 2장 Toy Application](Documentation/Chapter2.md) 64 | - [ ] [2.1 Application의 설계](Documentation/Chapter2.md#21-어플리케이션의-계획) 65 | - [ ] [2.1.1 User Modeling](Documentation/Chapter2.md#211-user-modeling) 66 | - [ ] [2.1.2 Micropost Modeling](Documentation/Chapter2.md#212-micropost-modeling) 67 | - [ ] [2.2 Users 리소스](Documentation/Chapter2.md#22-user-리소스) 68 | - [ ] [2.2.1 Users 화면을 움직여보자](Documentation/Chapter2.md#221-users-화면을-움직여보자) 69 | - [ ] [2.2.2 MVC의 처리](Documentation/Chapter2.md#222-mvc의-처리) 70 | - [ ] [2.2.3 Users 자원의 결점](Documentation/Chapter2.md#223-users-자원의-결점) 71 | - [ ] [2.3 Microposts 리소스](Documentation/Chapter2.md#23-microposts-리소스) 72 | - [ ] [2.3.1 Microposts 화면을 움직여보자](Documentation/Chapter2.md#231-microposts-화면을-움직여보자) 73 | - [ ] [2.3.2 Microposts를 micro하게 해보자](Documentation/Chapter2.md#232-microposts를-micro하게-해보자) 74 | - [ ] [2.3.3 유저는 많은 Microposts를 가지고 있다.](Documentation/Chapter2.md#233-유저는-많은-microposts를-가지고-있다.) 75 | - [ ] [2.3.4 상속의 계층](Documentation/Chapter2.md#234-상속의-계층) 76 | - [ ] [2.3.5 어플리케이션을 배포하자](Documentation/Chapter2.md#235-어플리케이션을-배포하자) 77 | - [ ] [2.4 마지막으로](Documentation/Chapter2.md#24-마지막으로) 78 | - [ ] [2.4.1 2장의 정리](Documentation/Chapter2.md#241-2장의-정리) 79 | 80 | - [ ] [제 3장 정적인(Static) 페이지의 작성](Documentation/Chapter3.md) 81 | - [ ] [3.1 프로젝트 생성](Documentation/Chapter3.md#31-프로젝트-생성) 82 | - [ ] [3.2 정적인 페이지](Documentation/Chapter3.md#32-정적인-페이지) 83 | - [ ] [3.2.1 Static Page를 생성해보자](Documentation/Chapter3.md#321-Static-Page를-생성해보자) 84 | - [ ] [3.2.2 Static Page를 편집해보자](Documentation/Chapter3.md#321-Static-Page를-편집해보자) 85 | - [ ] [3.3 테스트를 해보자](Documentation/Chapter3.md#33-테스트를-해보자) 86 | - [ ] [3.3.1 첫 번째 테스트](Documentation/Chapter3.md#331-첫-번째-테스트) 87 | - [ ] [3.3.2 Red](Documentation/Chapter3.md#322-Red) 88 | - [ ] [3.3.3 Green](Documentation/Chapter3.md#333-Green) 89 | - [ ] [3.3.4 Refactor](Documentation/Chapter3.md#334-Refactor) 90 | - [ ] [3.4 조금은 Dynamic한 페이지](Documentation/Chapter3.md#34-조금은-Dynamic한-페이지) 91 | - [ ] [3.4.1 타이틀을 테스트해보자 (Red)](Documentation/Chapter3.md#341-타이틀을-테스트해보자-(Red)) 92 | - [ ] [3.4.2 타이틀을 추가해보자 (Green)](Documentation/Chapter3.md#342-타이틀을-추가해보자-(Green)) 93 | - [ ] [3.4.3 레이아웃과 html에 직접 쓰는 Ruby (Refactor)](Documentation/Chapter3.md#343-레이아웃과-html에-직접-쓰는-Ruby-(Refactor)) 94 | - [ ] [3.4.4 Route의 설정](Documentation/Chapter3.md#344-Route의-설정) 95 | - [ ] [3.5 마지막으로](Documentation/Chapter3.md#35-마지막으로) 96 | - [ ] [3.5.1 3장의 정리](Documentation/Chapter3.md#351-3장의-정리) 97 | - [ ] [3.6 조금 난이도가 있는 환경설정](Documentation/Chapter3.md#36-조금-난이도가-있는-환경설정) 98 | - [ ] [3.6.1 minitest reporters](Documentation/Chapter3.md#361-minitest-reporters) 99 | - [ ] [3.6.2 Guard를 이용한 테스트 자동화](Documentation/Chapter3.md#362-Guard를-이용한-테스트-자동화) 100 | 101 | - [ ] [제 4장 Rails의 향기가 나는 Ruby](Documentation/Chapter4.md) 102 | - [ ] [4.1 작성 동기](Documentation/Chapter4.md#41-작성동기) 103 | - [ ] [4.1.1 Helper](Documentation/Chapter4.md#411-Helper) 104 | - [ ] [4.1.2 Custom Helper](Documentation/Chapter4.md#412-Custom-Helper) 105 | - [ ] [4.2 문자열과 메소드](Documentation/Chapter4.md#42-문자열과-메소드) 106 | - [ ] [4.2.1 코멘트](Documentation/Chapter4.md#421-코멘트) 107 | - [ ] [4.2.2 문자열 ](Documentation/Chapter4.md#422-문자열) 108 | - [ ] [4.2.3 오브젝트와 메세지 송수신](Documentation/Chapter4.md#423-오브젝트와-메세지의-송수신) 109 | - [ ] [4.2.4 Method의 정의](Documentation/Chapter4.md#424-Method의-정의) 110 | - [ ] [4.2.5 다시 Title Helper](Documentation/Chapter4.md#425-다시-한-번-Title-Helper) 111 | - [ ] [4.3 다른 데이터 구조](Documentation/Chapter4.md#43-다른-데이터-구조) 112 | - [ ] [4.3.1 배열과 범위 연산자](Documentation/Chapter4.md#431-배열과-범위연산자) 113 | - [ ] [4.3.2 블록](Documentation/Chapter4.md#432-블록) 114 | - [ ] [4.3.3 Hash와 Symbol](Documentation/Chapter4.md#433-Hash와-Symbol) 115 | - [ ] [4.3.4 다시 한 번 CSS](Documentation/Chapter4.md#434-다시-한-번-CSS) 116 | - [ ] [4.4 Ruby에서의 Class](Documentation/Chapter4.md#44-Ruby에서의-Class) 117 | - [ ] [4.4.1 Constructor](Documentation/Chapter4.md#441-Constructor) 118 | - [ ] [4.4.2 Class의 상속](Documentation/Chapter4.md#442-Class의-상속) 119 | - [ ] [4.4.3 기본 Class의 변경](Documentation/Chapter4.md#433-기본-Class의-변경) 120 | - [ ] [4.4.4 Controller Class](Documentation/Chapter4.md#444-Controller-Class) 121 | - [ ] [4.4.5 User Class](Documentation/Chapter4.md#445-User-Class) 122 | - [ ] [4.5 마지막으로](Documentation/Chapter4.md#45-마지막으로) 123 | - [ ] [4.5.1 4장의 정리](Documentation/Chapter4.md#451-4장의-정리) 124 | 125 | - [ ] [제 5장 레이아웃의 작성](Documentation/Chapter5.md) 126 | - [ ] [5.1 구조를 추가해보자](Documentation/Chapter5.md#51-구조를-추가해보자) 127 | - [ ] [5.1.1 Navigation](Documentation/Chapter5.md#511-Navigation) 128 | - [ ] [5.1.2 Bootstrap과 커스텀 CSS](Documentation/Chapter5.md#512-Bootstrap과-커스텀-CSS) 129 | - [ ] [5.1.3 파셜(Partial)](Documentation/Chapter5.md#513-파셜Partial) 130 | - [ ] [5.2 Sass와 Asset Pipeline](Documentation/Chapter5.md#52-Sass와-Asset-Pipeline) 131 | - [ ] [5.2.1 Asset Pipeline](Documentation/Chapter5.md#521-Asset-Pipeline) 132 | - [ ] [5.2.2 멋진 문법과 준비된 스타일 시트](Documentation/Chapter5.md#522-멋진-문법과-준비된-스타일-시트) 133 | - [ ] [5.3 레이아웃의 링크](Documentation/Chapter5.md#53-레이아웃의-링크) 134 | - [ ] [5.3.1 Contact 페이지](Documentation/Chapter5.md#531-Contact-페이지) 135 | - [ ] [5.3.2 Rails의 Route URL](Documentation/Chapter5.md#532-Rails의-Route-URL) 136 | - [ ] [5.3.3 이름이 붙은 Path](Documentation/Chapter5.md#533-이름이-붙은-Path) 137 | - [ ] [5.3.4 링크의 테스트](Documentation/Chapter5.md#534-링크의-테스트) 138 | - [ ] [5.4 User의 등록 : 첫 테스트](Documentation/Chapter5.md#54-User의-등록-첫-테스트) 139 | - [ ] [5.4.1 Users Controller](Documentation/Chapter5.md#541-Users-Controller) 140 | - [ ] [5.4.2 Users 등록용 URL](Documentation/Chapter5.md#542-User-등록용-URL) 141 | - [ ] [5.5 마지막으로](Documentation/Chapter5.md#55-마지막으로) 142 | - [ ] [5.5.1 5장의 정리](Documentation/Chapter5.md#551-5장의-정리) 143 | 144 | - [ ] [제 6장 유저의 모델을 작성해보자](Documentation/Chapter6.md) 145 | - [ ] [6.1 User 모델](Documentation/Chapter6.md/#61-User-모델) 146 | - [ ] [6.1.1 Database](Documentation/Chapter6.md#611-Database) 147 | - [ ] [6.1.2 Model 파일](Documentation/Chapter6.md#612-Model-파일) 148 | - [ ] [6.1.3 User Object를 생성해보자](Documentation/Chapter6.md#613-User-Object를-생성해보자) 149 | - [ ] [6.1.4 User Object를 검색해보자](Documentation/Chapter6.md#614-User-Object를-검색해보자) 150 | - [ ] [6.1.5 User Object를 수정해보자](Documentation/Chapter6.md#615-User-Object를-수정해보자) 151 | - [ ] [6.2 User를 검증해보자](Documentation/Chapter6.md#62-User를-검증해보자) 152 | - [ ] [6.2.1 유효성을 검증해보자](Documentation/Chapter6.md#621-유효성을-검증해보자) 153 | - [ ] [6.2.2 존재성을 검증해보자](Documentation/Chapter6.md#622-존재성을-검증해보자) 154 | - [ ] [6.2.3 길이를 검증해보자](Documentation/Chapter6.md#623-길이를-검증해보자) 155 | - [ ] [6.2.4 포맷을 검증해보자](Documentation/Chapter6.md#624-포맷을-검증해보자) 156 | - [ ] [6.2.5 유니크성을 검증해보자](Documentation/Chapter6.md#625-유니크성을-검증해보자) 157 | - [ ] [6.3 안전한 비밀번호를 추가해보자](Documentation/Chapter6.md#63-안전한-비밀번호를-추가해보자) 158 | - [ ] [6.3.1 해시화된 비밀번호](Documentation/Chapter6.md#631-해시화된-비밀번호) 159 | - [ ] [6.3.2 유저가 안전한 비밀번호를 가지고 있는지?](Documentation/Chapter6.md#632-유저가-안전한-비밀번호를-가지고-있는지) 160 | - [ ] [6.3.3 비밀번호의 최소문자수](Documentation/Chapter6.md#633-비밀번호의-최소문자수) 161 | - [ ] [6.3.4 유저 생성과 인증](Documentation/Chapter6.md#634-유저-생성과-인증) 162 | - [ ] [6.4 마지막으로](Documentation/Chapter6.md#64-마지막으로) 163 | - [ ] [6.4.1 6장의 정리](Documentation/Chapter6.md#641-6장의-정리) 164 | 165 | - [ ] [제 7장 유저의 등록](Documentation/Chapter7.md) 166 | - [ ] [7.1 유저를 표시해보자](Documentation/Chapter7.md#71-유저를-표시해보자) 167 | - [ ] [7.1.1 Debug와 Rails 환경](Documentation/Chapter7.md#711-Debug와-Rails-환경) 168 | - [ ] [7.1.2 Users Resource](Documentation/Chapter7.md#712-Users-Resource) 169 | - [ ] [7.1.3 Debugger 메소드](Documentation/Chapter7.md#713-Debugger-메소드) 170 | - [ ] [7.1.4 Gravatar 이미지와 사이드바](Documentation/Chapter7.md#714-Gravatar-이미지와-사이드바) 171 | - [ ] [7.2 유저 등록 Form](Documentation/Chapter7.md#72-유저-등록-Form) 172 | - [ ] [7.2.1 Form_for를 사용해보자](Documentation/Chapter7.md#721-Form_for를-사용해보자) 173 | - [ ] [7.2.2 Form HTML](Documentation/Chapter7.md#722-Form-HTML) 174 | - [ ] [7.3 유저 등록 실패](Documentation/Chapter7.md#73-유저-등록-실패) 175 | - [ ] [7.3.1 올바른 Form](Documentation/Chapter7.md#731-올바른-Form) 176 | - [ ] [7.3.2 Strong Parameters](Documentation/Chapter7.md#732-Strong-Parameters) 177 | - [ ] [7.3.3 에러 메세지](Documentation/Chapter7.md#733-에러-메세지) 178 | - [ ] [7.3.4 등록 실패시의 테스트](Documentation/Chapter7.md#734-등록-실패시의-테스트) 179 | - [ ] [7.4 유저 등록 성공](Documentation/Chapter7.md#74-유저-등록-성공) 180 | - [ ] [7.4.1 등록 Form의 완성](Documentation/Chapter7.md#741-등록-Form의-완성) 181 | - [ ] [7.4.2 flash](Documentation/Chapter7.md#742-flash) 182 | - [ ] [7.4.3 실제 유저 등록](Documentation/Chapter7.md#743-실제-유저-등록) 183 | - [ ] [7.4.4 유저 등록 성공시의 테스트](Documentation/Chapter7.md#744-유저-등록-성공시의-테스트) 184 | - [ ] [7.5 프로의 배포](Documentation/Chapter7.md#75-프로의-배포) 185 | - [ ] [7.5.1 실제 배포환경에서의 SSL](Documentation/Chapter7.md#751-실제-배포환경에서의-SSL) 186 | - [ ] [7.5.2 실제 배포환경용의 Web서버](Documentation/Chapter7.md#752-실제-배포환경용의-Web서버) 187 | - [ ] [7.5.3 실제 배포환경으로의 배포](Documentation/Chapter7.md#753-실제-배포환경으로의-배포) 188 | - [ ] [7.6 마지막으로](Documentation/Chapter7.md#76-마지막으로) 189 | - [ ] [7.6.1 7장의 정리](Documentation/Chapter7.md#761-7장의-정리) 190 | 191 | - [ ] [제 8장 기본적인 로그인 기능](Documentation/Chapter8.md#제-8장-기본적인-로그인-기능) 192 | - [ ] [8.1 Session](Documentation/Chapter8.md#81-Session) 193 | - [ ] [8.1.1 Sessions Controller](Documentation/Chapter8.md#811-Sesssions-Controller) 194 | - [ ] [8.1.2 Login Form](Documentation/Chapter8.md#812-Login-Form) 195 | - [ ] [8.1.3 유저의 검증과 인증](Documentation/Chapter8.md#813-유저의-검증과-인증) 196 | - [ ] [8.1.4 Flash Message를 표시해보자](Documentation/Chapter8.md#814-Flash-Message를-표시해보자) 197 | - [ ] [8.1.5 Flash의 테스트](Documentation/Chapter8.md#815-Flash의-테스트) 198 | - [ ] [8.2 Login](Documentation/Chapter8.md#82-Login) 199 | - [ ] [8.2.1 Log_in Method](Documentation/Chapter8.md#821-Log_in-Method) 200 | - [ ] [8.2.2 로그인 되어있는 유저](Documentation/Chapter8.md#822-로그인-되어있는-유저) 201 | - [ ] [8.2.3 레이아웃 링크를 변경해보자](Documentation/Chapter8.md#823-레이아웃-링크를-변경해보자) 202 | - [ ] [8.2.4 레이아웃의 변경을 테스트해보자](Documentation/Chapter8.md#824-레이아웃의-변경을-테스트해보자) 203 | - [ ] [8.2.5 회원가입 후의 로그인](Documentation/Chapter8.md#825-회원가입-후의-로그인) 204 | - [ ] [8.3 Logout](Documentation/Chapter8.md#83-Logout) 205 | - [ ] [8.4 마지막으로](Documentation/Chapter8.md#84-마지막으로) 206 | - [ ] [8.4.1 8장의 정리](Documentation/Chapter8.md#841-8장의-정리) 207 | 208 | - [ ] [제 9장 진보된 로그인 구조](Documentation/Chapter9.md) 209 | - [ ] [9.1 Remember me 기능](Documentation/Chapter9.md#91-remember-me-기능) 210 | - [ ] [9.1.1 Remember Token과 암호화](Documentation/Chapter9.md#911-remember-token과-암호화) 211 | - [ ] [9.1.2 로그인 상태의 저장](Documentation/Chapter9.md#912-로그인-상태의-저장) 212 | - [ ] [9.1.3 유저 정보 파기](Documentation/Chapter9.md#913-유저-정보-파기) 213 | - [ ] [9.1.4 2개의 눈에 띄지않는 버그](Documentation/Chapter9.md#914-2개의-눈에-띄지않는-버그) 214 | - [ ] [9.2 「Remember me」 Checkbox](Documentation/Chapter9.md#92-remember-me-체크-박스) 215 | - [ ] [9.3 「Remember me」 의 테스트](Documentation/Chapter9.md#93-remember-me-의-테스트) 216 | - [ ] [9.3.1 「Remember me」 박스를 테스트해보자](Documentation/Chapter9.md#931-remember-me-박스를-테스트해보자) 217 | - [ ] [9.3.2 「Remember me」 를 테스트해보자](Documentation/Chapter9.md#932-remember-me-를-테스트해보자) 218 | - [ ] [9.4 마지막으로](Documentation/Chapter9.md#94-마지막으로) 219 | - [ ] [9.4.1 9장의 정리](Documentation/Chapter9.md#941-9장의-정리) 220 | 221 | - [ ] [제 10장 유저의 업데이트, 출력, 삭제](Documentation/Chapter10.md) 222 | - [ ] [10.1 유저를 Update해보자.](Documentation/Chapter10.md#101-유저를-update해보자) 223 | - [ ] [10.1.1 유저 정보 수정 Form](Documentation/Chapter10.md#1011-유저-정보-수정-form) 224 | - [ ] [10.1.2 유저 정보 수정의 실패](Documentation/Chapter10.md#1012-유저-정보-수정의-실패) 225 | - [ ] [10.1.3 Update에 실패했을 때의 테스트](Documentation/Chapter10.md#1013-update에-실패했을-때의-테스트) 226 | - [ ] [10.1.4 TDD로 정보 수정을 성공시켜보자](Documentation/Chapter10.md#1014-tdd로-정보-수정을-성공시켜보자) 227 | - [ ] [10.2 권한 부여 (Authorization)](Documentation/Chapter10.md#102-권한-부여-authorization) 228 | - [ ] [10.2.1 유저에게 로그인을 요청해보자](Documentation/Chapter10.md#1021-유저에게-로그인을-요청해보자) 229 | - [ ] [10.2.2 올바른 유저를 요청해보자](Documentation/Chapter10.md#1022-올바른-유저를-요청해보자) 230 | - [ ] [10.2.3 Friendly Forwarding](Documentation/Chapter10.md#1023-friendly-forwarding) 231 | - [ ] [10.3 모든 유저를 표시해보자](Documentation/Chapter10.md#103-모든-유저를-표시해보자) 232 | - [ ] [10.3.1 유저 리스트를 표시해보자](Documentation/Chapter10.md#1031-유저-리스트를-표시해보자) 233 | - [ ] [10.3.2 Sample User](Documentation/Chapter10.md#1032-sample-user) 234 | - [ ] [10.3.3 Pagination](Documentation/Chapter10.md#1033-pagination) 235 | - [ ] [10.3.4 유저 리스트의 테스트](Documentation/Chapter10.md#1034-유저-리스트의-테스트) 236 | - [ ] [10.3.5 Partial Refactoring](Documentation/Chapter10.md#1035-partial-refactoring) 237 | - [ ] [10.4 유저를 삭제해보자](Documentation/Chapter10.md#104-유저를-삭제해보자) 238 | - [ ] [10.4.1 관리자](Documentation/Chapter10.md#1041-관리자) 239 | - [ ] [10.4.2 Destroy 액션](Documentation/Chapter10.md#1042-destroy-액션) 240 | - [ ] [10.4.3 유저 삭제 테스트](Documentation/Chapter10.md#1043-유저-삭제-테스트) 241 | - [ ] [10.5 마지막으로](Documentation/Chapter10.md#105-마지막으로) 242 | - [ ] [10.5.1 10장의 정리](Documentation/Chapter10.md#1051-10장의-정리) 243 | 244 | - [ ] [제 11장 Account의 유효화](Documentation/Chapter11.md) 245 | - [ ] [11.1 Account Activations Resource](Documentation/Chapter11.md#111-account-activations-resource) 246 | - [ ] [11.1.1 Account Activations Controller](Documentation/Chapter11.md#1111-account-activations-controller) 247 | - [ ] [11.1.2 Account Activation DataModel](Documentation/Chapter11.md#1112-account-activation-datamodel) 248 | - [ ] [11.2 Account의 유효화와 메일 발신](Documentation/Chapter11.md#112-account의-유효화와-메일-발신) 249 | - [ ] [11.2.1 발신 메일의 Template](Documentation/Chapter11.md#1121-발신-메일의-template) 250 | - [ ] [11.2.2 발신 메일의 Preview](Documentation/Chapter11.md#1122-발신-메일의-preview) 251 | - [ ] [11.2.3 발신 메일의 테스트](Documentation/Chapter11.md#1123-발신-메일의-테스트) 252 | - [ ] [11.2.4 User의 Create action을 수정해보자](Documentation/Chapter11.md#1124-user의-create-action을-수정해보자) 253 | - [ ] [11.3 Account를 유효하게 해보자](Documentation/Chapter11.md#113-account를-유효하게-해보자) 254 | - [ ] [11.3.1 authenticated? Method의 추상화](Documentation/Chapter11.md#1131-authenticated-method의-추상화) 255 | - [ ] [11.3.2 Edit action에서의 유효화](Documentation/Chapter11.md#1132-edit-action에서의-유효화) 256 | - [ ] [11.3.3 유효화의 테스트와 Refactoring](Documentation/Chapter11.md#1133-유효화의-테스트와-refactoring) 257 | - [ ] [11.4 실제 배포환경에서의 메일 송신](Documentation/Chapter11.md#114-실제-배포환경에서의-메일-송신) 258 | - [ ] [11.5 마지막으로](Documentation/Chapter11.md#115-마지막으로) 259 | - [ ] [11.5.1 11장의 정리](Documentation/Chapter11.md#1151-11장의-정리) 260 | 261 | - [ ] [제 12장 패스워드의 재설정](Documentation/Chapter12.md) 262 | - [ ] [12.1 PasswordResets Resource](Documentation/Chapter12.md#121-passwordresets-resource) 263 | - [ ] [12.1.1 PasswordResets Controller](Documentation/Chapter12.md#1211-passwordresets-controller) 264 | - [ ] [12.1.2 새로운 패스워드 설정](Documentation/Chapter12.md#1212-새로운-패스워드-설정) 265 | - [ ] [12.1.3 create action에서의 패스워드 재설정](Documentation/Chapter12.md#1213-create-action에서의-패스워드-재설정) 266 | - [ ] [12.2 패스워드 재설정 메일 발신](Documentation/Chapter12.md#122-패스워드-재설정의-메일-발신) 267 | - [ ] [12.2.1 패스워드 재설정 메일과 Template](Documentation/Chapter12.md#1221-패스워드-재설정의-메일과-template) 268 | - [ ] [12.2.2 발신 메일 테스트](Documentation/Chapter12.md#1222-발신-메일의-테스트) 269 | - [ ] [12.3 패스워드를 재설정해보자](Documentation/Chapter12.md#123-패스워드를-재설정해보자) 270 | - [ ] [12.3.1 edit action에서의 재설정](Documentation/Chapter12.md#1231-edit-action에서의-재설정) 271 | - [ ] [12.3.2 패스워드를 수정해보자](Documentation/Chapter12.md#1232-패스워드를-수정해보자) 272 | - [ ] [12.3.3 패스워드 재설정을 테스트해보자](Documentation/Chapter12.md#1233-패스워드-재설정을-테스트해보자) 273 | - [ ] [12.4 실제 배포환경에서의 메일 발신](Documentation/Chapter12.md#124-실제-배포환경에서의-메일-발신) 274 | - [ ] [12.5 마지막으로](Documentation/Chapter12.md#125-마지막으로) 275 | - [ ] [12.5.1 12장의 정리](Documentation/Chapter12.md#1251-12장의-정리) 276 | 277 | - [ ] [제 13장 유저의 Microposts](Documentation/Chapter13.md) 278 | 279 | - [ ] [13.1 Micropost Model](Documentation/Chapter13.md#131-micropost-model) 280 | - [ ] [13.1.1 기본적인 모델](Documentation/Chapter13.md#1311-기본적인-모델) 281 | - [ ] [13.1.2 Micropost의 Validation](Documentation/Chapter13.md#1312-micropost의-validation) 282 | - [ ] [13.1.3 User/Micropost의 관계맺기](Documentation/Chapter13.md#1313-user-micropost의-관계맺기) 283 | - [ ] [13.1.4 Microposts를 개선해보자](Documentation/Chapter13.md#1314-micropost를-개선해보자) 284 | - [ ] [13.2 Microposts를 표시해보자](Documentation/Chapter13.md#132-microposts를-표시해보자) 285 | - [ ] [13.2.1 Microposts의 표시](Documentation/Chapter13.md#1321-micropost의-표시) 286 | - [ ] [13.2.2 Microposts의 Sample](Documentation/Chapter13.md#1322-micropost의-sample) 287 | - [ ] [13.2.3 Profile화면의 Micropost를 테스트해보자](Documentation/Chapter13.md#1323-profile화면의-micropost를-테스트해보자) 288 | - [ ] [13.3 Microposts를 조작해보자](Documentation/Chapter13.md#133-micropost를-조작해보자) 289 | - [ ] [13.3.1 Micropost의 접근제어](Documentation/Chapter13.md#1331-micropost의-접근제어) 290 | - [ ] [13.3.2 Microposts를 작성해보자](Documentation/Chapter13.md#1332-micropost를-작성해보자) 291 | - [ ] [13.3.3 Feed의 원형](Documentation/Chapter13.md#1333-feed의-원형) 292 | - [ ] [13.3.4 Micropost를 삭제해보자](Documentation/Chapter13.md#1334-micropost를-삭제해보자) 293 | - [ ] [13.3.5 Feed화면의 Micropost를 테스트해보자](Documentation/Chapter13.md#1335-feed화면의-micropost를-테스트해보자) 294 | - [ ] [13.4 Microposts의 Image 첨부](Documentation/Chapter13.md#134-micropost의-image-첨부) 295 | - [ ] [13.4.1 기본적인 Image Upload](Documentation/Chapter13.md#1341-기본적인-image-upload) 296 | - [ ] [13.4.2 Image의 검증](Documentation/Chapter13.md#1342-image의-검증) 297 | - [ ] [13.4.3 Image의 Resize](Documentation/Chapter13.md#1343-image의-resize) 298 | - [ ] [13.4.4 실제 배포환경에서의 Image Upload](Documentation/Chapter13.md#1344-실제-배포환경에서의-image-upload) 299 | - [ ] [13.5 마지막으로](Documentation/Chapter13.md#135-마지막으로) 300 | - [ ] [13.5.1 13장의 정리](Documentation/Chapter13.md#1351-13장의-정리) 301 | 302 | - [ ] [14장 유저를 Follow해보자](Documentation/Chapter14.md) 303 | 304 | - [ ] [14.1 Relationship Model](Documentation/Chapter14.md#141-relationship-model) 305 | 306 | - [ ] [14.1.1 DataModel의 문제 ( 및 해결책)](Documentation/Chapter14.md#1411-datamodel의-문제-및-해결책) 307 | - [ ] [14.1.2 User/Relationship의 관계](Documentation/Chapter14.md#1412-user-relationship의-관계) 308 | - [ ] [14.1.3 Relationship의 Validation](Documentation/Chapter14.md#1413-relationship의-validation) 309 | - [ ] [14.1.4 Follow하고 있는 유저](Documentation/Chapter14.md#1414-follow하고-있는-유저) 310 | - [ ] [14.1.5 Follower](Documentation/Chapter14.md#1415-follower) 311 | - [ ] [14.2 Follow 의 Web Interface](Documentation/Chapter14.md#142-follow-의-web-interface) 312 | 313 | - [ ] [14.2.1 Follow 의 Sample data](Documentation/Chapter14.md#1421-follow의-sample-data) 314 | - [ ] [14.2.2 통계와 Follow Form](Documentation/Chapter14.md#1422-통계와-follow-form) 315 | - [ ] [14.2.3 Following 과 UnFollowing 페이지](Documentation/Chapter14.md#1423-following-과-unfollowing-페이지) 316 | - [ ] [14.2.4 Follow 버튼 (기본편)](Documentation/Chapter14.md#1424-follow-버튼-기본편) 317 | - [ ] [14.2.5 Follow 버튼 (Ajax편)](Documentation/Chapter14.md#1425-follow-버튼-ajax편) 318 | - [ ] [14.2.6 Follow를 테스트해보자](Documentation/Chapter14.md#1426-follow를-테스트해보자) 319 | - [ ] [14.3 Status Feed](Documentation/Chapter14.md#143-status-feed) 320 | - [ ] [14.3.1 동기와 계획](Documentation/Chapter14.md#1431-동기와-계획) 321 | - [ ] [14.3.2 Feed를 처음으로 구현해보자](Documentation/Chapter14.md#1432-feed를-처음으로-구현해보자) 322 | - [ ] [14.3.3 Sub Select](Documentation/Chapter14.md#1433-sub-select) 323 | - [ ] [14.4 마지막으로](Documentation/Chapter14.md#144-마지막으로) 324 | 325 | - [ ] [14.4.1 읽을거리](Documentation/Chapter14.md#1441-읽을거리) 326 | - [ ] [14.4.2 14장의 마무리](Documentation/Chapter14.md#1442-14장의-마무리) 327 | 328 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /Documentation/Chapter2.md: -------------------------------------------------------------------------------- 1 | # 제 2장 Toy Application 2 | 3 | 이번 장에서는, Rails의 강력한 기능 몇가지를 소개하기 위한 어플리케이션을 작성해봅니다. 많은 기능을 자동적으로 생성하는 *scaffold* 제네레이터라고 하는 스크립트를 사용하여, 어플리케이션 빠르게 생성하고, 그것을 기반으로 고도의 Rails 프로그래밍과 Web 프로그래밍의 개요를 배웁니다. 컬럼1.2에서 말씀드렸다시피, 본 튜토리얼의 이후 장에서는 기본적으로 이번 장에서 배우는 것과는 반대로 접근하여, 조금씩 어플리케이션을 만들어나가며 각 단계와 개념을 설명할 것입니다. *scaffold*는 Rails 어플리케이션의 전체적인 뼈대를 빠르게 작성하기에 좋기때문에 이번장에서만 일부러 사용해보겠습니다. 작성할 Toy 어플리케이션은 브라우저의 주소창에 URL을 입력하면 동작합니다. 이것을 이용하여 Rails 어플리케이션의 구조와 Rails에서 권장하는 *REST* 아키텍쳐에 대해 생각해보도록 하는 시간을 가지려 합니다. 4 | 5 | Toy 어플리케이션은 나중에 작성할 샘플 어플리케이션과 비슷한, 유저와 유저에 관련된 microposts로 구성되어 있습니다. Toy 어플레케이션은 동작은 합니다만, 완성품은 아닐뿐더러 많은 절차들이 마치 마법처럼 느껴지실 지도 모르겠습니다. 제 3장 이후에 작성할 샘플 어플리케이션에서는 이번에 작성할 Toy 어플리케이션의 기능을 하나씩 수동으로 만들 것입니다. 그만큼 시간이 걸리긴 하겠지만, 아무쪼록 마지막까지 본 튜토리얼을 진행해주실 것이라 믿습니다. 이번 장에서의 목적은 *scaffold* 를 사용한 즉각적이고 표면적인 이해가 아닌, 그 것을 뛰어너머 Rails의 깊은 레벨까지 이해할 수 있게 하는 것입니다. 6 | 7 | 8 | 9 | ## 2.1 어플리케이션의 계획 10 | 11 | 시작하기에 앞서, Toy 어플리케이션을 어떠한 것으로 만들 것인지, 계획을 세워보도록 합시다. 1.3에서 설명드렸다시피, `rails new` 커맨드를 이용하여 Rails의 버전번호를 지정하고, 어플리케이션의 뼈대를 생성하는 곳 부터 해봅시다. 12 | ``` 13 | $ cd ~/environment 14 | $ rails _5.1.6_ new toy_app 15 | $ cd toy_app/ 16 | ``` 17 | 18 | 1.2.1 에서 추천드렸던 클라우드IDE를 사용하시는 경우에는, 이번 어플리케이션을 첫 번째 어플리케이션과 같은 워크스페이스에 작성되는 것에 대해 주의할 필요가 있습니다. 두 번째 어플리케이션을 위해 별도의 워크스페이스를 작성할 필요는 없습니다. 파일이 표시되기 위해서는 파일 네비게이의 톱니바퀴 아이콘을 클릭하여 `[Refresh File Tree]`를 클릭합니다. 19 | 20 | 다음으로 Bundler를 이용하여 `Gemfile` 를 텍스트에디터로 편집해봅시다. 아래의 내용으로 수정해주세요. 21 | ```ruby 22 | source ‘https://rubygems.org’ 23 | 24 | gem ‘rails’, ‘5.1.6’ 25 | gem ‘puma’, ‘3.9.1’ 26 | gem ‘sass-rails’, ‘5.0.6’ 27 | gem ‘uglifier’, ‘3.2.0’ 28 | gem ‘coffee-rails’, ‘4.2.2’ 29 | gem ‘jquery-rails’, ‘4.3.1’ 30 | gem ‘turbolinks’, ‘5.0.1’ 31 | gem ‘jbuilder’, ‘2.7.0’ 32 | 33 | group :development, :test do 34 | gem ‘sqlite3’, ‘1.3.13’ 35 | gem ‘byebug’, ‘9.0.6’, platform: :mri 36 | end 37 | 38 | group :development do 39 | gem ‘web-console’, ‘3.5.1’ 40 | gem ‘listen’, ‘3.1.5’ 41 | gem ‘spring’, ‘2.0.2’ 42 | gem ‘spring-watcher-listen’, ‘2.0.1’ 43 | end 44 | 45 | group :production do 46 | gem ‘pg’, ‘0.20.0’ 47 | end 48 | 49 | # Windows 환경에서는 tzinfo-data 라고 하는 gem을 포함 시킬 필요가 있습니다. 50 | gem ‘tzinfo-data’, platforms: [:mingw, :mswin, :x64_mingw, :jruby] 51 | ``` 52 | 위 젬리스트는 [1.3.1](Chapter1.md#131-bundler) 에서 말씀드린 리스트와 동일합니다. 53 | 54 | [1.5.1](Chapter1.md#151-heroku-설치)에서 설명드린대로, `--without production` 옵션을 추가하는 것으로, 실제 배포환경의 gem을 제외한 로컬gem을 인스톨 합니다. 55 | `bundle install --without production` 56 | 혹시 실행되지 않는다면, 1.3.1에서 설명드린 것 처럼, `bundle update` 을 실행해보세요. 57 | 58 | 마지막으로 Git으로 이번 Toy 어플리케이션의 버전을 관리해줍니다. 59 | ``` 60 | $ git init 61 | $ git add -A 62 | $ git commit -m “Initialize repository” 63 | ``` 64 | 65 | 다음으로 Bitbucket 에 [Create] 버튼을 클릭하여 새로운 레포지토리를 생성합니다. 이어서 생성한 파일을 새로운 리모트 레포지토리에 푸시합니다. 66 | ``` 67 | $ git remote add origin git@bitbucket.org:/toy_app.git 68 | $ git push -u origin —all 69 | ``` 70 | ![](../image/Chapter2/create_demo_repo_bitbucket.png) 71 | 72 | 마지막으로 [1.3.4](Chapter1.md#134-hello-world)에서 소개해드린 *Hello world!* 와 같은 순서로 배포준비를 해주세요. 73 | 74 | *application_controller.rb* 75 | 76 | ```ruby 77 | class ApplicationController < ActionController::Base 78 | protect_from_forgery with: :exception 79 | 80 | def hello 81 | render html: “hello, world!” 82 | end 83 | end 84 | ``` 85 | 86 | *config.rb* 87 | 88 | ```ruby 89 | Rails.application.routes.draw do 90 | root ‘application#hello’ 91 | end 92 | ``` 93 | 94 | 이어서 현재 변경을 커밋하고, Heroku에 푸시합니다. 95 | ``` 96 | $ git commit -am “Add hello” 97 | $ heroku create 98 | $ git push heroku master 99 | ``` 100 | 101 | 1.5와 마찬가지로, 경고메세지가 표시될 수도 있습니다만, 무시하셔도 괜찮습니다. 자세한 것은 7.5에서 설명드리겠습니다. Heroku어플리케이션의 주소 이외에는 아래와 같은 상태가 되어 있을 것입니다. 102 | ![](../image/Chapter2/heroku_app_hello_world.png) 103 | 104 | 이 것으로 어플리케이션 자체를 작성하기 위한 밑작업을 끝냈습니다. Web어플리케이션을 만들 때에는 어플리케이션의 구조를 표현하기 위한 데이터 모델을 맨 처음에 작성하는 것이 보통입니다. 105 | 106 | 이번 장의 Toy 어플리케이션에서는 유저와 짧은 Micropost(Twitter에서의 트윗) 만을 표현하는 microblog를 작성해보겠습니다. 일단은 어플리케이션에서의 유저를 사용하기 위한 모델을 작성하고, 그 다음으로 micropost에서 사용하기 위한 모델을 작성합니다. 107 | 108 | ### 2.1.1 User Modeling 109 | 110 | Web에서의 유저등록 방법이 매우 많은 것에서부터 알 수 있듯, 유저 라고하는 개념을 데이터모델로 표현하기 위한 방법은 매우 많습니다. 본 장에서는 최소한의 표현방법을 사용해보겠습니다. 111 | 112 | 각 유저는, 중복이 없는 유일한 키가 되는 `integer`형의 ID번호(`id`라고 부르겠습니다.) 를 할당하고, 이 ID와 더불어 일반에 공개하는 `string`형의 이름(`name`), 그리고 `string` 형태의 이메일주소 (`email`)을 가집니다. 이메일 주소는 유저이름으로써 사용됩니다. 유저의 데이터모델의 개요는 아래와 같습니다. 113 | ![](../image/Chapter2/demo_user_model.png) 114 | 115 | 상세한 설명은 6.1.1에서부터 말씀드리겠습니다만, 위 그림의 `user`는 데이터베이스의 테이블(table)에 해당됩니다. 또한 `id`, `name`, `email` 의 속성은 각각 테이블의 컬럼에 해당됩니다. 116 | 117 | ### 2.1.2 Micropost Modeling 118 | 119 | 마이크로포스트의 데이터모델은 유저 모델보다도 좀 더 심플합니다. `id` 와 마이크로포스트의 텍스트 내용을 기록하는 `text` 형의 `context` 로 구성됩니다. 하지만 실제로는, 마이크로포스트를 유저와 관련짓게 할 필요가 있습니다. (associate) 그러기 위해서, 마이크로포스트의 포스트 작성자를 기록하기 위한 `user_id` 를 추가합니다. 이것으로 데이터 모델은 아래와 같습니다. 120 | ![](../image/Chapter2/demo_micropost_model.png) 121 | 122 | 위 그림에서는 `user_id` 라고하는 속성을 이용하여 1명의 유저에게 여러개의 마이크로포스트를 관련짓게 할 수 있는 구조를 간결하게 설명하고 있습니다. 상세하게는 제13장에서 설명하겠습니다. 123 | 124 | 125 | 126 | ## 2.2 Users 리소스 127 | 128 | 여기서는 2.1.1에서 설명한 유저를 위한 데이터모델을 표시하기 위해 Web 인터페이스를 만들어봅니다. 이 유저 데이터모델과 Web인터페이스를 조합하여 *User*리소스 라고 부르며, 유저라고 하는 것은 [HTTP 프로토콜](http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 을 경유하여 자유자재로 작성/조회/갱신/삭제 가 가능한 오브젝트로 구분할 수 있게됩니다. 앞에서 말씀드린 것 처럼 , 이 User리소스는 모든 Rails프로젝트에서 표준적으로 사용할 수 있는 *scaffold* 제네레이터로 생성해봅니다. scaffold로 생성된 많은 코드를 지금 상세하게 읽어볼 필요는 없습니다. 지금 단계에서는 분명 혼란스러울 테니까요. 129 | 130 | Rails의 scaffold는 `rails generate` 스크립트에 `scaffold` 커맨드를 입력하는 것으로 작성할 수 있습니다. `scaffold` 커맨드의 파라미터로는 리소스이름의 단수형을 한 형태로 (이번 경우에는 `User`) 를 사용하여, 필요한만큼의 데이터모델의 속성을 옵션으로써 파라미터로 추가할 수 있습니다. 131 | 132 | 133 | 134 | ``` 135 | $ rails generate scaffold User name:string email:string 136 | invoke active_record 137 | create db/migrate/20160515001017_create_users.rb 138 | create app/models/user.rb 139 | invoke test_unit 140 | create test/models/user_test.rb 141 | create test/fixtures/users.yml 142 | invoke resource_route 143 | route resources :users 144 | invoke scaffold_controller 145 | create app/controllers/users_controller.rb 146 | invoke erb 147 | create app/views/users 148 | create app/views/users/index.html.erb 149 | create app/views/users/edit.html.erb 150 | create app/views/users/show.html.erb 151 | create app/views/users/new.html.erb 152 | create app/views/users/_form.html.erb 153 | invoke test_unit 154 | create test/controllers/users_controller_test.rb 155 | invoke helper 156 | 157 | create app/helpers/users_helper.rb 158 | invoke test_unit 159 | invoke jbuilder 160 | create app/views/users/index.json.jbuilder 161 | create app/views/users/show.json.jbuilder 162 | invoke assets 163 | invoke coffee 164 | create app/assets/javascripts/users.coffee 165 | invoke scss 166 | create app/assets/stylesheets/users.scss 167 | invoke scss 168 | create app/assets/stylesheets/scaffolds.scss 169 | ``` 170 | 171 | 172 | 173 | `name:string`과 `email:string` 옵션을 추가하여 User모델의 내용이 위에서의 user모델그림과 같게 되도록 작성합니다. (또한, `id` 파라미터는 Rails에 의해 자동적으로 Primary Key로 데이터베이스에 추가되기 때문에, 추가할 필요는 없습니다.) 174 | 175 | 계속해서 Toy 어플리케이션의 개발을 진행하기 위해, `rails db:migrate`를 실행하여 데이터베이스를 migrate할 필요가 있습니다. 176 | ``` 177 | $ rails db:migrate 178 | == CreateUsers: migrating ================================= 179 | — create_table(:users) 180 | -> 0.0017s 181 | == CreateUsers: migrated (0.0018s) ======================== 182 | ``` 183 | 위 커맨드는, 단순히 데이터베이스를 갱신하여 `user` 데이터모델을 작성하기 위한 커맨드입니다. (데이터베이스의 마이그레이션의 상세한 설명은 6.1.1 이후에 설명하겠습니다.) 184 | 185 | 또한 Rails 5 이전의 버전에서는 `db:migrate` 커맨드는 `rails` 커맨드가 아닌, `rake` 커맨드가 사용되었습니다. Rails 4 이전의 오래된 Rails 어플리케이션에서는 Rake의 사용방법을 알아둘 필요가 있습니다. 상세한 내용은 컬럼2.1에서 확인해주세요. 186 | 187 | ###### 컬럼 2.1 Rake 188 | > Unix에서는, 소스코드에서 실행용 프로그램으로 빌드하기 위해 주로 *[Make](http://en.wikipedia.org/wiki/Make_(software))* 라고 하는 툴을 사용해왔습니다. *Rake*는 이른바 Ruby에서의 Make 라고도 할 수 있습니다. 189 | > Rails 4이전에는 Rake를 사용했기 때문에, 오래된 Rails 어플리케이션을 다루기 위해서는 Rake에 대해 배울 필요가 있습니다. 아마도 자주 사용되었던 Rake커맨드는, 데이터베이스의 데이터 모델을 갱신하기 위해 `rake db:migrate` 커맨드와, 자동화된 테스트케이스를 실행하기 위한 `rake test` 두가지 커맨드였을 것입니다. 190 | > 또한 Rails 4이전의 어플리케이션에서는, `rake` 커맨드의 버전을 `Gemfile` 에 정의하고 있기 때문에, Bundler의 `bundler exec` 커맨드를 이용하여 실행할 필요가 있습니다. 예를 들어, Rails 5에서의 마이그레이션 커맨드는 다음과 같이 할 수 있습니다 191 | > `rails db:migrate` 192 | > Rails 4이전에서는 다음과 같이 실행할 필요가 있습니다. 193 | > `bundle exec rake db:migrate` 194 | 195 | migration작업이 끝나면, 다음 커맨드로 로컬 Web서버를 실행할 수 있습니다. 196 | `rails server` 197 | 198 | 이것으로 [1.3.2](Chapter1.md#132-rails-server)에서 설명드린 대로, 로컬서버가 동작할 것입니다. (클라우드IDE로 작업하는 분은, IDE에 탑재된 간단한 브라우저가 아닌, 반드시 *Chrome*이나 *Firefox* 등의 범용브라우저의 별도 탭을 이용하여 development서버의 결과를 확인해주세요.) 199 | 200 | ### 2.2.1 Users 화면을 움직여보자. 201 | 브라우저에서 루트URL (/) 로 접속해보면, *hello world!* 페이지가 표시되겠지만, Users리소스를 scaffold로 생성하였기 때문에, 유저관리용 화면이 많이 추가되어있는 점이 이전과는 다른 점입니다. 예를 들어, /user를 표시하면 모든 유저의 리스트를 표시하며, /user/new를 표시하면 신규 유저 작성 페이지가 표시됩니다. 본 장에서는 유저에 관련된 페이지에 대해 간단하게 설명하겠습니다. 아래에 작성되어 있는 페이지와 URL의 관계를 참조한다면 알기 쉬울 것 입니다. 202 | 203 | | URL | Action | Remarks | 204 | | ------------- | ------ | -------------------------------- | 205 | | /users | index | すべてのユーザーを一覧するページ | 206 | | /users/1 | show | id=1のユーザーを表示するページ | 207 | | /users/new | new | 新規ユーザーを作成するページ | 208 | | /users/1/edit | edit | id=1のユーザーを編集するページ | 209 | 210 | 211 | 212 | 일단 유저 리스트를 표시하는 `index` 액션의 URL(/users)를 확인해봅시다. 물론 이 시점에는 아직 유저가 등록되어 있지는 않습니다. 213 | ![](../image/Chapter2/demo_blank_user_index_3rd_edition.png) 214 | 215 | 유저를 새롭게 등록하기 위해선, 위 그림의 `new` 페이지를 표시해봅니다. 또한 제 7장에서는 이 페이지가 유저 등록용 페이지로 바뀔 것입니다. 216 | ![](../image/Chapter2/demo_new_user_3rd_edition.png) 217 | 218 | 텍스트 필드에 이름과 이메일을 입력하고 [Create User] 버튼을 눌러보세요. 아래와 같은 `show` 페이지가 표시됩니다. (녹색의 welcome 메세지는 7.4.2에서 설명드리는 *flash* 라고 하는 기능을 이용하여 출력하고 있습니다.) 여기서 URL이 /user/1 이라고 표시되고 있는 것에 대해 주목해주세요. 생각하시는 것 처럼, 여기서 *1* 이라고 하는 숫자는, id속성을 나타내는 것입니다. 7.1에서는 이 페이지를 유저의 프로필 페이지로 만들 것입니다. 219 | ![](../image/Chapter2/demo_show_user_3rd_edition.png) 220 | 221 | 이번에는 유저의 정보를 변경하기 위해, /users/1/의 `edit` 를 출력해봅시다. 이 편집용 페이지에서 유저에 관한 정보를 변경하고, [Update User] 버튼을 누르면, Toy 어플리케이션 내부의 유저정보가 변경됩니다. (상세한 것은 제 6장에서 설명드리겠습니다만, 이 유저 정보는 Web 어플리케이션의 뒷단에 있는 데이터베이스에 저장되어 있습니다.) Sample 어플리케이션에서도 유저를 편집 혹은 업데이트하는 기능을 10.1에서 구현합니다. 222 | ![](../image/Chapter2/demo_edit_user_3rd_edition.png) 223 | 224 | ![](../image/Chapter2/demo_update_user_3rd_edition.png) 225 | 226 | 여기서 /user/new 페이지로 되돌아가서 유저를 한 명 더 작성해봅시다. index페이지에 접속해보면 아래와 같이 유저가 추가되어 있을 것입니다. 7.1에서는 좀 더 본격적인 유저 리스트페이지를 작성해볼 것입니다. 227 | ![](../image/Chapter2/demo_user_index_two_3rd_edition.png) 228 | 229 | 유저의 등록, 출력, 업데이트 방법 등에 대해 설명했습니다. 이번에는 유저를 삭제해봅시다. [Destroy] 링크를 클릭하면 유저는 삭제되고, index페이지의 유저는 1명만 남게 됩니다. (만약 이 설명대로 되지 않는다면, 브라우저의 Javascript가 유효한 상태인지 아닌지 확인해주세요. Rails에서는 유저의 삭제하는 리퀘스트를 작성할 때, Javascript를 사용합니다.) 230 | 또한 10.4에서는 Sample 어플리케이션에 유저를 삭제하는 기능을 구현해보고, 관리권한(admin)을 가진 유저 이외에는 삭제할 수 없게끔 제한을 걸 것입니다. 231 | ![](../image/Chapter2/demo_destroy_user_3rd_edition.png) 232 | 233 | 234 | 235 | ##### 연습 236 | 237 | 1. CSS를 알고 계신 분들께 : 새로운 유저를 등록하고, 브라우저의 HTML inspector를 사용하여 “User was successfully created.”의 부분을 알아봐주세요. 브라우저를 새로고침하면, 그 부분은 어떻게 되나요? 238 | 2. email을 입력하지 말고, 이름만 입력하고 등록하려고 하면 어떻게 되나요? 239 | 3. “@example.com” 과 같은 잘못된 메일주소를 입력하여 업데이트하려고 할 때, 어떻게 되나요? 240 | 4. 위의 연습문제에서 작성한 유저를 삭제해주세요. 유저를 삭제할 때, Rails는 어떠한 메세지를 표시하나요? 241 | 242 | 243 | 244 | ### 2.2.2 MVC의 처리 245 | 246 | 이 것으로 Users리소스의 개요에 대해 설명은 끝났습니다. 여기서 1.3.3에서 말씀드린 MVC(Model, View, Controller) 패턴의 관점에서 이 리소스를 확인해봅시다. 구체적으로는 “/user에 있는 index페이지를 브라우저에서 접속해보자“ 라는 조작을 할 경우, 내부에서는 어떠한 처리가 일어나고 있는지 MVC의 관점에서 설명하겠습니다. 247 | ![](../image/Chapter2/mvc_detailed.png) 248 | 249 | 위 그림에서 설명하고 있는 순서의 개요는 아래와 같습니다. 250 | 251 | 1. 브라우저로부터 /user 라고 하는 URL의 리퀘스트를 Rails의 서버로 송신합니다. 252 | 2. /user 리퀘스트는 Rails의 라우팅처리에 의해 Users 컨트롤러 내부의 `index` 액션에 할당됩니다. 253 | 254 | 3. `index` 액션이 실행되고, 거기서 User모델에게 “모든 유저를 출력해라” (`User.all`) 의 쿼리가 실행됩니다. 255 | 256 | 4. User모델은 쿼리를 실행하여 모든 유저정보를 데이터베이스에 접근, 출력하게됩니다. 257 | 258 | 5. 데이터베이스로부터 획득한 유저 정보를 User모델에서 컨트롤러로 넘깁니다. 259 | 260 | 6. User컨트롤러는 유저 정보(리스트)를 `@users` 변수 (Ruby는 @를 인스턴스변수로서 표현) 에 저장, `index` 뷰에 넘깁니다. 261 | 262 | 7. index뷰가 실행되어, ERB(Embedded RuBy: 뷰의 HTML에 기술되어 있는 Ruby코드) 를 실행하여 HTML을 생성(렌더링)합니다. 263 | 264 | 8. 컨트롤러는, 뷰에서 생성된 HTML을 받아 브라우저에게 넘깁니다. 265 | 266 | 267 | 268 | 위와 같은 처리를 좀 더 자세하게 알아봅시다. 제일 처음에는 브라우저로부터 송신되는 리퀘스트를 확인해봅시다. 이 리퀘스트는 주소창에 URL을 입력한다거나, 링크를 클릭했을 때 발생합니다(1번). 리퀘스트는 Rails Routing에 도착(2번)하고, 여기서 URL(과 리퀘스트의 종류는 컬럼 3.2를 참고)을 기반으로하여 적절한 컨트롤러의 액션에 할당됩니다.(dispatch) 유저로부터 리퀘스트받은 URL을, User리소스에서 사용하는 컨트롤러와 액션에 할당하는 코드는, 아래와 같습니다. 이러한 mapping하는 코드를 Rails의 라우팅설정파일(config/routes.rb)에 작성합니다. Rails에서는 config/routes.rb에서 URL과 액션의 조합으로 알기 쉽게 설정하고 있습니다. (`:user` 라고 하는 조금 신기하게 보이는 표현은, Ruby언어 특유의 “심볼” 이라고 불리는 것입니다. 자세한 것은 4.3.3에서 서술합니다.) 269 | ```ruby 270 | Rails.application.routes.draw do 271 | resources :users 272 | root ‘application#hello’ 273 | end 274 | ``` 275 | 276 | 그렇다면, 이 라우팅파일을 변경해봅시다. 서버의 루트URL에 접근하면, 디폴트 페이지 대신에 유저 리스트를 출력하게 해봅시다. 즉 “/“ 로 접속하면 /user를 표시하도록 합니다. 우리의 디폴트 페이지로 접속하는 루트URL은 다음과 같습니다. 277 | `root 'application#hello'` 278 | 279 | 이걸로 루트에 접속하면, Aplicaiton컨트롤러 내부의 `hello` 액션에 접속하게 됩니다. 이번에는 Users컨트롤러의 index액션으로 접속하도록, 아래처럼 수정해봅시다. 280 | ``` ruby 281 | Rails.application.routes.draw do 282 | resources :users 283 | root ‘users#index’ 284 | end 285 | ``` 286 | 287 | 2.21 이후로 소개하는 각 페이지는, User 컨트롤러 내의 액션에 제각각 대응하고 있습니다. 다음 리스트는 scaffold로 생성된 컨트롤러의 기본형태입니다. `class UsersController < ApplicationController` 라고 하는 표현은 Ruby의 클래스 상속 문법입니다. (상속에 대해서는 2.3.4, 4.4에서 설명합니다. 288 | ```ruby 289 | class UsersController < ApplicationController 290 | . 291 | . 292 | . 293 | def index 294 | . 295 | . 296 | . 297 | end 298 | 299 | def show 300 | . 301 | . 302 | . 303 | end 304 | 305 | def new 306 | . 307 | . 308 | . 309 | end 310 | 311 | def edit 312 | . 313 | . 314 | . 315 | end 316 | 317 | def create 318 | . 319 | . 320 | . 321 | end 322 | 323 | def update 324 | . 325 | . 326 | . 327 | end 328 | 329 | def destroy 330 | . 331 | . 332 | . 333 | end 334 | end 335 | ``` 336 | 337 | 페이지의 숫자보다 액션의 수가 더 많은 것을 눈치채셨나요? `index`, `show`, `new`, `edit` 액션은 2.2.1에서 다루는 페이지에 해당합니다만, 그 외에도 `create`, `update`, `destroy` 액션이 있습니다. 보통 이 액션들은 페이지를 출력하지 않고 데이터베이스 상의 유저정보를 핸들링합니다. (물론 페이지를 출력하려하면 할 수 있습니다.) 338 | 339 | 아래의 표는 Rails에서 REST아키텍처 (컬럼2.2)를 구성하는 모든 액션의 리스트입니다. *REST*는, 컴퓨터과학자 [Roy Fielding](http://en.wikipedia.org/wiki/Roy_Fielding) 에 의해 제창된 *REpresentational State Transfer* 라고 하는 개념의 줄임말입니다. 아래 표의 URL중 몇개는 중복되어 있다는 것을 확인해주세요. 예를 들어 `show`과 `update`액션은 어느쪽이던 /user/1 이라고 하는 URL이 적혀있습니다. 이러한 액션들간의 차이는, 이것들의 액션에 대응하는 [HTTP request메소드](http://en.wikipedia.org/wiki/HTTP_request#Request_methods) 의 차이도 포함됩니다. HTTP request 메소드는 3.3에서 설명하겠습니다. 340 | 341 | 342 | 343 | | HTTPリクエスト | URL | アクション | 用途 | 344 | | -------------- | ------------- | ---------- | ---------------------------------- | 345 | | GET | users | index | すべてのユーザーを一覧するページ | 346 | | GET | /users/1 | show | id=1のユーザーを表示するページ | 347 | | GET | /users/new | new | 新規ユーザーを作成するページ | 348 | | POST | /users | create | ユーザーを作成するアクション | 349 | | GET | /users/1/edit | edit | id=1のユーザーを編集するページ | 350 | | PATCH | /users/1 | update | id=1のユーザーを更新するアクション | 351 | | DELETE | /users/1 | destroy | id=1のユーザーを削除するアクション | 352 | 353 | 354 | 355 | ###### 컬럼 2.2 REpresentational State Transfer (REST) 356 | > Rails관련 책들을 읽다보면, “REST” 라고 하는 용어를 자주 볼 수 있을 것입니다. 이것은 Representational State Transfer의 약자입니다. REST는 인터넷이나 Web 어플리케이션 등의, 분산/네트워크화된 시스템이나 어플리케이션을 구축하기 위한 아키텍쳐의 스타일 중 하나입니다. REST이론 그 자체는 꽤나 추성적입니다만, Rails 어플리케이션에서의 REST는 어플리케이션을 만들 때 컴포넌트(유저나 마이크로포스트)를 “리소스” 로 모델링하는 것을 칭합니다. 이 리소스는 [관계형 데이터베이스의 등록, 조회, 편집, 삭제(Create/Read/Update/Delete: CRUD) 조작](http://ja.wikipedia.org/wiki/CRUD) 과, 4개의 기본적인 [HTTP request메소드](http://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol%23.E3.83.A1.E3.82.BD.E3.83.83.E3.83.89) (Post/Get/Patch/Delete)에 대응합니다. (HTTP request메소드에 대해서는 3.3 혹은 컬럼 3.2에서 설명하겠습니다.) 357 | > Rails 개발자에게는 RESTful 스타일을 구현하는 것으로 작성해야만 하는 컨트롤러나 액션의 결정이 많이 편해집니다. 등록(C), 조회(R), 편집(U), 삭제(D)를 실행하는 리소스만으로도 어플리케이션 전체를 작성할 수도 있습니다. 유저나 마이크로포스트 등에 대해서는 자연스럽게 리소스화할 수 있습니다. 제 14장에서 “유저를 팔로우해보자” 라고 하는 꽤나 복잡한 내용에 대해 REST이론을 이용한 자연스럽고 효율적인 모델링을 해봅니다. 358 | 359 | 360 | 361 | Users컨트롤러와 User모델의 관계를 좀 더 알아보기위해, 아래의 리스트에서 `index` 액션을 정리해보았습니다. (전부 이해하진 못하더라도, 코드를 읽어보는 방법에 대해 배우는 것은 매우 중요합니다.) 362 | 363 | ```ruby 364 | class UsersController < ApplicationController 365 | . 366 | . 367 | . 368 | def index 369 | @users = User.all 370 | end 371 | . 372 | . 373 | . 374 | end 375 | ``` 376 | 377 | `index` 액션에는 `@users = User.all` 이라고 하는 라인이 있습니다. (2.2.2의 그림에서 3번에 해당) 이로 인해 User모델로부터 모든 유저의 정보를 얻어 `@users` 라고하는 변수에 저장합니다(5번). User모델의 내용은 아래와 같습니다. 매우 놀랍게도 간단한 내용입니다만 상속(2.3.4 및 4.4) 에 의해 많은 기능을 갖추고 있습니다. 특히 *Active Record* 라고 하는 Ruby 라이브러리 덕분에 아래의 User모델은 `User.all` 이라고 하는 리퀘스트에 대해 DB의 모든 유저를 조회할 수 있습니다. 378 | 379 | ```ruby 380 | class User < ApplicationRecord 381 | end 382 | ``` 383 | `@users` 변수에 유저 리스트가 저장되면, 컨트롤러는 아래의 뷰를 호출합니다 (6번) `@`마크로 시작하는 변수는 Ruby에서는 *인스턴스 변수* 로 불리며, Rails의 컨트롤러 내부에서 선언한 인스턴스 변수는 뷰에서도 사용할 수 있습니다. 이 경우에는 아래 리스트의 `index.html.erb` 뷰는 `@user`의 리스트를 1줄씩 HTML의 1개의 라인으로 출력합니다. (현재는 이 코드가 무슨 뜻인지 잘 몰라도 괜찮습니다. 어디까지나 설명을 위한 것입니다.) 384 | ```html 385 |

Listing users

386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | <% @users.each do |user| %> 397 | 398 | 399 | 400 | 401 | 402 | 404 | 405 | <% end %> 406 |
NameEmail
<%= user.name %><%= user.email %><%= link_to ‘Show’, user %><%= link_to ‘Edit’, edit_user_path(user) %><%= link_to ‘Destroy’, user, method: :delete, 403 | data: { confirm: ‘Are you sure?’ } %>
407 | 408 |
409 | 410 | <%= link_to ‘New User’, new_user_path %> 411 | ``` 412 | 뷰는 그 내용을 HTML로 변환하여 (7번), 컨트롤러가 브라우저에게 HTML을 송신하여 브라우저에서 HTML 이 출력되도록 합니다.(8번) 413 | 414 | ##### 연습 415 | 1. 2.2.2의 그림을 참고하여 /users/1/edit 라고 하는 URL에 접근 하려고 할 때의 동작을 서술해보세요. 416 | 2. 그림을 확인해가며, scaffold에서 생성된 코드 중에 데이터베이스에서 유저 정보를 얻는 코드를 찾아보세요. 417 | 3. 유저 정보를 편집하는 페이지의 파일명은 무엇입니까? 418 | 419 | ### 2.2.3 User 리소스의 결함 420 | 421 | scaffold에서 작성된 User리소스는, Rails의 개념을 매우 간단하게 설명하기에는 좋습니다만, 다음과 같은 문제점들이 있습니다. 422 | 423 | - 데이터의 검증이 이루어지지 않는다. 424 | 425 | - scaffold의 기본 상태로는 유저이름이 빈칸이거나 대충 만든 이메일을 입력해도 데이터가 등록됩니다. 426 | 427 | - 유저 인증이 이루어지지 않는다. 428 | 429 | - 로그인, 로그아웃이 이뤄지지 않기 때문에 누구던지 무제한으로 조작할 수 있습니다. 430 | 431 | - 테스트 처리가 없다. 432 | 433 | - 엄밀히 말하면, 이것은 제대로된 표현은 아닙니다. 애당초 scaffold로 작성된 코드에는 매우 간단한 테스트코드가 포함되어 있습니다. 단, scaffold의 테스트 코드는 데이터 검증이나 유저인증, 그 외 필수적인 테스트 요구사항이 구현되어있지 않습니다. 434 | 435 | - 레이아웃이나 스타일이 제대로 되어있지 않다. 436 | 437 | - 사이트디자인이나 조작방법이 일반적이진 않습니다. 438 | 439 | - 이해하기 어렵다. 440 | 441 | - scaffold의 코드를 이해할 정도면, 애당초 본 튜토리얼을 읽을 필요도 없을 것입니다. 442 | 443 | 444 | 445 | ## 2.3 Microposts 리소스 446 | 447 | User 리소스를 생성하여 내용을 확인해보았습니다. 이번에는 Microposts리소스에 대해 똑같이 내용을 확인해보겠습니다. 또한 이번 섹션 전체에 대해 Microposts 리소스를 이해할 때에는, 2.2의 user요소와 비교해가며 섹션을 진행하는 것을 권해드립니다. 실제로 이 2개의 리소스는 여러 많은 곳에서 닮아있습니다. Rails의 RESTful구조를 이해하기 위해선, 반복적인 학습이 제일 좋습니다. User리소스와 Microposts리소스의 구조의 닮은점을 이해하는 것이, 이번 섹션의 주된 내용입니다. 448 | 449 | ### 2.3.1 Microposts 화면을 움직여보자 450 | User리소스와 마찬가지로, Microposts 리소스도 scaffold로 코드를 작성해봅시다. `rails generate scaffold` 커맨드를 사용하여 아래 그림처럼 데이터 모델을 작성해봅니다. 451 | ![](../image/Chapter2/demo_micropost_model%202.png) 452 | 453 | 454 | 455 | ``` 456 | $ rails generate scaffold Micropost content:text user_id:integer 457 | invoke active_record 458 | create db/migrate/20160515211229_create_microposts.rb 459 | create app/models/micropost.rb 460 | invoke test_unit 461 | create test/models/micropost_test.rb 462 | create test/fixtures/microposts.yml 463 | invoke resource_route 464 | route resources :microposts 465 | invoke scaffold_controller 466 | create app/controllers/microposts_controller.rb 467 | invoke erb 468 | create app/views/microposts 469 | create app/views/microposts/index.html.erb 470 | create app/views/microposts/edit.html.erb 471 | create app/views/microposts/show.html.erb 472 | create app/views/microposts/new.html.erb 473 | create app/views/microposts/_form.html.erb 474 | invoke test_unit 475 | create test/controllers/microposts_controller_test.rb 476 | invoke helper 477 | 478 | create app/helpers/microposts_helper.rb 479 | invoke test_unit 480 | invoke jbuilder 481 | create app/views/microposts/index.json.jbuilder 482 | create app/views/microposts/show.json.jbuilder 483 | invoke assets 484 | invoke coffee 485 | create app/assets/javascripts/microposts.coffee 486 | invoke scss 487 | create app/assets/stylesheets/microposts.scss 488 | invoke scss 489 | identical app/assets/stylesheets/scaffolds.scss 490 | ``` 491 | 492 | (Spring에 관련된 에러가 발생한다면, 같은 코드를 한 번 더 입력해보도록 합시다.) 493 | 새로운 데이터모델로 데이터베이스를 업데이트하기 위해서는, 2.2와 같이 마이그레이션을 실행합니다. 494 | ``` 495 | $ rails db:migrate 496 | == CreateMicroposts: migrating ============================= 497 | — create_table(:microposts) 498 | -> 0.0023s 499 | == CreateMicroposts: migrated (0.0026s) ====================== 500 | ``` 501 | 502 | 이 것으로 Microposts를 만들 준비가 끝났습니다. 만드는 방법은 2.2.1과 동일합니다. 이미 눈치채셨을 지도 모르겠습니다만, scaffold 제네레이터에 의하여 Rails의 route파일이 업데이트되었습니다. 내용은 아래처럼, Microposts 리소스용의 라우팅규약이 추가되었습니다. User의 경우와 마찬가지로, `resource :microposts` 이라고 하는 라우팅규약은, microposts용의 URI를 microposts 컨트롤러 내부의 액션에 할당해줍니다. 503 | ```ruby 504 | Rails.application.routes.draw do 505 | resources :microposts 506 | resources :users 507 | root ‘users#index’ 508 | end 509 | ``` 510 | 511 | 512 | 513 | | HTTPリクエスト | URL | アクション | 用途 | 514 | | -------------- | ------------------ | ---------- | ---------------------------------------- | 515 | | GET | /microposts | index | すべてのマイクロポストを表示するページ | 516 | | GET | /microposts/1 | show | id=1のマイクロポストを表示するページ | 517 | | GET | /microposts/new | new | マイクロポストを新規作成するページ | 518 | | POST | /microposts | create | マイクロポストを新規作成するアクション | 519 | | GET | /microposts/1/edit | edit | id=1のマイクロポストを編集するページ | 520 | | PATCH | /microposts/1 | update | id=1のマイクロポストを更新するアクション | 521 | | DELETE | /microposts/1 | destroy | id1のマイクロポストを削除する | 522 | 523 | Microposts의 컨트롤러 자체의 구조는 아래와 같습니다. `UsersController`가 `MicropostsController` 로 바뀌어있을 뿐, 그 외에는 2.2에서 확인한 Users 컨트롤러와 같다는 것에 주목해주세요. 이것은 REST아키텍쳐가 2개의 리소스에 똑같이 적용되어있는 것을 나타내고 있습니다. 524 | ```ruby 525 | class MicropostsController < ApplicationController 526 | . 527 | . 528 | . 529 | def index 530 | . 531 | . 532 | . 533 | end 534 | 535 | def show 536 | . 537 | . 538 | . 539 | end 540 | 541 | def new 542 | . 543 | . 544 | . 545 | end 546 | 547 | def edit 548 | . 549 | . 550 | . 551 | end 552 | 553 | def create 554 | . 555 | . 556 | . 557 | end 558 | 559 | def update 560 | . 561 | . 562 | . 563 | end 564 | 565 | def destroy 566 | . 567 | . 568 | . 569 | end 570 | end 571 | ``` 572 | 573 | /microposts/new페이지를 브라우저에서 열고, 새로운 마이크로포스트의 정보를 입력하여 마이크로포스트를 몇 개정도 생성해보세요 574 | 575 | ![](../image/Chapter2/demo_new_micropost_3rd_edition.png) 576 | 577 | 여기서는 일단 마이크로포스트를 1개나 2개정도 작성하고, 적어도 한쪽의 `user_id`가 `1` 이 되도록하여 2.2.1에서 만든 제일 첫 유저의 id와 일치하도록 합니다. 결과는 아래처럼 될 것 입니다. 578 | ![](../image/Chapter2/demo_micropost_index_3rd_edition.png) 579 | 580 | 581 | 582 | ##### 연습 583 | 584 | 1. CSS을 알고 계신 분이라면: 새로운 마이크로포스트를 작성하고 브라우저의 HTML inspector 기능을 사용하여 *Micropost was successfully created* 의 부분을 확인해보세요. 브라우저를 새로고침하면 그 부분은 어떻게 되나요? 585 | 2. 마이크로포스트의 작성화면에서, Content와 User를 빈칸으로 하여 작성한다면 어떠한 결과가 되나요? 586 | 3. 141문자 이상의 문자열을 Content에 입력하였을 때, 마이크로포스트를 작성하려고 하면 어떻게 되나요? (힌트 : [Wikipedia의 Ruby](https://ja.wikipedia.org/wiki/Ruby) 1단락이 마침 150자정도인데, 어떻게되나요?) 587 | 4. 위 연습으로 작성한 마이크로포스트를 삭제해봅시다. 588 | 589 | ### 2.3.2 Micropost를 micro하게 해보자 590 | 591 | micropost의 *micro* 라는 이름처럼, 무엇인가의 방법으로 문자수제한을 생각해봅시다. Rails에서는 이럴 때, *validation* 을 사용하여 간단하게 제한할 수 있습니다. 예를 들면 *Twitter*처럼 140문자제한을 걸고 싶을때, *Length* 를 쓰면됩니다. 텍스트에디터나 IDE를 사용하여 `app/models/micropost.rb` 를 열고, 아래의 내용으로 수정해봅시다. 592 | ```ruby 593 | class Micropost < ApplicationRecord 594 | validates :content, length: { maximum: 140 } 595 | end 596 | ``` 597 | 위 코드로 정말 움직이는 것인지 의문하시는 분들도 있으실 수도 있겠지만, 정말 제대로 움직입니다. (검증기능에 대해서는 6.2에서 설명합니다.) 141문자 이상의 새로운 마이크로포스트를 작성해보면 알게 됩니다. 아래의 그림과 같이 마이크로포스트의 내용이 너무 길다는 내용의 에러메세지가 Rails에 의해 출력됩니다. (에러 메세지에 대해서는 7.3.3에서 소개합니다. 598 | 599 | ![](../image/Chapter2/micropost_length_error_3rd_edition.png) 600 | 601 | 602 | 603 | ##### 연습 604 | 605 | 1. 방금 전 2.3.1.1에서 연습문제를 푼 것 처럼, 다시 한 번 더 Content의 내용을 141문자 이상 입력해봅시다. 어떻게 동작이 바뀌었나요? 606 | 2. CSS을 알고 계신 분들께: 브라우저의 HTML Inspector를 이용하여 표시된 에러메세지가 무엇인지 확인해주세요. 607 | 608 | ### 2.3.3 유저는 많은 마이크로포스트를 가지고 있다. 609 | 610 | 여러가지 다른 데이터모델 간의 관계를 만드는 것은, Rails의 강력한 기능입니다. 여기서는 한 명의 유저에 대해 여러개의 마이크로 포스트가 존재한다고 가정합니다. User모델과 Micropost모델을 제각각 아래처럼 수정하는 것으로 관계를 표현할 수 있습니다. 611 | ```ruby 612 | class User < ApplicationRecord 613 | has_many :microposts 614 | end 615 | ``` 616 | 617 | ```ruby 618 | class Micropost < ApplicationRecord 619 | belongs_to :user 620 | validates :content, length: { maximum: 140 } 621 | end 622 | ``` 623 | 624 | 이 데이터모델 간의 관계를 그림으로 나타낸 것이 아래의 그림입니다. `microposts` 테이블에는 `user_id` 컬럼을 선언해놓았기 때문에, Rails와 Active Record가 마이크로포스트와 유저 간의 관계를 생성해주는 것입니다. 625 | ![](../image/Chapter2/micropost_user_association.png) 626 | 627 | 628 | 629 | 제 13장과 제 14장에서는 관계가 맺어진 유저와 마이크로포스트를 동시에 표시하여 Twitter와 같은 마이크로포스트의 피드를 작성해볼 계획입니다. 여기선 Rails의 *console* 을 이용하여 유저와 마이크로포스트의 관계를 확인해봅니다.. 630 | Rails의 console은 Rails 어플리케이션과 커뮤니케이션을 할 수 있는 일종의 프로그램입니다. 일단 터미널에서 `rails console` 커맨드를 입력합니다. 이어서 `User.first` 라고 입력하여 데이터베이스에서 제일 앞의 유저 정보를 조회하여 `first_user` 변수에 저장합니다. 631 | ``` 632 | $ rails console 633 | >> first_user = User.first 634 | => # 636 | >> first_user.microposts 637 | => [#, 639 | "2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">, 640 | #] 642 | >> micropost = first_user.microposts.first 643 | => # 645 | >> micropost.user 646 | => # 648 | >> exit 649 | ``` 650 | 651 | (마지막 줄처럼 `exit` 를 입력하면, 실행하고 있는 rails console을 종료할 수 있습니다. 많은 시스템에서는 Ctrl+d키를 눌러 종료하기도 합니다.) 652 | `first_user.microposts` 라고 하는 코드를 실행하면, 그 유저에 관련된 마이크로포스트에 액세스할 수 있습니다. 이 때 Active Record는 `user_id`가 `first_user` 의 id (여기서는 1) 과 같은 마이크로포스트를 자동적으로 조회합니다. Active Record의 관계맺기 기능에서는 제 13장과 제 14장에서 상세히 설명합니다. 653 | 654 | ##### 연습 655 | 656 | 1. 유저의 show페이지를 수정하여 유저의 제일 첫 마이크로포스트를 표시하도록 해봅시다. 같은 파일 안에서 다른 코드로부터 문법을 추측해보세요. 제대로 되었는지 /users/1에 접속하여 확인해보세요. 657 | 2. `validates :content, length: { maximum: 140 }` 이 코드는 마이크로포스트의 Content가 존재하는지의 여부를 검증하는 validation입니다. 마이크로포스트가 아무것도 들어 있지 않을 때 검증되는지 실제로 확인해봅시다. (아래의 첫번째 캡쳐와 같이 된다면, 성공입니다.) 658 | 3. 아래에 `FILL_IN` 이라고 하는 부분을 수정하여, User모델의 name과 email이 존재하는 것을 검증해봅시다. 659 | 660 | ```ruby 661 | class Micropost < ApplicationRecord 662 | belongs_to :user 663 | validates :content, length: { maximum: 140 }, 664 | presence: true 665 | end 666 | ``` 667 | 668 | ![](../image/Chapter2/micropost_content_cant_be_blank.png) 669 | 670 | 671 | 672 | ```ruby 673 | class User < ApplicationRecord 674 | has_many :microposts 675 | validates FILL_IN, presence: true # 「FILL_IN」をコードに置き換えてください 676 | validates FILL_IN, presence: true # 「FILL_IN」をコードに置き換えてください 677 | end 678 | ``` 679 | 680 | 681 | 682 | ![](../image/Chapter2/user_presence_validations.png) 683 | 684 | ### 2.3.4 상속의 계층 685 | 686 | 마지막으로, Toy 어플리케이션에서 사용하고 있는 Rails의 컨트롤러와 모델의 클래스 계층에 대해 간단히 설명하겠습니다. 이번 섹션을 이해하기 위해서는, 조금이나마 오브젝트지향프로그래밍 (OOP) 의 경험이 필요합니다. (특히 *class* 에 대해 이해하고 있을 필요가 있습니다.) “아직 클래스를 몰라요” 라는 분들도 걱정하진 마세요. 4.4에서 클래스의 개념에 대해 설명해드리겠습니다. 여기서는 설명이 이해되지 않아도 큰 문제는 없습니다. 687 | 688 | 일단 모델의 상속구조에 대해 설명하겠습니다. 아래의 리스트를 비교해보면, User모델과 Micropost모델은 둘다 `ApplicationRecord` 라고 하는 클래스를 상속받고 있습니다. (Ruby에서는 상속관계를 `<` 로 표기합니다.) 또한 `ApplicationRecord` 클래스는 Active Record가 제공하는 기본 클래스인 `ActiveRecord::Base` 를 상속하고 있습니다. 아래의 그림에서는 이 클래스들의 관계를 정리하고 있습니다. `ActiveRecord::Base` 라고 하는 기본 클래스를 상속받는 것으로 인해, 작성한 모델 오브젝트는 데이터베이스에 접근할 수 있게 되고, 데이터베이스의 컬럼을 Ruby 속성처럼 다룰 수 있습니다. 689 | ```ruby 690 | class User < ApplicationRecord 691 | . 692 | . 693 | . 694 | end 695 | ``` 696 | 697 | ```ruby 698 | class Micropost < ApplicationRecord 699 | . 700 | . 701 | . 702 | end 703 | ``` 704 | 705 | 컨트롤러의 상속구조도, 모델의 상속구조도 본질적으로는 같습니다. 아래의 리스트를 비교하면 Users컨트롤러와 Microposts컨트롤러도 둘다 AplicationController를 상속받고 있습니다. 또한 `ApplicationController` 또한 `ActionController::Base` 라고 하는 클래스를 상속받는 것을 알 수 있습니다. 이 클래스는 Rails의 Action Pack 이라고 하는 라이브러리가 제공하는 컨트롤러의 기본 클래스 입니다. 706 | ```ruby 707 | class UsersController < ApplicationController 708 | . 709 | . 710 | . 711 | end 712 | ``` 713 | 714 | ```ruby 715 | class MicropostsController < ApplicationController 716 | . 717 | . 718 | . 719 | end 720 | ``` 721 | 722 | ```ruby 723 | class ApplicationController < ActionController::Base 724 | . 725 | . 726 | . 727 | end 728 | ``` 729 | 730 | ![](../image/Chapter2/demo_controller_inheritance.png) 731 | 732 | 733 | 734 | 모델의 상속관계와 마찬가지로, User컨트롤러와 Microposts컨트롤러는 결국에는 `ActionController::Base` 라고 하는 기본 클래스를 상속받고 있습니다. 그 때문에 어떤 컨트롤러라도 오브젝트의 조종이나 HTTP request의 필터링, 뷰를 HTML로 변환하여 출력하는 등의 다채로운 기능을 실행할 수 있습니다. Rails 컨트롤러는 반드시 `ApplicationController` 를 상속하고 있기 때문에, Application 컨트롤러에서 정의한 규칙은 어플리케이션의 모든 액션에 반영됩니다. 예를 들어 9.1에서는 로그인과 로그아웃용의 헬퍼 메소드를 Sample 어플리케이션의 모든 컨트롤러에서 사용할 수 있게 하고 있습니다. 735 | 736 | ##### 연습 737 | 738 | 1. Application컨트롤러 파일을 열고 `ApplicationController` 가 `ActionController::Base` 를 상속하고 있는 부분의 코드를 찾아보세요. 739 | 2. `ApplicationRecord` 가 `ActiveRecord::Base` 를 상속하는 코드는 어디에 있을까요? 찾아보세요. (힌트: 컨트롤러와 본질적으로 같은 구조이기 때문에 `app/models` 디렉토리안에 있는 파일을 찾아본다면…?) 740 | 741 | ### 2.3.5 어플리케이션을 배포해보자. 742 | 743 | micro posts 리소스의 설명이 끝났습니다. 여기서 레포지토리를 Bitbucket에 등록해봅시다. 744 | 745 | ```git 746 | $ git status 747 | $ git add -A 748 | $ git commit -m “Finish toy app” 749 | $ git push 750 | ``` 751 | 752 | Git 커밋은 되도록이면 자주 해주세요. 편집한 이력이 많이 쌓이지 않는 것이 바람직하나 본 장에서는 끝맺음의 뜻으로 변경이력이 제일 큰 커밋을 한 번 진행해주면, 문제는 없을 것입니다. 753 | 754 | 여기서 Toy 어플리케이션을 1.5와 같이 Heroku에 배포해주세요. 755 | `git push heroku` 756 | (위 커맨드는, 2.1에서 Heroku 어플리케이션을 만들었다는 것을 전제로 합니다. 어플리케이션을 만들지 않았다면, 먼저 `heroku create`, `git push heroku master` 를 실행한 후에 위 커맨드를 진행하세요.) 757 | 758 | 어플리케이션의 데이터베이스를 움직이게 하기 위해서는, 다음의 `heroku run` 커맨드를 실행하여 실제 배포환경의 데이터베이스의 마이그레이션을 할 필요가 있습니다. 759 | `heroku run rails db:migrate` 760 | 이 커맨드를 실행해보면, 아까 전 정의한 유저와 마이크로포스트의 데이터모델을 사용하여 Heroku의 데이터베이스가 업데이트됩니다. 마이그레이션이 완료되면, Toy 어플리케이션을 PostgreSQL 데이터베이스를 사용하는 실제 배포환경에서 이용할 수 있을 것입니다. 761 | 762 | 마지막으로 혹시 2.3.3.1의 연습문제를 했다면, *제일 처음의 유저의 마이크로포스트를 표시한다* 라는 코드를 삭제할 필요가 있으니 주의하세요. 삭제하는 걸 까먹은 경우라도, 당황하지말고 해당하는 코드를 삭제하고 다시 한 번 더 커밋 한 후에, Heroku에 push해주세요. 763 | 764 | ##### 연습 765 | 766 | 1. 실제 배포환경에서 2~3명의 유저를 작성해봅시다. 767 | 2. 실제 배포환경에서 제일 첫 번째 유저의 마이크로포스트를 작성해봅시다. 768 | 3. 마이크로포스트의 Content에 141 문자 이상을 입력한 상태로 작성해봅시다. validation이 실제 배포환경에서도 제대로 동작하는지를 확인해봅시다. 769 | 770 | 771 | 772 | ## 2.4 마지막으로 773 | 774 | 매우 간단했습니다만, Rails 어플리케이션을 제대로 완성해보았습니다. 이번 장에서 작성한 Toy 어플리케이션에 대해선 좋은 부분도 많습니다만 여러가지 안좋은 점도 있습니다. 775 | 776 | 좋은점 777 | 778 | - Rails의 전체를 매우 높은 레벨에서 둘러보았다. 779 | - MVC 모델을 확인해보았다. 780 | - REST 아키텍처에 대해 알 수 있게 되었다. 781 | - 데이터 모델을 작성해보았다. 782 | - 데이터베이스를 가진 실제 배포환경의 Web 어플리케이션을 움직여보았다. 783 | 784 | 안좋은 점 785 | 786 | - 레이아웃과 스타일이 하나도 설정되어 있지 않았다. 787 | - “Home”이나 “About” 과 같은 정석의 정적 페이지가 없다. 788 | - 유저가 패스워드 설정을 할 수 없다. 789 | - 유저가 영상이나 이미지를 올릴 수 없다. 790 | - 로그인 기능이 없다. 791 | - 보안 기능이 없다. 792 | - 유저와 마이크로포스트의 자동 관계짓기가 이루어지지 않았다. 793 | - Twitter와 같은 팔로우 기능이나 팔로잉 기능이 없다. 794 | - 마이크로포스트를 피드할 수 없다.(댓글을 달 수 없다) 795 | - 제대로 된 테스트를 하지 않았다. 796 | - **이해하기 어렵다.** 797 | 798 | 본 튜토리얼에서는 이후, Toy 어플리케이션의 좋은 점을 유지해가며 안좋은 점을 하나씩 극복해나갈 것입니다. 799 | 800 | ### 2.4.1 2장의 정리 801 | 802 | - Scaffold 기능에서 코드를 자동생성하면 Web의 어지간한 부분부터 모델데이터에 접근하여 조작까지 할 수 있다. 803 | 804 | - Scaffold 는 무엇보다도 간단하고 손쉽지만 이것을 바탕으로 Rails를 이해하기에는 적절하지 않다. 805 | 806 | - Rails에서는 Web 어플리케이션의 구성에 MVC (Model - View - Controller) 라고 하는 모델을 채용하고 있다. 807 | 808 | - Rails가 해석하는 REST에는 표준적인 URL 세트와 데이터모델을 조작하기 위한 컨트롤러액션이 포함되어 있다. 809 | 810 | - Rails는 데이터의 Validation(유효성) 검사 기능을 제공하고 있으며 데이터모델의 속성값을 제한하는 것이 가능하다. 811 | 812 | - Rails에서는 여러가지 데이터 모델간의 관계를 정의할 수 있으며, 그를 위한 많은 함수가 준비되어 있다. 813 | 814 | - Rails 콘솔을 사용하면, 커맨드 라인을 이용하여 Rails 어플리케이션을 조작할 수 있다. 815 | 816 | 817 | -------------------------------------------------------------------------------- /Documentation/Chapter12.md: -------------------------------------------------------------------------------- 1 | # 제 12장 패스워드의 재설정 2 | 3 | [제 11장](Chapter11.md) 에서 account의 유효화를 구현해보았습니다. 유저의 메일주소가 본인의 것이라는 확인도 했습니다. 이것으로 패스워드를 잊어버렸을 때의 *패스워드 재설정* 의 구현을 위한 준비도 되었습니다. 이번 챕터에서 확인할 내용의 대부분은, account 유효화에서 확인해온 내용들과 비슷합니다. 실제로 몇가지 구현은 이미 [11장](Chapter11.md) 에서 본 것들과 같은 흐름으로 개발해볼 것 입니다. 그렇다고는 해도 패스워드를 재설정하는 경우는 뷰를 한 가지 변경할 필요가 있으며 새로운 form이 추가로 2개 (메일 레이아웃용과 새로운 패스워드 송신용) 가 필요할 것 입니다. 4 | 5 | 6 | 7 | 코드를 실제로 작성하기 전에, 패스워드 재설정을 상정하는 순서를 목업으로 확인해봅시다. (=스크린샷을 수정한 모형) 우선 sample 어플리케이션의 로그인 form에 "Forgot password" 링크를 추가합니다. 이 "forgot password" 링크를 클릭하면 form이 표시되고 거기서 메일주소를 입력하여 메일을 발신하면, 해당 메일에 패스워드 재설정용의 링크가 기재되어 있습니다. 이 재설정용의 링크를 클릭하면 유저의 패스워드를 재설정해도 되는지 확인하는 form이 표시됩니다. 8 | 9 | ![](../image/Chapter12/login_forgot_password_mockup.png) 10 | 11 | ![](../image/Chapter12/forgot_password_form_mockup.png) 12 | 13 | ![](../image/Chapter12/reset_password_form_mockup.png) 14 | 15 | [제 11장](Chapter11.md) 을 진행하면, 패스워드재설정용의 메일러를 이미 생성했을 것입니다. ([11.2](Chapter11.md#112-account- 유효화의-메일-발신)) 이번 챕터에서는 11장에서 생성한 메일러에 리소스와 데이터 모델을 추가하여, 패스워드의 재설정을 구현해볼 것 입니다. 또한 실제로 구현은 [12.3](#123-패스워드를-재설정해보자)에서부터 진행해볼 것 입니다. 16 | 17 | 18 | 19 | account 유효화 구현할 떄와 많이 비슷합니다. PasswordResets 리소스를 생성하고, 재설정용의 토큰과 그것에 대응하는 Digest를 저장하는 것이 이번 챕터의 목적입니다. 전체적인 흐름은 다음과 같습니다. 20 | 21 | 1. 유저가 패스워드의 재설정을 리퀘스트하면, 유저가 입력한 메일주소를 Key로하여 데이터베이스로부터 유저를 검색한다. 22 | 2. 해당 메일주소가 데이터베이스에 존재하는 경우, 재설정용 토큰과 재설정용 Digest을 생성한다. 23 | 3. 재설정용 Digest는 데이터베이스에 저장해두고, 재설정용 토큰은 메일주소와 같이 유저에게 발신하는 유효화용 메일의 링크에 같이 보낸다. 24 | 4. 유저가 메일 링크를 클릭하면, 메일주소를 Key로 하여 유저를 검색하고, 데이터베이스 내의 저장한 재설정용 Digest와 비교한다 (토큰을 인증한다.) 25 | 5. 인증에 성공하면, 패스워드 변경용의 Form을 유저에게 표시한다. 26 | 27 | 28 | 29 | ## 12.1 PasswordResets Resource 30 | 31 | 세션([8.1](Chapter8.md#81-session)) 이나 Account 유효화([제 11장](Chapter11.md)) 때와 마찬가지로, 일단 PasswordResets 리소스의 모델링부터 시작해봅시다. 이번에도 새로운 모델은 만들지 않고, 대신에 필요한 데이터 (재설정용의 Digest등) 을 User모델에게 추가해나가는 형태로 진행해봅시다. 32 | 33 | 34 | 35 | PasswordResets도 리소스로서 다루어보고자합니다. 우선 표준적인 RESTful한 URL을 준비해봅시다. 유효화를 진행할 때는 `edit` 액션만을 다루었습니다만, 이번에는 패스워드를 재설정하는 Form이 필요하기 때문에, 뷰를 출력하기 위해 `new` 액션과 `edit` 액션이 필요합니다. 또한 각각의 액션에 대응하는 최종적인 RESTful한 라우팅이 필요합니다. 36 | 37 | 38 | 39 | 위 변경을 적용하기 전에, 언제나처럼 토픽 브랜치를 생성해봅시다. 40 | 41 | `$ git checkout -b password-reset` 42 | 43 | ### 12.1.1 PasswordResets 컨트롤러 44 | 45 | 준비가 되었으니, 제일 첫 번째 단계로는 패스워드 재설정용의 컨트롤러를 생성해봅시다. 아까 말씀드렸다시피 이번에는 뷰도 생성하기 때문에, `new` 액션과 `edit` 액션도 같이 생성하고 있는 점을 주의해주세요. 46 | 47 | `$ rails genenrate controller PasswordResets new edit --no-test-framework` 48 | 49 | 위 커맨드에서는 테스트를 생성하지 않는 옵션을 지정하고 있는 점을 확인해주세요. 이것은 컨트롤러의 UnitTest를 하는 대신에 [11.3.3](Chapter11.md#1133-유효화와-테스트의-Refactoring) 에서의 IntegrationTest 로 커버할 것 입니다. 50 | 51 | 52 | 53 | 또한 이번 구현에서는 새로운 패스워드를 재설정하기 위한 Form 과 User 모델 내부의 패스워드를 변경하기 위한 Form이 필요하기 때문에, `new`,`create`,`edit`,`update` 의 라우팅도 준비합시다. 이 변경은 저번과 마찬가지로 라우팅 파일의 `resource` 코드에서 구현할 수 있습니다. 54 | 55 | ```ruby 56 | # confign/routes.rb 57 | Rails.application.routes.draw do 58 | root 'static_pages#home' 59 | get '/help', to: 'static_pages#help' 60 | get '/about', to: 'static_pages#about' 61 | get '/contact', to: 'static_pages#contact' 62 | get '/signup', to: 'users#new' 63 | get '/login', to: 'sessions#new' 64 | post '/login', to: 'sessions#create' 65 | delete '/logout', to: 'sessions#destroy' 66 | resources :users 67 | resources :account_activations, only: [:edit] 68 | resources :password_resets, only: [:new, :create, :edit, :update] 69 | end 70 | ``` 71 | 72 | 위 코드는 RESTful의 라우팅을 따릅니다. 예를들어 "forgot password" form으로의 링크 생성 시에, 아래 표에 있는 named root를 사용합니다. 73 | 74 | `new_password_reset_path` 75 | 76 | | **HTTP Request** | **URL** | **Action** | **Named root** | 77 | | ---------------- | ----------------------------- | ---------- | -------------------------------- | 78 | | `GET` | /password_resets/new | `new` | `new_password_reset_path` | 79 | | `POST` | /password_resets | `create` | `password_resets_path` | 80 | | `GET` | /password_resets//edit | `edit` | `edit_password_reset_url(token)` | 81 | | `PATCH` | /password_resets/ | `update` | `password_reset_url(token)` | 82 | 83 | ```erb 84 | 85 | <% provide(:title, "Log in") %> 86 |

Log in

87 | 88 |
89 |
90 | <%= form_for(:session, url: login_path) do |f| %> 91 | 92 | <%= f.label :email %> 93 | <%= f.email_field :email, class: 'form-control' %> 94 | 95 | <%= f.label :password %> 96 | 97 | <%= link_to "(forgot password)", new_password_reset_path %> 98 | 99 | <%= f.password_field :password, class: 'form-control' %> 100 | 101 | <%= f.label :remember_me, class: "checkbox inline" do %> 102 | <%= f.check_box :remember_me %> 103 | Remember me on this computer 104 | <% end %> 105 | 106 | <%= f.submit "Log in", class: "btn btn-primary" %> 107 | <% end %> 108 | 109 |

New user? <%= link_to "Sign up now!", signup_path %>

110 |
111 |
112 | ``` 113 | 114 | ![](../image/Chapter12/forgot_password_link.png) 115 | 116 | ##### 연습 117 | 118 | 1. 이 시점에서 테스트 코드가 전부 통과하는 것을 확인해봅시다. 119 | 2. 위 표에서의 named root에서는, `_path` 가 아닌 `_url` 을 사용하도록 기재되어 있습니다. 왜일까요 생각해봅시다. *Hint*: Account 유효화에서 다루어보았던 연습에서의 이유과 같은 이유입니다. ([11.1.1](Chapter11.md#1111-accountactivations-controller)) 120 | 121 | ### 12.1.2 새로운 패스워드의 설정 122 | 123 | 패스워드 재설정의 데이터모델도, account 유효화의 경우와 닮아있습니다. remember 토큰 ([제 9장](Chapter9.md)) 이나 유효화 토큰 ([제 11장](Chapter11.md)) 의 구현 패턴처럼, 패스워드의 재설정도 토큰용의 가상의 속성과, 그것에 대응하는 Digest를 준비해봅시다. 만약 토큰을 해시화하지 않고 (즉 평문인 채로) 데이터베이스에 저장하면, 공격자가 데이터베이스를 공격하여 토큰을 빼내었을 때, 보안상의 문제가 생깁니다. 즉, 공격자가 유저의 메일 주소로 패스워드 재설정의 리퀘스트를 송신하고, 해당 메일과 훔쳐낸 토큰을 조합하여 공격자가 패스워드 재설정 링크로 접속하면, Account를 해킹할 수 있습니다. 따라서 패스워드의 재설정에는 반드시 Digest를 사용하도록 합시다. 보안상의 주의점은 1가지 더 있습니다. 그것은 재설정용의 링크는 되도록 단시간 (몇 시간 정도)의 시간제한을 설정해야만 합니다. 때문에 재설정 메일의 발신시간도 기록할 필요가 있습니다. 위의 배경을 기반으로하여 `reset_digest` 속성과 `reset_sent_at` 속성을 User 모델에 추가한 결과는 아래와 같습니다. 124 | 125 | ![](../image/Chapter12/user_model_password_reset.png) 126 | 127 | 다음 커맨드를 실행합시다. 128 | 129 | `$ rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime` 130 | 131 | 언제나처럼 마이그레이션을 실행해봅시다. 132 | 133 | `$ rails db:migrate` 134 | 135 | 새로운 패스워드 재설정의 화면을 생성하기 위해, 이전에 소개한 방법으로 해보도록 합시다. 즉, 새로운 세션을 생성하기 위해 로그인 Form을 사용해봅시다. 아래 코드를 확인해주세요. 136 | 137 | ```erb 138 | 139 | <% provide(:title, "Log in") %> 140 |

Log in

141 | 142 |
143 |
144 | <%= form_for(:session, url: login_path) do |f| %> 145 | 146 | <%= f.label :email %> 147 | <%= f.email_field :email, class: 'form-control' %> 148 | 149 | <%= f.label :password %> 150 | <%= f.password_field :password, class: 'form-control' %> 151 | 152 | <%= f.label :remember_me, class: "checkbox inline" do %> 153 | <%= f.check_box :remember_me %> 154 | Remember me on this computer 155 | <% end %> 156 | 157 | <%= f.submit "Log in", class: "btn btn-primary" %> 158 | <% end %> 159 | 160 |

New user? <%= link_to "Sign up now!", signup_path %>

161 |
162 |
163 | ``` 164 | 165 | 새로운 패스워드 재설정 Form은 위 코드와 매우 비슷합니다만, 중요한 차이점으로는 `form_for` 에서 다루는 소스와 URL 이 다른 점과, 패스워드 속성이 생략되어있는 점입니다. 변경을 반영한 결과는 아래와 같습니다. 166 | 167 | ```erb 168 | 169 | <% provide(:title, "Forgot password") %> 170 |

Forgot password

171 | 172 |
173 |
174 | <%= form_for(:password_reset, url: password_resets_path) do |f| %> 175 | <%= f.label :email %> 176 | <%= f.email_field :email, class: 'form-control' %> 177 | 178 | <%= f.submit "Submit", class: "btn btn-primary" %> 179 | <% end %> 180 |
181 |
182 | ``` 183 | 184 | ![](../image/Chapter12/forgot_password_form.png) 185 | 186 | ##### 연습 187 | 188 | 1. 위 코드에서 `form_for` 메소드에서는 어째서 `@password_reset` 이 아닌 `:password_reset` 을 사용하고 있는 걸까요? 생각해봅시다. 189 | 190 | ### 12.1.3 create action에서의 패스워드 재설정 191 | 192 | Form 에서 데이터를 송신한 다음, 해당 메일주소를 Key로하여 유저를 데이터베이스에서 검색합니다. 패스워드 재설정용 토큰과 메일 발신시의 타임스탬프에서 데이터베이스의 속성을 업데이트할 필요가 있습니다. 그 다음, 루트URL로 리다이렉트하여 플래시메세지를 유저에게 표시합니다. 발신이 무효한 경우는, 로그인때와 마찬가지로 `new` 페이지를 출력하고 `flash.now` 메세지를 표시합니다. 수정의 결과는 아래와 같습니다. 193 | 194 | ```ruby 195 | # app/controllers/password_resets_controller.rb 196 | class PasswordResetsController < ApplicationController 197 | 198 | def new 199 | end 200 | 201 | def create 202 | @user = User.find_by(email: params[:password_reset][:email].downcase) 203 | if @user 204 | @user.create_reset_digest 205 | @user.send_password_reset_email 206 | flash[:info] = "Email sent with password reset instructions" 207 | redirect_to root_url 208 | else 209 | flash.now[:danger] = "Email address not found" 210 | render 'new' 211 | end 212 | end 213 | 214 | def edit 215 | end 216 | end 217 | ``` 218 | 219 | User 모델 내부의 코드는 `before_create` 콜백 내부에서 사용되는 `create_activation_digest` 메소드와 비슷합니다. 220 | 221 | ```ruby 222 | # app/models/user.rb 223 | class User < ApplicationRecord 224 | attr_accessor :remember_token, :activation_token, :reset_token 225 | before_save :downcase_email 226 | before_create :create_activation_digest 227 | . 228 | . 229 | . 230 | # account를 유효화한다. 231 | def activate 232 | update_attribute(:activated, true) 233 | update_attribute(:activated_at, Time.zone.now) 234 | end 235 | 236 | # 유효화용 메일을 발신한다. 237 | def send_activation_email 238 | UserMailer.account_activation(self).deliver_now 239 | end 240 | 241 | # 패스워드 재설정용 속성을 설정한다. 242 | def create_reset_digest 243 | self.reset_token = User.new_token 244 | update_attribute(:reset_digest, User.digest(reset_token)) 245 | update_attribute(:reset_sent_at, Time.zone.now) 246 | end 247 | 248 | # 패스워드 재설정용 메일을 발신한다. 249 | def send_password_reset_email 250 | UserMailer.password_reset(self).deliver_now 251 | end 252 | 253 | private 254 | 255 | # 메일 주소를 소문자화한다. 256 | def downcase_email 257 | self.email = email.downcase 258 | end 259 | 260 | # 유효화 토큰과 Digest를 생성 및 대입한다. 261 | def create_activation_digest 262 | self.activation_token = User.new_token 263 | self.activation_digest = User.digest(activation_token) 264 | end 265 | end 266 | ``` 267 | 268 | 아래 스크린샷처럼 이 시점에서의 어플리케이션은 무효한 메일주소를 입력한 경우에도 정상적으로 동작합니다. 올바른 메일 주소를 입력한 경우에도 어플리케이션은 정상적으로 동작하기 위해서는 패스워드 재설정의 메일러 메소드를 정의할 필요가 있습니다. 269 | 270 | ![](../image/Chapter12/invalid_email_password_reset.png) 271 | 272 | ##### 연습 273 | 274 | 1. 시험삼아 유효한 메일주소를 Form에 입력해봅시다. 어떠한 에러 메세지가 표시됩니까? 275 | 2. 콘솔에서, 앞서 연습문제에서 입력한 결과 (에러로 표시된 것)에 해당하는 user 오브젝트에는 `reset_digest` 와 `reset_sent_at` 이 있는 것을 확인해봅시다. 각각의 값은 어떻게 되어있나요? 276 | 277 | 278 | 279 | ## 12.2 패스워드 재설정의 메일 발신 280 | 281 | [12.1](#121-passwordresets-리소스) 의 PasswordResets 컨트롤러에서 `create` 액션이 거의 동작할 수 있는 상태까지 만들어보았습니다. 남은 부분은 패스워드 재설정에 관한 메일을 발신하는 부분입니다. 282 | 283 | 284 | 285 | 이미 [11.1](Chapter11.md#111-AccountActivations_Resource) 를 했다면, User mailer (`app/mailers/user_mailer.rb`) 를 생성했을 때 디폴트로 `password_reset` 메소드도 같이 생성했을 것 입니다. 만약 [제 11장](Chapter11.md) 을 건너뛰었다면, 아래 커맨드를 실행시켜서 필요한 파일을 생성해주세요. (`account_activation` 에 관한 메소드는 생성하지 않아도 괜찮습니다.) 286 | 287 | ``` 288 | $ rails generate mailer UserMailer account_activation password_reset 289 | ``` 290 | 291 | ### 12.2.1 패스워드 재설정의 메일과 Template 292 | 293 | [11.3.3](Chapter11.md#1133-유효화와-테스트의-Refactoring) 에서는 User 메일러에 있는 코드를 User 모델로 옮기는 Refactoring을 하였습니다. 비슷한 Refactoring 작업을 패스워드 재설정에 대해서도 해보겠습니다. 294 | 295 | `UserMailer.password_reset(self).deliver_now` 296 | 297 | 위 코드의 구현에 필요한 메소드는, [11.2](Chapter11.md#112-Account-유효화의-메일-발신) 에서 구현한 Account 유효화용 메소드와 거의 같습니다. 제일 처음으로 User 메일러에 `password_reset` 메소드를 작성하고, 이어서 텍스트 메일의 템플릿과 HTML 메일의 템플릿을 각각 정의합니다. 298 | 299 | ```ruby 300 | class UserMailer < ApplicationMailer 301 | 302 | def account_activation(user) 303 | @user = user 304 | mail to: user.email, subject: "Account activation" 305 | end 306 | 307 | # 수정 308 | def password_reset(user) 309 | @user = user 310 | mail to: user.email, subject: "Password reset" 311 | end 312 | end 313 | ``` 314 | 315 | ```erb 316 | 317 | To reset your password click the link below: 318 | 319 | <%= edit_password_reset_url(@user.reset_token, email: @user.email) %> 320 | 321 | This link will expire in two hours. 322 | 323 | If you did not request your password to be reset, please ignore this email and 324 | your password will stay as it is. 325 | ``` 326 | 327 | ```erb 328 | 329 |

Password reset

330 | 331 |

To reset your password click the link below:

332 | 333 | <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, 334 | email: @user.email) %> 335 | 336 |

This link will expire in two hours.

337 | 338 |

339 | If you did not request your password to be reset, please ignore this email and 340 | your password will stay as it is. 341 |

342 | ``` 343 | 344 | Account 유효화 메일의 경우 ([11.2](Chapter11.md#112-Account-유효화의-메일-발신)) 와 마찬가지로, Rails의 메일 프리뷰 기능으로 패스워드 재설정의 메일을 프리뷰해봅시다. 프리뷰를 코드는 이전 11장에서 봤던 코드와 똑같습니다. 345 | 346 | ```ruby 347 | # Preview all emails at http://localhost:3000/rails/mailers/user_mailer 348 | class UserMailerPreview < ActionMailer::Preview 349 | 350 | # Preview this email at 351 | # http://localhost:3000/rails/mailers/user_mailer/account_activation 352 | def account_activation 353 | user = User.first 354 | user.activation_token = User.new_token 355 | UserMailer.account_activation(user) 356 | end 357 | 358 | # Preview this email at 359 | # http://localhost:3000/rails/mailers/user_mailer/password_reset 360 | #추가 361 | def password_reset 362 | user = User.first 363 | user.reset_token = User.new_token 364 | UserMailer.password_reset(user) 365 | end 366 | end 367 | ``` 368 | 369 | 위 코드로 인하여 HTML메일과 텍스트 메일을 각각 프리뷰할 수 있게 되었습니다. 370 | 371 | ![](../image/Chapter12/password_reset_html_preview_4th_ed.png) 372 | 373 | ![](../image/Chapter12/password_reset_text_preview_4th_ed.png) 374 | 375 | 올바른 메일 주소로 발신했을 떄의 화면은 아래와 같으며, 해당 메일은 서버 로그에서는 아래 두 번째 내용와 같습니다. 376 | 377 | ![](../image/Chapter12/valid_email_password_reset.png) 378 | 379 | ``` 380 | Sent mail to michael@michaelhartl.com (66.8ms) 381 | Date: Mon, 06 Jun 2016 22:00:41 +0000 382 | From: noreply@example.com 383 | To: michael@michaelhartl.com 384 | Message-ID: <8722b257d04576a@mhartl-rails-tutorial-953753.mail> 385 | Subject: Password reset 386 | Mime-Version: 1.0 387 | Content-Type: multipart/alternative; 388 | boundary="--==_mimepart_5407babbe3505_8722b257d045617"; 389 | charset=UTF-8 390 | Content-Transfer-Encoding: 7bit 391 | 392 | 393 | ----==_mimepart_5407babbe3505_8722b257d045617 394 | Content-Type: text/plain; 395 | charset=UTF-8 396 | Content-Transfer-Encoding: 7bit 397 | 398 | To reset your password click the link below: 399 | 400 | https://rails-tutorial-mhartl.c9users.io/password_resets/3BdBrXe 401 | QZSWqFIDRN8cxHA/edit?email=michael%40michaelhartl.com 402 | 403 | This link will expire in two hours. 404 | 405 | If you did not request your password to be reset, please ignore 406 | this email and your password will stay as it is. 407 | ----==_mimepart_5407babbe3505_8722b257d045617 408 | Content-Type: text/html; 409 | charset=UTF-8 410 | Content-Transfer-Encoding: 7bit 411 | 412 |

Password reset

413 | 414 |

To reset your password click the link below:

415 | 416 | Reset password 419 | 420 |

This link will expire in two hours.

421 | 422 |

423 | If you did not request your password to be reset, please ignore 424 | this email and your password will stay as it is. 425 |

426 | ----==_mimepart_5407babbe3505_8722b257d045617-- 427 | ``` 428 | 429 | ##### 연습 430 | 431 | 1. 브라우저로부터 발신되는 메일의 프리뷰를 확인해봅시다. "Date" 란의 정보는 어떤가요? 432 | 2. 패스워드 재설정 Form에서 유효한 메일 주소로 발신해봅시다. 또한, Rails 서버의 로그를 보고 생성된 발신 메일의 내용을 확인해봅시다. 433 | 3. 콘솔에서 방금 전의 연습문제에서의 패스워드 재설정을 한 User 오브젝트를 찾아주세요. 오브젝트를 찾는다면 해당 오브젝트가 가지고 있는 `reset_digest` 와 `reset_sent_at` 의 값을 확인해보세요. 434 | 435 | ### 12.2.2 발신 메일의 테스트 436 | 437 | Account 유효화의 테스트와 마찬가지로 메일러 메소드의 테스트 코드를 작성해봅시다. 438 | 439 | ```ruby 440 | # test/mailers/user_mailer_test.rb 441 | require 'test_helper' 442 | 443 | class UserMailerTest < ActionMailer::TestCase 444 | 445 | test "account_activation" do 446 | user = users(:michael) 447 | user.activation_token = User.new_token 448 | mail = UserMailer.account_activation(user) 449 | assert_equal "Account activation", mail.subject 450 | assert_equal [user.email], mail.to 451 | assert_equal ["noreply@example.com"], mail.from 452 | assert_match user.name, mail.body.encoded 453 | assert_match user.activation_token, mail.body.encoded 454 | assert_match CGI.escape(user.email), mail.body.encoded 455 | end 456 | 457 | test "password_reset" do 458 | user = users(:michael) 459 | user.reset_token = User.new_token #추가 460 | mail = UserMailer.password_reset(user) 461 | assert_equal "Password reset", mail.subject 462 | assert_equal [user.email], mail.to 463 | assert_equal ["noreply@example.com"], mail.from 464 | assert_match user.reset_token, mail.body.encoded 465 | assert_match CGI.escape(user.email), mail.body.encoded 466 | end 467 | end 468 | ``` 469 | 470 | 이 코드는 테스트가 통과될 것 입니다. 471 | 472 | `$ rails test` 473 | 474 | ##### 연습 475 | 476 | 1. 메일러의 테스트만 실행해봅시다. 이 테스트는 통과되나요? 477 | 2. 2번째 `CGI.escape` 를 삭제하면, 테스트가 실패되는 것을 확인해봅시다. 478 | 479 | 480 | 481 | ## 12.3 패스워드를 재설정해보자 482 | 483 | 무사히 발신 메일을 생성할 수 있게 되었습니다. 다음으로는 PasswordResets 컨트롤러의 `edit` 액션을 구현해봅시다. 또한 [11.3.3](Chapter11.md#1133-유효화와-테스트의-Refactoring) 과 마찬가지로 결합테스트를 사용하여 제대로 동작하고 있는지를 테스트해봅시다. 484 | 485 | ### 12.3.1 edit action에서 재설정 486 | 487 | 패스워드 재설정의 발신 메일에는 다음과 같은 링크가 포함되어있을 것 입니다. 488 | 489 | `https://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=fu%40bar.com` 490 | 491 | 이 링크를 동작하게 하기 위해서는, 패스워드 재설정 Form을 표시할 뷰가 필요합니다. 이 뷰는 유저의 정보 수정 Form과 비슷합니다만, 이번에는 패스워드 입력 필드와 확인용 필드만 있으면 충분합니다. 492 | 493 | 494 | 495 | 단, 이번 작업은 조금 귀찮은 부분이 있습니다. 왜냐하면 메일주소를 Key로하여 유저를 검색하기 위해서는, `edit` 와 `update` 액션 양쪽에 메일주소가 필요하기 때문입니다. 위 예시와 같은 메일주소가 들어있는 링크 덕분에, `edit` 액션에서 메일주소를 얻는 것은 문제가 없습니다. 그러나 Form의 정보를 한 번 송신해버리면, 이 정보는 삭제되어버리고 맙니다. 해당 값을 어디에 저장해야 좋을까요? 이번에는 이 메일 주소를 저장하기 위해 `hidden field` 를 사용하여 페이지 내부에 저장하는 방법을 사용합니다. 이것으로 Form으로 부터 정보가 송신되었을 때, 다른 정보와 같이 메일주소가 송신되도록 해볼 것 입니다. 실제 코드는 아래와 같습니다. 496 | 497 | ```erb 498 | 499 | <% provide(:title, 'Reset password') %> 500 |

Reset password

501 | 502 |
503 |
504 | <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %> 505 | <%= render 'shared/error_messages' %> 506 | 507 | <%= hidden_field_tag :email, @user.email %> 508 | 509 | <%= f.label :password %> 510 | <%= f.password_field :password, class: 'form-control' %> 511 | 512 | <%= f.label :password_confirmation, "Confirmation" %> 513 | <%= f.password_field :password_confirmation, class: 'form-control' %> 514 | 515 | <%= f.submit "Update password", class: "btn btn-primary" %> 516 | <% end %> 517 |
518 |
519 | ``` 520 | 521 | 위 코드에서 Form 태그 헬퍼를 사용하고 있는 점을 주의해주세요. 522 | 523 | `hidden_field_tag :email, @user.email` 524 | 525 | 지금까지는 다음과 같은 코드를 작성했습니다만, 이번에는 코드가 조금 다릅니다. 526 | 527 | `f.hidden_field :email, @user.email` 528 | 529 | 이것은 재설정용 링크를 클릭하면, 전자 (`hidden_field_tag`) 에서는 메일주소가 `params[:email]` 이 저장되지만, 후자는 `params[:user][:email]` 이 저장되기 때문입니다. 530 | 531 | 532 | 533 | 이번에는 이 Form을 표시하기 위해 PasswordResets 컨트롤러의 `edit` 액션 내부의 `@user` 인스턴스 변수를 정의해보겠습니다. Account 유효화의 경우와 마찬가지로, `params[:email]` 의 메일주소에 대응하는 유저를 해당 변수에 저장합니다. 이어서 `params[:id]` 의 재설정용 토큰과, 이전 11장에서 추상화시킨 `authenticated?` 메소드를 사용하여, 이 유저가 정당한 유저인지 (유저가 존재하고, 유효화 되어있고, 인증이 끝난) 확인합니다. `edit` 액션과 `update` 액션 어느쪽이던 정당한 `@user` 가 존재할 필요가 있기 때문에, 몇개의 before 필터를 사용하여 `@user` 의 검색과 validation을 진행합니다. 534 | 535 | ```ruby 536 | # app/controllers/password_resets_controller.rb 537 | class PasswordResetsController < ApplicationController 538 | before_action :get_user, only: [:edit, :update] 539 | before_action :valid_user, only: [:edit, :update] 540 | . 541 | . 542 | . 543 | def edit 544 | end 545 | 546 | private 547 | 548 | def get_user 549 | @user = User.find_by(email: params[:email]) 550 | end 551 | 552 | # 올바른 유저인지 확인한다. 553 | def valid_user 554 | unless (@user && @user.activated? && 555 | @user.authenticated?(:reset, params[:id])) 556 | redirect_to root_url 557 | end 558 | end 559 | end 560 | ``` 561 | 562 | 여기서는 다음 코드를 사용하고 있습니다. 563 | 564 | `authenticated?(:reset, params[:id])` 565 | 566 | 위 코드를 아래 코드와 비교해봅시다. 567 | 568 | `authenticated?(:remember, cookies[:remember_token])` 569 | 570 | 11장에서 쓰인 코드는 아래와 같습니다. 571 | 572 | `authenticated?(:activation, params[:id])` 573 | 574 | 위 코드가 인증 메소드이며, 이번에 추가한 코드에서 모든 구현이 끝난 것을 의미하기도 합니다. 575 | 576 | 577 | 578 | 이것으로 메일의 링크를 클릭하면 패스워드 재설정의 Form이 출력되게 됩니다. 결과는 아래와 같습니다. 579 | 580 | ![](../image/Chapter12/password_reset_form.png) 581 | 582 | ##### 연습 583 | 584 | 1. [12.2.1](#1221-패스워드-재설정의-메일과-템플릿) 의 연습문제에서의 순서를 따라서, Rails의 서버의 로그로부터 발신 메일을 찾아내고, 로그에 기재되어있는 링크를 확인해주세요. 링크를 브라우저에서 열어 확인하면, 위 화면이 출력되는지 확인해봅시다. 585 | 2. 앞서 표시한 페이지에서, 실제로 새로운 패스워드를입력해봅시다. 어떻게 되나요? 586 | 587 | ### 12.3.2 패스워드를 수정해보자 588 | 589 | AccountActivation 컨트롤러의 `edit` 액션에서는 유저의 유효화 상태를 `false` 에서 `true` 로 변경하였습니다. 이번경우에는 Form에서 새로운 패스워드를 입력받아야합니다. 따라서 Form에서 전송되는 정보를 받아들일 `update` 액션이 필요합니다. 이 `update` 액션에서는 다음 4개의 케이스를 고려할 필요가 있습니다. 590 | 591 | 1. 패스워드 재설정의 유효기간이 남아있는가 592 | 2. 무효한 패스워드라면 실패해야한다. (실패하는 이유도 출력) 593 | 3. 새로운 패스워드가 빈 문자열인가 (유저 정보 수정에서는 OK이었습니다.) 594 | 4. 올바른 패스워드라면 갱신한다. 595 | 596 | 1과 2와 4는 지금까지의 지식이라면 할만합니다만, 3은 어떻게 대응하면 좋을지 좋은 수가 떠오르지 않습니다. 일단 위 케이스를 하나씩 대응해보도록 합시다. 597 | 598 | 599 | 600 | 1에 대해서는 `edit` 와 `update` 액션에 다음과 같은 메소드와 before 필터를 입력하는 것으로 대응할 수 있을 것 같습니다. 601 | 602 | `before_action :check_expiration, only: [:edit, :update] # 1번의 대응책` 603 | 604 | 이 `check_expiration` 메소드는 유효기한을 체크하는 Private 메소드로서 정의합니다. 605 | 606 | ```ruby 607 | # 유효기한이 다 되었는지 확인한다. 608 | def check_expiration 609 | if @user.password_reset_expired? 610 | flash[:danger] = "Password reset has expired." 611 | redirect_to new_password_reset_url 612 | end 613 | end 614 | ``` 615 | 616 | 위 `check_expiration` 메소드에서는 유효기간의 상태를 확인하는 인스턴스 메소드 `password_reset_expired?` 를 사용하고 있습니다. 이 새로운 메소드에 대해서는 나중에 설명하도록 하겠습니다. 지금은 위 4가지 케이스에 대해서 우선적으로 생각해봅시다.( 실행결과는 추후 확인합니다.) 617 | 618 | 619 | 620 | 우선, 위 before필터에서 보호하고 있는 `update` 액션을 사용하는 것으로 2번과 4번 케이스에 대응할 수 있을 것 같습니다. 예를 들어 2번에 대해서는 패스워드 업데이트에 실패했을 때에, `edit` 뷰가 다시 표시되고, 파셜에 에러메세지를 표시하도록 하게하면 해결될 것 입니다. 4번에 대해서는, 패스워드 업데이트에 성공했을 때, 패스워드를 재설정하고, 그 이후는 로그인에 성공했을 때와 똑같은 처리를 하게하면 문제는 없을 것 같습니다. 621 | 622 | 623 | 624 | 지금 조금 어려운 문제점으로는, 패스워드가 빈 문자일 때의 처리입니다. 이전 User모델을 만들 때에, 패스워드가 비어있어도 좋다(10장에서의 `allow_nil`) 는 구현을 했기 때문입니다. 따라서 이 케이스에 대해서는 명시적으로 캐치하는 코드를 추가할 필요가 있습니다. 이것이 앞서 말한 고려해야할 점의 3번에 해당합니다. 이것을 해결하는 방법으로는, 이번에는 `@user` 오브젝트에 에러메세지를 추가하는 방법을 해보겠습니다. 구체적으로는 다음과 같이 `errors.add` 를 사용하여 에러메세지를 추가해보겠습니다. 625 | 626 | `@user.errors.add(:password, :blank)` 627 | 628 | 이렇게 작성하면, 패스워드가 빈 문자열일 때, 빈 문자열에 대하는 디폴트 메세지를 출력하게 될 것 입니다. 629 | 630 | 631 | 632 | 위 결과를 정리하면, 1번의 `password_reset_expired?` 의 구현을 제외하면, 모든 케이스에 대응하는 `update` 액션이 완성됩니다. 633 | 634 | ```ruby 635 | # app/controllers/password_resets_controller.rb 636 | class PasswordResetsController < ApplicationController 637 | before_action :get_user, only: [:edit, :update] 638 | before_action :valid_user, only: [:edit, :update] 639 | before_action :check_expiration, only: [:edit, :update] # 문제 1의 대응 640 | 641 | def new 642 | end 643 | 644 | def create 645 | @user = User.find_by(email: params[:password_reset][:email].downcase) 646 | if @user 647 | @user.create_reset_digest 648 | @user.send_password_reset_email 649 | flash[:info] = "Email sent with password reset instructions" 650 | redirect_to root_url 651 | else 652 | flash.now[:danger] = "Email address not found" 653 | render 'new' 654 | end 655 | end 656 | 657 | def edit 658 | end 659 | 660 | def update 661 | if params[:user][:password].empty? # 문제 3의 대응 662 | @user.errors.add(:password, :blank) 663 | render 'edit' 664 | elsif @user.update_attributes(user_params) # 문제 4의 대응 665 | log_in @user 666 | flash[:success] = "Password has been reset." 667 | redirect_to @user 668 | else 669 | render 'edit' # 문제 2의 대응 670 | end 671 | end 672 | 673 | private 674 | 675 | def user_params 676 | params.require(:user).permit(:password, :password_confirmation) 677 | end 678 | 679 | # before 필터 680 | 681 | def get_user 682 | @user = User.find_by(email: params[:email]) 683 | end 684 | 685 | # 유효한 유저인지 확인한다. 686 | def valid_user 687 | unless (@user && @user.activated? && 688 | @user.authenticated?(:reset, params[:id])) 689 | redirect_to root_url 690 | end 691 | end 692 | 693 | # 토큰이 유효기한 확인 694 | def check_expiration 695 | if @user.password_reset_expired? 696 | flash[:danger] = "Password reset has expired." 697 | redirect_to new_password_reset_url 698 | end 699 | end 700 | end 701 | ``` 702 | 703 | (위 코드는 [7.3.2](Chapter7.md#732-strong-parameter) 를 구현할 떄와 마찬가지로, `user_params`메소드를 사용하여 `password` 와 `password_confirmaion` 속성을 확인하고 있습니다. 704 | 705 | 706 | 707 | 이 다음으로는, 위 코드의 남아있는 부분을 구현하면 됩니다. 이번에는 User 모델에다가 코드를 작성하는 것을 전제로하고 다음 코드를 작성해봅시다. 708 | 709 | `@user.password_reset_expired?` 710 | 711 | 위 코드를 동작시키게 하기 위해서는 `password_reset_expired?` 메소드를 User 모델에서 정의해봅시다. [12.2.1](#1221-패스워드-재설정의-메일과-템플릿) 을 참고하여, 이 메소드에서는 패스워드 재설정의 기한을 설정하고, 2시간 이상 패스워드가 재설정되지 않은 경우에는 유효기한이 지났다고 판단하는 처리를 구현해봅시다. 이것을 Ruby로 표현하면 다음과 같이 됩니다. 712 | 713 | ``` 714 | reset_sent_at < 2.hours.ago 715 | ``` 716 | 717 | 위 기호를 "~보다 작은" 이라고 읽어버리면, "패스워드 재설정 메일 발신시로부터 경과한 시간이, 2시간보다 작은 경우" 가 되어버려 곤란해질지도 모릅니다. 여기서의 처리는 "직은" 이 아닌 "빠른, 이른" 이라고 이해해야합니다. 즉, `<` 기호를 "~보다 빠른 시간" 이라고 이해해주세요. 이렇게하면 "패스워드 재설정 메일의 발신시간이, 현재 시간보다 2시간 이상 전" 이 됩니다. 이렇게하면 *기대 했던 대로의 조건* 이 됩니다. 따라서 이 조건을 만족하는지 어떤지를 확인하는 `password_reset_expired?` 메소드는 아래와 같이 됩니다. ( 이 비교는 12.6에 부록으로 추가해놓았습니다.) 718 | 719 | ```ruby 720 | # app/models/user.rb 721 | class User < ApplicationRecord 722 | . 723 | . 724 | . 725 | # 패스워드 재설정의 유효기한을 확인하여, 유효기한이 지나있으면 true를 리턴. 726 | def password_reset_expired? 727 | reset_sent_at < 2.hours.ago 728 | end 729 | 730 | private 731 | . 732 | . 733 | . 734 | end 735 | ``` 736 | 737 | 위 코드를 사용하면, `update` 액션이 동작하게됩니다. 발신이 무효한 경우와 유효한 경우의 화면은 각각 아래 캡쳐와 같습니다. (확인을 위해 2시간을 기다릴 순 없으니, 테스트는 한 가지 더 분기를 추가해봅니다만, 이것은 [12.3.3](#1233-패스워드를-재설정을-테스트해보자)의 연습문제로 돌립니다.) 738 | 739 | ![](../image/Chapter12/password_reset_failure_4th_ed.png) 740 | 741 | ![](../image/Chapter12/password_reset_success_4th_ed.png) 742 | 743 | ##### 연습 744 | 745 | 1. [12.2.1](#1221-패스워드-재설정의-메일과-템플릿) 에서 얻은 링크(Rails 서버의 로그에서 확인한) 를 브라우저에서 표시하고, password와 confirmation의 문자열을 일부러 다르게 해봅시다. 어떠한 에러 메세지가 표시됩니까? 746 | 2. 콘솔을 기동시킵니다. 패스워드 재설정 정보를 발신한 유저 오브젝트를 확인해주세요. 확인했다면 해당 오브젝트의 `password_digest` 의 값을 확인해봅시다. 그 다음으로 패스워드 재설정 Form으로부터 유효한 패스워드를 입력해봅시다. 패스워드 재설정이 성공한다면, 다시 `password_digest` 의 값을 확인하고 값이 다르게 변했는지 확인해봅시다. *Hint* : `user.reload`를 이용하여 확인해봅시다. 747 | 748 | ### 12.3.3 패스워드 재설정을 테스트해보자 749 | 750 | 이번 섹션에서는, `password_resets_controller.rb` 의 2가지 (혹은 3가지)의 분기, 즉 발신에 성공했을 때와 실패했을 떄의 결합테스트 코트를 작성해볼 것 입니다. (앞서 말씀드린대로, 3번째 분기에 대해서는 연습문제로 내겠습니다.) 우선 패스워드 재설정의 테스트파일을 생성해보겠습니다. 751 | 752 | ``` 753 | $ rails generate integration_test password_resets 754 | invoke test_unit 755 | create test/integration/password_resets_test.rb 756 | ``` 757 | 758 | 패스워드 재설정을 테스트해보는 순서는, account 유효화의 테스트와 많은 공통사항이 있습니다만 테스트 첫 시작부분에는 다음과 같은 차이점이 있습니다. 맨 처음에, "forgot password" Form 을 표시하여 무효한 메일 주소를 입력하고, 그 다음으로는 해당 Form에서 유효한 메일 주소를 입력합니다. 후자에서는 패스워드 재설정용 토큰이 생성되고, 재설정용 메일이 발신됩니다. 이어서 메일의 링크를 열어서 무효한 정보를 송신하고, 그 다음으로 해당 링크에서 유효한 정보를 송신하여 각각이 기대했던 대로 동작하는 지를 확인합니다. 작성한 테스트 코드는 아래와 같습니다. 이 테스트는 코드리딩의 좋은 예시가 될 것입니다. 잘 읽어주세요. 759 | 760 | ```ruby 761 | # test/integration/password_resets_test.rb 762 | require 'test_helper' 763 | 764 | class PasswordResetsTest < ActionDispatch::IntegrationTest 765 | 766 | def setup 767 | ActionMailer::Base.deliveries.clear 768 | @user = users(:michael) 769 | end 770 | 771 | test "password resets" do 772 | get new_password_reset_path 773 | assert_template 'password_resets/new' 774 | # 무효한 메일 주소 775 | post password_resets_path, params: { password_reset: { email: "" } } 776 | assert_not flash.empty? 777 | assert_template 'password_resets/new' 778 | # 유효한 메일 주소 779 | post password_resets_path, 780 | params: { password_reset: { email: @user.email } } 781 | assert_not_equal @user.reset_digest, @user.reload.reset_digest 782 | assert_equal 1, ActionMailer::Base.deliveries.size 783 | assert_not flash.empty? 784 | assert_redirected_to root_url 785 | # 패스워드 재설정용 Form 테스트 786 | user = assigns(:user) 787 | # 무효한 메일 주소 788 | get edit_password_reset_path(user.reset_token, email: "") 789 | assert_redirected_to root_url 790 | # 무효한 유저 791 | user.toggle!(:activated) 792 | get edit_password_reset_path(user.reset_token, email: user.email) 793 | assert_redirected_to root_url 794 | user.toggle!(:activated) 795 | # 메일주소는 유효하지만, 토큰이 무효한 경우 796 | get edit_password_reset_path('wrong token', email: user.email) 797 | assert_redirected_to root_url 798 | # 메일주소도 토큰도 유효한 경우 799 | get edit_password_reset_path(user.reset_token, email: user.email) 800 | assert_template 'password_resets/edit' 801 | assert_select "input[name=email][type=hidden][value=?]", user.email 802 | # 무효한 패스워드와 패스워드 확인 803 | patch password_reset_path(user.reset_token), 804 | params: { email: user.email, 805 | user: { password: "foobaz", 806 | password_confirmation: "barquux" } } 807 | assert_select 'div#error_explanation' 808 | # 패스워드가 비어있는 상태일때 809 | patch password_reset_path(user.reset_token), 810 | params: { email: user.email, 811 | user: { password: "", 812 | password_confirmation: "" } } 813 | assert_select 'div#error_explanation' 814 | # 유효한 패스워드와 패스워드 확인 815 | patch password_reset_path(user.reset_token), 816 | params: { email: user.email, 817 | user: { password: "foobaz", 818 | password_confirmation: "foobaz" } } 819 | assert is_logged_in? 820 | assert_not flash.empty? 821 | assert_redirected_to user 822 | end 823 | end 824 | ``` 825 | 826 | 위 코드를 사용하는 아이디어의 대부분은, 본 튜토리얼에서 이미 나왔던 적이 있습니다. 새로운 요소로는 `input` 요소 정도입니다. 827 | 828 | ``` 829 | assert_select "input[name=email][type=hidden][value=?]", user.email 830 | ``` 831 | 832 | 위 코드는 `input` 태그의 올바른 이름, type="hidden", 메일주소가 있는지 없는지를 확인합니다. 833 | 834 | ```html 835 | 836 | ``` 837 | 838 | 테스트는 통과할 것 입니다. 839 | 840 | `$ rails test` 841 | 842 | ##### 연습 843 | 844 | 1. `create_reset_digest` 메소드는 `update_attribute` 를 2번 호출하고 있습니다만, 이것은 각각의 라인에서 한 번씩 데이터베이스로 조회를 하고 있습니다. 아래 첫 번째 코드를 사용하여 `update_attribute` 의 호출을 1번의 `update_columns` 호출로 바꾸어보세요. (이것으로 데이터베이스로의 조회가 한 번으로 줄어들 것 입니다.) 또한 변경 후에 테스트를 실행하여 테스트 통과가되는 것을 확인해주세요. 여담으로, 아래 첫 번째 코드는 11장의 연습문제의 해답도 포함되어 있습니다. 845 | 2. 아래 두 번째 코드의 빈칸을 메꾸어서 유효기간이 초과된 패스워드 재설정 처리에서 발생하는 분기를 결합테스트에서 확인해봅시다. (아래 두 번째 코드에 있는 `response.body` 는 해당 페이지의 HTML 본문을 모두 리턴하는 메소드입니다.) 유효기간이 지난 것을 테스트하는 방법은 몇가지가 있습니다만, 두 번째 코드에서 추천하는 방법을 사용해보면, response의 본문에 "expired" 라고 하는 단어가 있는지 없는지를 체크하는 것 입니다. (또한, 대소문자는 구별하지 않습니다.) 846 | 3. 2시간이 지나면 패스워드를 재설정하지 못하게 하는 방침은, 보안상으로는 매우 바람직할 것 입니다. 그러나 좀 더 좋게하는 방법은 따로 있습니다. 예를들어 공유된 컴퓨터에서 패스워드 재설정을 한 경우를 생각해보세요. 로그아웃하고 자리를 떠났다고 하더라도, 2시간 이내라면 해당 컴퓨터의 이력으로부터 패스워드 재설정용 Form을 표시시킬 수 있고, 패스워드를 갱신할 수도 있습니다. (게다가 그대로 로그인 기능까지 돌파해버립니다.) 이 문제를 해결하기 위해, 아래 세 번째 코드를 추가하고, 패스워드의 재설정에 성공하면 digest를 `nil` 로 변경하는 변경을 해봅시다. 847 | 4. 위 테스트코드에 한 줄을 추가하여, 연습문제에 대한 테스트를 작성해봅시다. *Hint* : `assert_nil` 메소드와 `user.reload` 메소드를 조합하여, `reset_digest` 속성을 직접 테스트해봅시다. 848 | 849 | ```ruby 850 | # app/models/user.rb 851 | class User < ApplicationRecord 852 | attr_accessor :remember_token, :activation_token, :reset_token 853 | before_save :downcase_email 854 | before_create :create_activation_digest 855 | . 856 | . 857 | . 858 | # account를 유효하게 한다. 859 | def activate 860 | update_columns(activated: true, activated_at: Time.zone.now) 861 | end 862 | 863 | # 유효화용의 메일을 발신한다. 864 | def send_activation_email 865 | UserMailer.account_activation(self).deliver_now 866 | end 867 | 868 | # 패스워드 재설정 속성을 설정한다. 869 | def create_reset_digest 870 | self.reset_token = User.new_token 871 | update_columns(reset_digest: FILL_IN, reset_sent_at: FILL_IN) 872 | end 873 | 874 | # 패스워드 재설정용 메일을 발신한다. 875 | def send_password_reset_email 876 | UserMailer.password_reset(self).deliver_now 877 | end 878 | 879 | private 880 | 881 | # 메일주소를 모두 소문자로 한다. 882 | def downcase_email 883 | self.email = email.downcase 884 | end 885 | 886 | # 유효화용 토큰과 Digest를 생성하고 대입한다. 887 | def create_activation_digest 888 | self.activation_token = User.new_token 889 | self.activation_digest = User.digest(activation_token) 890 | end 891 | end 892 | ``` 893 | 894 | ```ruby 895 | # test/integration/password_resets_test.rb 896 | require 'test_helper' 897 | 898 | class PasswordResetsTest < ActionDispatch::IntegrationTest 899 | 900 | def setup 901 | ActionMailer::Base.deliveries.clear 902 | @user = users(:michael) 903 | end 904 | . 905 | . 906 | . 907 | test "expired token" do 908 | get new_password_reset_path 909 | post password_resets_path, 910 | params: { password_reset: { email: @user.email } } 911 | 912 | @user = assigns(:user) 913 | @user.update_attribute(:reset_sent_at, 3.hours.ago) 914 | patch password_reset_path(@user.reset_token), 915 | params: { email: @user.email, 916 | user: { password: "foobar", 917 | password_confirmation: "foobar" } } 918 | assert_response :redirect 919 | follow_redirect! 920 | assert_match /FILL_IN/i, response.body 921 | end 922 | end 923 | ``` 924 | 925 | ```ruby 926 | # app/controllers/password_resets_controller.rb 927 | class PasswordResetsController < ApplicationController 928 | . 929 | . 930 | . 931 | def update 932 | if params[:user][:password].empty? 933 | @user.errors.add(:password, :blank) 934 | render 'edit' 935 | elsif @user.update_attributes(user_params) 936 | log_in @user 937 | @user.update_attribute(:reset_digest, nil) 938 | flash[:success] = "Password has been reset." 939 | redirect_to @user 940 | else 941 | render 'edit' 942 | end 943 | end 944 | . 945 | . 946 | . 947 | end 948 | ``` 949 | 950 | 951 | 952 | ## 12.4 실제 배포환경에서의 메일 발신 953 | 954 | 이 것으로 패스워드 재설정의 구현도 끝냈습니다. 이 다음으로는 앞선 챕터와 마찬가지로, development 환경만 아니라, production 환경에서도 동작하게 하는 것 뿐입니다. 셋업의 순서는 Account 유효화와 완전 똑같습니다. 따라서 이미 이전 챕터에서 셋업을 끝낸 분은, ([11.4](Chapter11.md#114-실제-배포환경에서의-메일-발신)) 이번 섹션의 첫 번째 코드 (production.rb) 를 수정하는 부분까지 스킵하셔도 됩니다. 955 | 956 | 957 | 958 | 실제 배포 환경에서 메일 발신하기 위해서, "SendGrid" 라고 하는 Heroku 애드온을 이용하여 account을 검증합니다. (이 애드온을 이용하기 위해서는 Heroku 계정의 신용카드를 설정할 필요가 있으나, 계정 검증할 때에는 요금은 발생하지 않습니다.) 본 튜토리얼에서는 "starter tier" 라는 서비스를 사용해보겠습니다. 이것은 1일 메일 수가 최대 400통까지라는 제한은 있습니다만, 무료로 이용할 수 있습니다. 애드온을 어플리케이션에 추가하기 위해서는 다음 커맨드를 실행합니다. 959 | 960 | `$ heroku addons:create sendgrid:starter` 961 | 962 | *주의* : heroku 커맨드의 버전이 오래된 버전이라면, 여기서 실패할 수도 있습니다. 그 경우에는 [Heroku Toolbelt](https://toolbelt.heroku.com/) 를 사용하여 최신판으로 업데이트하던지, 조금 옛날 커맨드를 사용해주세요. 963 | 964 | `$ heroku addons:add sendgrid:starter` 965 | 966 | 어플리케이션에서 SendGrid 애드온을 사용하기 위해서는 production 환경의 [SMTP](https://ja.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) 에 정보를 기입할 필요가 있습니다. 아래와 같이, 실제 배포 Web 사이트의 주소를 `host` 변수에 정의할 필요도 있습니다. 967 | 968 | ```ruby 969 | # config/environments/production.rb 970 | Rails.application.configure do 971 | . 972 | . 973 | . 974 | config.action_mailer.raise_delivery_errors = true 975 | config.action_mailer.delivery_method = :smtp 976 | host = '.herokuapp.com' 977 | config.action_mailer.default_url_options = { host: host } 978 | ActionMailer::Base.smtp_settings = { 979 | :address => 'smtp.sendgrid.net', 980 | :port => '587', 981 | :authentication => :plain, 982 | :user_name => ENV['SENDGRID_USERNAME'], 983 | :password => ENV['SENDGRID_PASSWORD'], 984 | :domain => 'heroku.com', 985 | :enable_starttls_auto => true 986 | } 987 | . 988 | . 989 | . 990 | end 991 | ``` 992 | 993 | 이전 11장에서의 메일 설정에서는 SendGrid Account의 `user_name` 와 `password` 설정을 기입하는 줄도 있습니다만, 거기에는 기입하지 말고, 반드시 환경변수 "`ENV`" 에 설정하도록 주의해주세요. 실제 배포환경에서 운용하는 어플리케이션에서는 암호화되어있지 않은 ID나 패스워드와 같이 중요한 보안정보는 "절대로" 소스코드에 직접 작성하지 말아주세요. 그러한 정보는 환경변수에 저장하고, 환경변수로부터 어플리케이션으로 읽어들이게할 필요가 있습니다. 이번 경우에는 그러한 변수는 SendGrid 애드온이 자동적으로 설정해줍니다만, 추후 13.4.4에서는 자신이 직접 환경변수를 설정해야할 필요가 있습니다. 994 | 995 | 996 | 997 | 이 시점에서 Git의 토픽브랜치를 master에 Merge해봅시다. 998 | 999 | ``` 1000 | $ rails test 1001 | $ git add -A 1002 | $ git commit -m "Add password reset" 1003 | $ git checkout master 1004 | $ git merge password-reset 1005 | ``` 1006 | 1007 | 이어서 리모트 레포지토리에 푸시하여 Heroku에 Deploy해봅시다. 1008 | 1009 | ``` 1010 | $ rails test 1011 | $ git push 1012 | $ git push heroku 1013 | $ heroku run rails db:migrate 1014 | ``` 1015 | 1016 | Heroku에 Deploy가 끝나면, 로그인 페이지의 [forgot password] 링크를 클릭하여, production 환경에서 패스워드의 재설정을 해봅시다. Form에서부터 데이터를 송신하면, 아래 스크린샷처럼 메일이 올 것 입니다. 기재되어져있는 링크를 클릭하여 무효한 패스워드와 유효환 패스워드를 각각 시험해봅시다. 여기서 구현이 제대로 되어있다면, 테스팅에서 확인해보았던 화면처럼 될 것 입니다. 1017 | 1018 | ![](../image/Chapter12/reset_email_production_4th_ed.png) 1019 | 1020 | ##### 연습 1021 | 1022 | 1. production환경에서 유저 등록을 해봅시다. 유저 등록시에 입력한 메일주소로 메일이 왔습니까? 1023 | 2. 메일을 받았다면, 실제로 메일을 클릭하여 Account를 유효화해봅시다. 또한 Heroku 상의 로그를 알아보고, 유효화에 관한 로그는 어떤지 알아보세요. *Hint* : 터미널에서 `heroku logs` 커맨드를 실행해봅시다. 1024 | 3. Account가 유효화에 성공했다면, 이번에는 패스워드를 재설정해봅시다. 올바르게 패스워드가 재설정되었습니까? 1025 | 1026 | 1027 | 1028 | ## 12.5 마지막으로 1029 | 1030 | 패스워드 재설정의 구현이 끝났으므로, Sample 어플리케이션의 유저 등록, 로그인, 로그아웃의 처리는 실제 어플리케이션과 매우 비슷한 레벨까지 구현해보았습니다. *Rails Tutorial* 의 남은 챕터에서는 Twitter와 같은 micropost 기능(제13장) 과, 팔로우 중의 유저의 투고를 표시하는 스테이터스 피드기능 (제14장) 을 구현해볼 것 입니다. 이 챕터에서는 Rails의 강력한 기능 (이미지 업로드, 커스터마이즈한 데이터베이스로의 조회기능, `has_many`, `has_many :through` 등을 사용한 고도의 데이터베이스 모델링)을 다수 소개해볼 예정입니다. 1031 | 1032 | ### 12.5.1 12장의 마무리 1033 | 1034 | - 패스워드의 재설정은 Active Record 오브젝트는 아니지만, 세션이나 Account 유효화의 경우와 마찬가지로, 리소스로 모델화할 수 있다. 1035 | - Rails에서는 메일 발신에서 다루는 Action Mailer 의 액션과 뷰를 생성할 수 있다. 1036 | - Action Mailer 에서는 텍스트 메일과과 HTML 메일 양 쪽 다 사용할 수 있다. 1037 | - 메일러 액션에서 정의한 인스턴스 변수는, 다른 액션이나 뷰와 마찬가지로, 메일러의 뷰에서 참조할 수 있다. 1038 | - 패스워드를 재설정하기 위해서 생성한 토큰을 사용하여 유일한 URL을 생성한다. 1039 | - 보다 더 안전한 패스워드 재설정을 위해 해시화한 토큰(Digest) 를 사용한다. 1040 | - 메일러의 테스트와 결합테스트는 양쪽 다 User 메일러의 동작을 확인하는데에 유용하다. 1041 | - SendGrid 를 사용하면 Production 환경에서 메일을 송신할 수 있다. 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | -------------------------------------------------------------------------------- /Documentation/Chapter8.md: -------------------------------------------------------------------------------- 1 | # 제 8장 기본적인 로그인 기능 2 | 3 | [제 7장](Chapter7.md) 에서 Web사이트에서의 신규 유저 등록이 가능하게 되었습니다. 이번에는 유저가 로그인하거나 로그아웃할 수 있게 해봅시다. 이번 챕터에서는 로그인의 기본적인 구조를 구현해보겠습니다. 여기서 말하는 로그인의 기본적인 구조란, 브라우저가 로그인하고 있는 상태를 유지하고, 유저에 의해 브라우저가 닫혀지면 그 상태를 없애버리는 구조 (*인증 시스템(Authentification System*)) 입니다. 이 인증시스템의 기반을 구현한다면, 로그인되어져있는 유저 (current user) 만이 접근할 수 있는 페이지나, 무언가를 조작할 수 있는 기능 등을 제어할 수 있습니다. 또한 이러한 제한이나 제어의 구조를 *인가 모델(Authorization Model)* 이라 부릅니다. 예를 들어 이번 챕터에서 구현하는 로그인상태인지 아닌지를 헤더부분에서 확인할 수 있는 구조도 이것에 해당합니다. 4 | 5 | 이 인증시스템과 인가모델은, 나중에 구현할 Sample 어플리케이션의 여러가지 기능의 기반이 될 구조입니다. 예를 들어 제 10장에서는 로그인한 유저만이 유저의 리스트페이지로 이동할 수 있게 한다던지, 문제가 없는 유저만이 자신의 프로필 정보를 편집할 수 있게 한다던지, 관리자만이 다른 유저를 데이터베이스로부터 삭제할 수 있게 됩니다. 제 13장에서는 이번 챕터에서 사용할 수 있게되는 로그인 상태의 유저 (current user) 를 이용하여 유저의 아이디와 마이크로포스트를 연관시키게 하는 구조를 구현합니다. 마지막 제 14장에서는 다른 유저를 팔로우하는 기능이나 자신의 피드 리스트를 구현할 때, "누가 로그인해있는가" 라는 정보가 필요하게 됩니다. 6 | 7 | 또한 제 9장에서는 이번 챕터에서 구축한 기본적인 로그인 기능을 개선하여 보다 더 발전적인 로그인 기능을 구현할 것입니다. 예를 들어 이번 챕터에서 구축하는 기능으로는 브라우저를 닫으면 로그인한 유저 정보를 강제적으로 삭제하도록 합니다만, 제 9장에서 개선한 인증 기능에서는 유저가 *임의* 의 브라우저에 로그인정보를 기억시키는 (remember me) 기능을 구현할 것입니다. (구체적으로는 [remember me] 라고 하는 체크박스를 로그인 폼에 준비할 것입니다.) 이번 8장과 9장을 통해 8 | 9 | 1. 브라우저를 닫으면 로그인 정보를 파기한다 (Session) 10 | 2. 유저의 로그인 정보를 자동으로 보존한다 (Cookie) 11 | 3. 유저가 체크박스를 On 한 경우에만 로그인 정보를 저장한다 (Remember me) 12 | 13 | 라고 하는 3개의 일반적인 로그인 구조를 구현할 것입니다. 14 | 15 | 16 | 17 | ## 8.1 Session 18 | 19 | [HTTP](https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol) 는 [*Stateless 한 프로토콜*](https://ja.wikipedia.org/wiki/ステートレス・プロトコル) 입니다. 문자대로 "상태(State)" 가 "없기(less)" 떄문에 HTTP의 리퀘스트 하나하나는 그 이전의 리퀘스트 정보를 전혀 이용할 수 없는, 독립적인 트랜잭션으로써 다루게됩니다. HTTP는 말하자면, 리퀘스트가 끝나면 무엇이든 잃어버리고, 다음부터는 다시 제일 처음부터 다시 시작해야하는 건망증적인 프로토콜이며, 과거를 기억해내지 못하는 여행자와 같은 느낌입니다. (그러나, 그렇기 때문에 이 프로토콜은 매우 좋은 것입니다.) 이 본질적인 특성으로 인하여, 브라우저의 어느 페이지에서 다른 페이지로 이동할 때에 유저의 ID를 저장할 수단이 [HTTP 프로토콜 "내부"](https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol#.E5.8B.95.E4.BD.9C) 에는 전혀 없습니다. 유저 로그인이 필요한 Web 어플리케이션에는 [*세션 (Session)*](https://ja.wikipedia.org/wiki/セッション#.E3.82.B3.E3.83.B3.E3.83.94.E3.83.A5.E3.83.BC.E3.82.BF) 이라고 불리는 반영속적인 접속을 컴퓨터 사이에(유저의 개인 컴퓨터의 Web 브라우저와 Rails의 서버 등) 별도로 설정합니다. 20 | 21 | 세션은 HTTP 프로토콜의 계층과는 다르기 (좀 더 상위 계층) 때문에, HTTP의 특성과는 별도로 (약간 영향은 있지만) 접속을 확보할 수 있습니다. 22 | 23 | Rails의 세션을 구현하기 위한 방법으로 제일 일반적인 것은, [*cookies*](https://ja.wikipedia.org/wiki/HTTP_cookie) 를 사용하는 방법입니다. cookie는 유저의 브라우저에 저장되는 어떠한 작은 텍스트 데이터입니다. cookie는 어떤 페이지에서 다른 페이지로 이동할 때에도 없어지지 않기 때문에 여기에 유저 ID 등의 정보를 저장할 수 있습니다. 어플리케이션은 cookie내의 데이터를 사용하여, 예를 들어 로그인 중인 유저가 가지고 있는 정보를 데이터베이스에서 조회할 수 있습니다. 이번 섹션 및 [8.2](#82-Login)에서는, 그 이름을 또한 `session` 이라고 하는 Rails의 메소드를 사용하여 일시적인 세션을 작성해볼 것입니다.. 이 일시적인 세션은, 브라우저를 닫으면 자동적으로 종료합니다. 이어서 제 9장에서는 Rails의 `cookies` 메소드를 사용하여 좀 더 오래가는 세션의 작성방법에 대해 배워볼 것입니다. 24 | 25 | 세션을 RESTful한 리소스로서 모델링할 수 있다면, 다른 RESTful 리소스와돠 한 번에 이해할 수 있어서 매우 편리합니다. 로그인 페이지에서는 `new` 로 세로운 세션을 출력하여, 해당 페이지에 로그인하면 `create` 로 세션을 실제로 작성하고 저장한 후, 로그아웃하면 `destroy` 로 이용하여 파기하는 이러한 처리를 해볼 것입니다. 단, Users 리소스와 다른 점은, Users 리소스에서는 백엔드에 User모델을 거쳐 데이터베이스 상의 영속적인 데이터에 접속할 수 있는 것에 반해, Session 리소스에서는 대신에 cookies를 저장장소로써 사용하는 점입니다. 로그인의 구조의 대부분은, cookies를 사용한 인증메커니즘에 의해 구축됩니다. 이번 섹션과 다음 섹션에서는, 세션기능을 작성할 준비로써 Session 컨트롤러, 로그인 전용 폼, 양쪽을 이어줄 컨트롤러 상의 액션을 작성해볼 것입니다. 8.2에서는 세션을 조작하기 위해 필요한 코드를 몇 가지 추가해보고, 유저 로그인 기능을 완성시킬 예정입니다. 26 | 27 | 전 챕터와 마찬가지로 토픽브랜치로 작업을 하고 마지막에 수정사항을 머지할 것입니다. 28 | 29 | `$ git checkout -b basic-login` 30 | 31 | ### 8.1.1 Sessions Controller 32 | 33 | 로그인과 로그아웃의 요소를, Sessions 컨트롤러의 특정한 REST 액션에 각각 대응하게끔 해봅시다. 로그인 폼은 이번 섹션에서 다루는 `new` 액션에서 처리합니다. `create` 액션에 POST 리퀘스트를 송신하면, 실제로 로그인하게 됩니다.(8.2) `destroy` 액션에 DELETE 리퀘스트를 송신하면, 로그아웃 하게 됩니다. (8.3) (이전 7장에서 HTTP 메소드와 REST 액션에 대해 관련지은 것을 떠올려보세요.) 34 | 35 | 일단, Sessions 컨트롤러와 `new` 액션을 생성하고나서 시작해봅시다. 36 | 37 | `$ rails generate controller Sessions new` 38 | 39 | (또한, `rails generate`로 `new` 액션을 생성하면, 그것에 대응하는 뷰도 생성됩니다. `create`나 `destroy` 에는 대응할 뷰가 필요없기 때문에, 필요 없는 뷰를 작성하지 않기 위해 여기서는 `new` 만 지정합니다. ) [7.2](Chapter7.md#72-유저-등록-Form) 의 유저 등록 페이지와 마찬가지로, 아래의 목업을 바탕으로 세션을 시작하기 위한 로그인 폼을 작성해봅시다. 40 | 41 | ![](../image/Chapter8/login_mockup.png) 42 | 43 | Users 리소스의 떄에는 전용의 `resource` 메소드를 사용하여 RESTful한 라우팅을 자동적으로 처리해주도록 하였습니다만, Session 리소스에는 그러한 처리는 필요없습니다. 따라서 "Named Route" 만 사용합니다. 이 named route 에는 GET 리퀘스트나 POST 리퀘스트를 `login` 라우팅으로, DELETE 리퀘스트를 `logout` 라우팅으로 다룹니다. 이 라우팅을 반영한 것이 아래의 코드입니다. 또한 `rails generate controller` 에서 생성한 불필요한 라우팅은 아래 코드에서 삭제하였습니다. 44 | 45 | ```ruby 46 | # config/routes.rb 47 | Rails.application.routes.draw do 48 | root 'static_pages#home' 49 | get '/help', to: 'static_pages#help' 50 | get '/about', to: 'static_pages#about' 51 | get '/contact', to: 'static_pages#contact' 52 | get '/signup', to: 'users#new' 53 | get '/login', to: 'sessions#new' #추가 54 | post '/login', to: 'sessions#create' #추가 55 | delete '/logout', to: 'sessions#destroy' #추가 56 | resources :users 57 | end 58 | ``` 59 | 60 | 위 라우팅으로 인하여 로그인용 named route를 사용할 수 있게 할 필요가 있습니다. controller를 작성할 때 생성된 테스트파일을 이용하여 테스트를 진행해봅니다. 61 | 62 | ```ruby 63 | # test/controllers/sessions_controller_test.rb 64 | require 'test_helper' 65 | 66 | class SessionsControllerTest < ActionDispatch::IntegrationTest 67 | 68 | test "should get new" do 69 | get login_path 70 | assert_response :success 71 | end 72 | end 73 | ``` 74 | 75 | `routes.rb` 에서 정의한 라우팅의 URL이나 액션은 유저용의 URL이나 액션과 대강 비슷합니다. 76 | 77 | | **HTTP 리퀘스트** | **URL** | **Named Routes** | **Action** | **용도** | 78 | | ----------------- | ------- | ---------------- | ---------- | ----------------------------- | 79 | | `GET` | /login | `login_path` | `new` | 새로운 세션의 페이지 (로그인) | 80 | | `POST` | /login | `login_path` | `create` | 새로운 세션의 생성 (로그인) | 81 | | `DELETE` | /logout | `logout_path` | `destroy` | 세션의 삭제 (로그아웃) | 82 | 83 | Named route도 꽤나 늘어났습니다. 여기까지 추가한 모든 라우팅을 표시할 수 있으면 편리할 것 같습니다. 이러한 때 `rails routes` 커맨드를 실행해봅시다. 언제든지 현재의 라우팅을 확인할 수 있습니다. 84 | 85 | ```ruby 86 | $ rails routes 87 | Prefix Verb URI Pattern Controller#Action 88 | root GET / static_pages#home 89 | help GET /help(.:format) static_pages#help 90 | about GET /about(.:format) static_pages#about 91 | contact GET /contact(.:format) static_pages#contact 92 | signup GET /signup(.:format) users#new 93 | login GET /login(.:format) sessions#new 94 | POST /login(.:format) sessions#create 95 | logout DELETE /logout(.:format) sessions#destroy 96 | users GET /users(.:format) users#index 97 | POST /users(.:format) users#create 98 | new_user GET /users/new(.:format) users#new 99 | edit_user GET /users/:id/edit(.:format) users#edit 100 | user GET /users/:id(.:format) users#show 101 | PATCH /users/:id(.:format) users#update 102 | PUT /users/:id(.:format) users#update 103 | DELETE /users/:id(.:format) users#destroy 104 | ``` 105 | 106 | 지금은 위 라우팅을 완전히 이해할 필요는 없습니다. 그렇지만 이 리스트를 대강 보고만 있어도 어플리케이션에서 서포트하고 있는 모든 액션이 이 리스트에 있음을 알아챌 수 있을겁니다. 107 | 108 | ##### 연습 109 | 110 | 1. `GET login_path` 와 `POST login_path` 와의 차이점을 설명할 수 있겠습니까? 111 | 2. 터미널의 파이프기능을 사용하여 `rails routes` 의 실행결과와 `grep` 커맨드를 이어서, Users 리소스에 관한 라우팅만을 표시할 수 있습니다. 마찬가지로 Session 리소스의 관한 결과만을 표시해 봅시다. 현재 몇가지의 Sessions 리소스가 있나요? *Hint* : 파이프나 `grep` 의 사용방법을 잘 모를 때에는 [*Learn Enough Command Line to Be Dangerous*](http://learnenough.com/command-line-tutorial) 의 [Section on Grep](https://www.learnenough.com/command-line-tutorial#sec-grepping) 을 참고해보세요. 112 | 113 | ### 8.1.2 Login Form 114 | 115 | 컨트롤러와 라우팅을 정의했기 때문에, 이번에는 새로운 세션에서 사용할 뷰, 즉 로그인 폼을 정의해봅시다. 이전 목업들이 기억나신다면 아시겠지만, 로그인폼과 유저 등록 폼은 거의 차이가 없다는 것을 아실 것입니다. 차이라고 한다면, 4개 있던 필드가 "Email" 과 "Password" 2가지로 줄었다는 점입니다. 116 | 117 | 로그인 폼에서 입력한 정보가 잘못되었을 때에는 로그인 페이지를 한 번 더 표시하여 에러메세지를 출력시킵니다. [7.3.3](Chapter7.md#733-에러-메세지) 에서 에러메세지의 표시를 위해 전용 파셜을 사용하였습니다만, 이 파셜에는 Active Record에 의해 자동생성되는 메세지를 사용하고 있다는 것을 떠올려주세요. 이번에 다루는 세션은 Active Record 오브젝트가 아니기 때문에, 이전처럼 Active Record가 알아서 에러 메세지를 표시해준다고는 기대할 수 없습니다. 그렇기 때문에, 이번에는 플래시 메세지로 에러를 표시해보겠습니다. 118 | 119 | ![](../image/Chapter8/login_failure_mockup.png) 120 | 121 | 이전 7장에서의 유저 등록 폼에서는 다음과 같이 `form_for` 헬퍼를 사용하여 유저의 인스턴스 변수 `@user` 를 파라미터로 사용하였습니다. 122 | 123 | ```erb 124 | <%= form_for(@user) do |f| %> 125 | . 126 | . 127 | . 128 | <% end %> 129 | ``` 130 | 131 | 세션 폼과 유저 등록 폼의 제일 큰 차이는, 세션에는 Session 모델이라고 하는 것이 없으며, 그렇기 때문에 `@user` 와 같은 인스턴스 변수에 상응하는 것이 없다는 것입니다. 따라서 새로운 세션 폼을 작성할 때에는 `form_for` 헬퍼에 추가 정보를 알아서 적당히 입력하지 않으면 안됩니다. 132 | 133 | `form_for(@user)` 134 | 135 | Rails에서는 위와 같이 작성하는 것 만으로도 "Form의 `action` 은 /user 이라고 하는 URL로의 POST형태이다." 라는 것을 자동적으로 인식합니다만, 세션의 경우에는 리소스의 *이름* 과 그것에 대응하는 URL을 구체적으로 지정해줄 필요가 있습니다. 136 | 137 | `form_for (:session, url:login_path)` 138 | 139 | 적절한 `form_for` 를 사용하는 것으로, 유저 등록용 폼을 참고하여 목업과 비슷한 로그인 폼을 간단히 작성해볼 수 있습니다. 140 | 141 | ```erb 142 | 143 | <% provide(:title, "Log in") %> 144 |

Log in

145 | 146 |
147 |
148 | <%= form_for(:session, url: login_path) do |f| %> 149 | 150 | <%= f.label :email %> 151 | <%= f.email_field :email, class: 'form-control' %> 152 | 153 | <%= f.label :password %> 154 | <%= f.password_field :password, class: 'form-control' %> 155 | 156 | <%= f.submit "Log in", class: "btn btn-primary" %> 157 | <% end %> 158 | 159 |

New user? <%= link_to "Sign up now!", signup_path %>

160 |
161 |
162 | ``` 163 | 164 | 유저가 바로 클릭할 수 있게, 유저 등록 페이지의 링크를 추가해놓은 것을 주목해주세요. 위 코드를 사용하면 아래와 같은 로그인 폼이 표시됩니다. ([Log in] 링크가 아직 유효하지 않기 때문에, 자신이 브라우저의 주소창에 "/login" 과 URL을 직접 입력해보세요. 로그인 링크는 8.2.3에서 동작하게 될 것입니다.) 165 | 166 | ![](../image/Chapter8/login_form.png) 167 | 168 | 생성된 HTML 폼은 아래와 같습니다. 169 | 170 | ```html 171 |
172 | 173 | 175 | 176 | 178 | 179 | 181 | 183 |
184 | ``` 185 | 186 | 이전 유저 등록 폼과 비교해보면, 폼의 정보 송신 후에 `params` 해시에 들어갈 값이 메일주소와 패스워드 필드에 각각 대응한 `params[:session][:email]` 과 `params[:session][:password]` 라는 점을 알 수 있습니다. 187 | 188 | ##### 연습 189 | 190 | 1. `new.html.erb` 에서 작성한 폼의 정보를 송신하면, Sessions 컨트롤러의 `create` 액션에 보내지게됩니다. Rails는 이것을 어떻게 구현하고 있는 것일까요? 생각해봅시다. *Hint*: 위 HTML의 맨 첫 줄을 주목해주세요. 191 | 192 | ### 8.1.3 유저의 검증과 인증 193 | 194 | 유저의 등록에서는 최초로 유저를 작성해보았습니다. 로그인에서 세션을 작성하는 경우, 제일 처음 이루어지는 것은, 입력이 *무효* 한 경우의 처리입니다. 제일 처음으로, 폼 데이터가 송신되어졌을 때의 동작을 생각하고 이해해봅시다. 그 다음으로 로그인이 실패했을 경우에 표시되는 에러메세지를 배치할 것입니다. 그 다음으로 로그인에 성공한 경우 ([8.2](#82-Login))에 사용하는 기초부분을 작성해볼 것입니다. 일단 여기서는 로그인 데이터가 송신될 때마다 패스워드와 메일주소의 조합이 유효한지를 판정하는 로직을 구현해볼 것입니다. 195 | 196 | 제일 처음으로, 최소한의 `create` 액션을 Sessions 컨트롤러에서 정의하고, 아무것도 정의되어있지 않은 `new` 액션과 `destroy` 액션도 겸사겸사 작성해봅시다. 아래의 코드에서 `create` 액션의 안에서는 아무것도 이루저지지 않습니다만, 액션을 실행하면 `new` 뷰가 표시될 것이기 때문에 이걸로 충분합니다. 결과적으로는 /session/new 폼에서 데이터를 보내면 아래 두 번째 예시처럼 될 것입니다. 197 | 198 | ```ruby 199 | class SessionsController < ApplicationController 200 | 201 | def new 202 | end 203 | 204 | def create 205 | render 'new' 206 | end 207 | 208 | def destroy 209 | end 210 | end 211 | ``` 212 | 213 | ![](../image/Chapter8/initial_failed_login_3rd_edition.png) 214 | 215 | 위 캡쳐에서 표시되어지고 있는 디버그 정보를 확인해주세요. [8.1.2](#812-로그인-Form) 의 마지막에서도 간단히 다루어보았던, `params` 해시에서는 다음과 같이 `session` 키의 아래에 메일주소와 패스워드가 있습니다. 216 | 217 | ``` 218 | --- 219 | session: 220 | email: 'user@example.com' 221 | password: 'foobar' 222 | commit: Log in 223 | action: create 224 | controller: sessions 225 | ``` 226 | 227 | 유저 등록의 경우와 마찬가지로, 이러한 파라미터는 네스트화된 해시로 되어있습니다. 특히 `params` 는 다음과 같은 네스트 해시로 되어있습니다. 해시 안에 해시가 있는 구조입니다. 228 | 229 | `{ session: { password: "foobar", email: "user@example.com" } }` 230 | 231 | 즉, 다음과 같은 해시가 있는 경우 232 | 233 | `params[:session]` 234 | 235 | 이 해시에 다시 해시가 포함되어 있으며 236 | 237 | `{ password: "foobar", email:"user@example.com" }` 238 | 239 | 결과적으로는 다음과 같이 데이터를 접근할 수 있습니다. 240 | 241 | `params[:session][:password]` 242 | 243 | 또한 위처럼 한다면 폼으로부터 송신되어진 패스워드를 확인할 수 있습니다. 244 | 245 | 요컨대 `create` 액션의 내부에는, 유저의 인증에 필요한 모든 정보를 `params` 해시로부터 간단하게 얻을 수 있는 것입니다. 그리고 인증에 필요한 모든 메소드도 8장까지 오면서 다 배웠습니다. (그렇게 되도록 본 튜토리얼이 구성되어 있습니다.) 여기서는 Active Record가 제공하는 `User.find_By` 메소드와, `has_secure_password` 가 제공하는 `authenticate` 메소드를 사용하고 있습니다. 여기서 `authenticate` 메소드는 인증에 실패했을 때 `false` 를 리턴하는 것을 기억하고 계시나요? 이상의 내용을 정리하여 유저의 로그인 부분을 구현한 것이 아래의 코드입니다. 246 | 247 | ```ruby 248 | # app/controller/sessions_controller.rb 249 | class SessionsController < ApplicationController 250 | 251 | def new 252 | end 253 | 254 | def create 255 | user = User.find_by(email: params[:session][:email].downcase) 256 | if user && user.authenticate(params[:session][:password]) 257 | # 유저 로그인 후에 유저 정보 페이지로 리다이렉트 258 | else 259 | # 에러메세지 작성 260 | render 'new' 261 | end 262 | end 263 | 264 | def destroy 265 | end 266 | end 267 | ``` 268 | 269 | 270 | 271 | `create`의 첫 번째 행에서는 전달받은 로그인폼의 메일주소를 사용하여 데이터베이스로부터 유저를 조회하고 있습니다. ([6.2.5](Chapter6.md#625-유니크성을-검증해보자) 에서는 메일주소를 모두 소문자로 변환하여 저장하고 있었던 것을 기억하시나요? 그렇기 때문에 여기서는 `downcase`메소드를 사용하여 유효한 메일주소가 입력되었을 때 확실하게 매칭될 수 있게 하고 있습니다.) 그 다음 행은 살짝 이해하기 어려울 지도 모르겠습니다만, Rails 프로그래밍에서는 정석적인 방법입니다. 272 | 273 | `user && user.authenticate(params[:session][:password])` 274 | 275 | `&&`(논리곱, and) 는, 조회결과로 얻은 유저가 유효한지를 판단하기 위해 사용됩니다. Ruby에서는 `nil`과 `false` 이외의 모든 오브젝트는, 진리값으로는 `true` 가 되는([4.2.3](Chapter4.md#423-오브젝트-메세지의-송수신)) 성질을 고려한다면, `&&` 의 전후의 값과 조합한 결과는 아래의 표와 같은 결과가 됩니다. 입력된 메일 주소를 가진 유저가 데이터베이스에 존재하며, 또한 입력된 패스워드가 해당 유저의 패스워드와 일치하는 경우에만 `if` 문의 결과가 `true` 가 되는 것을 알 수 있습니다. 조금 더 간략하게 설명하자면, "유저가 데이터베이스에 존재하면서, 인증에 성공했을 경우에만" 이라는 조건이 됩니다. 276 | 277 | | **User** | **Password** | **a && b** | 278 | | ------------------ | ------------------- | ------------------------------ | 279 | | 존재하지 않는 유저 | *아무거나 상관없음* | `(nil && [오브젝트]) == false` | 280 | | 有効なユーザー | 誤ったパスワード | `(true && false) == false` | 281 | | 有効なユーザー | 正しいパスワード | `(true && true) == true` | 282 | 283 | ##### 연습 284 | 285 | 1. Rails 콘솔을 사용하여 위 표의 조건의 식이 올바른지를 확인해봅시다. 일단 `user = nil` 의 경우를, 그 다음으로는 `user = User.first` 의 경우를 확인해봅시다. *Hint* : 반드시 논리값을 가진 오브젝트가 될 수 있도록, [4.2.3](Chapter4.md#423-오브젝트-메세지의-송수신) 에서 소개해드린 `!!` 의 기술을 사용해봅시다. 예시 : `!!(user && user.authenticate('foobar'))` 286 | 287 | ### 8.1.4 Flash message를 사용해보자 288 | 289 | [7.3.3](Chapter7.md#733-에러-메세지) 에서는 유저 등록 시의 에러메세지를 표시할 때, User모델의 에러메세지를 이용했던 것을 기억하시나요? 유저 등록의 경우, 에러메세지는 특정한 Active Record 오브젝트에 관련지어져 있었기 때문에, 그 방법을 이용했습니다. 그러나 세션에서는 Active Record의 모델을 사용하고 있지 않기 때문에, 그 때의 그 방법대로는 사용할 수 없습니다. 여기서 로그인에 실패했을 경우에는 대신에 플래시 메세지를 표시하도록 해봅시다. 제일 첫 코드는 아래와 같습니다. (이 코드는 일부러 조금 틀리게 작성했습니다.) 290 | 291 | ```ruby 292 | class SessionsController < ApplicationController 293 | 294 | def new 295 | end 296 | 297 | def create 298 | user = User.find_by(email: params[:session][:email].downcase) 299 | if user && user.authenticate(params[:session][:password]) 300 | # 유저 로그인 후에 유저 정보 페이지로 리다이렉트 301 | else 302 | flash[:danger] = 'Invalid email/password combination' # 본래는 정확하지 않은 표현 303 | render 'new' 304 | end 305 | end 306 | 307 | def destroy 308 | end 309 | end 310 | ``` 311 | 312 | 플래시 메세지는 Web사이트의 레이아웃으로 표시되기 때문에, `flash[:danger]` 으로 설정한 메세지는 자동적으로 표시됩니다. Bootstrap CSS의 덕분으로 적절한 스타일도 표시됩니다. 313 | 314 | ![](../image/Chapter8/failed_login_flash_3rd_edition.png) 315 | 316 | 위에서 설명드렸다시피, 코드가 조금 잘못되어있습니다. 페이지에서는 제대로 에러메세지를 출력하고 있습니다만, 어디가 문제일까요? 실은 위 코드대로라면은 *리퀘스트* 의 플래시 메세지가 한 번 표시되면 사라지지않고 계속 남아있게 됩니다. 7장에서는 리다이렉트를 사용했던 것 과는 다르게, 표시된 템플레이트를 `render` 메소드로 강제적으로 리렌더링하여도 리퀘스트로 판단하지 않기 때문에, 리퀘스트의 메세지가 사라지지 않습니다. 예를들어 일부러 무효한 정보를 입력하여 송신하여 에러메세지를 표시하면, Home 페이지를 클릭하여 이동하면 해당 화면에서도 플래시 메세지가 표시된 채로 남아있습니다. 이 문제는 [8.1.5](#815-Flash의-테스트)에서 수정하도록 해보겠습니다. 317 | 318 | ![](../image/Chapter8/flash_persistence_3rd_edition.png) 319 | 320 | ### 8.1.5 Flash 의 테스트 321 | 322 | 플래시 메세지가 사라지지 않는 문제는, 우리의 어플리케이션의 작은 버그입니다. [컬럼 3.3](Chapter3.md#컬럼-33-결국-테스트는-언제-하는-것이-좋은가) 에서 해설한 테스트의 가이드라인에 따르면, 이것은 "에러를 캐치하는 테스트를 먼저 작성하고, 그 후에 에러가 해결되게끔 코드를 작성한다" 라는 항목에 해당하는 상황입니다. 그러면 로그인 폼의 송신에 대해 간단한 결합테스트 코드를 작성해보도록 합시다. 이 결합테스트 코드는 해당 버그에 대한 문서로도 활용될 수도 있으며 이후에 회귀버그의 발생을 막아줄 수 있는 효과도 있습니다. 게다가 앞으로 이 결합테스트를 기반으로하여 보다 더 본격적인 결합테스트 코드를 작성할 때도 편리하게 될 것입니다. 323 | 324 | 어플리케이션의 로그인의 움직임을 테스트하기 위해선, 제일 처음으로 결합테스트 코드를 작성해봅시다. 325 | 326 | ``` 327 | $ rails generate integration_test users_login 328 | invoke test_unit 329 | create test/integration/users_login_test.rb 330 | ``` 331 | 332 | 다음으로 앞서 보았던 버그들을 재현하기 위한 필요가 있습니다. 기본적인 순서는 다음과 같습니다. 333 | 334 | 1. 로그인용 주소로 접속합니다. 335 | 2. 새로운 세션의 폼이 제대로 표시되는지를 확인합니다. 336 | 3. 일부러 무효한 `params` 해시를 사용하여 세션용 패스를 POST로 보냅니다. 337 | 4. 새로운 세션의 폼이 다시 출력되고, 플래시메세지가 추가되어 있는 것을 확인한다. 338 | 5. 다른 페이지 (Home 등)으로 이동한다. 339 | 6. 이동한 페이지에서 플래시메세지가 표시*되지 않는 것* 을 확인한다. 340 | 341 | 위 순서를 코드로 작성한 것이 아래와 같습니다. 342 | 343 | ```ruby 344 | # test/intergration/users_login_test.rb 345 | require 'test_helper' 346 | 347 | class UsersLoginTest < ActionDispatch::IntegrationTest 348 | 349 | test "login with invalid information" do 350 | get login_path 351 | assert_template 'sessions/new' 352 | post login_path, params: { session: { email: "", password: "" } } 353 | assert_template 'sessions/new' 354 | assert_not flash.empty? 355 | get root_path 356 | assert flash.empty? 357 | end 358 | end 359 | ``` 360 | 361 | 위 테스트 코드를 실행하면, 실패(RED)할 것입니다. 362 | 363 | ``` 364 | $ rails test test/integration/users_login_test.rb 365 | ``` 366 | 367 | 또한 위의 예시와 같이, `rails test` 의 파라미터에 테스트파일을 전달하면, 해당 테스트파일만을 실행할 수 있습니다. 368 | 369 | 위의 실패하는 테스트코드를 성공시키기 위해서는, 컨트롤러에서의 `flash` 를 `flash.now` 로 수정해야합니다. `flash.now` 는 렌더링이 끝난 페이지에서 특별하게 플래시메세지를 표시할 수 있습니다. `flash` 메세지와는 다르게, `flash.now` 의 메세지는 이 이후 리퀘스트가 발생하였을 때 사라지게 됩니다. 수정된 코드는 아래와 같습니다. 370 | 371 | ```ruby 372 | # app/controllers/sessions_controller.rb 373 | class SessionsController < ApplicationController 374 | 375 | def new 376 | end 377 | 378 | def create 379 | user = User.find_by(email: params[:session][:email].downcase) 380 | if user && user.authenticate(params[:session][:password]) 381 | # 유저 로그인 후 프로필 페이지로 리다이렉트 합니다. 382 | else 383 | flash.now[:danger] = 'Invalid email/password combination' 384 | render 'new' 385 | end 386 | end 387 | 388 | def destroy 389 | end 390 | end 391 | ``` 392 | 393 | 이어서, 로그인의 결합테스트를 포함한 모든 테스트 스위트를 실행해보면, 전투 통과 (GREEN)하는 것을 확인할 수 있을 것입니다. 394 | 395 | ``` 396 | $ rails test test/integration/users_login_test.rb 397 | $ rails test 398 | ``` 399 | 400 | ##### 연습 401 | 402 | 1. [8.1.4](#814-flash-message를-사용해보자) 의 처리 플로우가 제대로 동작하고 있는지 브라우저에서 확인해봅시다. 특히 flash가 제대로 기능하는지를 flash 메세지가 표시된 다음에 다른 페이지로 꼭 이동해보세요. 403 | 404 | 405 | 406 | ## 8.2 Login 407 | 408 | 무효한 값을 송신해도, 로그인폼에서 제대로 처리할 수 있게끔 하였습니다. 그 다음으로는 실제로 로그인 도중의 상태에서 유효한 값을 송신했을 때, 로그인 폼이 제대로 핸들링할 수 있도록 해봅시다. 이번 섹션에서는 cookie를 사용한 일시적인 세션을 사용하여 유저를 로그인할 수 있게 해보겠습니다. 이 cookies는 브라우저를 닫으면 자동적으로 무효하게 되는 성질을 가지고 있습니다. 9.1에서는 브라우저를 닫아도 보존되는 세션을 추가해볼 것입니다. 409 | 410 | 세션을 구현하기 위해서는 많은 컨트롤러나 뷰에서 매우 복잡하고 많은 수의 메소드를 정의할 필요가 있습니다. Ruby의 *모듈* 을 사용한다면, 그러한 많은 메소드르를 한 곳으로 모아 패키지화할 수 있다는 것을 [4.2.5](Chapter4.md#425-다시-한-번-Title-Helper) 에서 배웠습니다. 매우 감사하게도 Sessions 컨트롤러 ([8.1.1](#811-Sessions-Controller)) 를 생성했을 때의 시점에서 이미 세션용의 헬퍼 모듈이 생성되어 있습니다. 게다가 Rails의 세션용 헬퍼는 뷰에서도 자동적으로 인식되어 사용할 수 있습니다. Rails의 모든 컨트롤러의 상위 클래스인 Application 컨트롤러에 이 모듈을 읽어들일 수 있게한다면, 어떠한 컨트롤러에서도 사용할 수 있게 됩니다. 411 | 412 | ```ruby 413 | # app/controllers/application_controller.rb 414 | class ApplicationController < ActionController::Base 415 | protect_from_forgery with: :exception 416 | include SessionsHelper 417 | end 418 | ``` 419 | 420 | 설정이 끝났다면, 드디어 유저 로그인 기능의 코드를 작성해볼 차례입니다. 421 | 422 | ### 8.2.1 Log_in Method 423 | 424 | Rails에서 사전 정의가 되어있는 `session` 메소드를 사용하여, 단순한 로그인이 가능하게끔 해봅시다. (또한 이것은 [8.1.1](#811-Sessions-Controller) 에서 생성한 Sessions 컨트롤러와 아무런 관계가 없습니다.) 이 `session` 메소드는 해시처럼 사용할 수 있습니다. 값은 다음과 같이 입력합니다. 425 | 426 | `session[:user_id] = user.id` 427 | 428 | 위 코드를 실행하면, 유저의 브라우저 내부의 일시적인 cookies에 암호화된 유저 id가 자동으로 생성됩니다. 그 다음의 페이지에서 `session[:user_id]` 를 사용하여 유저 ID의 원래 값으로 꺼내어 사용할 수 있습니다. 한편, `cookies` 메소드와 (9.1) 대조적으로 `session` 메소드에서 생성된 일시적인 cookies는 브라우저를 닫은 순간에 무효하게 되어버립니다. 429 | 430 | 같은 로그인 방법을 여러 장소에서 사용할 수 있게 하기 위해, Session 헬퍼에 `log_in` 이라고 하는 이름의 메소드를 정의해보도록 합시다. 431 | 432 | ```ruby 433 | # app/helpers/sessions_helper.rb 434 | module SessionsHelper 435 | 436 | def log_in(user) 437 | session[:user_id] = user.id 438 | end 439 | end 440 | ``` 441 | 442 | `session` 메소드에서 생성한 일시 cookies는 자동적으로 암호화되기 때문에 위 코드는 보호받게 됩니다. 그리고 여기가 중요한 포인트입니다만, 공격자가 설령 이 정보를 cookies로부터 빼내려한다고 하더라도, 그 것을 사용하여 진짜 유저로서 로그인할 수는 없습니다. 단, 지금 여기서 말씀드린 `session` 메소드를 "일시적인 세션" 에만 해당하는 말입니다. `cookies` 메소드를 사용하여 작성한 "영구적인 세션" 에서는 정보가 빠져나갔어도 그 것을 이용하여 로그인할 수 없다고는 *단언할 수 없습니다.* 영속적인 cookies에는 *세션 하이잭* 이라는 공격을 받을 가능성이 항상 있습니다. 유저의 브라우저 상의 저장되는 정보에 대해서는 제 9장에서 조금 더 주의깊게 다루어보도록 해보겠습니다. 443 | 444 | 위 코드에서 `log_in` 이라고 하는 헬퍼 메소드를 정의하였기에, 드디어 유저 로그인하여 세션의 `create` 메소드를 완료하고, 유저의 프로필 페이지로 리다이렉트할 준비가 되었습니다. 작성한 코드는 아래와 같습니다. 445 | 446 | ```ruby 447 | # app/controllers/sessions_controller.rb 448 | class SessionsController < ApplicationController 449 | 450 | def new 451 | end 452 | 453 | def create 454 | user = User.find_by(email: params[:session][:email].downcase) 455 | if user && user.authenticate(params[:session][:password]) 456 | log_in user 457 | redirect_to user 458 | else 459 | flash.now[:danger] = 'Invalid email/password combination' 460 | render 'new' 461 | end 462 | end 463 | 464 | def destroy 465 | end 466 | end 467 | ``` 468 | 469 | 위 코드에서는 리다이렉트를 사용하고 있습니다만, 470 | 471 | `redirect_to user` 472 | 473 | 이 것은 [7.4.1](Chapter7.md#741-등록-Form의-완성) 에서 사용한 메소드와 동일한 구조입니다. Rails에서는 위의 코드를 자동적으로 변환하여 다음과 같은 프로필페이지로 라우팅합니다. 474 | 475 | `user_url(user)` 476 | 477 | 위 `sessions_controller.rb` 코드의 `create` 액션의 정의가 완성되었습니다. 8.4에서 정의할 로그인폼도 제대로 동작할 것입니다. 지금은 로그인해도 화면표시가 변하지 않기 때문에 유저가 로그인중인지 아닌지는 브라우저 세션을 직접 확인하지 않으면 알 수 없습니다. 이 상태로는 곤란하니, 로그인하고 있는 것을 알 수 있게 해봅시다. [8.2.2](#822-현재의-유저) 에서는 세션에 포함되어 있는 ID를 이용하여, 데이터베이스로부터 현재의 유저이름을 조회하여 화면에 표시할 예정입니다. 8.2.3에서는 어플리케이션의 레이아웃 상의 링크를 변경할 예정입니다. 이 링크를 클릭하면 현재 로그인하고 있는 유저의 프로필이 표시됩니다. 478 | 479 | ##### 연습 480 | 481 | 1. 유효한 유저로 실제로 로그인하여, 브라우저에서 cookies의 정보를 확인해보세요. 이 때, session값은 어떻게 되어있습니까? *Hint* : 브라우저에서 cookies를 알아보기 위한 방법은 알고 계신가요? 한 번 검색해보세요. 482 | 2. 1번을 한 것과 마찬가지로 `Expires` 의 값에 대해 알아보세요. 483 | 484 | ### 8.2.2 로그인 되어있는 유저 485 | 486 | 유저 ID를 일시적인 세션의 안에 안전하게 보관할 수 있게 되었습니다. 이번에는 해당 유저 ID를 다른 페이지에서도 사용할 수 있게 해봅시다. 그렇게 하기 위해서는 `current_user` 메소드를 정의하여, 세션ID에 대응하는 유저이름을 데이터베이스로부터 조회할 수 있도록 해봅시다. `current_user` 메소드의 목적은 다음과 같은 코드를 작성하기 위함입니다. 487 | 488 | `<%= current_user.name %>` 489 | 490 | 또한 이러한 코드로 유저의 프로필 페이지에 간단하게 리다이렉트할 수 있게 하려합니다. 491 | 492 | `redirect_to current_user` 493 | 494 | 이 때, 현재 유저를 검색하는 방법으로써 떠오르는 것은, 프로필 페이지와 마찬가지로 다음의 `find` 메소드를 사용하는 것입니다. 495 | 496 | `User.find(session[:user_id])` 497 | 498 | 그러나 [6.1.4](Chapter6.md#614-User-Object를-검색해보자) 에서 이미 해본 것 처럼, 유저 ID가 존재하지 않는 상태에서 `find` 를 사용하면 Exception이 발생해버립니다. find의 이러한 동작은, 프로필 페이지에서는 적절한 동작이었습니다. ID가 유효하지 않은 경우에는 Exception을 발생시키지 않으면 안되었기 때문입니다. 그러나 "유저가 로그인 하고 있지 않다." 등의 상황을 생각할 수 있는 이번 케이스 같은 경우에는, `session[:user_id]` 의 값은 `nil` 이 됩니다. 이 상태를 수정하기 위해서는 `create` 메소드 내부에 메일주소를 검색하는 것과 마찬가지인, `find_by` 메소드를 사용해야합니다. 단, 이번에는 `email` 이 아닌 `id` 로 검색합니다. 499 | 500 | `User.find_by(id:session[:user_id])` 501 | 502 | 이번에는 ID가 유효하지 않은 경우 (유저가 존재하지 않는 경우) 에도 메소드는 예외를 발생시키지 않고, `nil` 를 리턴합니다. 503 | 504 | 이 방법을 이용하여 `current_user` 를 다음과 같이 수정해봅시다. 505 | 506 | ```ruby 507 | def current_user 508 | if session[:user_id] 509 | User.find_by(id: session[:user_id]) 510 | end 511 | end 512 | ``` 513 | 514 | 세션에 유저 ID가 존재하지 않는 경우, 이 코드는 단순히 종료하고 자동적으로 `nil` 을 리턴하게 됩니다. 이것이 우리가 원하는 동작입니다. `current_user` 메소드가 하나의 리퀘스트내부의 처리에서 몇번이나 호출된다면, 호출된 횟수분 데이터베이스로의 조회가 발생하고, 결과적으로는 처리가 완료하기 까지의 시간이 길어지게 되어버립니다. 515 | 516 | 또한 Ruby의 관습에 따라, `User.find_by` 의 실행결과를 인스턴스 변수에 저장하는 방법도 생각해야합니다. 이렇게 함으로써 하나의 리퀘스트 내부에서 발생하는 데이터베이스로의 조회는 최초의 단 한 번만 실행하게 되며, 이후 발생하는 호출에 대해서는 인스턴스 변수의 결과를 재이용하게 됩니다. 조금 멋지지 않을수도 있습니다만, 이러한 생각이 Web 서비스를 고속화하게되는 중요한 테크닉 중 하나입니다. 517 | 518 | ```ruby 519 | if @current_user.nil? 520 | @current_user = User.find_by(id: session[:user_id]) 521 | else 522 | @current_user 523 | end 524 | ``` 525 | 526 | *OR 연산자* ( || ) ([4.2.3](Chapter4.md#423-오브젝트-메세지의-송수신)) 의 코드를 사용하면, 위 코드를 간단하게 작성할 수도 있습니다. 527 | 528 | ```ruby 529 | @current_user = @current_user || User.find_by(id: session[:user_id]) 530 | ``` 531 | 532 | 여기서 중요한 것은, User 오브젝트 그 자체의 논리값은 항상 True라는 것입니다. 그 덕분에 `@current_user` 에 아무것도 대입되어있지 않은 경우만 `find_by` 가 실행되어 쓸모없는 데이터베이스로의 검색이 발생하지 않게됩니다. 533 | 534 | 위 코드는 일단은 움직입니다만, 실제로는 아직 "Ruby 답지 않은", 올바른 코드가 아닙니다. `@current_user` 로의 대입은, Ruby에서는 다음과 같이 단축형으로 작성하는 것이 올바른 방법이긴 합니다. 535 | 536 | `@current_user ||= User.find_by(id:session[:user_id])` 537 | 538 | 처음 본 분이라면 꽤나 헷갈릴 수 있다고 생각합니다. 그러나 Ruby 커뮤니티에서는 이러한 " ||= " 기법이 널리 보급되어 있습니다. 539 | 540 | ###### 컬럼 8.1 "||=" 은 무엇인가? 541 | 542 | > 이 "||="(or equal) 이라고 하는 대입연산자는, Ruby에서는 널리 쓰이고 있으며, Ruby 개발자가 되고싶다면 이 연산자에 익숙해지는 것이 중요합니다. *or equal* 이라고 하는 개념은, 조금은 신기하게 생각하실 수도 있으나, 다른 것과 비교해본다면 그다지 신기하지 않을 수도 있습니다. 543 | > 544 | > 545 | > 546 | > 많은 컴퓨터 프로그램은 다음과 같은 기법으로 변수의 값을 하나씩 증가 시킬 수 있습니다. 547 | > 548 | > x = x + 1 549 | > 550 | > 그리고 Ruby (및 C, C++, Perl, Python, Java 등의 많은 프로그래밍 언어) 에서는 위의 연산은 아래와 같은 축약형으로 표현하는 것도 가능합니다. 551 | > 552 | > x += 1 553 | > 554 | > 다른 연산에 대해서도 마찬가지로 축약형을 사용할 수 있습니다. 555 | > 556 | > $ rails console 557 | > 558 | > x = 1 559 | > 560 | > => 1 561 | > 562 | > x += 1 563 | > 564 | > => 2 565 | > 566 | > x *= 3 567 | > 568 | > => 6 569 | > 570 | > x -= 8 571 | > 572 | > => -2 573 | > 574 | > x /= 2 575 | > 576 | > => -1 577 | > 578 | > 579 | > 580 | > 어떤 경우에도 ● 라고 하는 연산자가 있는 경우, " x = x ● y" 와 " x ●= y " 동작은 똑같이 동작합니다. 581 | > 582 | > 583 | > 584 | > Ruby에서는 "변수의 값이 nil이라면 변수에 대입하고, nil이 아니라면 대입하지 않는다(변수의 값을 바꾸지 않는다.)" 라고 하는 명령이 매우 많습니다. [4.2.3](Chapter4.md#423-오브젝트-메세지의-송수신) 에서 설명한 OR 연산자 || 를 사용한다면, 다음과 같이 작성할 수 있습니다. 585 | > 586 | > 587 | > 588 | > @foo 589 | > 590 | > => nil 591 | > 592 | > @foo = @foo || "bar" 593 | > 594 | > => "bar" 595 | > 596 | > @foo = @foo || "baz" 597 | > 598 | > => "bar" 599 | > 600 | > 601 | > 602 | > nil의 논리값은 false가 되기 때문에, @foo로의 첫 대입 "nil || "bar"" 의 결과값은 "bar" 가됩니다. 마찬가지로 두번쨰 대입 "@foo || "baz" " ("bar" || "baz" 등) 의 결과값은 "bar" 가 됩니다. Ruby에서는 nil과 false 를 제외하고, 모든 오브젝트의 논리값이 true가 되도록 설계되어 있습니다. 게다가 Ruby에서는 || 연산자를 몇번이나 연속해서 식의 안에 사용하는 경우, 항의 왼쪽으로부터 순서대로 평가하여 제일 처음으로 true가 된 시점에서 처리를 끝내도록 설계되어있습니다. 또한 이러한 ||식을 왼쪽에서 오른쪽으로 평가하여 연산자의 왼쪽 값이 제일 처음으로 true가 되는 시점에서 처리를 종료하는 이러한 방법은 *short-circuit evaluation* 이라고 합니다. 논리곱의 && 연산자도 비슷하게 설계되어 있으나 항을 왼쪽에서부터 평가하고 제일 처음에 false가 되는 시점에서 처리를 종료하게 되는 점이 다릅니다. 603 | > 604 | > 605 | > 606 | > 위 연산자를 콘솔세션 상에서 실제로 실행하여 비교해보면, @foo = @foo || "bar" 는 x = x ● y에 해당하고, ●가 || 로 바뀐 것 뿐이라는 것을 알 수 있습니다. 607 | > 608 | > ``` 609 | > x = x + 1 -> x += 1 610 | > x = x * 3 -> x *= 3 611 | > x = x - 8 -> x -= 8 612 | > x = x / 2 -> x /= 2 613 | > @foo = @foo || "bar" -> @foo ||= "bar" 614 | > ``` 615 | > 616 | > 이것으로 " `@foo = @foo || "bar" "` 와 "`@foo ||= "bar" "` 는 같은 처리를 한다는 것을 이해할 수 있으실 것입니다. 이 방법을 `current_user` 의 문맥에서 사용하면 다음과 같이 간결한 코드가 됩니다. 617 | > 618 | > 619 | > 620 | > @current_user || = User.find_by(id: session[:user_id]) 621 | > 622 | > 623 | > 624 | > 한 번 직접 작성해보세요. 625 | 626 | 앞서 말씀드린 간결한 기법을 `current_user` 메소드에 적용한 결과가 아래와 같습니다. (덧붙여서, 이러한 기술 방법은 `session[:user_id` 가 조금 불필요하게 반복되어 기술되어 있습니다. 9.1.2에서 이러한 중복을 해결해볼 것입니다.) 627 | 628 | ```RUBY 629 | # app/helper/sessions_helper.rb 630 | module SessionsHelper 631 | 632 | # 입력받은 유저로 로그인한다 633 | def log_in(user) 634 | session[:user_id] = user.id 635 | end 636 | 637 | # 현재 로그인 중인 유저 정보를 입력한다. 638 | def current_user 639 | if session[:user_id] 640 | @current_user ||= User.find_by(id: session[:user_id]) 641 | end 642 | end 643 | end 644 | ``` 645 | 646 | 위 `current_user` 메소드가 움직이게 되었습니다. 유저가 로그인해있는지 아닌지에 따라 어플리케이션의 동작을 변경할 수 있는 조건이 되었습니다. 647 | 648 | ##### 연습 649 | 650 | 1. Rails 콘솔을 사용하여 `User.find_by(id: ...)` 에 대응하는 유저가 검색되지 않았을 때, `nil` 을 리턴하는 지 확인해봅시다. 651 | 2. 위와 마찬가지로, 이버넹는 `:user_id` 키를 가지는 `session` 해시를 작성해봅시다. 아래의 코드의 순서로 `||=` 연산자가 제대로 동작하는지를 확인해봅시다. 652 | 653 | ```ruby 654 | >> session = {} 655 | >> session[:user_id] = nil 656 | >> @current_user ||= User.find_by(id: session[:user_id]) 657 | <여기에 무엇이 표시되나요?> 658 | >> session[:user_id]= User.first.id 659 | >> @current_user ||= User.find_by(id: session[:user_id]) 660 | <여기에 무엇이 표시되나요?> 661 | >> @current_user ||= User.find_by(id: session[:user_id]) 662 | <여기에 무엇이 표시되나요?> 663 | ``` 664 | 665 | ### 8.2.3 레이아웃 링크를 변경해보자 666 | 667 | 로그인 기능의 제일 처음으로, 구체적인 응용 방법으로서 유저가 로그인하고 있을 때와 그렇지 않을 때의 레이아웃을 변경해봅시다. 특히 아래와 같은 목업으로 표시한 것 처럼, "로그아웃" 링크, "유저 설정" 링크, "유저 리스트" 링크, "프로필 표시" 링크도 추가해봅시다. 아래 목업에서는 로그아웃의 링크와 프로필 링크는 "Account" 메뉴의 항목으로써 표시되는 점에 주목해주세요. 이후에 Bootstrap을 사용하여 아래와 같은 메뉴를 구현해볼 것 입니다. 668 | 669 | ![](../image/Chapter8/login_success_mockup.png) 670 | 671 | 필자라면 이 시점에서 메뉴에 대한 통합 테스트 코드를 작성할 것 같습니다. [컬럼 3.3](Chapter3.md#컬럼-33-결국-테스트는-언제-하는-것이-좋은가) 에서도 설명드렸다 시피, Rails의 테스트 툴을 몸에 익혀감에 따라, 아무것도 제시되어 있지 않은 상황이라도 필자처럼 이 시점에서 테스트를 작성해보고 싶어질 것입니다. 그렇다고는 해도 지금은 무리하지 않아도 됩니다. 이 테스트 또한 몇가지 새로운 개념을 배울 필요가 있기 때문에, 테스트 코드의 작성은 [8.2.4](#824-레이아웃의-변경을-테스트해보자) 이후에 해보겠습니다. 672 | 673 | 그럼 레이아웃의 링크를 변경하는 방법으로써 생각해봄직한 것은, ERB코드의 안에서 if-else 를 이용하여 조건에 맞게 표시하는 링크를 구분하는 것입니다. 674 | 675 | ``` erb 676 | <% if logged_in? %> 677 | // 로그인해있는 유저용의 링크 678 | <% else %> 679 | // 로그인해있지 않았을 때의 링크 680 | <% end %> 681 | ``` 682 | 683 | 이 코드를 작성하기 위해서는, 논리값을 리턴하는 `logged_in?` 메소드가 필요하기에, 일단 해당 메소드를 정의해봅시다. 684 | 685 | 유저가 로그인 중의 상태라는 것은, "session에 유저 ID가 존재한다" 라는 것입니다. 즉, `current_user` 가 `nil` 이 아니라는 상태를 의미합니다. 이것을 체크하기 위해서는 부정연산자 ([4.2.3](Chapter4.md#423-오브젝트-메세지의-송수신)) 가 필요하기 때문에, `!` 를 사용해봅시다. 사용한 `logged_in?` 메소드는 아래와 같습니다. 686 | 687 | ```ruby 688 | # app/helpers/sessions_helper.rb 689 | 690 | module SessionsHelper 691 | 692 | # 전달받은 유저로 로그인한다 693 | def log_in(user) 694 | session[:user_id] = user.id 695 | end 696 | 697 | # 현재 로그인 중인 유저를 리턴한다 698 | def current_user 699 | if session[:user_id] 700 | @current_user ||= User.find_by(id: session[:user_id]) 701 | end 702 | end 703 | 704 | # 유저가 로그인 해 있으면 true, 아니라면 false를 리턴한다. 705 | def logged_in? 706 | !current_user.nil? 707 | end 708 | end 709 | ``` 710 | 711 | 위 코드를 추가했으니, 이것으로 유저가 로그인했을 떄의 레이아웃을 변경할 준비가 되었습니다. 또한 새롭게 만들 링크는 4개입니다만, 이 중 다음 2개의 링크에 대해서는 일단은 작성하지는 않을 것입니다. (제 10장에서 작성합니다.) 712 | 713 | ``` 714 | <%= link_to "Users", '#' %> 715 | <%= link_to "Settings", '#' %> 716 | ``` 717 | 718 | 로그아웃 용의 링크는, 이전에 정의한 로그아웃 용 패스를 사용합니다. 719 | 720 | ``` 721 | <%= link_to "Log out", logout_path, method: :delete %> 722 | ``` 723 | 724 | 위 코드에서는 로그아웃 용 링크의 파라미터로써 해시값을 넘기고 있는 점을 확인해주세요. 이 해시는 HTTP의 DELETE 리퀘스트를 사용하도록 지시하고 있습니다. 프로필용 링크에 대해서도 마찬가지로 다음과 같이 변경해줍니다. 725 | 726 | ``` 727 | <%= link_to "Profile", current_user %> 728 | ``` 729 | 730 | 위 코드는 생략형으로, 아래와 같이 작성하는 것도 가능합니다. 731 | 732 | ``` 733 | <%= link_to "Profile", user_path(current_user) %> 734 | ``` 735 | 736 | 하지만 이번 경우에서는 `current_user` 를 사용하는 것이, Rails에 의해 `user_path(current_user)` 로 변환되어, 프로필 링크가 자동적으로 생성되도록 하는 것이 편리할 것입니다. 다음으로, 유저가 로그인*하고 있지 않은 경우* , 로그인용 패스를 사용하여 다음과 같이 로그인 폼으로의 링크를 작성합니다. 737 | 738 | ``` 739 | <%= link_to "Log in", login_path %> 740 | ``` 741 | 742 | 여기까지의 수정결과를 헤더의 파셜부분에 적용하면, 아래와 같이 될 것 입니다. 743 | 744 | ```ERB 745 | 746 | 747 | 748 | 775 | ``` 776 | 777 | 레이아웃에 새롭게 링크를 추가했습니다. 위 코드에는 Bootstrap의 드롭다운 메뉴 기능을 적용할 수 있는 상태가 되었습니다. 구체적으로는 Bootstrap에 포함되어 있는 CSS의 `dropdown` 클래스와 `dropdown-menu` 등을 사용합니다. 이러한 드롭다운기능을 유효하게 하기 위해서, Rails의 `application.js` 파일을 통해, Bootstrap의 적용되어 있는 Javascript 라이브러리와 jQuery를 읽어들일 수 있도록, 에셋 파이프라인에 작성합니다. 778 | 779 | ```javascript 780 | // app/assets/javascripts/appllication.js 781 | 782 | //= require rails-ujs 783 | //= require jquery 784 | //= require bootstrap 785 | //= require turbolinks 786 | //= require_tree . 787 | ``` 788 | 789 | 이 시점에서, 로그인 패스에 접속하여 유효한 유저 (유저 이름이 `example@railstutorial.org`, 패스워드는 `foobar`) 로서 로그인할 수 있게 되었습니다. 지금까지의 3개의 섹션에서의 코드를 효율좋게 테스트할 수 있게 되었습니다. 위 2개의 코드에 의하여 아래와 같이 드롭다운 메뉴와 로그인 중의 유저용의 링크가 표시되는 것을 확인해봅시다. 790 | 791 | 또한 브라우저를 완전히 닫으면, 예상대로 어플리케이션의 로그인 정보가 삭제되고, 다시 로그인해야하는 것을 확인해봅시다. 792 | 793 | ![](../image/Chapter8/profile_with_logout_link_3rd_edition.png) 794 | 795 | ##### 연습 796 | 797 | 1. 브라우저의 cookie 인스펙터 기능을 사용하여, 세션용의 cookie를 삭제해보세요. 헤더부분에 있는 링크는 로그인하지 않은 상태의 링크로 돌아가나요? 확인해봅시다. 798 | 2. 한 번 더 로그인해서, 헤더의 레이아웃이 변한 것을 확인해봅시다. 그 다음, 브라우저를 새로 열고, 다시 로그인하지 않은 상태로 되어있는지 확인해보세요. *주의* : 만약, 브라우저의 "닫기 전 상태로 되돌아가기" 기능을 사용하고 있다면, 세션정보도 복원될 가능성이 있습니다. 만약 이 기능을 사용하고 있는 경우에는, 잊지말고 Off로 해주시길 바랍니다. 799 | 800 | ### 8.2.4 레이아웃의 변경을 테스트해보자. 801 | 802 | 어플리케이션에서의 로그인 성공을 수동으로 확인해보았습니다. 일단 좀 더 진행하기 전에, 결합테스트 코드를 작성하여 해당 동작을 테스트로 표현하고, 이후의 회귀버그의 발생을 캐치할 수 있도록 해봅시다. 아래의 조작순서를 테스트 코드로 작성하여 확인해봅시다. 803 | 804 | 1. 로그인용의 패스로 접속한다. 805 | 2. 세션용의 패스에 유효한 정보를 POST로 보낸다. 806 | 3. 로그인 용 링크가 표지되지 않는 것을 확인한다. 807 | 4. 로그아웃용 링크가 표시되는 것을 확인한다. 808 | 5. 프로필용 링크가 표시되는 것을 확인한다. 809 | 810 | 위 변경을 확인하기 위해서는, 테스트 시에 등록이 끝난 유저로 로그인해놓을 필요가 있습니다. 당연하지만, 데이터베이스에 해당 유저가 등록되어있지 않으면 안될 것입니다. Rails에서는, 이러한 테스트요으이 데이터를 *fixture* 로 작성할 수 있습니다. 이 fixture를 사용하여, 테스트에 필요한 데이터를 test데이터베이스에 저장해놓을 수도 있습니다. [6.2.5](Chapter6.md#625-유니크성을-검증해보자) 에서는 메일의 유니크성 테스트가 통과할 수 있기 위한 디폴트값의 fixture를 삭제할 필요가 있었습니다. 이번에는 자신이 직접 빈 fixture 파일을 작성하여 데이터에 추가해봅시다. 811 | 812 | 813 | 814 | 현 시점에서의 테스트에서는, 유저는 한 명이면 충분합니다. 해당 유저에게는 유효한 이름과 유효한 메일주소를 설정합니다. 테스트중에 해당 유저로써 자동로그인하기 위해서, 해당 유저의 유효한 패스워드도 준비하여 Session 컨트롤러의 `create` 액션에 송신된 패스워드와 비교할 수 있도록 할 필요가 있습니다. 이전 6장에서의 데이터 모델을 한 번 더 확인해보면, `password_diegest` 속성을 유저의 fixture 에 추가하면 된다는 것을 알 수 있을 것입니다. 그 때문에, `digest` 메소드를 독자적으로 정의해보도록 합시다. 815 | 816 | 817 | 818 | [6.3.1](Chapter6.md#631-해시화된-비밀번호) 에서 설명드린 것 처럼, `has_secure_password` 에서 bcrypt 패스워드가 생성되기 때문에, 같은 방법으로 fixture용의 패스워드를 생성해봅시다. Rails의 [secure_password의 소스코드](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb) 를 확인해보면, 다음의 부분에서 패스워드가 생성되는 것을 알 수 있을 것입니다. 819 | 820 | ``` 821 | BCrypt::Password.create(string, cost: cost) 822 | ``` 823 | 824 | 위 `string` 은 해시화하는 문자열, `cost` 는 *코스트 파라미터* 로 불리는 값입니다. 코스트 파라미터에서는, 해시를 산출하기 위한 계산 코스트를 지정합니다. 코스트 파라미터의 값을 높게 설정하면, 해시로부터 오리지널의 패스워드를 계산하여 추축하는 것은 매우 어렵게 되기에, 실제 배포환경에서는 보안상 매우 중요합니다. 하지만 테스트 중에서는 코스트를 높게할 의미는 없기 때문에, `digest` 메소드의 계산은 되도록 가볍게 해놓고 싶습니다. 이 점에 대해서도, `secure_password` 의 소스코드에는 다음과 같은 행을 참고할 수 있습니다. 825 | 826 | ``` 827 | cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : 828 | BCrypt::Engine.cost 829 | ``` 830 | 831 | 조금 읽기 어려울 수 있습니다만, 코스트 파라미터를 테스트 중에는 최소화하고, 실제 배포환경에서는 제대로된 계산을 할 수 있도록하면 충분합니다. 또한 `?` ~ `:` 라고 하는 문법에서는, 9.2 에서 설명합니다. 832 | 833 | 이 `digest` 메소드는, 이후 여러가지 장면에서 활용할 것입니다. 예를 들어 9.1.1 에서는 `digest` 를 다시 사용하기 때문에, 이 `digest` 메소드는, User 모델 (`user.rb`) 에 작성해도록 합시다. 이 계산은 유저마다 적용할 필요는 없기 때문에, fixture 파일 등에서 일부러 유저 오브젝트에 액세스할 필요는 없습니다. (즉, 인스턴스 메소드에서 정의할 필요는 없습니다.) 그렇기 때문에, `digest` 메소드를 User 클래스 자신 내부에 작성하고 클래스 메소드로 사용하도록 합시다. (*클래스 메소드* 의 작성방법에 대해서는 [4.4.1](Chapter4.md#441-Constructor) 에서 간단히 설명했습니다.) 작성한 코드는 아래와 같습니다. 834 | 835 | ```ruby 836 | # app/models/user.rb 837 | 838 | class User < ApplicationRecord 839 | before_save { self.email = email.downcase } 840 | validates :name, presence: true, length: { maximum: 50 } 841 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i 842 | validates :email, presence: true, length: { maximum: 255 }, 843 | format: { with: VALID_EMAIL_REGEX }, 844 | uniqueness: { case_sensitive: false } 845 | has_secure_password 846 | validates :password, presence: true, length: { minimum: 6 } 847 | 848 | # 추가 # 849 | # 입력받은 문자열의 해시값을 리턴 850 | def User.digest(string) 851 | cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : 852 | BCrypt::Engine.cost 853 | BCrypt::Password.create(string, cost: cost) 854 | end 855 | # 추가 # 856 | end 857 | ``` 858 | 859 | 위 코드에서 `digest` 메소드를 작성했습니다. 유효한 유저를 나타낼 fixture 를 작성할 수 있게 되었습니다. 860 | 861 | ```ruby 862 | michael: 863 | name: Michael Example 864 | email: michael@example.com 865 | password_digest: <%= User.digest('password') %> 866 | ``` 867 | 868 | 위 코드에 있는 것 처럼, fixture 에서는 ERB코드를 작성할 수 있는 것을 확인해주세요. 869 | 870 | `<%= User.digest('password') %>` 871 | 872 | 위 ERB 코드에서 테스트 유저용의 유효한 패스워드를 작성할 수 있습니다. 873 | 874 | `has_secure_password` 에서 필요한 `password_digest` 속성은 이것으로 준비가 끝났습니다만, 해시화되지 않은 패스워드도 참조할 수 있으면 편리할 것입니다. 그러나 안타깝게도, fixture 에서는 이렇게 하는 것은 불가능합니다. 게다가 위 코드에서 `password` 속성을 추가하면, 그러한 컬럼은 데이터베이스에 존재하지 않는다고하는 에러가 발생합니다. 실제로, 데이터베이스에는 그러한 컬럼은 존재하지 않습니다. 이런 상황을 돌파하기 위해, 테스트용의 fixture 에서는 모두 같은 패스워드 `password` 라는 단어를 사용하도록 합시다. (이 것은 fixture에서 자주 사용되는 방법입니다.) 875 | 876 | 유효한 유저용의 fixture도 작성했습니다. 테스트에서는 다음과 같이 fixture 의 데이터를 참조할 수 있게 되었습니다. 877 | 878 | ``` 879 | user = users(:michael) 880 | ``` 881 | 882 | 위 `users` 는 fixture의 파일이름 `users.yml` 을 나타내며, `:michael` 이라고 하는 심볼은, fixture의 유저를 참조하기 위한 키값입니다. 883 | 884 | fixture의 유저를 참조할 수 있게 되었습니다. 레이아웃의 링크를 테스트할 준비가 되었습니다. 레이아웃의 링크를 테스트하기 위해서는 앞서 말씀드린 조작순서대로 테스트 코드를 작성해야합니다. 885 | 886 | ```ruby 887 | # test/integration/users_login_test.rb 888 | 889 | require 'test_helper' 890 | 891 | class UsersLoginTest < ActionDispatch::IntegrationTest 892 | 893 | def setup 894 | @user = users(:michael) 895 | end 896 | . 897 | . 898 | . 899 | test "login with valid information" do 900 | get login_path 901 | post login_path, params: { session: { email: @user.email, 902 | password: 'password' } } 903 | assert_redirected_to @user 904 | follow_redirect! 905 | assert_template 'users/show' 906 | assert_select "a[href=?]", login_path, count: 0 907 | assert_select "a[href=?]", logout_path 908 | assert_select "a[href=?]", user_path(@user) 909 | end 910 | end 911 | ``` 912 | 913 | 위 코드에서 다음 코드는 914 | 915 | `assert_redirectd_to @user` 916 | 917 | 리다이렉트를 하는 곳이 올바른지를 체크하는 코드입니다. 918 | 919 | `follow_redirect!` 920 | 921 | 또한, 위 코드는 해당 페이지에 실제로 이동합니다. 위 `user_login_test.rb` 에서는 로그인 용 링크가 표시되지 않는 것도 확인하고 있습니다. 이 체크는 로그인 패스의 링크가 페이지에 없는지를 판정합니다. 922 | 923 | ``` 924 | assert_select "a[href=?]", login_path, count: 0 925 | ``` 926 | 927 | `count: 0` 이라는 옵션을 `assert_select` 에 추가하면, 넘겨진 패턴에 일치하는 링크가 0인지를 확인하게 됩니다. 928 | 929 | 어플리케이션의 코드는 이미 동작할 수 있게 되어있습니다. 여기서 테스트를 실행하면 GREEN이 될 것입니다. 930 | 931 | `$ rails test test/integration/users_login_test.rb` 932 | 933 | ##### 연습 934 | 935 | 1. 시험삼아, Session 헬퍼의 `logged_in?` 메소드에서 `!` 를 삭제하여 위 테스트가 RED가 되는 것을 확인해봅시다. 936 | 2. 위에서 삭제한 부분을 다시 되돌리고, 테스트가 GREEN이 되는 것을 확인해봅시다. 937 | 938 | ### 8.2.5 회원가입 후의 로그인 939 | 940 | 이상으로, 인증시스템이 동작하게 되었습니다만, 이대로는 등록이 끝난 유저가 디폴트로는 로그인하고 있지 않은 상태이기 때문에, 유저가 당황할 가능성이 있습니다. 유저 등록이 끝나고 유저에게 수동으로 로그인하게 하면, 유저에게 불필요한 조작을 강제하는 꼴이 됩니다. 유저 등록을 하면서 로그인되게끔 해봅시다. 유저 등록을 하면서 로그인하게 하려면, Users 컨트롤러의 `create` 액션에 `log_in` 을 추가하는 것으로 끝납니다. 941 | 942 | ```ruby 943 | # ap/controllers/users_controller.rb 944 | class UsersController < ApplicationController 945 | 946 | def show 947 | @user = User.find(params[:id]) 948 | end 949 | 950 | def new 951 | @user = User.new 952 | end 953 | 954 | def create 955 | @user = User.new(user_params) 956 | if @user.save 957 | log_in @user #추가 958 | flash[:success] = "Welcome to the Sample App!" 959 | redirect_to @user 960 | else 961 | render 'new' 962 | end 963 | end 964 | 965 | private 966 | 967 | def user_params 968 | params.require(:user).permit(:name, :email, :password, 969 | :password_confirmation) 970 | end 971 | end 972 | ``` 973 | 974 | 위 코드를 테스트하기 위해서는, 이전에 작성해놓은 테스트 코드에 한 줄을 추가하여, 유저가 로그인 중인지를 체크해봅니다. 그렇게 하기 위해서는, 위에서 정의한 `logged_in?` 헬퍼 메소드와는 별도로, `is_logged_in?` 헬퍼 메소드를 정의해놓는다면 편리할 것입니다. 이 헬퍼 메소드는, 테스트의 세션에 유저가 있다면 `true`, 그렇지 않으면 `false` 를 리턴합니다. 아쉽게도, 헬퍼 메소드는 테스트에서부터 호출할 수 없습니다. `session` 메소드는 테스트에서도 이용할 수 있기 때문에, 이것을 대신 사용해봅시다. 여기에서는 실수를 막기 위해, `logged_in?` 대신에 `is_logged_in?` 을 사용하여, 헬퍼 메소드 이름이 테스트 헬퍼와 Session 헬퍼에서 호출되는 것을 막기 위함입니다. 975 | 976 | ```ruby 977 | # test/test_helper.rb 978 | # 테스트 중의 로그인 상태의 논리값을 리턴하는 메소드 979 | 980 | ENV['RAILS_ENV'] ||= 'test' 981 | . 982 | . 983 | . 984 | class ActiveSupport::TestCase 985 | fixtures :all 986 | 987 | # 테스트 유저가 로그인 중인지 상태를 리턴하는 메소드 988 | def is_logged_in? 989 | !session[:user_id].nil? 990 | end 991 | end 992 | ``` 993 | 994 | 위 코드를 작성하면, 유저 등록이 끝난 유저가 로그인한 상태인지를 알 수 있게 됩니다. 995 | 996 | ```ruby 997 | # test/integration/users_signup_test.rb 998 | require 'test_helper' 999 | 1000 | class UsersSignupTest < ActionDispatch::IntegrationTest 1001 | . 1002 | . 1003 | . 1004 | test "valid signup information" do 1005 | get signup_path 1006 | assert_difference 'User.count', 1 do 1007 | post users_path, params: { user: { name: "Example User", 1008 | email: "user@example.com", 1009 | password: "password", 1010 | password_confirmation: "password" } } 1011 | end 1012 | follow_redirect! 1013 | assert_template 'users/show' 1014 | assert is_logged_in? #추가 1015 | end 1016 | end 1017 | ``` 1018 | 1019 | 이 것으로 테스트를 실행하면 GREEN이 될 것입니다. 1020 | 1021 | `$ rails test` 1022 | 1023 | ##### 연습 1024 | 1025 | 1. Users 컨트롤러의 `create` 액션에서 `log_in` 라인을 코멘트아웃하면, 테스트 코드의 실행결과가 RED가 되나요? 아니면 GREEN이 되나요? 확인해봅시다. 1026 | 2. 현재 사용하고 있는 테스트 에디터의 기능을 사용하여, Users 컨트롤러의 코드를 한 번에 코멘트할 수 있는지 알아봅시다. 또한 코멘트처리 전후로 테스트 코드를 실행하여, 코멘트하면 RED로, 코멘트를 풀면 GREEN이 되는 것을 확인해봅시다. 1027 | 1028 | 1029 | 1030 | ## 8.3 Logout 1031 | 1032 | [8.1](#81-Session) 에서 말씀드린 것 처럼, 이 어플리케이션에서 다루는 인증 모델에서는, 유저가 명시적으로 로그아웃할 떄까지는 로그인 상태를 유지해야만 합니다. 이번 섹션에서는 이러한 처리를 위해 필요한 로그아웃 기능을 추가해보도록 하겠습니다. 로그아웃용 링크는 이전에 작성해놓았기 때문에, 유저 세션을 없애기 위한 유효한 액션을 컨트롤러에서 작성하기만 하면 끝날 것입니다. 1033 | 1034 | 지금까지, Sessions 컨트롤러의 액션은 RESTful 룰에 따라 작성해왔습니다. `new` 로그인 페이지를 표시하고, `create` 에서 로그인을 완료하는 이러한 처리들이 RESTful한 처리입니다. 세션을 없애기 위한 `destroy` 액션도, 마찬가지 방법으로 작성해볼 것입니다. 단, 로그인의 경우와는 다르게, 로그아웃의 처리는 한 곳에서만 처리하기 때문에, `destroy` 액션에 직접 로그아웃 처리를 작성해볼 것입니다. 9.3에서도 설명할 예정입니다만, 이러한 설계(및 약간의 리팩토링) 덕분에 인증 메커니즘의 테스트를 실행하기 쉬워집니다. 1035 | 1036 | 로그아웃의 처리에서는 이전에 `log_in` 메소드의 실행결과를 취소합니다. 즉, 세션에서 유저 ID를 삭제합니다. 다음과 같이 `delete` 메소드를 실행합니다. 1037 | 1038 | `session.delete(:user_id)` 1039 | 1040 | 위 코드에서, 현재의 유저를 `nil` 로 설정합니다. 이번에는 로그인하지 않은 경우, 바로 루트 URL로 리다이렉트할 수 있게 하고 있기 때문에, 이 코드에서 특별히 문제가 될만한 것은 없습니다. 다음으로 `log_in` 및 관련 메소드와 마찬가지로, Session헬퍼 모듈에 놓을 `log_out` 메소드를 아래와 같이 정의해봅시다. 1041 | 1042 | ```ruby 1043 | # app/helpers/sessions_helper.rb 1044 | module SessionsHelper 1045 | 1046 | # 입력받은 유저로 로그인합니다. 1047 | def log_in(user) 1048 | session[:user_id] = user.id 1049 | end 1050 | . 1051 | . 1052 | . 1053 | # 현재의 유저를 로그아웃 합니다. 1054 | def log_out 1055 | session.delete(:user_id) 1056 | @current_user = nil 1057 | end 1058 | end 1059 | ``` 1060 | 1061 | 여기서 정의한 `log_out` 메소드는, Sessions 컨트롤러의 `destroy` 액션에서도 마찬가지로 사용할 수 있습니다. 1062 | 1063 | ```ruby 1064 | # app/controllers/sessions_controller.rb 1065 | class SessionsController < ApplicationController 1066 | 1067 | def new 1068 | end 1069 | 1070 | def create 1071 | user = User.find_by(email: params[:session][:email].downcase) 1072 | if user && user.authenticate(params[:session][:password]) 1073 | log_in user 1074 | redirect_to user 1075 | else 1076 | flash.now[:danger] = 'Invalid email/password combination' 1077 | render 'new' 1078 | end 1079 | end 1080 | 1081 | def destroy # 추가 1082 | log_out 1083 | redirect_to root_url 1084 | end 1085 | end 1086 | ``` 1087 | 1088 | 로그아웃 기능을 테스트하기 위해, 유저 로그인의 테스트에 순서를 약간 추가해놓읍시다. 로그인 후, `delete` 메소드 에서 DELETE 리퀘스트를 로그아웃용 패스에 보내어, 유저가 로그아웃하여 루트 URL로 리다이렉트하게 되는 것을 확인합니다. 로그인용 링크가 다시 표시되는 것과 로그아웃용 링크와 프로필용 링크가 표시되지 않는 것을 확인해봅시다. 순서대로 추가한 테스트는 아래와 같습니다. 1089 | 1090 | ```ruby 1091 | # test/integration/users_login_test.rb 1092 | require 'test_helper' 1093 | 1094 | class UsersLoginTest < ActionDispatch::IntegrationTest 1095 | . 1096 | . 1097 | . 1098 | test "login with valid information followed by logout" do 1099 | get login_path 1100 | post login_path, params: { session: { email: @user.email, 1101 | password: 'password' } } 1102 | assert is_logged_in? # 추가 1103 | assert_redirected_to @user 1104 | follow_redirect! 1105 | assert_template 'users/show' 1106 | assert_select "a[href=?]", login_path, count: 0 1107 | assert_select "a[href=?]", logout_path 1108 | assert_select "a[href=?]", user_path(@user) 1109 | # 추가 1110 | delete logout_path 1111 | assert_not is_logged_in? 1112 | assert_redirected_to root_url 1113 | follow_redirect! 1114 | assert_select "a[href=?]", login_path 1115 | assert_select "a[href=?]", logout_path, count: 0 1116 | assert_select "a[href=?]", user_path(@user), count: 0 1117 | # 추가 1118 | end 1119 | end 1120 | ``` 1121 | 1122 | 테스트에서 `is_logged_in?` 헬퍼메소드를 사용할 수 있게 된 덕분에, 유효한 정보를 세션용 패스에 POST 로 송신한 직후에 `assert is_logged_in?` 에서 간단하게 테스트할 수 있습니다. 1123 | 1124 | 세션용의 `destroy` 액션의 정의와 테스트도 완성했습니다. 드디어 sample 어플리케이션의 기본이 되는 유저 등록, 로그인, 로그아웃 기능을 모두 오나성했습니다. 지금 이 시점에서도 테스트 코드는 GREEN이 될 것 입니다. 1125 | 1126 | `$ rails test` 1127 | 1128 | ##### 연습 1129 | 1130 | 1. 브라우에서 [Log out] 링크를 클릭하여, 어떠한 변화가 일어나는지 확인해봅시다. 또한, 직전에 수정한 테스트코드에서 3개의 스텝을 실행해보고, 제대로 움직이는지 확인해봅시다. 1131 | 2. cookies의 내용을 알아보고, 로그아웃 후에 session이 정상적으로 삭제되었는지를 확인해봅시다. 1132 | 1133 | ## 8.4 마지막으로 1134 | 1135 | 이번 챕터에서는 sample 어플리케이션의 기본적인 로그인 기능 (인증 시스템)을 구현해보았습니다. 다음 챕터에서는 이 로그인 기능을 좀 더 개선하여, 세션보다 오래 로그인 정보를 가지고 있는 방법에 대해 배워볼 것이빈다. 1136 | 1137 | 그러면 다음 장으로 가기 전에, 이번 장에서의 변경사항을 master 브런치에 머지해봅시다. 1138 | 1139 | ``` 1140 | $ rails test 1141 | $ git add -A 1142 | $ git commit -m "Implement basic login" 1143 | $ git checkout master 1144 | $ git merge basic-login 1145 | ``` 1146 | 1147 | 머지 후, 리모트 레포지토리에 Push 해봅시다. 1148 | 1149 | ``` 1150 | $ rails test 1151 | $ git push 1152 | ``` 1153 | 1154 | 마지막으로 Heroku에 배포해봅시다. 1155 | 1156 | `$ git push heroku` 1157 | 1158 | ### 8.4.1 8장의 정리 1159 | 1160 | - Rails의 `session` 메소드를 사용하면, 어떤 페이지에서 다른 페이지로 이동할 때의 상태를 유지할 수 있다. 일시적인 상태의 보존에는 cookies를 사용할 수 있다. 1161 | - 로그인폼에는 유저가 로그인하기 위해 새로운 세션을 생성한다. 1162 | - `flash.now` 메소드를 사용하면, 이미 표시된 페이지에도 플래시메세지를 표시할 수 있다. 1163 | - 테스트 주도 개발은, 회귀 버그를 막기에 편리하다. 1164 | - `session` 메소드를 사용하면, 유저 ID 를 브라우저에 일시적으로 저장할 수 있다. 1165 | - 로그인 상태에 따라 페이지 내부에 표시하는 링크를 바꿀 수 있다. 1166 | - 통합 테스트에서는 라우팅, 데이터베이스의 갱신, 레이아웃의 변경이 제대로 되는지를 확인할 수 있다. 1167 | 1168 | --------------------------------------------------------------------------------