2013년 12월 30일 월요일

[ROR] virtual attributes

안녕하세요 belhyun입니다.
rails에서는 model에서 virtual attributes를 사용할 수 있습니다.
예를 들어, model field의 이름이 name이라고 해보겠습니다.
만약 name에 @을 붙인 name을 attribute를 요청하고 싶을 때 virtual attributes를 사용하게 됩니다.

먼저 @을 붙인 이름을 twitter_name으로 정의하고 해당 이름에 대한 gettter/setter를 정의합니다.

attr_accessor: twitter_name

그런 후, get 메소드에서 해당하는 형태로 정의합니다.

def twitter_name
  "@{name}"
end

위와 같이 지정한 후 만약 name 필드가 User Model에 정의되었다면 User.twitter_name을 호출하게 되면 @을 붙인 문자열이 반환됩니다. 이러한 virtual attributes를 사용하여 좀 더 확장성 있는 표현을 할 수 있게 됩니다.

감사합니다.

2013년 12월 22일 일요일

[PHP] nginx + php fpm(fast-cgi process management)

안녕하세요 belhyun입니다.
phalcon(http://phalconphp.com/en/)을 설치할 이슈가 있었습니다. nginx와 연동된 설치 가이드를 기술해 보겠습니다.
먼저 phalcon을 설치합니다.
http://docs.phalconphp.com/en/latest/reference/install.html#installation-notes
그 다음 nginx를 설치한 후, conf 파일을 다음과 같이 생성합니다.

server {
    listen   80; 
    server_name dev.eventstore.co.kr;

    index index.php index.html index.htm;
    set $root_path '/home/www/phalcon/blog/public';
    #set $root_path '/home/www/phalcon/$host/public';
    root $root_path;
    #rewrite ^/(.*)$ /index.php?_url=$1;

    try_files $uri $uri/ @rewrite;

    location @rewrite {
        rewrite ^/(.*)$ /index.php?_url=/$1;
    }   

    location ~ \.php {
        #try_files $uri = 404;

        fastcgi_index /index.php;
        fastcgi_pass 127.0.0.1:9000;

        include /etc/nginx/fastcgi_params;
        fastcgi_split_path_info       ^(.+\.php)(/.+)$;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }   

    location ~* ^/(css|img|js|flv|swf|download)/(.+)$ {
        root $root_path;
    }   

    location ~ /\.ht {
        deny all;
    }   
}
주의할 점은 fastcgi를 위해 php-fpm을 설치해야 합니다.
yum으로 설치 한후, php-fpm을 시작합니다. 그런 후 fastcgi_pass에 현재 프로세스를 연결해 줍니다.(localhost:9000)
그런 후, phalcon 테스트를 위해 튜토리얼을 따라해 봅니다.(http://vimeo.com/phalconphp)

감사합니다.

2013년 12월 19일 목요일

[ROR] model에서 instance variable의 의미

안녕하세요 belhyun입니다. 오늘 개발을 하면서 model에서 접근할 수 있는 변수가 필요했습니다. 실제 구현은 다음과 같이 되었습니다.

class UserTweet
  include Mongoid::Document
  field :user_desc, type:String
  field :type, type:String
  field :tweet_uuid,  type:String
  belongs_to :tweet
  scope :tweet_uuid_exists, ->(tweet_uuid){ where(tweet_uuid: tweet_uuid) }
  scope :type_exists, ->(type){ where(type: type) }
  scope :user_desc_exists, ->(user_desc){ where(user_desc: user_desc) }

  def self.params=(params)
   @params = params
  end

  def self.params
    @params
  end

  def self.criteria
    UserTweet.where(user_desc: params[:user_desc],
                    type: params[:type],
                    tweet_uuid: params[:tweet_uuid])
  end

  def self.save
    if !criteria.exists?
      tweet = Tweet.find_by(uuid: params[:tweet_uuid])
      tweet.user_tweets.create(Hash[params])
      _success
    else
      _fail('already requested')
    end
  end

  def self.delete
    if criteria.exists?
       criteria.delete
      _success
    else
      _fail('resource not exists')
    end
  end

  private
  def self._success
    {:result => 1, :msg => 'success'}
  end

  private
  def self._fail(msg)
    {:result => 0, :msg => msg}
  end
end

위의 코드를 보시면 아시겠지만 params의 setter/getter 함수에서 인스턴스 변수를 사용했습니다. 실제 컨트롤러에서는 다음과 같이 호출했습니다.

      def set_params
        UserTweet.params = params.except(:action, :controller)
      end 
그럼 이제부터 model에서 instance variable이 갖는 의미를 알아보도록 하겠습니다.
먼저 @의 의미부터 알아봐야 될 것 같습니다.
만약 variable, @variable 2 가지의 변수가 있다고 해보겠습니다.
각각의 변수가 가지는 의미는 다음과 같습니다.
variable : 지역변수, 현재 블락에서만 사용 가능하다.
@variable : 인스턴스 변수 클래스내의 모든 메소드에서 사용 가능하다.
테스트를 해보겠습니다. 저 위의 코드에서 실제로 @params를 제거하면 "undefined method" 에러가 발생합니다. 실제로 attr_accessor 또한 인스턴스 gettter/setter를 생성시키므로 attr_accessor을 명시한다고 해도 같은 에러가 발생합니다. 따라서 params의 getter/settter에서 params 인스턴스 변수를 생성시킴으로써 모든 메소드에서 해당 변수에 접근할 수 있게 됩니다.
그럼 model에서 인스턴스 변수가 갖는 의미를 알아보겠습니다. model에서도 같은 의미를 사용될 수 있는데 저렇게 사용되면 각 객체의 변수라기 보다는 클래스의 변수라고 보면 됩니다. 그렇기 때문에 위 처럼 사용해도 문제가 없습니다.
그럼 상단의 코드를 좀 더 깔끔하게 수정해보겠습니다.

class UserTweet
  class << self
    attr_accessor :params 
  end 
params에 대한 getter/settter를 제거하고 위와 같이 변경하였습니다.
저렇게 변경해도 에러가 발생하지 않고 상단코드와 똑같은 의미를 가지게 됩니다.
그럼 저 코드를 분석해 보도록 하겠습니다.
상단의 코드는 params attribute_accessor를 인스턴스 레벨이 아닌 클래스 레벨에 추가하게 됩니다.

class << self는 self 싱글턴 클래스를 가져오게 되고 그러므로 self 안에 정의된 attr_accessor이 self 객체에 재 정의됩니다.  이 문법은 특히 메타클래스라 부릅니다. 그러므로 이 문법은 상단의 gettter/setter와 일치하게 됩니다.

감사합니다.

2013년 12월 18일 수요일

[ROR] mongoid 사용시 관계성 명시에 있어서 키

안녕하세요 belhyun입니다.
mongoid에서 1:다 관계를 명시하고 실제 insert를 수행하였을 경우 다음과 같이 키가 생성되어 document에 들어가게 됩니다.
만약 1에 해당하는 모델을 a라고 하고 n에 해당하는 모델을 b라 했을 때, b에 a를 build해서 insert할 경우 a의 document에는 b_id라는 키와 mongodb object id를 값을 속성으로 가진 docuemnt가 생성됩니다. mongoid에서 특정필드를 명시할 때 주의해서 사용하셔야 합니다.
다음은 1에 해당하는 관계명시입니다.

field :user_desc, type:String
field :type, type:String
field :tweet_uuid,  type:String
belongs_to :tweet

따라서 tweet_id를 명시하지 않아도 1에 해당하는 document에 자동으로 tweet_id가 들어가게 됩니다.

감사합니다.

[ROR] mongoid에서 belongs_to, has_many를 관계를 명시했음에도 에러가 발생

안녕하세요 belhyun입니다. 오늘 개발을 하면서 아래와 같이 belongs_to 관계를 명시했음에도 에러가 발생했습니다.
tweet = Tweet.where(uuid: params[:tweet_uuid])
if !UserTweet.where(user_desc: params[:user_desc],
                    type: params[:type],
                    tweet_uuid: params[:tweet_uuid]).exists?
    tweet.user_tweets.create(Hash[params])
    self.success
end
다음과 같이 관계가 명시되었습니다.
belongs_to :tweet
이유는
tweet = Tweet.where(uuid: params[:tweet_uuid])
에 있습니다. mongoid에서 where 조건을 사용하면 그 리턴값으로 #"406437296649404416"} options: {} class: Tweet embedded: false> 위와 같은 Criteria가 리턴됩니다. 
반면 find_by를 사용했을 경우 다음과 같이 리턴됩니다. # mongoid 공식 문서(http://mongoid.org/en/mongoid/docs/querying.html)에는 다음과 같이 기술되어 있습니다. "mongoid에서 모든 쿼리는 criteria이다. criteria는 몽고 DB의 실제 쿼리로 늦어지고 연결되는 쿼리이다. criteria는 필요에 의해 사용된다." 

그렇기 때문에 build나 create하는 과정에서 계속 에러(no method)가 발생했습니다. 

감사합니다.

[ROR] 파라미터에서 특정 값 삭제하기

안녕하세요 belhyun입니다. 오늘 개발을 하면서 전달된 POST 파라미터에서 특정 해쉬 키를 삭제할 이슈가 발생했습니다. 해당 방법은 다음과 같이 진행합니다.
UserTweet.save(params.except(:key1, :key2))
감사합니다.

[ROR] InvalidAuthenticityToken 발생

안녕하세요 belhyun입니다.
오늘 개발을 하다가 특정 HTTP request 툴을 이용하여 요청을 하려고 했는데, InvalidAuthenticityToken에러가 발생했습니다.
rails에서는 외부로부터의 불법적인 요청(CSRF)을 막기 위해서 authenticity token을 발행하여 요청 시 같이 보내도록 하고 있습니다.
불법적인 요청에 관한 글은 http://en.wikipedia.org/wiki/Cross-site_request_forgery에 자세히 명시되어 있습니다. 따라서 페이지에서도 보면 csrf_meta_tags를 노출하고 있는데 이 또한 이러한 이유 때문입니다. 이를 피하는 방법은 해당 컨트롤러/액션에 대해서 토큰 검사를 하지 않겠다고 명시하면 됩니다. 그 방법은 다음과 같습니다.

skip_before_filter :verify_authenticity_token
또는 특정 액션에 대해서 지정할 수도 있습니다.
skip_before_filter :verify_authenticity_token, :only => [:index, :show]
감사합니다.

2013년 12월 16일 월요일

[javascript] JS 디자인패턴 스터디 - 3번째

안녕하세요 belhyun입니다. 어제는 강남토즈2호점에서 JS 디자인패턴 스터디를 진행하였습니다. 전 3회때부터 참석하게 되었습니다. 대기자 명단에서 제 순서가 되었고 기분좋게 정식 스터디원이 되었습니다. 어제 배운내용을 머릿속에 남은 기억과 외국 blog(http://www.dofactory.com/javascript-patterns.aspx)를 참고하여 기술해 나가도록 하겠습니다. 어제 배운내용은 크게 iterate 패턴과, composite 패턴이였습니다. 먼저 iterate 패턴을 왜 사용하는지에 대한 언급을 해보도록 하겠습니다. 먼저 다음과 같은 예문을 보도록 하겠습니다.
function mc(num,cnt){
    if(cnt-- == 0) return 1;
    else return num*mc(num,cnt);
}
위의 식은 들어온 숫자(num)의 cnt만큼 곱한 숫자를 리턴하는 재귀표현식입니다. 우리가 보통 많이 쓰는 재귀표현식과 다르지 않습니다. 하지만 이 재귀표현식은 stack memory overflow가 발생하게 됩니다. 재귀식에서는 그 전단계를 기억하고 있어야 합니다. 그렇기 때문에 각 상태마다 메모리가 하나씩 잡히게 됩니다. return문을 통해서 다시 그 메모리가 해제되기 됩니다. 마치 피라미드형태와 같습니다. cnt의 숫자가 적으면 상관없지만 cnt의 숫자가 커지게 되면 각 상태마다 메모리를 가지게 되고 그렇기 때문에 memory overflow가 발생하게 됩니다. 위 재귀표현식을 다음과 같이 바꾸어 보겠습니다.
function mc(num,cnt,res){
    if(typeof res == "undefined") res = 1;
    if(cnt-- == 0) return res;
    else{
        return mc(num,cnt,num*res);
    }
}
그 전과 달라진 점은 각각의 상태에 대한 결과 값을 저장하는 파라미터 res가 추가되었다는 점입니다. 전 재귀식에서 문제가 됬던 부분은 num * 식입니다. 이 식을 제거하고 결과값을 파라미터로 넘김으로써, 각 상태에 대한 메모리를 차지 하지 않고 res 메모리를 하나 잡아 결과값을 저장하는 형태입니다. 이렇게 함으로써 메모리 overflow를 방지할 수 있습니다. 위 두 예에서 보는바와 같이 재귀식은 지양되어야 합니다. 그렇다면 일반 반복식은 큰 문제가 없을까요? javascript에서 지원하는 while문도 무한히 반복하게 되면 javascript run time out error가 발생하게 됩니다. 이렇게 반복에 대한 이슈는 자바스크립트에서 매우 취약한 부분이라고 할 수 있습니다. 그렇기 때문에 iterate패턴을 적용하여 구현하게 되면 반복에 대한 이슈를 문제없이 해결할 수 있습니다. 무릇 디자인 패턴이라 함은 구상객체의 인터페이스가 있고 구상객체를 실제로 사용하는 클라이언트 구상객체의 모음을 가지는 컨텍스트로 이루어진다고 볼 수 있습니다. 하지만 javascript에서는 함수나 객체나 모두 object이기 때문에 구상객체를 생성하여 컨텍스트에서 조합하여 바로 사용할 수 있습니다. 굉장히 flexible하다고 볼 수 있는 것입니다. 그렇다면 이러한 개념에 기반하여 iterate 패턴에 대하여 알아보도록 하겠습니다.
iterate 패턴의 기본개념은 다음과 같습니다.
결합된 객체의 요소들을 그 내재된 표현의 노출없이 접근하는 방법을 제공하는 패턴이다. 즉 객체의 요소들을 접근하는데 있어 캡슐화된 접근으로 그 객체의 각 요소들을 접근할 수 있는 방법을 제공하는 패턴입니다.
이 패턴은 다음과 같은 특징을 가지고 있습니다.
1. 클라이언트가 객체들의 집합을 순회할 수 있도록 합니다.
2. 일반적 역할은 객체들의 집합을 순회하고 조작하는데 있습니다.
3. 위의 컬렉션은 간단한 배열부터 복잡한 트리까지 다양한 형태가 될 수 있습니다.
4. 객체를 순회하는데 있어 앞에서부터 뒤로, 뒤에서부터 앞으로 등 다양한 방법이 존재할 수 있는데 이 문제를 해결할 수 있습니다.

이어서 다이어그램을 보도록 하겠습니다.
먼저 클라이언트는 iterator 객체에게 다양한 요청을 할 수 있습니다. 실제 컬렉션의 아이템에 접근하는 녀석은 iterator이기 때문에 클라이언트 입장에서는 items에 관한 정보를 가지고 있을 필요가 없습니다. 그저 iterator에 요청을 하기만 하면 됩니다.
이제 sample code를 통해서 실제 구현을 알아보도록 하겠습니다.


var Iterator = function(items){
  this.index = 0;
  this.items = items;
}
Iterator.prototype = {
  first: function(){
    this.reset();
    return this.next();
  },
  next: function(){
    return this.items[this.index++];
  },
  hasNext: function(){
    return this.index <= this.items.length;
  },
  reset: function(){
    this.index = 0;
  },
  each: function(callback){
    for(var item=this.first();this.hasNext();item=this.next()){
      callback(item);
    }
  }
}
//로그 헬퍼 즉시호출함수
var log = (function() {
  var log = "";
  return {
    add: function(msg){ log += msg+"\n";},
    show: function(){ alert(log); log = "";}
  }
})();

function run(){
  var items = ["one", 2, "circle", true, "Apple"];
  var iter = new Iterator(items);
  for(var item = iter.first(); iter.hasNext(); item = iter.next()){
    log.add(item);
  }
  log.add("");
  log.show
  
  iter.each(function(item){
    log.add(item);
  });
  
  log.show();
}
위 식의 결과값은 one, 2, circle, true, Apple이 2번 나오게 됩니다.
최초 저는 Iterator 생성자 함수를 정의했습니다. 그 생성자 함수는 new 키워드로 객체를 생성 시, 자신과 똑같은 복사본을 가지지만 this는 본래 객체를 가리키는 prototype 객체 하나를 생성시키게 됩니다. 따라서 Iterator.prototype에서 index, items등에 접근 가능하게 됩니다. 다시 패턴적으로 봤을 때 클라이언트(run())에서는 내부적으로 iterator가 어떻게 구현되있는지 몰라도 단지 next, hasNext등을 호출함으로써 items에 있는 원소들을 순회할 수 있게 됩니다.
이러한 패턴을 이용하여 다음과 같은 이점을 얻을 수 있습니다.
1. 다양한 타입으로 이루어진 collection을 순회할 수 있다.
2. 클라이언트에서는 실제 iteration 로직을 가지고 있을 필요가 없다.따라서 모듈화, 캡슐화가 가능해진다.
3. 다양한 콜렉션 형태(배열, 해쉬..)를 다룰 수 있다.

다음은 composite 패턴에 대해서 알아보도록 하겠습니다.
composite 패턴은 전체 계층을 표현하기 위해 객체를 트리 형태로 결합하는 것을 의미합니다. composite 패턴은 클라이언트가 각각의 객체와 객체의 결합을 균일하게 다룰 수 있도록 해줍니다. composite 패턴은 기본 아이템과 객체의 콜렉션이 될 수 있는 프로퍼티로 이루어진 객체들의 생성을 허용해 줍니다. 콜렉션의 각각의 아이템은 다른 콜렉션을 가질 수도 있고, 복잡한 구조의 콜렉션을 가질 수도 있습니다. 트리 구조는 완벽한 composite 패턴의 예입니다. 다음은 다이어그램을 나타냅니다.
다이어그램에서 보시는 것처림 하나의 component들은 leaf 또는 composite를 가지게 됩니다. 또한 각각의 composite는 또 다른 composite나 leaf를 가질 수도 있습니다.
각각의 용어는 다음을 의미합니다.
component - composition(결합)에 있는 객체를 위한 인터페이스를 선언합니다.
leaf - leaf 객체를 표현합니다. 특징은 더 이상의 child node를 가질 수 없다는 점입니다.
composite - composition(결합)의 branche를 나타냅니다. 하위 component들의 collection(집합)을 유지합니다.
그럼 코드를 통해서 자세히 알아보도록 하겠습니다.
예제에서는 트리 구조는 Node 객체들로부터 생성됩니다. 각각의 노드들은 이름과 add, remove, getChild, hasChilderen 메소드를 가지고 있습니다. 메소드들은 Node의 프로토타입으로 추가됩니다. 이렇게 함으로써 모든 노드들이 해당 메소드를 공유할 수 있기 때문에 메모리의 절약을 가져올 수 있습니다.
마찬가지로 log 함수는 출력을 위한 헬퍼 함수입니다.

var Node = function(name){
  this.children = [];
  this.name = name;
}
Node.prototype = {
  add: function(child){
    this.children.push(child);
  },
  remove: function(child){
    var length = this.children.length;
    for(var i=0; i< length;i++){
      if(this.children[i] === child){
        this.children.splice(i, 1);//remove
        return;
      }
    }
  },
  getChildren: function(i){
    return this.children[i];
  },
  hasChildren: function(){
    return this.children.length > 0;
  }
}
//재귀적으로 sub tree를 순회합니다.
function traverse(indent, node){
  log.add(Array(indent++).join("--") + node.name);

  for (var i = 0, len = node.children.length; i < len; i++) {
    traverse(indent, node.getChild(i));
  }
}

// logging helper
var log = (function () {
  var log = "";
  return {
    add: function (msg) { log += msg + "\n"; },
    show: function () { alert(log); log = ""; }
  }
})();

function run() {

    var tree = new Node("root");
    var left = new Node("left")
    var right = new Node("right");
    var leftleft = new Node("leftleft");
    var leftright = new Node("leftright");
    var rightleft = new Node("rightleft");
    var rightright = new Node("rightright");

    tree.add(left);
    tree.add(right);
    tree.remove(right);  // note: remove
    tree.add(right);
    left.add(leftleft);
    left.add(leftright);
    right.add(rightleft);
    right.add(rightright);

    traverse(1, tree);

    log.show();
}
옆의 그림은 결과 값을 나타냅니다.
traverse의 의해 comosite들이 순회하게 됩니다.
만약 leaf 노드까지 다달았다면 다시 상위 composite로 가고 그 다음 left -> right 방향으로 leaft 노드를 순회하게 됩니다. Array(indent++).join("--")은 indent 크기만큼 배열을 생성하여 "--"으로 연결하게 됩니다. 예를 들어 Array(3).join("--")은 "----"를 출력합니다. 이러한 composite 패턴은 다음과 같은 장점이 있습니다.

1. client에서는 leaf와 composite를 구분할 필요가 없기 때문에 관리하기 쉬워지게 됩니다.
2. leaf/composite로 구성하여 일관된 계층도를 유지할 수 있다.
3. 새로운 요소를 쉽게 추가할 수 있다. composite와 leaf는 기존에 존재하는 구조들과 독립적으로 동작하므로 새로운 요소가 추가되었다고 해서 프로그램의 변경이 필요하지 않습니다.

감사합니다.



2013년 12월 12일 목요일

[ROR] __FILE__

안녕하세요 belhyun입니다.
ROR에서는 __FILE__ 이 자주 쓰입니다.
이 키워드의 뜻을 종종 헷갈릴 때가 있는데요. 이 키워드의 의미는 다음과 같습니다.
이 키워드는 현재 파일 이름에 대한 참조를 가지게 됩니다.
예를 들어, foo.rb 파일의 __FILE__은 "foo.rb"로 해석되게 됩니다.

감사합니다.

[ROR] protect_from_forgery

안녕하세요 belhyun입니다.
rails를 사용하다 보면 다음과 같은 메소드가 나타납니다.
protect_from_forgery
이 메소드의 의미는 다음과 같습니다.

위조된 요청에 대한 검사를 수행하게 되며, 명심해야 할 것은 GET이 아니거나 HTML/JavaScript 요청이 체크된다는 점입니다.
다음과 같은 형태로 사용됩니다.

protect_from_forgery secret: "123456789012345678901234567890..."
위의 식은 자동으로 현재 세션과 서버측 시크릿으로부터 계산된 보안 토큰을 포함시키게 됩니다. 만약 보안 토큰이 기대한대로 일치하지 않는다면 세션은 다시 설정되게 됩니다. 레일즈 버전 3.0.4 이후로는 ActionController::InvalidAuthenticityToken 에러를 발생시킨다. 감사합니다.

[ROR] Routes, Scope

안녕하세요 belhyun입니다.

만약 라우팅과 매칭되는 네임스페이스를 다르게 하고 싶을 때 rails의 모듈을 사용합니다.
간단히 사용해보도록 하겠습니다.
만약 라우팅을 /test로 하되, namespace는 abcd::test로 하고 싶을 때 라우팅 룰 정의를 다음과 같이 하게 됩니다.

scope module: 'abcd' do
  resources :test
end

또는 다음과 같이 할수도 있습니다.

resources :test, module: 'abcd'

만약 /abcd/test를 TestsController로 전환하고 싶다면, 다음과 같이 사용할 수 있습니다.

scope '/abcd' do
  resources :test, :comments
end
또는 다음과 같이 간단히 표현 할 수도 있습니다.
resources :posts, path: '/admin/posts'
감사합니다.

[설치]centos 5.4 32bit mongoDB 설치

1. Create the following file /etc/yum.repos.d/10gen.repo with the following contents:

[10gen]
name=10gen Repository
baseurl = http://downloads-distro.mongodb.org/repo/redhat/os/x86_64
gpgcheck=0
enabled=1

2. Configure MongoDB

다음에서 설정을 할 수 있다.
/etc/mongod.conf
자동로드 설정
chkconfig mongod on
몽고 시작
service mongod start
몽고 스탑
service mongod stop
몽고 재시작
service mongod restart

3. 테스팅

mongo
db.test.save({a:1})
db.test.find()

[iOS] tableview dynamic height

//
//  MainTableViewController.m
//  mifd
//
//  Created by 이종현 on 2013. 11. 20..
//  Copyright (c) 2013년 belhyun. All rights reserved.
//

#import "MainTableViewController.h"
#import "MainTableViewCell.h"
#import "HttpClient.h"
#import "AppDelegate.h"

#define FONT_SIZE 14.0f
#define CELL_CONTENT_WIDTH 320.0f
#define CELL_CONTENT_MARGIN 5.0f
#define CELL_EXTRA_AREA 60.0f;

const int kLoadingCellTag = 1273;
@interface MainTableViewController ()
@property(nonatomic,assign) Boolean isExpand;
@property (nonatomic, retain) NSMutableArray *tweets;
-(void)pullToRefresh;
-(void)stopRefresh;
-(void)scrollToTop;
-(void)retweetButtonPressed:(id)sender;
-(void)favoriteButtonPressed:(id)sender;
-(void)retweetDelButtonPressed:(id)sender;
-(void)favoriteDelButtonPressed:(id)sender;
-(void)snsRequest:(NSString *)url :(id)sender :(NSMutableDictionary *)params :(NSString *)type :(void (^)(void))callbackBlock;
-(void)mifdRequest:(NSMutableDictionary *)params :(NSUInteger) rowId;
@end
@implementation MainTableViewController

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;
 
    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
    self.tableView.separatorColor = [UIColor yellowColor];
    self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 10, 0);
    UITabBarItem *tabBarItem = [self.tabBarController.tabBar.items objectAtIndex:0];
    tabBarItem.image = [UIImage imageNamed:@"twitter_thumb.png"];
    self.tweets = [[NSMutableArray alloc]init];
    self.isExpand = false;
    self.curPage = 1;
    self.HUD = [[MBProgressHUD alloc] initWithView:self.view];
    [self.view addSubview:self.HUD];
    self.HUD.delegate = self;
    [self.HUD show:YES];
    UIRefreshControl *refresh = [[UIRefreshControl alloc]init];
    refresh.attributedTitle = [[NSAttributedString alloc]initWithString:@"Pull to refresh" attributes:nil];
    [refresh addTarget:self action:@selector(pullToRefresh) forControlEvents:UIControlEventValueChanged];
    self.refreshControl = refresh;
    self.tableView.dataSource = self;
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fetchTweetsWithInit) name:@"fetchTweets" object:nil];
    [self fetchTweets];
}

