커피스크립트로 다음과 같은 작업을 하였다.

 - fields_for 헬퍼에 하위 객체를 생성하기 위한 스크립트 코드를 생성하였다.

 - 회원이름 검색을 위한 ajax 쿼리

  필터를 이용하여 시술정보 생성후 포인트 갱신을 하였다.

  json builder를 이용하여 ajax 요청에 대한 데이터를 전송하였다.



1. 커피스크립트

  커피스크립트는 자바스크립트로 컴파일되는 스크립트 언어로, 자바스크립트의 단점을 보완하는 언어이다. 자바스크립트와 제이쿼리에 대한 약간의 지식만 있으면 쉽게 사용할 수 있다.

  레일즈에서 처음 스크립트를 적용하면서 당황한 것은 페이지를 넘어갈 때마다 스크립트의 작동이 멈춰버리는 것이다. 기본 gem으로 'turbolink'가 적용되어 있기에 발생하는 문제였다. 여러 페이지를 하나의 페이지처럼 동작하게 하는 gem으로, 아마도 하나의 페이지에서 동적으로 엘리먼트가 생성되고 삭제되기 때문에 bind된 메서드가 반응하지 않는것으로 보인다.

  때문에 제이쿼리를 작성할 때는 .ready()에서 작성하지 말고 .on('turbolink:load')에서 작성하여야 페이지 이동을 하더라도 스크립트가 작동한다. 이러한 형태의 페이지를 원하지 않는다면 젬을 주석처리 해버려야 할 듯 하다.

$(document).on('turbolinks:load', ->
  # 코드 입력
  return
)

  엘리먼트 동적 생성과 ajax를 위한 코드를 member.coffee 파일과 category.coffee 파일에 작성하였다. rails와 관련된 특별한 코드는 없으므로 넘어가도록 하겠다.


