* Scaffold를 원하는 기능에 맞춰 수정하다 보니 특별한 중단점을 찾을 수 없어서 어느정도 기능을 만들어 놓고 리뷰 해본다.


메인 페이지 home/index.html

회원 관리 members/index.html

회원 보기 members/show.html

디자이너 관리 designers/index.html

시술목록 관리 contents/index.html

  Scaffold를 다섯개 만들긴 했으나 하나의 페이지로 기능을 합치거나 모델만 사용해서 세개의 주요 페이지만 남았다.

  앞서 요구사항을 적용하기 위해는 미리 회원정보와 디자이너 정보, 시술 목록이 저장되어 있어야 한다. Scaffold로 생성된 기본적인 CRUD에서 필요한 부분만 수정을 가했다.


app/views/member/_form.html.erb

  <!-- 생략 -->

  <div class="field">
    <%= f.label "전화번호" %>
    <%= f.telephone_field :phone, size: 20, maxlength: 11 %>
  </div>

  <div class="field">
    <%= f.label "담당 디자이너" %>
    <%= f.collection_select :designer_id, designers, :id, :name %>
  </div>

  <!-- 생략 -->


 _form.html.erb 파일은 Create와 Edit을 위한 공통 폼이다. 하지만 이 앱에서는 포인트입력을 받지 않는다. 초기 값을 입력거나 수정할 수 없다. 다만 컨트롤러에서 0으로 초기값을 설정해 주고, 회원의 이력에 따라 포인트가 자동으로 누적되게 할 것이다.

  html5 필드에 맞게 전화번호는 telephone_field로 설정하고, 휴대전화 번호는 최대 11자리 이므로 maxlength를 11로 설정한다.

  담당 디자이너는 직접 입력하지 않고 select폼으로 입력 받을 수 있게 디자이너 이름을 designers 테이블에서 가져올 수 있도록 collection_select 메서드를 사용하였다.


app/controllers/members_controller.rb

class MembersController < ApplicationController
  # 생략

  # POST /members
  # POST /members.json
  def create
    @member = Member.new(member_params)
    @member.acc_point = 0    # 이하 포인트 초기화
    @member.use_point = 0
    @member.rem_point = 0

    respond_to do |format|
      if @member.save
        format.html { redirect_to @member, notice: '회원이 추가되었습니다.' }
        format.json { render :show, status: :created, location: @member }
      else
        @designers = Designer.all
        format.html { render :new }
        format.json { render json: @member.errors, status: :unprocessable_entity }
      end
    end
  end
  
  # 생략
end

  members/index.html.erb와 show.html.erb에서는 designer 이름을 가져오는 것이 아니라 객체를 그대로 출력하므로 아래와 같이 액티브레코드의 주소를 표시한다. 이름을 출력하기 위해 컨트롤러에서 designers 테이블에서 이름을 조인한 레코드를 가져오게 하였다.

app/controllers/members_controller.rb

  # 생략

  # GET /members
  # GET /members.json
  def index
    @members = Member.joins(:designer).order('members.name').select('members.*, designers.name as designer_name')
  end

  # GET /members/1
  # GET /members/1.json
  def show
    @member = Member.joins(:designer).select('members.*, designers.name as designer_name').find(params[:id])

  # 생략

  디자이너 이름을 designer_name으로 접근할 수 있다.


app/view/members/index.html.erb

  <!-- 생략 -->

  <tbody>
    <% @members.each do |member| %>
      <tr>
        <td><%= link_to member.name, member %></td>
        <td><%= member.phone.insert(3, '-').insert(-5, '-') %></td>
        <td><%= member.designer_name %></td>
        <td><%= number_with_delimiter(member.acc_point) %></td>
        <td><%= number_with_delimiter(member.use_point) %></td>
        <td><%= number_with_delimiter(member.rem_point) %></td>
        <td><%= member.note %></td>
      </tr>
    <% end %>
  </tbody>

 <!-- 생략 -->

  디자이너 이름을 member_name으로 접근한다.

  그 외에 전화번호 형식을 표시하기 위해 문자열 처리를 하였다. 또한 포인트는 1,000 단위에 콤마를 넣기 위해 number_with_delimiter 메소드를 사용하였다.

  아래와 같이 표시된다.



  시술정보에는 시술의 큰 범주에 속하는 category 모델과 세부 범주의 content 모델이 동시에 표시된다. 카테고리와 상세정보 생성은 위와 비슷하다. category와 content를 동시에 표시하기 위해 contents_controller에서 두개의 객체를 동시에 가져왔다.


