* 시작에 앞서.

  본래는 이보다 일찍, 더 여유롭게 글을 쓰려했다. 하지만 어머니와 동생이 같이 운영하는 미용실을 개업하면서, 그 가게에서 사용할 소프트웨어를 만들어 달라는 부탁이 있었기에 그동안 이 글쓰기 계획을 미룰 수밖에 없었다. 간단한 기능을 가진 소프트웨어지만 공부를 겸하면서 개발했기에 시간이 오래 걸렸고 어제나 되어서야 완성 할 수 있었다.

  한해의 독서 생활을 정리하고 내년의 독서 계획과 다짐을 세우겠다는 목표로 시작한 글이기에 꼭 마무리해야 한다고 생각하기에 짧게나마 리뷰를 해보려 한다.



<어떻게 살 것인가> 유시민 | 생각의길

<유시민의 글쓰기특강> 유시민 | 생각의길

<대통령의 글쓰기> 강원국 | 메디치미디어

<대통령의 말하기> 윤태영 | 위즈덤하우스


  올해 주요 독서 테마 중 하나가 글쓰기가 된 이유는 하나의 책 때문이다. 그 책은 유시민 작가의 <어떻게 살 것인가>이다. 토론 프로그램에서 보여주는 논리적인 유시민 작가의 모습을 보며 그 논리의 바탕이 되는 생각은 무엇인지 궁금했기에 이 책을 찾아보게 되었다.

  책의 주제는 단순하다. ‘자신이 삶의 주체가 되어 하고 싶은 놀이와 일, 사랑, 그리고 연대하며 살아라’ 하는 것이다. 죽음에 대한 두려움이나 망각으로 ‘죽지 못해 사는 삶’을 살기보다, 죽음을 직시하며 삶의 의미를 찾으라 이야기한다. 유시민 작가는 이 책에서 삶의 중요한 요소에 놀이, 일, 사랑에 연대를 더하며 진보적 가치에 대한 예찬도 동시에 하고 있다.

  이 책의 주제가 나에게 특별하게 다가오진 않았다. 평소에 내가 가지고 있는 생각과도 큰 차이가 없었기에 무덤덤했다. 단지 인상 깊었던 것은 그러한 생각을 표현하기 위해 유시민 작가가 보여주는 글쓰기 방식이다. 나는 생각만 가지고 있을 뿐 <어떻게 살 것인가>처럼 말끔하고 또렷하게 생각을 전달하지는 못한다. 자신의 경험은 물론 철학과 인문학, 통계 뿐 아니라 과학적 연구 결과를 동원하여 자신의 생각을 뒷받침 하며 뚜렷한 주제를 전달하는 글쓰기 방식에 매혹되었다. 이런 이유로 <유시민의 글쓰기특강>을 읽어 보게 되었고, 연말에는 대한민국의 리더십 실종을 목격하며 자신의 뚜렷한 생각을 갖고 표현했던 전임 대통령들의 글쓰기 방법을 이해하고자 <대통령의 말하기>, <대통령의 글쓰기>를 읽었다.


  이 세 권의 책을 읽으면서 공통적으로 이야기 하는 좋은 글쓰기 방법을 발견 할 수 있었다. 크게 보면 네 가지로 나눌 수 있을 것 같다.

  첫 번째로는 생각이 중요하다는 것이다. 자신만의 정확하고 논리적인 생각을 가지고 있어야 글을 쓸 수 있다. 글쓰기라는 것은 결국 내 생각을 전달하는 도구일 뿐이다. 아주 뚜렷하고 명확한 생각을 가지고 있지 못하면 아무리 멋있는 글을 써도 겉만 번지르르할 뿐이다.

  두 번째로는 많이 읽고 많이 써야 한다는 것이다. 무슨 일을 하든 결국 노력할수록 느는 법이다. 많이 읽는 것은 자신의 생각을 발전시키고, 독해력을 키우고, 논리적 사고를 할 수 있게 한다. 많이 쓰는 것은 주제에 알맞은 군더더기 없는 글을 쓸 수 있게 한다. 이를 위해 메모하는 습관을 가지는 것도 좋다.

  세 번째로는 뚜렷한 주제를 가지고 논증하는 습관을 가지라는 것이다. 주제와 동떨어진 이야기를 지양하고, 자신의 주제를 타인에게 설득시키기 위해 충분한 논증을 하라는 것이다.

  마지막으로 독자를 고려하여 써야 한다는 것이다. 좋은 생각과 뚜렷한 주제를 가지고 있어도 쉽게 읽히지 않으면 무용지물이다. 글도 결국 말에서 나오는 것이므로, 말로서 표현해도 막힘없이 읽을 수 있는 글을 만들어야 한다. 가급적 단문으로 표현하며, 한국어로 글을 작성한다면 외국어 번역투를 지양해야 한다. 중학생 수준의 독자가 읽는다고 가정하고 쉬운 문장을 써야 한다.

  위에 언급한 네가지 이 외에도 다양한 조언이 있으니 찾아 읽어보면 도움이 될 것이다.


  물론 세 책은 다른 작가가 조금씩 다른 의도를 가지고 쓴 글이기에 차이도 있다. <유시민의 글쓰기특강>은 일반적인 논리적 글쓰기에 대한 이야기를 한다. 때문에 다양한 상황에서의 글쓰기에 대한 조언과 마음가짐도 이야기 하고, 논리적 글쓰기를 위한 추천도서도 포함하고 있다. 반면 뒤의 두 책은 대통령의 글쓰기, 즉 리더라는 특정 인물의 글쓰기에 대해 이야기 하고 있다. 내가 쓰는 블로그의 글은 취향이 맞지 않는 누군가를 고려하며 써야할 의무를 갖진 않는다. 하지만 대통령의 글쓰기라면 상황은 다르다. 리더 자신에게 관심이 없거나 혹은 반대적 입장을 가진 사람도 자신에게 주의 집중 시키고, 자신의 생각을 또렷하게 전달해야 한다. 때문에 뒤의 두 책에서는 그러한 방법과 관련된 노하우를 이야기한다. 이러한 차이에 유의하여 책을 선택하면 좋을 듯하다. 그리고 두 책 모두 시나 일상에서 느끼는 감정을 실감나게 전하는 감수성 있는 글쓰기와는 거리가 멀다.


  이 네 권의 책을 읽으면서 많이 읽는 것 뿐 아니라 많이 써야겠다는 다짐을 하게 되었다. 블로그를 시작하게 된 계기도 이 책들 덕분이라 할 수 있겠다. 평소에 그리 많은 대화를 하는 성격이 아니라 정작 중요할 때 표현력의 부족을 많이 느끼곤 한다. 내 생각이 다른 사람에게 효과적으로 전달되지 못하는 경험을 종종 한다. 좋은 글을 쓰기 위해, 즉 나의 생각을 정확히 전달하기 위해 이 블로그에서 많은 글을 써나갈 것이다. 일 년 뒤 이맘때 지금 내가 쓴 글을 돌아보며 성장해 있음을 느끼게 된다면 더할 나위 없이 기쁘지 않을까 상상해본다.

