베스트 북: 내 Fullstack React 및 Ruby On Rails 애플리케이션

이것은 내가 Flatiron을 위해 두 번째이자 마지막 프로젝트이다. 이 단계는 Ruby on Rails에 관한 것이다.내가 알기로는 루비 온 레일스는 5년 전처럼 그렇게 인기가 없었지만, 여전히 좋은 이해 언어로 백엔드 웹 개발을 배울 수 있도록 도와주었다.

프로젝트에서 사용한 내용

  • 내 프런트엔드
  • 의 React 프레임워크
  • 내 프런트엔드 라우터의 React 라우터
  • 스타일링용 Mui
  • 내 백엔드 Ruby on Rails
  • ActiveRecord는 내 모델과 데이터베이스와의 통신을 처리
  • 프로젝트 개요


    베스트북스라는 독서클럽 앱을 만들었어요.친구와 함께 독서 클럽을 만들 수 있다. 목표를 추적하고 토론 문제를 만들고 토론 문제를 논평할 수 있다.

    베스트 도서 모형


    이용자

  • 서우회 사용자가 많음
  • 댓글이 많네요
  • 도서 클럽 사용자

  • 사용자 전용
  • 독서 클럽에 속한다
  • 독서 클럽

  • 한 권의 책에 속한다
  • 독서 클럽에 속한다
  • 목표가 많음
  • 많은 부팅 문제
  • 목표

  • 독서클럽에 속하는 책
  • 유도 문제

  • 독서클럽에 속하는 책
  • 댓글이 많네요
  • 코멘트

  • 사용자 전용
  • 부팅 문제
  •                                                           :deadline
                                                              :pages
                                                              :priority
                                                              :complete
                                                              :notes
                                                              :meetingURL
                                                              :bookclub_book_id
                                                              Goal
                                                              V
                                                              |
    User --------------< BookClubUser >---- BookClub ----< BookClubBook >-------- Book
    :email               :user_id           :name          :bookclub_id           :imageURL
    :password_digest     :bookclub_id                      :book_id               :title
    :first_name          :isAdmin                          :archived              :series
    :last_name                                             :status                :author
    :location                                              :suggested_by          :description
    :profile_color                                         :current               :pages
    |                                                       |                     :publicationDate
    |                                                       |                     :genres
    |                                                       |
    |                                                       |
    |                                                       ^
      -------------------< Comment >----------------- GuideQuestion
                           :user_id                   :bookclub_book_id 
                           :guide_question_id         :chapter
                           :comment                   :question
    

    항목 중의 장애


    사용자 생성 및 지속적인 로그인 처리


    이것은 제가 처음으로 사용자 기능을 만들 수 있는 항목입니다. 계정을 만들고 로그인하고 로그아웃할 수 있으며 쿠키를 사용하여 지속적으로 로그인할 수 있습니다.bcrypt gem을 사용하여 보호 암호를 만들고 RoR에서 세션을 추적하여 사용자의 로그인을 유지합니다.

    사용자 및 Cookies 구현


    쿠키 사용
    API로 RoR을 사용하기 때문에 쿠키를 사용하는 기능을 다시 활성화해야 합니다.
    #application.rb
    
    require_relative "boot"
    require "rails"
    
    module BestBooksApi
     class Application < Rails::Application
       config.load_defaults 6.1
       config.api_only = true
    
       # Adding back cookies and session middleware
       config.middleware.use ActionDispatch::Cookies
       config.middleware.use ActionDispatch::Session::CookieStore
    
       # Use SameSite=Strict for all cookies to help protect against CSRF
       config.action_dispatch.cookies_same_site_protection = :strict
     end
    end
    
    
    세션 및 사용자 라우팅
    #routes.rb
    
    Rails.application.routes.draw do
      namespace :api do
        resources :users, only: [:index, :destroy, :update]
        post "/signup", to: "users#create"
        get "/me", to: "users#show"
    
        post "/login", to: "sessions#create"
        delete "/logout", to: "sessions#destroy"
      end
    
      get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
    
    end
    

    사용자 작성



    새 사용자를 만들 때 사용자가 로그인할 수 있도록 세션 쿠키를 만듭니다.일단 사용자가 데이터베이스에 입력되면 사용자 정보는 전방으로 설정될 것이다.
    백엔드
    #user_controller.rb
    class Api::UsersController < ApplicationController
    
       skip_before_action :authorize, only: :create
    
       def create
        user = User.create(user_params)
    
        if user.valid?
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
        end
       end
    
       def show
        user = @current_user
        render json: user, include: ['bookclubs', 'bookclubs.users', 'bookclubs.bookclub_books', 'bookclubs.bookclub_books.book', 'bookclubs.bookclub_books.goals', 'bookclubs.bookclub_books.guide_questions', 'bookclubs.bookclub_books.guide_questions.comments']
        # render json: user
       end
    
       def update
        user = @current_user
        user.update(user_params)
        render json: user, status: :accepted
       end
    
       def destroy
        @current_user.destroy
        head :no_content
       end
    
       private
    
       def user_params
        params.permit(:email, :first_name, :last_name, :location, :profile_color, :password, :password_confirmation, :bookclubs)
       end
    end
    
    #user_serializer.rb
    
    class UserSerializer < ActiveModel::Serializer
      attributes :id, :email, :first_name, :last_name, :full_name, :location, :profile_color
    
      has_many :bookclubs
    
      def full_name
        "#{self.object.first_name} #{self.object.last_name}"
      end
    end
    
    프런트엔드
    import * as React from 'react'
    import { Button, TextField, Alert, Stack } from '@mui/material'
    import { useNavigate } from 'react-router'
    
    const FormSignup = ({ onLogin }) => {
      const [firstName, setFirstName] = React.useState('')
      const [lastName, setLastName] = React.useState('')
      const [email, setEmail] = React.useState('')
      const [password, setPassword] = React.useState('')
      const [passwordConfirmation, setPasswordConfirmation] = React.useState('')
      const [location, setLocation] = React.useState('')
      const [errors, setErrors] = React.useState([])
    
      let navigate = useNavigate()
    
      const handleSubmit = (e) => {
        e.preventDefault()
        setErrors([])
        fetch('/api/signup', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            first_name: firstName,
          last_name: lastName,
            password,
            password_confirmation: passwordConfirmation,
            email,
            location,
            profile_color: '#004d40',
        }),
        }).then((response) => {
        if (response.ok) {
            response
            .json()
            .then((user) => onLogin(user))
            .then(navigate('/'))
        } else {
            response.json().then((err) => setErrors(err.errors || [err.error]))
        }
        })
      }
    
      return (
        <form onSubmit={handleSubmit} className='form'> 
        </form>
      )
    }
    
    export default FormSignup
    

    사용자 로그인 유지



    프로그램이 처음 사용자에게 불러올 때 세션 쿠키가 존재하는지 확인하기 위해 /me 요청을 보냅니다.쿠키가 존재하지 않으면 권한이 부여되지 않은 오류를 전방으로 보냅니다.권한 수여 방법은 application_controller.rb 파일에 설정되어 있습니다.
    백엔드
    class ApplicationController < ActionController::API
       include ActionController::Cookies
       rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
    
    
       before_action :authorize
        private
        def authorize
        @current_user = User.find_by_id(session[:user_id])
        render json: { errors: ["Not Authorized"] }, status: :unauthorized unless @current_user
       end
        def render_unprocessable_entity(exception)
        render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
       end
     end
    
    프런트엔드
     React.useEffect(() => {
        // auto-login
        handleCheckLogin()
    
        //fetch list recommendations
        handleFetchRecommendations()
      }, [])
    
      const handleCheckLogin = () => {
        fetch('/api/me').then((response) => {
        if (response.ok) {
            response.json().then((user) => {
            setUser(user)
            })
        } else {
            response.json().then((err) => console.log(err))
        }
        })
      }
    
    

    베스트 도서 등록 및 퇴장


    /login/logout 라우팅이 세션 컨트롤러로 전송됩니다.사용자와 비밀번호를 찾으면 세션을 만들고 사용자 정보를 전방으로 보냅니다.사용자가 로그아웃하면 세션 쿠키가 삭제됩니다.
    백엔드
    #sessions_controller.rb
    
    class Api::SessionsController < ApplicationController
       skip_before_action :authorize, only: :create
    
       def create
        user = User.find_by(email: params[:email])
    
        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { errors: ["Invalid username or password"] }, status: :unauthorized
        end
       end
    
       def destroy
        session.delete :user_id
        head :no_content
       end
    end
    
    프런트엔드
     import * as React from 'react'
    import { Button, TextField, Alert, Stack } from '@mui/material'
    import { useNavigate } from 'react-router'
    
    //login
    const FormLogin = ({ onLogin }) => {
      const [email, setEmail] = React.useState('')
      const [password, setPassword] = React.useState('')
      const [errors, setErrors] = React.useState([])
    
      let navigate = useNavigate()
    
      const handleSubmit = (e) => {
        e.preventDefault()
        setErrors([])
        fetch('/api/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            password,
            email,
        }),
        }).then((response) => {
        if (response.ok) {
            response
            .json()
            .then((user) => onLogin(user))
             .then(navigate('/'))
        } else {
            response.json().then((err) => setErrors(err.errors || [err.error]))
        }
        })
      }
    
      return (
        <form onSubmit={handleSubmit} className='form'>
        </form>
      )
    }
    
    export default FormLogin
    
    //logout
      const handleLogout = () => {
        fetch('/api/logout', {
        method: 'DELETE',
        }).then((response) => {
        if (response.ok) setUser(null)
        })
      }
    
    

    도서 클럽을 운영하다



    사용자는 새로운 도서 클럽을 창설하고 도서 클럽 정보를 갱신하며 도서 클럽에 도서를 추가하고 도서 클럽을 삭제할 수 있다.책클럽 페이지에 접근할 때마다 백엔드를 추출하여 책클럽 정보를 전송합니다.

    도서 클럽의 실시


    백엔드
    GET가 어떤 도서 클럽을 검색해 달라고 요청할 때마다 대부분의 데이터베이스 정보가 발송된다.책 클럽을 만들면 현재 로그인한 사용자를 사용하여 자동 책 클럽 사용자를 만들고 책 클럽 관리자가 됩니다.
    #bookclubs_controller.rb
    
    class Api::BookclubsController < ApplicationController
        rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
        before_action :set_bookclub, only: [:show, :destroy]
        skip_before_action :authorize, only: [:index, :show]
    
        def index
            bookclubs = Bookclub.all
            render json: bookclubs, status: :ok
        end
    
        def show
            bookclub = @bookclub
            render json: bookclub, include: ['users', 'bookclub_books', 'bookclub_books.book', 'bookclub_books.goals', 'bookclub_books.guide_questions', 'bookclub_books.guide_questions.comments'], status: :ok
        end
    
        def create
            user = @current_user
            bookclub = user.bookclubs.create(bookclub_params)
            bookclub_user = user.bookclub_users.find_by(bookclub_id: bookclub.id)
            bookclub_user.isAdmin = true
            bookclub_user.save
    
    
            render json: bookclub, include:  ['users', 'bookclub_books', 'bookclub_books.book', 'bookclub_books.goals', 'bookclub_books.guide_questions', 'bookclub_books.guide_questions.comments'], status: :created
    
        end
    
        def destroy
            @bookclub.destroy
            head :no_content
        end
    
        private
    
        def bookclub_params
            params.permit(:name)
        end
    
        def set_bookclub
            @bookclub = Bookclub.find(params[:id])
        end
    
        def render_not_found_response
            render json: { error: 'Book Club Not Found' }, status: :not_found
        end
    
    end
    
    프런트엔드
    반응 라우터가 있는 라우팅
    <Route path='bookclub' element={<BookClubPage />}>
                    <Route
                    path=':id'
                    element={
                        <BookClub
                        user={user}
                        loading={loading}
                        bookclub={currentBookclub}
                        handleFetchBookClub={handleFetchBookClub}
                        />
                    }>
                    <Route
                        path='admin-dashboard'
                        element={
                        <BookClubDashboard
                            bookclub={currentBookclub}
                            setCurrentBookclub={setCurrentBookclub}
                            fetchUser={handleCheckLogin}
                            user={user}
                        />
                        }
                    />
                    <Route
                        path='current-book'
                        element={
                        <BookClubCurrenBook
                            bookclub={currentBookclub}
                            user={user}
                            loading={loading}
                            handleFetchBookClub={handleFetchBookClub}
                        />
                       }
                    />
                    <Route
                        path='wishlist'
                        element={
                        <BookClubWishlist
                            bookclub={currentBookclub}
                            user={user}
                         setCurrentBookclub={setCurrentBookclub}
                            setCurrentBook={setCurrentBook}
                            handleFetchBookClub={handleFetchBookClub}
                        />
                        }
                    />
                      <Route
                        path='history'
                        element={
                        <BookClubHistory
                            bookclub={currentBookclub}
                            user={user}
                            setCurrentBookclub={setCurrentBookclub}
                            handleFetchBookClub={handleFetchBookClub}
                        />
                        }
                    />
                    </Route>
    </Route>
    
    신분증을 가지고 독서 클럽에 가다
     const handleFetchBookClub = (bookClubId) => {
        setCurrentBookclub(null)
        setLoading(true)
        fetch(`/api/bookclubs/${bookClubId}`)
        .then((response) => response.json())
        .then((data) => {
            setLoading(false)
            setCurrentBookclub(data)
        })
        .catch((err) => {
            console.error(err)
        })
      }
    
    import * as React from 'react'
    import { Grid, Typography } from '@mui/material'
    import BookClubMenu from '../../components/nav/BookClubMenu'
    import Loading from '../../components/Loading'
    import { useParams, Outlet } from 'react-router'
    
    const Bookclub = ({ user, handleFetchBookClub, loading, bookclub }) => {
      let params = useParams()
    
      React.useEffect(() => {
        handleFetchBookClub(params.id)
      }, [])
    
      return loading ? (
        <Grid container alignItems='center' justifyContent='center'>
        <Loading />
        </Grid>
      ) : (
        <>
        {bookclub &&
            (bookclub.error || bookclub.errors ? (
            <Grid
                item
                container
                flexDirection='column'
                wrap='nowrap'
                alignItems='center'>
                <Typography component='h1' variant='h4' align='center'>
                {bookclub.error ? bookclub.error : bookclub.errors}
                </Typography>
            </Grid>
            ) : (
            <>
                <Grid item xs={12} md={4} lg={3}>
                <BookClubMenu user={user} bookclub={bookclub} />
                </Grid>
    
                <Grid
                item
                container
                flexDirection='column'
                spacing={3}
                xs={12}
                md={8}
                lg={9}
                sx={{ pl: 4 }}>
                <Outlet />
                </Grid>
            </>
            ))}
        </>
      )
    }
    
    export default Bookclub
    

    Book Club 사용자를 업데이트하는 데 어려움



    내가 이 앱을 사용하는 가장 큰 어려움 중 하나는 책 클럽 사용자를 업데이트하는 것이다.이 과정에서 나는 몇 번이나 하마터면 이 앱을 포기할 뻔했다.도서 클럽에 다른 사용자를 추가하는 능력은 나의 응용 프로그램의 기능에 매우 중요하다.어쨌든 독서 클럽에 한 사람만 있다면 그것은 무엇입니까?
    프로젝트 개요에서 알 수 있듯이, 나는 다대다 관계를 가진 3개의 연결표를 만들어야 한다.이것은 내가 처음으로 연합표를 처리하는 것이기 때문에, 나는 어디에서 업데이트와 통화를 진행하기가 매우 어렵다.
    노선.
    나는 책 클럽 컨트롤러에서 책 클럽 사용자를 위한 컨트롤러를 만드는 것이 아니라 책 클럽 사용자와 관련된 모든 호출을 처리하기로 결정했다.나는 여전히 이것이 변혁 호소를 실시하는 가장 좋은 방식인지는 확실하지 않지만, 나는 이것이 청구를 한 후에 필요한 정보를 앞에서 얻는 가장 효과적인 방식이라고 생각한다.
    Rails.application.routes.draw do
      # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
      # Routing logic: fallback requests for React Router.
      # Leave this here to help deploy your app later!
      namespace :api do
        patch "/bookclubs/:id/current-book", to: "bookclubs#current_book"
        resources :bookclubs
    
        resources :books, only: [:show, :create, :destroy]
    
        resources :bookclub_books, only: [:index, :destroy, :update]
    
        resources :goals, only: [:show, :create, :update, :destroy]
    
        resources :guide_questions, only: [:show, :create, :update, :destroy]
    
        resources :comments, only: [:create, :destroy]
    
      end
    
      get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
    
    end
    
    프런트엔드
    만약 사용자가 도서 클럽의 관리자라면, 그들은 관리자 계기판을 방문할 수 있을 것이다.이곳에서 사용자는 도서 클럽의 명칭을 갱신할 수 있다.사용자 보기, 추가 및 삭제도서 클럽의 관리인을 교체한다.
    admin dashboard 폼을 불러올 때, 모든 사용자를 수신하기 위해 백엔드로 추출됩니다.이를 통해 관리자는 이미 Best Books 계정을 가진 모든 사용자를 추가할 수 있습니다.관리자는 새 관리자를 설정할 수 있지만 삭제할 수 없습니다.관리 대시보드에 액세스할 수 있는 경우 관리자가 됩니다.
    import * as React from 'react'
    import '../../css/Form.css'
    import { useNavigate } from 'react-router-dom'
    
    const FormBookClub = ({ bookclub, setCurrentBookclub, fetchUser }) => {
      let navigate = useNavigate()
      const [name, setName] = React.useState(bookclub ? bookclub.name : '')
      const [adminId, setAdminId] = React.useState(
        bookclub ? bookclub.admin.id : null
      )
      const [currentUsers, setCurrentUsers] = React.useState(
        bookclub ? bookclub.users : []
      )
      const [deleteUsers, setDeleteUsers] = React.useState([])
      const [allUsers, setAllUsers] = React.useState([])
    
      const [newUsers, setNewUsers] = React.useState([])
      const [errors, setErrors] = React.useState([])
      const [updated, setUpdated] = React.useState(false)
      const [loading, setLoading] = React.useState(false)
    
      React.useEffect(() => {
        setName(bookclub ? bookclub.name : '')
        setAdminId(bookclub ? bookclub.admin.id : null)
        setCurrentUsers(bookclub ? bookclub.users : [])
    
        fetch('/api/users')
        .then((response) => response.json())
        .then((data) => setAllUsers(data))
        .catch((err) => {
            console.error(err)
        })
      }, [bookclub])
    
      const handleSubmit = (e) => {
        e.preventDefault()
        setErrors([])
        setLoading(true)
        setUpdated(false)
    
        const deleteUserIds = deleteUsers ? deleteUsers.map((user) => user.id) : []
        const addUserIds = newUsers ? newUsers.map((user) => user.id) : []
    
        fetch(`/api/bookclubs/${bookclub.id}`, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
        },
        body: JSON.stringify({
            name,
            admin_id: adminId,
            delete_users: deleteUserIds,
            add_users: addUserIds,
        }),
        }).then((response) => {
        setLoading(false)
        setDeleteUsers([])
        setNewUsers([])
        if (response.ok) {
            setUpdated(true)
            response.json().then((data) => {
            setCurrentBookclub(data)
            fetchUser()
            })
        } else {
            response.json().then((err) => {
            if (err.exception) {
                fetchUser()
               navigate('/profile/my-bookclubs')
            } else {
                setErrors(err.errors || [err.error])
            }
            })
        }
        })
      }
    
      const handleDeleteCurrentMemberClick = (user) => {
        setDeleteUsers((prevUsers) => [...prevUsers, user])
      }
    
      const handleAddCurrentMemberClick = (user) => {
        const newDeltedUsers = deleteUsers.filter((u) => u.id !== user.id)
        setDeleteUsers(newDeltedUsers)
      }
    
      let filteredOptions = () => {
        const currentUserIds = currentUsers
        ? currentUsers.map((user) => user.id)
        : []
    
        const allUserIds = allUsers ? allUsers.map((user) => user.id) : []
    
        const filteredIds = allUserIds.filter((id) => currentUserIds.includes(id))
    
        const filteredUsers =
        filteredIds.length === 0
            ? []
            : allUsers.filter((user) => !filteredIds.includes(user.id))
        return filteredUsers
      }
    
      return (
        <form onSubmit={handleSubmit} className='form'>
        </form>
      )
    }
    
    export default FormBookClub
    
    
    백엔드
    #bookclub_controller.rb
    
    class Api::BookclubsController < ApplicationController
        rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
        before_action :set_bookclub, only: [:show, :destroy]
        skip_before_action :authorize, only: [:index, :show]
    
        def update
            bookclub = Bookclub.find(params[:id])
            bookclub.update(bookclub_params)
    
            #check if admin is changed
            admin_bookclub_user = bookclub.bookclub_users.find {|user| user.isAdmin == true }
            admin_id = admin_bookclub_user.user_id
    
            if params[:admin_id] != admin_id
                admin_bookclub_user.update(isAdmin: false)
                new_admin_bookclub_user = bookclub.bookclub_users.find_by(user_id: params[:admin_id])
                new_admin_bookclub_user.update(isAdmin: true)
            end
    
    
            # delete users if needed
            if !params[:delete_users].empty?
                users = params[:delete_users].each do |user_id|
                    bookclub_user = bookclub.bookclub_users.find_by(user_id: user_id)
                    bookclub_user.destroy
                end
            end
    
            # add users if needed
            if !params[:add_users].empty?
               params[:add_users].each do |user_id|
                    bookclub.bookclub_users.create(user_id: user_id, isAdmin: false)
                end
            end
    
            render json: bookclub, include: ['users', 'bookclub_books', 'bookclub_books.book', 'bookclub_books.goals', 'bookclub_books.guide_questions', 'bookclub_books.guide_questions.comments'], status: :accepted
        end
    
        private
    
        def bookclub_params
            params.permit(:name)
        end
    
        def set_bookclub
            @bookclub = Bookclub.find(params[:id])
        end
    
        def render_not_found_response
            render json: { error: 'Book Club Not Found' }, status: :not_found
        end
    
    end
    
    
    

    기타 최상의 도서 기능


    독서 클럽에 책 한 권을 추가하다


    Good Reads API를 사용하여 도서 정보를 검색하고 얻습니다. 그러면 사용자가 도서 클럽에 추가할 수 있습니다.

    도서 클럽에서 이사하다


    사용자는 책 한 권을 책 클럽 소망 목록에 추가하여 책 클럽의 현재 책이 되고 완성된 후에 책을 저장할 수 있습니다.

    독서 클럽에 목표, 문제와 평론을 추가하다


    사용자는 현재 도서에 목표를 추가하고 문제를 추가하며 그들이 속한 도서 클럽의 지도 문제에 대해 논평할 수 있다.
    목표 추가

    질문 및 설명 추가

    마지막 생각


    나는 이 응용 프로그램의 기능에 자부심을 느낀다.이 글에서, 나는 응용 프로그램의 모든 기능 (개인 정보 업데이트 및 삭제 포함) 을 포함하지 않았지만, 모든 모델에 의미 있는 CRUD 조작을 사용하려고 시도했다.
    나는 여전히 이 프로그램에 모든 도서 클럽을 검색하고 가입을 요청할 수 있는 기능을 추가하고 싶다.관리자가 로그인하면 요청을 승인하거나 거부할 수 있습니다.현재, 당신은 서우회 관리자의 초청을 받은 후에만 서우회에 가입할 수 있습니다.
    예전과 같이 이 글을 훑어보아 주셔서 감사합니다.나는 이것이 네가 나의 과정을 더 많이 이해하는 데 도움을 줄 수 있기를 바란다.나는 편강의 마지막 단계와 프로젝트를 진행하고 있다.

    좋은 웹페이지 즐겨찾기