-(void)pullToRefresh{
    self.curPage = 1;
    self.tweets = [[NSMutableArray alloc]init];
    [self fetchTweets];
    [self performSelector:@selector(stopRefresh) withObject:nil afterDelay:1.5];
}

-(void)stopRefresh{
    [self.refreshControl endRefreshing];
}

-(void) scrollToTop
{
    if ([self numberOfSectionsInTableView:self.tableView] > 0)
    {
        NSIndexPath* top = [NSIndexPath indexPathForRow:NSNotFound inSection:0];
        [self.tableView scrollToRowAtIndexPath:top atScrollPosition:UITableViewScrollPositionTop animated:YES];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    if(self.curPage < self.totalPage){
        return self.tweets.count + 1;
    }
    return self.tweets.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return 1;
}

-(void)changeLoginView{
    [[[UIAlertView alloc] initWithTitle:@"MIFD" message:@"트위터 로그인이 필요합니다." delegate:self cancelButtonTitle:@"확인" otherButtonTitles:nil, nil] show];
    self.tabBarController.selectedViewController = [self.tabBarController.viewControllers objectAtIndex:1];
}

-(void)changeTweetView{
    self.tabBarController.selectedViewController = [self.tabBarController.viewControllers objectAtIndex:0];
}

- (Boolean)isDefinedEle:(NSArray *)array :(NSInteger)tag{
    NSEnumerator *enumerator = [array objectEnumerator];
    id anObject;
    if([array count] == 0) return false;
    while (anObject = [enumerator nextObject]) {
        if(((UIView *)anObject).tag == tag){
            return  true;
        }
    }
    return false;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    UIView *headerView = [[UIView alloc] init];
    headerView.backgroundColor = [UIColor clearColor];
    return headerView;
    /*
     UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,tableView.frame.size.width,30)];
     
     UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(60, 0, headerView.frame.size.width-120.0, headerView.frame.size.height)];
     
     headerLabel.textAlignment = UITextAlignmentRight;
     headerLabel.backgroundColor = [UIColor clearColor];
     NSInteger tbHeight = 50;
     UIToolbar *tb = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, tbHeight)];
     tb.translucent = YES;
     UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
     UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:@"완료" style:UIBarButtonItemStyleBordered target:self action:@selector(completeSelect)];
     NSArray *barButton  =   [[NSArray alloc] initWithObjects:flexibleSpace,doneButton,nil];
     [tb setItems:barButton];
     [headerView addSubview:tb];
     barButton = nil;
     [headerView addSubview:headerLabel];
     
     return headerView;
     */
}

-(void) expandRow:(UITapGestureRecognizer *)gr{
    /*
    MainTableViewCell *view = (MainTableViewCell *)gr.view;
    if(self.isExpand){
        self.isExpand = false;
     }else{
     self.isExpand = true;
     }
     [self.tableView beginUpdates];
     [self.tableView endUpdates];
     */
}

-(void) fetchTweetsWithInit{
    self.curPage = 1;
    self.tweets = [[NSMutableArray alloc]init];
    [self fetchTweets];
}

-(void) fetchTweets{
    HttpClient *httpClient = [HttpClient sharedClient];
    [httpClient GET:[NSMutableString stringWithFormat:@"%@?page=%d&user_desc=%@",RANK,self.curPage,[User getUserDesc]] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        [self.HUD hide:YES];
        self.totalPage = [[responseObject objectForKey:@"total_page"] intValue];
        self.totalCount = [[responseObject objectForKey:@"total_count"] intValue];
        responseObject = [responseObject objectForKey:@"tweets"];
        for(id tweetDictionary in responseObject){
            Tweet *tweet = [[Tweet alloc] initWithDictionary:tweetDictionary];
            [self.tweets addObject:tweet];
        }
        [self.tableView reloadData];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        [self.HUD hide:YES];
        NSLog(@"Error: %@", error);
    }];
}