app/controllers/contents_controller.rb

  # 생략

  # GET /contents
  # GET /contents.json
  def index
    @contents = Content.all
    @categories = Category.all
  end

  # 생략

app/views/contents/index.html.erb

  <!-- 생략 -->

  <tbody>
    <% @categories.each do |category| %>
      <tr>
        <td><%= link_to category.name, category %></td>
      </tr>

      <% contents = @contents.select{|c| c.category_id == category.id} %>
      <% if contents.empty? %>
        <tr>
          <td></td>
          <td colspan="3">[ 비 어 있 음 ]</td>
        </tr>
      <% else %>
        <% contents.each do |content| %>
        <tr>
          <td></td>
          <td><%= link_to content.name, content %></td>
          <td><%= number_to_currency(content.price, unit: '원', precision: 0, format: '%n%u') %></td>
          <td><%= content.note %></td>
        </tr>
        <% end %>
      <% end %>
    <% end %>
  </tbody>

  <!-- 생략 -->

  카테고리를 탐색해서 같은 category_id 를 가진  content 인스턴스가 있으면 출력하고 아니면 [ 비 어 있 음 ] 이라는 문자를 표시한다.

  가격을 통화단위를 포함해서 출력하기 위해 number_to_currency 메서드를 활용하였다.

  이제 시술기록이다. Scaffold로는 History 컨트롤러가 관리하는 뷰에서 생성해야 하지만 회원 정보에서 모든 이력을 표시하고 생성하기 위해 member/show.html.erb 안에서 이력을 생성하고 출력한다. 


app/views/members/show.html.erb

<!-- 생략 -->

<h2>시술 기록</h2>
<%= form_for(@history) do |f| %>
  <% if @history.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@history.errors.count, "error") %> prohibited this history from being saved:</h2>

      <ul>
      <% @history.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <%= f.hidden_field :member_id, value: @member.id %>
  <%= f.hidden_field :date, value: Time.now %>

  <div class="field">
    <%= f.label '시술종류' %>
    <%= f.grouped_collection_select :content_id, @categories, :contents, :name, :id, :name %>
  </div>

  <div class="field">
    <%= f.label '가격' %>
    <%= f.number_field :price %>
  </div>

  <div class="field">
    <%= f.label '포인트' %>
    <%= f.number_field :point, readonly: true %>
  </div>

  <div class="field">
    <%= f.label '비고' %>
    <%= f.text_area :note %>
  </div>

  <div class="field">
    <%= f.label '현금결제' %>
    <%= f.check_box :is_cash %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

<table>
  <thead>
    <tr>
      <th>일자</th>
      <th>시술</th>
      <th>가격</th>
      <th>포인트</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <% @histories.each do |history| %>
      <tr>
        <td><%= history.date.strftime('%Y년 %m월 %d일') %></td>
        <td><%= history.category_name + ' - ' + history.content_name %></td>
        <td><%= number_with_delimiter(history.price) %></td>
        <td><%= number_with_delimiter(history.point) %></td>
        <td><%= number_with_delimiter(history.note) %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<!-- 생략 -->

  아래와 같은 그룹화 된 select 메서드를 사용하기 위해 grouped_collection_select 메서드를 사용하였다. 이 메서드를 사용하기 위해서는 모델간 association 관계를 정의해야한다.


app/models/category.rb

class Category < ApplicationRecord
  has_many :contents

  # 생략
end

  Category와 Content는 1:n 관계를 가지므로 'Category has many contents.'라고 말할 수 있을 것이다. 모델에서도 이와 같은 영문법처럼 1:n 관계를 정의해준다.


app/models/content.rb

class Content < ApplicationRecord
  belongs_to :category
  has_many :history

  # 생략
end

  마찬가지로 Content는 하나의 카테고리만 가지므로 'Content belongs to category.'라 볼 수 있다. 또한 Content 하나에 여러개의 History가 있을 수 있다.


app/models/history.rb

class History < ApplicationRecord
  belongs_to :member
  belongs_to :content