2. 필터

  필터는 여러개의 액션 메서드 실행 이전 또는 이후에 공통된 작업을 수행하는 것을 말한다. 자바 스프링을 배울때 기억하기로는 aspect oriented programming, 관점 지향 프로그래밍의 일환으로 설명되었던 개념으로 기억하고 있다. 위키피디아에서는 관점지향 프로그래밍을 '컴퓨팅에서 메인 프로그램의 비즈니스 로직으로부터 2차적 또는 보조 기능들을 고립시키는 프로그램 패러다임'이라 설명한다(https://ko.wikipedia.org/wiki/관점_지향_프로그래밍). 주요 비즈니스 로직과는 관련없는 로그, 보안/인증, 트랜잭션, 리소스 풀링, 에러검사, 정책 적용, 멀티쓰레드 관리, 데이터 영속성을 주요 로직과 격리된 곳에서 수행하는 것을 뜻한다고 한다(http://www.zdnet.co.kr/news/news_view.asp?artice_id=00000039147106&type=det&re=).

  여기서는 단순히 여러개의 액션 메서드에 공통된 기능을 수행하기 위해 사용하였다. 시술내역이 업데이트 될 때마다 자동으로 회원의 누적, 사용, 잔여 포인트 점수가 갱신되도록 한 것이다.


app/controllers/history_controller.rb

class HistoriesController < ApplicationController
  before_action :set_history, only: [:show, :edit, :update, :destroy]
  after_action :update_point, only: [:create, :update, :destroy]

  # 생략

  # GET /histories/1
  # GET /histories/1.json
  def show
  end

  # GET /histories/1/edit
  def edit
    @categories = Category.all
  end

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

    respond_to do |format|
      if @history.save
        format.html { redirect_to @history.member, notice: '기록이 저장되었습니다.' }
        format.json { render :show, status: :created, location: @history }
      else
        format.html { render 'hello/show' }
        format.json { render json: @history.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /histories/1
  # PATCH/PUT /histories/1.json
  def update
    DetailHistory.where(history_id: @history.id).destroy_all

    respond_to do |format|
      if @history.update(history_params)
        format.html { redirect_to @history.member, notice: 'History was successfully updated.' }
        format.json { render :show, status: :ok, location: @history }
      else
        format.html { render :edit }
        format.json { render json: @history.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /histories/1
  # DELETE /histories/1.json
  def destroy
    DetailHistory.where(history_id: @history.id).destroy_all

    @history.destroy
    respond_to do |format|
      format.html { redirect_to @history.member, notice: 'History was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_history
      @history = History.find(params[:id])
    end

    def update_point
      histories = History.where(member_id: @history.member_id)
      acc_point = histories.sum(:point_accumulated)
      use_point = histories.sum(:point_used)
      rem_point = acc_point - use_point

      @history.member.update(acc_point: acc_point, use_point: use_point, rem_point: rem_point)
    end

    # 생략
end

  scaffold로 CRUD기능이 정의된 모델을 생성하면 기본적으로 before_action으로 필터가 생성되어 있다. show, edit, update, destroy 메서드를 요청하면 자동으로 private에 있는 set_history 메서드가 먼저 실행하도록 한다. set_history 메서드는 사용자가 요청한 id에 해당하는 history 액티브 레코드를 반환한다. show 메서드를 보면 알겠지만 어떠한 내용도 없지만 실제로는 show.html.erb 파일에 set_history에서 생성한 @history 액티브 레코드가 전달된다.

  나는 히스토리가 생성되거나 수정, 삭제된 후 마다 member 객체의 포인트가 자동으로 계산 될 수 있는 코드를 작성하기 위해 after_action 필터를 사용하였다. update_point 메서드를 새로 정의하고 위와 같이 코드를 작성하였다. 해당 회원의 id에 해당하는 history를 모두 가져와서 새로 갱신하는 계산을 한다.


3. json builder

  레일즈에는 사용자 요청에 따른 http뿐 아니라 json이나 xml로 반환할 수 있다. scaffold로 생성하면 기본적으로 해당 모델의 모든 리스트를 반환하는 index.json.jbuilder 파일과 하나의 모델의 정보를 반환하는 show.json.jbuilder 파일을 포함하고 있다.


app/views/histories/_history.json.builder

json.extract! history, :id, :member_id, :date, :content_id, :total_price, :point_used, :point_accumulated, :is_credit, :created_at, :updated_at
json.url history_url(history, format: :json)

app/views/histories/index.json.builder

json.array! @histories, partial: 'histories/history', as: :history

app/views/histories/show.json.builder

json.partial! "histories/history", history: @history

  show.json.builder는 하나의 객체를 json문서로 만들기 때문에 json.array! 라는 반복 코드 없이 _history.json.builder 파일을 그대로 호출한다. index.json.builder는 여러개의 객체정보를 json문서로 바꿔야 하므로 json.array로 반복작업을 요청한다. _history.json.builder는 json.extract! 메소드를 통해 해당 필드에 대한 정보를 자동으로 json 형태로 만들어준다. 또한 json.url로 해당 객체의 페이지로 이동하는 url까지 함께 저장할 수 있다.

  _history.json.builder와 index.json.builder를 합친 코드는 아래와 같다고 볼 수 있다.

json.array!(@histories) do |history|
  json.extract! history, :id, :member_id, :date, :content_id, :total_price, :point_used, :point_accumulated, :is_credit, :created_at,
    :updated_at
  json.url history_url(history, format: :json)
end

  이번 애플리케이션에서는 기존에 정의된 json파일과 회원 검색을 위한 목록 생성을 위해 search.json.jbuilder 파일을 생성하였다.


app/assets/javascripts/member.coffee

$(document).on('turbolinks:load', ->
  # 생략

  # select 선택자가 바뀔때
  changeListener = ->
    content_id = $(this).val()
    priceObject = $(this).closest('tr').find('input')

    $.ajax
      url: '/contents/'+content_id
      type: 'GET'
      dataType: 'json'
      success: (result) ->
        priceObject.val(result['price'])
        getTotalPrice()
        return
    return

  # 생략

  # 시술목록 '추가하기' 눌렀을 때
  $('button#add_detail').click ->
    size = $('#detail_list').children().size()
    detail = $('#detail_list tr:first').clone()

    if size == 0
      select = $('<select>', {
          id: 'history_detail_histories_attributes_0_content_id',
          name: 'history[detail_histories_attributes][0][content_id]',
          change: changeListener
        })

      $.ajax
        url: '/categories',
        type: 'GET',
        dataType: 'json',
        success: (results) ->
          for result in results
            select.append($('<optgroup>', {
                label: result['name']
              }))
            for content in result['contents']
              select.find('optgroup').last().append($('<option>', {
                  value: content['id'],
                  text: content['name']
                }))
          # select.val(results[0]['contents'][0]['id'])
          return

      # 생략

  # 검색 요청
  $('input#keywd').keyup ->
    keyword = $(this).val()

    $.ajax
      url: '/members/search/',
      type: 'GET',
      data: { keyword: keyword },
      dataType: 'json',
      success: (results) ->
        $('#member_list').empty()
        for result in results
          $('#member_list').append(build_member_list(result['id'], result['name'], result['phone'], result['designer_name'],
                                                      result['gender'], result['ages'], result['acc_point'], result['use_point'],
                                                      result['rem_point'], result['note']))
        return
    return

  return
)

  첫번째 ajax요청은 select 엘리멘트가 바뀔때 해당 content_id에 대한 price를 얻기 위한 것이다. /contents/content_id 로 요청을 하였으므로 서버에서는 show.json.jbuilder를 통해 생성된 json을 전송 해 줄 것이다.

  


  두번째는 아래와 같이 새로 객체 추가 시에 복사할 객체가 없으면 새로운 select 엘리멘트를 생성하기 위한 정보를 category와 content로 부터 가져오기 위한 요청이다.



  하나의 객체 내용을 가져오는 것이 아니라 하위 객체의 내용도 가져와야 하기 때문에 jbuilder내용의 수정이 필요하다. '/categories'로 요청을 보냈으므로 모든 category객체 내용을 담고 액티브 레코드가 반환된다.


app/views/categories/index.json.jbuilder

json.array! @categories do |category|
  json.extract! category, :id, :name, :created_at, :updated_at
  json.contents do
    json.array! category.contents do |content|
      json.extract! content, :id, :name, :price, :note
    end
  end
  json.url category_url(category, format: :json)
end

  category 객체에 대한 내용은 기존 그대로 생성하고, 하위 객체의 내용은 json.contents do ... end 메서드로 다시 정의할 수 있다. 그 안에서는 content 객체에 대한 내용을 다시 생성한다. 위의 코드는 아래처럼 json문서로 반환된다.

[
  {
    "id":1,
    "name":"컷트",
    "created_at":"2016-12-19T07:44:40.518Z",
    "updated_at":"2016-12-19T07:44:40.518Z",
    "contents":[
      {
        "id":1,
        "name":"남",
        "price":10000,
        "note":""
      },
      {
        "id":2,
        "name":"학생",
        "price":8000,
        "note":""
      }
    ],
    "url":"http://localhost:3000/categories/1.json"
  },
  {
    "id":2,
    "name":"드라이",
    "created_at":"2016-12-19T07:44:46.374Z",
    "updated_at":"2016-12-19T07:44:46.374Z",
    "contents":[
      {
        "id":3,
        "name":"일반",
        "price":12000,
        "note":""},
      {
        "id":4,
        "name":"아이롱",
        "price":15000,
        "note":""
      }
    ],
    "url":"http://localhost:3000/categories/2.json"
  },
  {
    "id":3,
    "name":"크리닉",
    "created_at":"2016-12-19T07:44:53.720Z",
    "updated_at":"2016-12-19T07:44:53.720Z",
    "contents":[
      {"id":5,"name":"두피","price":30000,"note":""},
      {"id":6,"name":"모발A","price":30000,"note":""}
    ],
    "url":"http://localhost:3000/categories/3.json"
  },
  {
    "id":4,
    "name":"펌",
    "created_at":"2016-12-23T04:32:15.431Z",
    "updated_at":"2016-12-23T04:32:15.431Z",
    "contents":[
      {
        "id":7,
        "name":"일반",
        "price":35000,
        "note":""
      },
      {
        "id":8,
        "name":"특수",
        "price":45000,
        "note":""
      },
      {
        "id":9,
        "name":"앰플",
        "price":10000,
        "note":""
      }
    ],
    "url":"http://localhost:3000/categories/4.json"
  },
  {
    "id":5,
    "name":"열펌",
    "created_at":"2016-12-23T04:38:22.368Z",
    "updated_at":"2016-12-23T04:38:22.368Z",
    "contents":[
      {
        "id":10,
        "name":"볼매B",
        "price":70000,
        "note":""
      }
    ],
    "url":"http://localhost:3000/categories/5.json"
  },
  {
    "id":6,
    "name":"염색",
    "created_at":"2016-12-23T04:39:32.901Z",
    "updated_at":"2016-12-23T04:39:32.901Z",
    "contents":[
      {
        "id":11,
        "name":"저렴뿌염",
        "price":25000,
        "note":""
      },
      {
        “id":12,
        "name":"기본뿌염",
        "price":30000,
        "note":""
      },
      {
        "id":13,
        "name":"전체염색",
        "price":50000,
        "note":""
      },
      {
        "id":14,
        "name":"남)헤나",
        "price":40000,
        "note":""
      }
    ],
    "url":"http://localhost:3000/categories/6.json"
  }
]

  위처럼 contents 키 안에 다시 객체가 배열형태로 들어가 있음을 알 수 있다. select 엘리멘트에서 아래와 같이 표시된다.

  


  마지막으로 회원 검색을 위해 사용하였다. 회원 검색에는 search.json.jbuilder파일을 만들고 member 컨트롤러에도 search 메소드를 생성하였다.


app/views/members/search.json.jbuilder

json.array!(@members) do |member|
  json.extract! member, :id, :name, :phone, :gender, :ages, :acc_point, :use_point, :rem_point, :note, :created_at, :updated_at
  json.designer_name member.designer.name
  json.url member_url(member, format: :json)
end

app/controllers/members_controller.rb

  # GET /members/search/keyword.json
  def search
    @members = Member.where("members.name LIKE '%#{params[:keyword]}%' OR members.phone LIKE '%#{params[:keyword]}'").order('members.name')
  end

  컨트롤러의 search 메서드에서 이름 또는 전화번호로 검색한 회원의 목록을 @members 인스턴스 변수에 저장한다. search.json.jbuilder 에서 member객체의 내용을 json형태로 바꾸고, member객체에 없는 designer_name은 member.designer.name으로 접근한다.

  search 메서드는 라우팅 되어있지 않으므로 routes.rb에 추가해준다.


config/routes.rb

Rails.application.routes.draw do
  resources :histories
  resources :contents
  resources :categories
  resources :members do
    get :search, on: :collection
  end
  resources :designers

  get "home/index"
  root "home#index"
end

  Member 리소스 밑에 GET으로 search 메소드를 추가한다. 여러 객체를 다루는 액션이므로 on: :collection으로 설정한다.


  json 형태로 받은 객체 정보로 회원 정보를 검색할 수 있게 되었다.


  이 외에도 단순한 마이너한 수정들이 있었다. github에서 확인 할 수 있다(https://github.com/Stardust-kr/charmbitHair). 대략적으로 중요한 기능은 모두 작성한 듯 하다. 앞으로 크게 3가지 문제를 해결해 나갈 것이다.

1. 배포

  집에서 안쓰던 노트북으로 서버를 구성해서 배포하려 했으나, 노트북을 가게로 보내고 가게에서 사용하던 저사양 pc를 서버로 사용하기로 했다. CPU는 G530, 메모리는 2GB정도 였던 것으로 기억한다. CentOS를 올리고 nginx 웹 서버에 passenger, DB로는 mysql을 사용 할 것이다. Capistrano를 이용해서 처음으로 배포 해볼 것이다.

2. 디자인

  기본적인 디자인을 위한 레이아웃 조차도 제대로 갖춰져 있지 않다. 프론트엔드 개발이라 해봐야 자바스크립트와 제이쿼리로 DOM과 ajax를 쓰는 것만 자주 했을 뿐 디자인에는 거의 손 대지 않았기에 이번 기회에 부트스트랩을 시작으로 차근차근 알아갈 것이다.

3. 추가기능 - 통계, 스케줄

  디자이너 별 매출, 기간당 매출과 같은 기본적인 통계 뿐 아니라 성별, 연령대 별 매출, 요일, 시간, 날씨별 매출 등 다양한 통계를 작성해 볼 수 있을 것이다. 또한 예약자 관리를 위한 스케줄 기능도 필요로 했기 때문에 해당 기능도 추가할 것이다. 외부 API를 적용해 보는것도 좋을 것 같다.

+ Recent posts