-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    return nil;
}

-(void)retweetButtonPressed:(id)sender{
    UIButton *clicked = (UIButton *) sender;
    if([User isLogged]){
        [self.HUD show:true];
        [self snsRequest:[NSString stringWithFormat:@"https://api.twitter.com/1.1/statuses/retweet/%@.json",((Tweet *)[self.tweets objectAtIndex:clicked.tag]).uuid] :sender :nil :@"R" :^(void){
            NSMutableDictionary *params = [[NSMutableDictionary alloc]init];
            [params setObject:[MifdKeychainItemWrapper keychainStringFromMatchingIdentifier:@"desc"] forKey:@"user_desc"];
            [params setObject:((Tweet *)[self.tweets objectAtIndex:clicked.tag]).uuid forKey:@"tweet_uuid"];
            [params setObject:@"R" forKey:@"type"];
            [self mifdRequest:params :clicked.tag];
        }];
    }else{
        [self changeLoginView];
    }
}

-(void)favoriteButtonPressed:(id)sender{
    UIButton *clicked = (UIButton *) sender;
    if([User isLogged]){
        [self.HUD show:true];
        NSMutableDictionary *dictionary = [[NSMutableDictionary alloc]init];
        [dictionary setObject:((Tweet *)[self.tweets objectAtIndex:clicked.tag]).uuid forKey:@"id"];
        [self snsRequest:@"https://api.twitter.com/1.1/favorites/create.json" :sender :dictionary :@"F" :^(void){
            NSMutableDictionary *params = [[NSMutableDictionary alloc]init];
            [params setObject:[MifdKeychainItemWrapper keychainStringFromMatchingIdentifier:@"desc"] forKey:@"user_desc"];
            [params setObject:((Tweet *)[self.tweets objectAtIndex:clicked.tag]).uuid forKey:@"tweet_uuid"];
            [params setObject:@"F" forKey:@"type"];
            [self mifdRequest:params :clicked.tag];
        }];
    }else{
        [self changeLoginView];
    }
}