end

  History는 각각 Member와 Content에 n:1 관계를 가지므로 두 곳에 모두 belongs_to 설정되어있다. 

  기본적으로 Model 생성시 필드가 references 속성을 가지면 관련된 모델에 자동으로 belongs_to 설정이 된다. 다만 위와 같이 has_many나 has_one등은 직접 입력해 주여야 하는듯 하다.

  이제 아래와 같이 그룹이 생성된 select 메소드를 볼 수 있다.


  시술 기록 생성을 위한 폼은 _form.html.erb의 내용을 사용하였으나 일부 변경해서 사용하기 위해 render하지 않고 코드를 긁어서 붙여넣었다. 생성시간과 member_id는 히든 폼으로 컨트롤러에 넘겨준다.


  시술 종류를 선택하면 가격을 아래에 표시해줘야 하는데 이 과정은 자바스크립트를 통해 넣어줘야 한다. 또한 가격의 10%는 포인트로 누적되며 현금결제에 체크 되어있을 때만 포인트가 누적되게 하는 것도 스크립트로 작성하였다. erb안의 루비 코드는 서버 html 코드를 생성해서 클라이언트에게 제공하므로 클라이언트에서 능동적인 변화를 만들어 내지 않는다. 자바스크립트를 간단히 구현하기 위해 커피스크립트를 사용하였다.


app/javascripts/members.coffee

$(document).ready ->
  $('#history_content_id').change ->
    content_id = $(this).val()

    $.ajax
      url: '/contents/'+content_id
      type: 'GET'
      dataType: 'json'
      success: (result) ->
        $('#history_price').val(result['price'])
        if $('#history_is_cash').is(":checked")
          $('#history_point').val($('#history_price').val()/10)
        else
          $('#history_point').val(0)

        return
    return
  .change()

  $('#history_is_cash').click ->
    if $('#history_is_cash').is(":checked")
      $('#history_point').val($('#history_price').val()/10)
    else
      $('#history_point').val(0)
    return
  return

  jQuery로 select폼에 접근하여 내용이 변할 때 마다 ajax로 해당 시술의 가격을 받아온다. 체크박스가 선택되어있는지 여부를 확인하여 포인트 입력폼에 값을 추가시킨다.


  History가 저장될 때마다 회원의 포인트 정보가 업데이트 되어야 한다. History저장은 histories_controller에서 일어나므로 해당 코드에 업데이트 구문까지 추가한다.


app/controllers/histories_controllers.rb

  # 생략

  # POST /histories
  # POST /histories.json
  def create
    @history = History.new(history_params)
    member = Member.find(@history.member_id)

    respond_to do |format|
      if @history.save and member.update(acc_point: member.acc_point + @history.point, rem_point: member.rem_point + @history.point)
        format.html { redirect_to Member.find(@history.member_id), notice: '기록이 저장되었습니다.' }
        format.json { render :show, status: :created, location: @history }
      else
        format.html { render :new }
        format.json { render json: @history.errors, status: :unprocessable_entity }
      end
    end
  end

  # 생략

  history에 있는 member_id로 member객체를 찾아서 point를 업데이트 시켜주는 코드이다. 물론 포인트 사용시 차감되는 코드도 추가될 것이다.

  

  기능을 구현하기 위한 주요 코드는 위와 같다.

  이 외에도 유효성 검사를 위한 validate 코드를 모델에 추가하였다.


  아직 완성된 상황은 아니지만 피드백을 받기 위해 실제 이 애플리케이션을 사용할 사용자인 동생에게 중간결과를 보여줬다. 여러가지 문제점을 발견 할 수 있었다. 특히 실제 사용시 고객들이 이발만 하고 돌아가는게 아니라 같이 염색을 하는 경우가 있는데, 위 애플리케이션은 history가 하나가 아닌 두개로 저장 되기 때문에 방문 횟수를 카운트 하거나 포인트 누적에서 오류를 발생시킬 수 있다는 문제가 있었다. 또한 회원 정보에 나이와 성별 필드를 추가하는 문제, 현금결제 체크가 아니라 카드체크 버튼으로 바꿔달라는 요구를 받았다.

  이 외에도 포인트를 통한 결제금액 차감 기능 구현을 위한 모델 수정이 필요하며 자바스크립트 코드가 제대로 적용되지 않는 문제가 있으며, 포인트 관리를 위한 정규화 문제, 공통 레이아웃을 설정하는 문제등을 해결해야 할 것으로 보인다. 또한 데이터 삭제시 관련 데이터들을 삭제 시키거나 유지 시킬지 결정할 제약조건을 설정해야 하는 문제가 있다. rails의 마이그레이션은 db에 독립적으로 동작하나, db마다 상이한 제약조건은 설정하지 않으므로 직접 설정해줘야 한다.

  다음에는 이 부분을 수정하도록 하겠다.


GitHub: https://github.com/Stardust-kr/charmbitHair

+ Recent posts