0. 서버 사양

CPU: Intel G530
Ram: 2GB
OS: Ubuntu Server 16.04.1 LTS, https://www.ubuntu.com/download/server
Web Server: Nginx
App Server: Unicorn


1. 서버 환경 구성

  먼저 Ubuntu 설치를 위한 부팅USB를 만들었다. Win32 Disk Manager 라는 프로그램을 이용하여 부팅USB를 만들었다. 

  (참고: http://sourceforge.net/projects/win32diskimager/)

  Ubuntu 설치 과정은 다음의 블로그를 참고하였다. http://goproprada.tistory.com/260 마지막 과정에서 추가 소프트웨어 설치는 Standard system utilities, PostgreSQL database, OpenSSH server를 선택하였다.


  설치가 끝난 후 자신이 생성한 계정으로 접속하고 IP주소를 확인해보자.

$ ifconfig


  이제 동일한 내부 네트워크에 있는 개발환경의 PC에서 접속하자. 윈도우즈 OS라면 putty를 설치해서 ssh 연결을 하자. 리눅스와 맥은 터미널을 열어 ssh 연결을 한다.

$ ssh [사용자 계정명]@[IP 주소]


  이제 웹 서버 배포를 위해 필요한 패키지를 설치한다. 초보자를 위한 레일즈가이드의 글과 여러 블로그의 글을 참고하여 패키지를 설치하고 설정하였다.

 - 우분투 16.04 서버 세팅하기: https://rorlab.gitbooks.io/railsguidebook/content/appendices/ubuntu16server.html

 - 운영서버 환경구축: https://rorlab.gitbooks.io/railsguidebook/content/contents/pro_env.html

 - 우분투 MySQL 설정: http://webdir.tistory.com/217

 - 우분투 방화벽(UFW) 설정: http://webdir.tistory.com/206


#  배포용 계정 생성

$ sudo adduser deployer

$ sudo addgroup admin

$ sudo usermod -aG admin deployer

$ sudo visudo

...

# Members of the admin group may gain root privileges

%admin ALL=(ALL) NOPASSWD: ALL

...


# build-essential 설치

$ sudo apt-get -y install git curl build-essential openssl libssl-dev libreadline-dev python-software-properties python g++ make


# Nginx 서버 설치

$ sudo apt-get install -y nginx


# mysql 설치

$ sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev

$ sudo vi /etc/mysql/my.cnf

[client]

default-character-set = utf8


[mysqld]

character-set-client-handshake=FALSE

init_connect="SET collation_connection = utf8_general_ci"

init_connect="SET NAMES utf8"

character-set-server = utf8 

collation-server = utf8_general_ci


[mysqldump]

default-character-set = utf8


[mysql]

default-character-set = utf8

$ mysql -u root -p

mysql> grant all privileges on *.* to deployer@localhost identified by 'password';  # deployer 계정 생성

$ sudo service mysql restart


# ImageMagick 설치

$ sudo apt-get -y install libmagickwand-dev imagemagick


# Nodejs 설치
$ sudo apt-get -y install nodejs

# sqlite3 설치
$ sudo apt-get install sqlite3 libsqlite-dev

# 방화벽 설정
$ sudo ufw enable
$ less /etc/services  # 미리 정의된 포트 목록이 출력된다. 아래 목록 외에 필요한 것을 찾아서 추가하자.
$ sudo ufw dhcpv6-client
$ sudo ufw ssh
$ sudo ufw http
$ sudo ufw https


2. 앱 설정

  앱 설정을 시작하기 전에 서버에 ssh 연결을 시도할 때 매번 비밀번호를 입력하는 번거로움을 없애기 위해 다음과 같은 과정을 수행한다.

$ ssh-copy-id -i ~/.ssh/id_rsa [사용자 계정명]@[ip 주소]


  서버 설정을 끝내고 이제 앱에서 필요한 설정을 해야한다. gemfile과 capfile을 수정하고, 배포 관련 설정을 해야한다. 관련 설정은 아래의 사이트를 참고하였다.

 - Capistrano 3로 배포하기 – 2015 업데이트: https://withrails.com/2015/05/25/capistrano-3로-배포하기-2015-업데이트/


Gemfile

# 추가
gem 'mysql2'
gem 'unicorn'

gem 'capistrano-rails', group: :development
gem 'capistrano-rbenv' # required
gem 'capistrano-rbenv-install'
gem 'capistrano-unicorn-nginx'
gem 'capistrano-upload-config'
gem 'capistrano-safe-deploy-to'
gem 'capistrano-ssh-doctor'
gem 'capistrano-rails-console'
gem 'capistrano-rails-collection'
gem 'capistrano-rails-tail-log'
gem 'capistrano-faster-assets'

$ bundle install

$ cap install

# 터미널에 출력되는 내용

mkdir -p config/deploy

create config/deploy.rb

create config/deploy/staging.rb

create config/deploy/production.rb

mkdir -p lib/capistrano/tasks

create Capfile

Capified


  capistrano를 설치하면 위와 같이 배포와 관련된 파일들이 프로젝트의 config 폴더 안에 생성된다. staging 서버 없이 바로 운영서버에 배포할 것이므로 관련 파일들만 설정한다.

 

Capfile

# 추가
require 'capistrano/bundler' # Rails needs Bundler, right?
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
 
require 'capistrano/rbenv'
require 'capistrano/rbenv_install'
require 'capistrano/unicorn_nginx'
require 'capistrano/faster_assets'
require 'capistrano/upload-config'
require 'capistrano/safe_deploy_to'
require 'capistrano/ssh_doctor'
require 'capistrano/rails/console'
require 'capistrano/rails/collection'
require 'capistrano/rails_tail_log'


config/deploy.rb

# 대괄호로 써있는 내용은 자신의 설정에 따라 바꿔준다.
lock "3.7.1"
 
set :application, '[application-name]'
set :repo_url, "git@github.com:[user-name]/#{fetch(:application)}.git"
set :deploy_to, "/home/[deployer-account ex: deployer]/apps/#{fetch(:application)}"
 
set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, '[ruby-version]'
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default values
 
# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
set :config_files, fetch(:linked_files)
set :pty, true
set :keep_releases, 5
 
before 'deploy:check:linked_files', 'config:push'
 
namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
end


config/deploy/production.rb

server '[server-ip]', user: '[deploy-username]', roles: %w{web app db}
 
set :nginx_server_name, '[domain-name]'
set :unicorn_workers, 4


  버전관리에 포함되어서는 안되는 database.yml, secrets.ymlgitignore에 추가한다.

.gitignore

...
# Ignore config/database.yml, secrets.yml
config/database.yml
config/secrets.yml


$ cap production config:init

00:00 config:init

      Created: config/database.production.yml as empty file

      Created: config/secrets.production.yml as empty file


  위에서 추가된 파일은 버전관리에 포함되는 파일이기에 중요한 민감정보를 포함하지 않고 대신 시스템 환경변수로 대체한다. 시스템 환경변수를 서버에 직접 등록해 주어야 한다.

# 서버에 ssh 연결 후

$ sudo vi /etc/environment

...

DEPLOY_USERNAME=[deploy-username]

DEPLOY_PASSWORD=[deploy_password]

SECRET_KEY_BASE=[secret_key_base]

 다시 로컬로 돌아와 위에서 생성된 두 파일을 작성한다.


config/database.production.yml

# 추가
production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: [database_name]
  pool: 5
  username: <%= ENV['DEPLOY_USERNAME'] %>
  password: <%= ENV['DEPLOY_PASSWORD'] %>
  host: localhost


config/secrets.production.yml

# 추가
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

git에 현재 수정된 내용을 커밋한다. 그리고 배포 명령을 수행한다.

$ git add .

$ git commit -m 'capistrano 배포를 위한 설정'

$ git push origin master


$ cap production setup

$ cap production rails:rake:db:setup

$ cap production deploy


  이제 배포가 완료되었다. 해당 ip로 접속해서 확인해보자.

 외부에서도 접속할 수 있도록 공유기 설정을 바꿔준다. 192.168.0.1로 접속해서 설정을 바꾼다. 필자의 경우에는 iptime공유기를 예로 들겠다. [관리도구] - [고급 설정] -  [NAT/라우터 관리] - [포트포워드 설정]에 들어가서 아래 그림과 같이 현재 사용하는 인터넷 ip의 80번 포트로 들어오는 요청을 현재 서버의 내부 ip로 포워딩 될 수 있도록 설정한다.

  [기본 설정] - [시스템 요약 정보] 에서 [외부 IP 주소]를 확인하고 브라우저에서 이 주소로 접속해보자.

  드디어 배포 끝!


* 여담: 서버를 처음 구성하고 배포하면서 많은 시행착오를 겪었다. 같은 리눅스인데도 데비안 계열의 ubuntu와 redhat 계열의 centos에서 환경 차이는 존재했다. 처음에는 centos를 사용했으나 ubuntu가 설정에서 좀 더 쉽다는 느낌을 받았고, 결정적으로 rails gem중에서 unicorn 앱 서버와 nginx 웹 서버를 설정해주는 'capistrano-unicorn-nginx' 젬이 ubuntu에서만 작동하는게 컸다.

  패키지를 설치하고, 내가 만든 앱에 배포 설정하는 작업까지는 무리없이 진행되나 처음 deploy 과정에서 엄청난 오류를 맞는다. mysql의 권한 설정이 문제가 되기도 하고, 시스템 환경변수가 제대로 설정되지 않았다거나, git push를 하지 않아서 오류를 내거나 하는 사소하면서도 정말 잘 찾아보지 않으면 스트레스 무지하게 받을만 한 문제를 발생시켰다.

  또한 deploy 과정이 문제없이 끝나도 막상 해당 ip로 접속해도 여전히 'Welcome to nginx' 화면만 띄우고 내가 원하는 페이지 화면을 보여주지 않아 당황하기도 한다. 자동으로 nginx와 앱 서버간의 설정을 해주는 gem의 도움을 받아 설정 할 수도 있지만, 그렇지 않은경우에는 직접 nginx.conf 파일을 작성하고 /etc/nginx/sites-enabled에 추가 시켜줘야 한다. We're sorry, but something went wrong. 을 띄운다면 다양한 문제가 존재하기에 특정할 수 없지만 나의 경우에는 SECRET_KEY_BASE 설정 실수로 발생한 것이었다.

  영어도 잘 못하는데 구글링하며 많이 애먹었다. 여전히 명확하게 어떻게 동작하는가 의문인 부분이 수도 없이 많지만 그럼에도 이 과정을 통해 약간이나마 서버에 대한 이해가 늘은 것 같다.

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

 - 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