-(void)retweetDelButtonPressed:(id)sender{
    if([User isLogged]){
        [self.HUD hide:true];
        [[[UIAlertView alloc] initWithTitle:@"MIFD" message:@"이미 retweet 하셨네요!" delegate:self cancelButtonTitle:@"확인" otherButtonTitles:nil, nil] show];
    }else{
        [self changeLoginView];
    }
}

-(void)favoriteDelButtonPressed:(id)sender{
    if([User isLogged]){
        [self.HUD hide:true];
        [[[UIAlertView alloc] initWithTitle:@"MIFD" message:@"이미 favorite 하셨네요!" delegate:self cancelButtonTitle:@"취소" otherButtonTitles:nil, nil] show];
    }else{
        [self changeLoginView];
    }
}

-(void)mifdRequest:(NSMutableDictionary *)params :(NSUInteger)tag{
    HttpClient *httpClient = [HttpClient sharedClient];
    [httpClient POST:[NSMutableString stringWithFormat:@"%@",USER_TWEET] parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
        [self.HUD hide:YES];
        if([[responseObject objectForKey:@"result"] integerValue] == 1){
            UserTweet *userTweet = [[UserTweet alloc]init];
            userTweet.tweetUuid = [params objectForKey:@"tweet_uuid"];
            userTweet.userDesc = [User getUserDesc];
            if([[params objectForKey:@"type"] isEqualToString:@"F"]){
                userTweet.type = @"F";
            }else if([[params objectForKey:@"type"] isEqualToString:@"R"]){
                userTweet.type = @"R";
            }
            [((Tweet *)[self.tweets objectAtIndex:tag]).userTweets addObject:userTweet];
            [self.tableView reloadData];
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        [self.HUD hide:YES];
        NSLog(@"Error: %@", error);
    }];
}


-(void)snsRequest:(NSString *)url :(id)sender :(NSMutableDictionary *)params :(NSString *)type :(void (^)(void))callbackBlock{
    ACAccountStore *accountStore = [[ACAccountStore alloc]init];
    ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
    [self.HUD show:YES];
    if([MifdKeychainItemWrapper keychainStringFromMatchingIdentifier:@"desc"] != nil){
        NSArray *accountsArray = [accountStore accountsWithAccountType:accountType];
        if ([accountsArray count] > 0) {
            ACAccount *twitterAccount = [accountsArray objectAtIndex:0];
            NSURL *requestUrl = [NSURL URLWithString:url];
            SLRequest *posts = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodPOST URL:requestUrl parameters:params];
            [posts setAccount:twitterAccount];
            [posts performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                callbackBlock();
            }];
        }
    }else{
        //로그인이 되어있지 않을 때
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"tweet";
    UITableViewCell *cell = nil;
    
    if(indexPath.section >= self.tweets.count){
        return [self loadingCell];
    }
    
    cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    if(cell == nil){
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
    }
    
    if ((([cell.contentView viewWithTag:1])))
    {
        [[cell.contentView viewWithTag:1]removeFromSuperview];
    }
    Tweet *tweet = [self.tweets objectAtIndex:indexPath.section];
    MainTableViewCell *subCell = [[MainTableViewCell alloc]init];
    subCell.itemId = indexPath.section;
    [subCell setFrame:CGRectMake(10, 0, cell.contentView.bounds.size.width-18, cell.bounds.size.height)];
    subCell.backgroundColor = [UIColor yellowColor];
    [subCell setTag:indexPath.section];
    subCell.tag = 1;
    //UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(expandRow:)];
    //[subCell addGestureRecognizer:tap];
    [cell.contentView addSubview:subCell];
    
    TTTAttributedLabel *text = [[TTTAttributedLabel alloc] init];
    text.delegate = self;
    text.enabledTextCheckingTypes = NSTextCheckingTypeLink;
    text.text = (NSString *)[[self getText:tweet] mutableCopy];
    [text setNumberOfLines:0];
    [text setFrame:CGRectMake(60, 0, cell.contentView.bounds.size.width-85, cell.bounds.size.height)];
    //[text setBackgroundColor:[UIColor redColor]];
    [text sizeToFit];
    [[subCell contentView] addSubview:text];
    
    UIImageView * imageView = [[UIImageView alloc] init];
    [imageView setFrame:CGRectMake(0, 0, 50.0, 50.0)];
    [[subCell contentView] addSubview:imageView];
    [HttpClient downloadingServerImageFromUrl:imageView AndUrl:tweet.user.image];
    
    
    UIButton *retweetBtn = [[UIButton alloc]initWithFrame:CGRectMake(55.0, text.frame.size.height+((cell.bounds.size.height-text.frame.size.height)/6.0), 30.0, 30.0)];
    [retweetBtn setReversesTitleShadowWhenHighlighted:YES];
    [retweetBtn setShowsTouchWhenHighlighted:YES];
    retweetBtn.tag = indexPath.section;
    [retweetBtn setBackgroundImage:[UIImage imageNamed:@"twitter_retweet"] forState:UIControlStateNormal];
    if(![self isAlreadyRetweet:tweet]){
        [retweetBtn addTarget:self action:@selector(retweetButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    }else{
        [retweetBtn addTarget:self action:@selector(retweetDelButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    }
    [subCell.contentView addSubview:retweetBtn];
    
    UIButton *favoriteBtn = [[UIButton alloc]initWithFrame:CGRectMake(110.0, text.frame.size.height+((cell.bounds.size.height-text.frame.size.height)/6.0), 30.0, 30.0)];
    [favoriteBtn setReversesTitleShadowWhenHighlighted:YES];
    [favoriteBtn setShowsTouchWhenHighlighted:YES];
    favoriteBtn.tag = indexPath.section;
    [favoriteBtn setBackgroundImage:[UIImage imageNamed:@"favorite"] forState:UIControlStateNormal];
    if(![self isAlreadyFavorite:tweet]){
        [favoriteBtn addTarget:self action:@selector(favoriteButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    }else{
        [favoriteBtn addTarget:self action:@selector(favoriteDelButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    }
    [subCell.contentView addSubview:favoriteBtn];
    
    [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
    [cell setSelected:NO animated:NO];
    cell.userInteractionEnabled = YES;
    return cell;
}

-(Boolean)isAlreadyRetweet:(Tweet *)tweet{
    if(tweet.userTweets == nil){
        return false;
    }
    for(NSUInteger i=0;i

[ROR] something(&:hash)

안녕하세요 belhyun입니다.
ruby에서는 &:hash 문법이 많이 사용됩니다. 그 의미는 다음과 같습니다.
먼저 & 호출은 to_proc을 호출하게 됩니다.
그렇게 되면 something 객체는 Proc 객체로 변환되게 됩니다. Proc 객체는 독립적으로 수행되는 객체를 의미하며 block을 반환합니다.
따라서 something 객체에 :hash 블락을 전달한다. something.map{|obj| obj.hash}와 같은 의미로 사용되게 됩니다.

감사합니다.

[ROR] attr_accessor, attr_accessible의 개념 및 차이

안녕하세요 belhyun입니다.
rails의 model을 보게되면, attr_accessor, attr_accessible이가 자주 나타납니다.
이름이 비슷하여 헷갈릴 수 있는데 이에 대한 개념을 정리해 보도록 하겠습니다.
먼저 그 개념부터 언급하도 넘어가겠습니다.

attr_accessible : 이 속성은 대량으로 업데이트 되는 것이 허용된 속성의 whitelist(가능한 리스트로 생각하시면 됩니다.)를 기술합니다. 이 속성의 주요 목적은 mass assignment에 의한 공격에 대한 방어입니다.

attr_accessor : 이 속성은 gettter/setter를 생성시키는 짧은 코드입니다.

다음과 같은 테이블을 가정해보겠습니다.
CREATE TABLE users (
  firstname string,
  lastname string
  role string
);
Model의 형태는 다음과 같을 것입니다.
class User < ActiveRecord::Base

end
만약 특정 user의 정보를 업데이트 하고자 원할 경우 다음과 같이 할 수 있습니다.
def update
    @user = User.find_by_id(params[:id])
    @user.firstname = params[:user][:firstname]
    @user.lastname = params[:user][:lastname]

    if @user.save
        # Use of I18 internationlization t method for the flash message
        flash[:success] = t('activerecord.successful.messages.updated', :model => User.model_name.human)
    end

    respond_with(@user)
end
하지만 위의 방법은 너무 복잡합니다. 이때, attr_accessible을 기술하고 간단히 업데이트 해보겠습니다.
class User < ActiveRecord::Base

  attr_accessible :firstname, :lastname
end

def update
    @user = User.find_by_id(params[:id])

    if @user.update_attributes(params[:user])
        # Use of I18 internationlization t method for the flash message
        flash[:success] = t('activerecord.successful.messages.updated', :model => User.model_name.human)
    end

    respond_with(@user)
end
여기서 주목할 점은 role이 빠져있다는 점입니다. params[:user]로 업데이트 한다고 해도(mass assignment), role은 변경되지 않습니다. 왜냐하면 attr_accessible에 기술하지 않았기 때문입니다. 따라서 다음과 같은 추가코드가 필요합니다.
@user.role = DEFAULT_ROLE
다음은 attr_accessor에 대해서 알아보겠습니다. 만약 다음과 같은 코드를 기술했다고 해보겠습니다.
class User < ActiveRecord::Base

  attr_accessible :firstname, :lastname
  attr_accessor :test

end
위의 코드로 인해 test에 대한 getter/setter 메소드가 생성됩니다. 실제 위 코드는 아래와 동일합니다.
def test
  @test
end
def test=(value)
  @test = value
end
따라서 user.test의 속성을 정의할 수 있게 됩니다. 이 속성은 실제 DB 컬럼은 아니기 때문에 user.save등으로 저장한다고 해도 실제 저장되지는 않습니다. 감사합니다.

[ROR] public 디렉토리 접근

안녕하세요 belhyun입니다.
rails에서 public 디렉토리에 접근할 이슈가 발생하였습니다. rails는 만약 파일이 존재한다면 public 디렉토리의 파일을 찾게됩니다.(컨트롤러 스택에 쌓이지 않는다.) 그렇기 때문에 만약 파일이 존재한다면
#{Rasil.root}/public/directory/file.png ==> domain.com/directory/file.png
매칭되게 됩니다.

감사합니다.

[ROR] Proc.new, block, lambda에 관해서..

안녕하세요 belhyun입니다.
rails 공부를 하면서 이해하기 어려웠던 점이 Proc.new, Block, lambda에 관한 것이였습니다.
그래서 이 부분을 정리해보려고 이렇게 글을 쓰게 되었습니다.
먼저 이 세가지의 개념은 작은 코드조각이라고 생각할 수 있습니다.
예제를 확인한 후 구체적으로 알아보도록 하겠습니다.

#Block Examples
[1,2,3].each { |x| puts x*2 } #블락은 {}으로 묶입니다.

[1,2,3].each do |x|
  puts x*2
end

#Proc Examples
p = Proc.new { |x| puts x*2 }
[1,2,3].each(&p) #&기호는 proc을 block으로 바꾸라는 의미입니다.

p = Proc.new { puts "Hello World" }
p.call #proc 객체의 몸체가 호출될 때 실행됩니다. 

#Lambda Examples
lam = lambda { |x| puts x*2}
[1,2.3].each(&lam)

lam = lambda{ puts "Hello World"}
lam.call #역시 lambda의 몸체가 호출될 때 실행됩니다. 

위에서 보시는 것과 같이 이 세가지의 개념은 비슷하게 쓰입니다. 차이점을 알아보도록 하겠습니다.

Block과 Procs의 차이점

1. Procs은 객체이고, blocks의 객체가 아닙니다. 
p 객체는 Proc 클래스의 인스턴스입니다.
p = Proc.new { puts "Hello World" }
그리고 이 사실은 객체의 메소드를 호출할 수 있고, 변수에 객체를 할당할 수 있다는 것을 알려줍니다. Procs는 또한 자기자신을 리턴할 수도 있습니다. 다음은 그 예제입니다.
p.call #prints 'Hello World'
p.class #return 'Proc'
a = p 
p
대조적으로, block은 메소드 호출을 위한 문법중의 하나일 뿐입니다. block은 단독의 의미가 아닙니다. 인자 리스트의 하나로도 사용될 수 있습니다. 다음은 그 예제입니다.
{puts "Hello World"} #문법에러가 발생합니다. 단독으로 block은 쓰일 수 없기 때문입니다.
a = {puts "Hello World"} #마찬가지의 이유로 문법에러가 발생합니다.
[1,2,3].each{ |x| puts x*2} # 메소드 호출을 위한 문법의 한 부분으로 쓰입니다.

Proc과 Lambda의 차이점

1. Proc과 Lambda는 'return'키워드를 다루는데 있어 차이점이 있습니다.
다음의 코드를 확인해 보도록 하겠습니다.

def lambda_test
  lam = lambda { return }
  lam.call
  puts "Hello world"
end

lambda_test                 # calling lambda_test prints 'Hello World'
위에서 보시는 것처럼, { return }은 lambda의 바로 바깥쪽에 있는 코드를 실행시킵니다.
def proc_test
  proc = Proc.new { return }
  proc.call
  puts "Hello world"
end

proc_test                 # calling proc_test prints nothing
반면 Proc은 아무것도 호출하지 않습니다. 왜냐하면 Proc은 Lambda와 달리 Proc이 실제로 호출되는 시점의 바로 다음의 코드를 실행시키기 때문입니다.

[CSS] ul in div 가운데 정렬(centering)

  • Three
  • Blind
  • Mice
위와과 같은 마크업이 존재 ul in div를 가운데 정렬하고 싶다. 다음과 같이 처리한다.

[iOS] 트위터 연동하기

안녕하세요 belhyun입니다.
이번에 트위터앱을 개발하면서 트위터 인증을 사용할 필요가 생겼습니다.
저 또한 처음해보는 내용이였고 다양한 블로그와 API 문서를 참조하여 구현하였습니다.
먼저 동작 프로세스부터 소개하겠습니다.
저의 트위터앱은 다음과 같은 과정으로 동작합니다.

트윗을 가져올 사람을 지정
->
해당 트윗을 리트윗 수, favorites 수대로 정렬
->
트윗을 몽고 DB에 저장
->
트윗을 가져와 테이블뷰에 노출한다.
->
해당 트윗은 다시 리트윗이 가능
다음은 참조이미지입니다.

트위터 앱
아래의 retweet 버튼을 누르게 되면 해당 트윗은 리트윗이 됩니다. 그럼 이 부분을 어떻게 구현하였는지 코드로 보여드리도록 하겠습니다.
먼저 해당 버튼을 누르게 되면 다음과 같이 호출됩니다.

루비 코드 테스트

def story
end