GitHub ReleaseEvent at 2014-05-21 03:43:43
Repository: MEANStack.net
Actor: ysyun
Payload: {"action":"published","release":{"url":"https://api.github.com/repos/MEAN-STACK/MEANStack.net/releases/331710","assets_url":"https://api.github.com/repos/MEAN-STACK/MEANStack.net/releases/331710/assets","upload_url":"https://uploads.github.com/repos/MEAN-STACK/MEANStack.net/releases/331710/assets{?name}","html_url":"https://github.com/MEAN-STACK/MEANStack.net/releases/tag/v0.1.0","id":331710,"tag_name":"v0.1.0","target_commitish":"master","name":"서버 Question, Answer API 개발","draft":false,"author":{"login":"ysyun","id":1870072,"avatar_url":"https://avatars.githubusercontent.com/u/1870072?","gravatar_id":"5d6dcf3eb96a3ac6fa9f655aceebb488","url":"https://api.github.com/users/ysyun","html_url":"https://github.com/ysyun","followers_url":"https://api.github.com/users/ysyun/followers","following_url":"https://api.github.com/users/ysyun/following{/other_user}","gists_url":"https://api.github.com/users/ysyun/gists{/gist_id}","starred_url":"https://api.github.com/users/ysyun/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ysyun/subscriptions","organizations_url":"https://api.github.com/users/ysyun/orgs","repos_url":"https://api.github.com/users/ysyun/repos","events_url":"https://api.github.com/users/ysyun/events{/privacy}","received_events_url":"https://api.github.com/users/ysyun/received_events","type":"User","site_admin":false},"prerelease":false,"created_at":"2014-05-21T03:33:00Z","published_at":"2014-05-21T03:43:43Z","assets":[],"tarball_url":"https://api.github.com/repos/MEAN-STACK/MEANStack.net/tarball/v0.1.0","zipball_url":"https://api.github.com/repos/MEAN-STACK/MEANStack.net/zipball/v0.1.0","body":"# Questions, Answers API 구현\r\n- http://apist.net/#/projects/11/category/156\r\n- http://apist.net/#/projects/11/category/157\r\n\r\n## 인증된 사용자 제한\r\n- API 접근을 로그인된 사용자로 제한하는 기능 구현\r\n- 권한 별 접근은 미구현\r\n\r\n`./lib/routes.js`\r\n\r\n```javascript\r\nauthorize: function () {\r\n  return function (req, res, next) {\r\n    if (!req.isAuthenticated()) {\r\n      return res.send(401, 'Unauthorized');\r\n    }\r\n    next();\r\n  };\r\n}\r\n```\r\n\r\n## 라우팅 설정\r\n- 인증 함수 생성\r\n\r\n`./lib/routes.js`\r\n\r\n```javascript\r\nvar authorize = middleware.authorize();\r\n```\r\n\r\n- `/api/v1`을 기본 경로로 설정\r\n- `/questions{/:id}` 루트 설정\r\n- `POST`, `PUT`, `DELETE` 메서드는 사용자 인증 필요\r\n\r\n`./lib/routes.js`\r\n\r\n```javascript\r\nvar base = '/api/v1';\r\n\r\napp.get(base + '/questions', questions.get);\r\napp.get(base + '/questions/:id', questions.getById);\r\napp.post(base + '/questions', authorize, questions.post);\r\napp.put(base + '/questions/:id', authorize, questions.put);\r\napp.del(base + '/questions/:id', authorize, questions.del);\r\n\r\n```\r\n\r\n## 모델 정의\r\n\r\n### Question\r\n\r\n#### 스키마\r\n- 작성자(`user`), 제목(`title`), 내용(`content`), 태그 목록(`tags`), 투표 수(`vote_count`), 긍정적 투표자(`pos_voters`), 부정적 투표자,(`neg_voters`) 답변 목록(`answers`), 생성일자(`created`), 수정일자(`updated`) 필드 정의\r\n- 투표 수 = 긍정적 투표자의 수 - 부정적 투표자의 수\r\n- 답변 목록은 답변 식별자의 배열\r\n\r\n`./lib/models/question.js`\r\n\r\n```javascript\r\nvar QuestionSchema = new Schema({\r\n  user: {\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'User'\r\n  },\r\n  title: {\r\n    type: String,\r\n    trim: true\r\n  },\r\n  content: {\r\n    type: String,\r\n    'default': '',\r\n    trim: true\r\n  },\r\n  tags: {\r\n    type: [String],\r\n    'default': []\r\n  },\r\n  vote_count: {\r\n    type: Number,\r\n    'default': 0\r\n  },\r\n  answers: [{\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'Answer'\r\n  }],\r\n  pos_voters: {\r\n    type: [Schema.Types.ObjectId],\r\n    'default': []\r\n  },\r\n  neg_voters:{\r\n    type: [Schema.Types.ObjectId],\r\n    'default': []\r\n  },\r\n  created: {\r\n    type: Date,\r\n    'default': Date.now\r\n  },\r\n  updated: {\r\n    type: Date,\r\n    'default': Date.now\r\n  }\r\n});\r\n```\r\n#### 메서드\r\n##### toViewModel()\r\n- 엔터티 모델을 API 클라이언트 노출 모델로 변환\r\n- 식별자, 생성일, 수정일, 제목, 내용, 태그 목록, 투표 수, 답변 수, 사용자, 댓글 포함\r\n- 투표자(긍정적, 부정적) 정보는 숨김\r\n- 사용자 정보는 이메일과 이름만 공개\r\n\r\n`./lib/models/question.js`\r\n\r\n```javascript\r\nQuestionSchema.methods.toViewModel = function (comments) {\r\n  console.log(this);\r\n  return {\r\n    _id: this._id,\r\n    created: this.created,\r\n    updated: this.updated,\r\n    title: this.title,\r\n    content: this.content,\r\n    tags: this.tags,\r\n    vote_count: this.vote_count,\r\n    answer_count: this.answers.length,\r\n    user: {\r\n      name: this.user.name,\r\n      email: this.user.email\r\n    },\r\n    comments: comments ? this.comments : undefined\r\n  };\r\n};\r\n```\r\n\r\n##### toResponse()\r\n- 엔터티 모델을 API 응답 모델롤 변환\r\n- 메타데이터와 API 클라이언트 노출 모델을 캡슐화\r\n\r\n`./lib/models/question.js`\r\n\r\n```javascript\r\nQuestionSchema.methods.toResponse = function (metadata) {\r\n  return {\r\n    metadata: metadata || {},\r\n    result: this.toViewModel()\r\n  };\r\n};\r\n```\r\n\r\n### Answer\r\n\r\n#### 스키마\r\n- 작성자(`user`), 질문(`question`), 내용(`content`), 투표 수(`vote_count`), 긍정적 투표자(`pos_voters`), 부정적 투표자,(`neg_voters`), 생성일자(`created`), 수정일자(`updated`) 필드 정의\r\n- 투표 수 = 긍정적 투표자의 수 - 부정적 투표자의 수\r\n\r\n`./lib/models/answer.js`\r\n\r\n```javascript\r\nvar AnswerSchema = new Schema({\r\n  user: {\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'User'\r\n  },\r\n  question: {\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'Question'\r\n  },\r\n  content: {\r\n    type: String,\r\n    'default': '',\r\n    trim: true\r\n  },\r\n  vote_count: {\r\n    type: Number,\r\n    'default': 0\r\n  },\r\n  pos_voters: {\r\n    type: [Schema.Types.ObjectId],\r\n    'default': []\r\n  },\r\n  neg_voters: {\r\n    type: [Schema.Types.ObjectId],\r\n    'default': []\r\n  },\r\n  created: {\r\n    type: Date,\r\n    'default': Date.now\r\n  },\r\n  updated: {\r\n    type: Date,\r\n    'default': Date.now\r\n  }\r\n});\r\n```\r\n#### 메서드\r\n##### toViewModel()\r\n- 엔터티 모델을 API 클라이언트 노출 모델로 변환\r\n- 식별자, 생성일, 수정일, 내용, 투표 수, 사용자, 댓글 포함\r\n- 투표자(긍정적, 부정적) 정보는 숨김\r\n- 사용자 정보는 이메일과 이름만 공개\r\n\r\n`./lib/models/answer.js`\r\n\r\n```javascript\r\nAnswerSchema.methods.toViewModel = function (comments) {\r\n  return {\r\n    _id: this._id,\r\n    created: this.created,\r\n    updated: this.updated,\r\n    content: this.content,\r\n    vote_count: this.vote_count,\r\n    user: {\r\n      name: this.user.name,\r\n      email: this.user.email\r\n    },\r\n    comments: comments ? this.comments: undefined\r\n  };\r\n};\r\n```\r\n\r\n##### toResponse()\r\n- 엔터티 모델을 API 응답 모델롤 변환\r\n- 메타데이터와 API 클라이언트 노출 모델을 캡슐화\r\n\r\n`./lib/models/answer.js`\r\n\r\n```javascript\r\nAnswerSchema.methods.toResponse = function (metadata) {\r\n  return {\r\n    metadata: metadata || {},\r\n    result: this.toViewModel()\r\n  };\r\n};\r\n```\r\n# ====================\r\n# Renewed\r\n# ====================\r\n\r\n## POST a question\r\n- 새로운 질문을 등록하는 API\r\n- 현재 사용자로 작성자가 설정됨\r\n\r\n```javascript\r\nexports.post = function (req, res) {\r\n  var question = new Question(req.body);\r\n  question.user = req.user;\r\n  question.save(function (err) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        errors: err.errors,\r\n        question: question\r\n      });\r\n    } else {\r\n      res.jsonp(question);\r\n    }\r\n  });\r\n};\r\n```\r\n\r\n## GET a question\r\n- URL 세그먼트로 조회할 질문의 식별자를 지정\r\n- 지정한 식별자를 갖는 질문을 발견하지 못하면 404 상태 반환\r\n\r\n```javascript\r\nexports.getById = function (req, res) {\r\n  var id = req.params.id;\r\n  Question.findById(id, function (err, question) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        errors: err.errors\r\n      });\r\n    } else {\r\n      if (question) {\r\n        res.json(question);\r\n      } else {\r\n        res.send(404);\r\n      }\r\n    }\r\n  });\r\n};\r\n```\r\n\r\n## GET questions\r\n- `top`, `skip`  URL 쿼리로 최대 질문 수와 건너뛸 질문 수를 선택적으로 지정\r\n- created 필드를 기준으로 내림차순 정렬\r\n\r\n```javascript\r\nexports.get = function (req, res) {\r\n  var top = req.query.top || 20,\r\n      skip = req.query.skip || 0;\r\n  Question.find().sort('-created').skip(skip).limit(top).exec(function (err, questions) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        errors: err.errors\r\n      });\r\n    } else {\r\n      res.jsonp(questions);\r\n    }\r\n  });\r\n};\r\n```\r\n\r\n## Answer 모델 정의\r\n- 사용자(작성자), 질문 식별자, 내용, 생성일자 필드 추가\r\n- 투표(vote)는 미구현\r\n\r\n```javascript\r\nvar AnswerSchema = new Schema({\r\n  user: {\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'User'\r\n  },\r\n  question: {\r\n    type: Schema.Types.ObjectId,\r\n    ref: 'Question'\r\n  },\r\n  content: {\r\n    type: String,\r\n    'default': '',\r\n    trim: true\r\n  },\r\n  created: {\r\n    type: Date,\r\n    'default': Date.now\r\n  }\r\n});\r\n```\r\n\r\n## POST an answer\r\n- 질문을 먼저 검색해서 찾지 못하면 404 상태 반환\r\n- 질문이 존재하면 답변 등록\r\n\r\n```javascript\r\nexports.post = function (req, res) {\r\n  var answer = new Answer(req.body),\r\n      questionId = answer.question;\r\n  answer.user = req.user;\r\n  Question.findById(questionId, function (err, question) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        errors: err.errors\r\n      });\r\n    }\r\n    if (!question) {\r\n      return res.send(404);\r\n    }\r\n    answer.save(function (err) {\r\n      if (err) {\r\n        return res.send(500, {\r\n          errors: err.errors\r\n        });\r\n      } else {\r\n        res.jsonp(answer);\r\n      }\r\n    });\r\n  });\r\n};\r\n```\r\n\r\n## GET answers of the specific question\r\n- 질문을 검색해서 찾지 못하면 404 상태 반환\r\n- 질문이 존재하면 질문에 대한 답변 컬렉션을 조회해서 반환\r\n\r\n```javascript\r\nexports.get = function (req, res) {\r\n  var questionId = req.query.question;\r\n  Question.findById(questionId, function (err, question) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        errors: err.errors\r\n      });\r\n    }\r\n    if (!question) {\r\n      return res.send(404);\r\n    }\r\n    Answer.find({ question: questionId }, function (err, answers) {\r\n      if (err) {\r\n        return res.send(500, {\r\n          errors: err.errors\r\n        });\r\n      } else {\r\n        res.jsonp(answers);\r\n      }\r\n    });\r\n  });\r\n};\r\n```\r\n\r\n## 'created' 필드 주입 오류 수정\r\n- `created` 필드가 API 클라이언트에 의해 수정될 수 있는 가능성을 차단\r\n\r\n```javascript\r\n  if (req.body) {\r\n    delete req.body.created;\r\n  }\r\n```\r\n\r\n## 질문 조회에 'sort' 쿼리 추가\r\n- 지정한 필드를 기준으로 질문 목록을 정렬\r\n- `sort` 쿼리를 사용해 정렬 기준 필드를 지정\r\n\r\n```javascript\r\n  var top = req.query.top || 20,\r\n      skip = req.query.skip || 0,\r\n      sort = req.query.sort || '-created';\r\n  Question.find().sort(sort).skip(skip).limit(top).exec(...\r\n```\r\n\r\n## '내 질문' 필터 추가\r\n- 현재 사용자가 작성한 질문만을 조회\r\n- `filter` 쿼리 값을 `mines`로 지정\r\n\r\n```javascript\r\n  var top = req.query.top || 20,\r\n      skip = req.query.skip || 0,\r\n      sort = req.query.sort || '-created',\r\n      filter = req.query.filter,\r\n      query = Question.find().sort(sort).skip(skip).limit(top);\r\n  if (filter) {\r\n    if (filter === 'mines') {\r\n      if (req.user) {\r\n        query = query.where('user').equals(req.user._id);\r\n      }\r\n    }\r\n  }\r\n```\r\n\r\n## Question 모델에 최종 갱신일자 필드 추가\r\n- 기존 모델에 최종 갱신일자 필드가 누락\r\n- `updated` 필드 추가\r\n\r\n```javascript\r\n  updated: {\r\n    type: Date,\r\n    'default': Date.now\r\n  }\r\n```\r\n\r\n- `updated` 필드가 API 클라이언트에 의해 수정될 수 있는 가능성을 차단\r\n\r\n```javascript\r\nfunction refine(body) {\r\n  delete body.created;\r\n  delete body.updated;\r\n}\r\n\r\nexports.post = function (req, res) {\r\n  if (req.body) {\r\n    refine(req.body);\r\n  }\r\n```\r\n\r\n## Hot Fix 브랜치 병합\r\n- 테스트 실행시 `development` 구성이 로드되는 오류\r\n- 해당 오류를 수정하는 코드가 `develop` 브랜치에 병합\r\n- 작업 브랜치에 `develop` 브랜치를 병합\r\n\r\n## PUT a question\r\n- 루트 등록\r\n- 인증된 사용자만 접근 가능\r\n\r\n```javascript\r\n  app.put(base + '/questions', authorize(), questions.put);\r\n```\r\n\r\n- 요청 본문을 정제(`created`, `updated` 필드 보호)\r\n- 질문을 검색해서 찾지 못하면 404 상태 반환\r\n- 검색된 질문이 현재 사용자가 작성한 질문이 아니면 401 상태 반환\r\n- 수정된 내용이 없으면 304 상태 반환\r\n- 수정된 내용이 있으면 반영하고 `updated` 필드를 수정한 뒤 변경된 문서를 반환\r\n\r\n```javascript\r\nexports.put = function (req, res) {\r\n  if (req.body) {\r\n    refine(req.body);\r\n  }\r\n  var id = req.body._id;\r\n  Question.findById(id, function (err, question) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        error: err,\r\n      });\r\n    }\r\n    if (!question) {\r\n      return res.send(404);\r\n    }\r\n    if (question.user.id !== req.user._id.id) {\r\n      return res.send(401, 'Unauthorized');\r\n    }\r\n    var updated = false;\r\n    if (typeof req.body.title === 'string' && req.body.title !== question.title) {\r\n      question.title = req.body.title;\r\n      updated = true;\r\n    }\r\n    if (typeof req.body.content === 'string' && req.body.content !== question.content) {\r\n      question.content = req.body.content;\r\n      updated = true;\r\n    }\r\n    if (updated) {\r\n      question.updated = new Date(Date.now());\r\n      question.save(function (err) {\r\n        if (err) {\r\n          return res.send(500, {\r\n            error: err\r\n          });\r\n        }\r\n        res.jsonp(question);\r\n      });\r\n    } else {\r\n      return res.send(304);\r\n    }\r\n  });\r\n};\r\n```\r\n\r\n## DELETE a question\r\n- 루트 등록\r\n- 인증된 사용자만 접근 가능\r\n\r\n```javascript\r\n  app.del(base + '/questions/:id', authorize(), questions.del);\r\n```\r\n\r\n- 질문을 검색해서 찾지 못하면 404 상태 반환\r\n- 검색된 질문이 현재 사용자가 작성한 질문이 아니면 401 상태 반환\r\n- 질문에 대한 모든 답변을 삭제\r\n- 질문을 삭제\r\n\r\n```javascript\r\nexports.del = function (req, res) {\r\n  var id = req.params.id;\r\n  Question.findById(id, function (err, question) {\r\n    if (err) {\r\n      return res.send(500, {\r\n        error: err\r\n      });\r\n    }\r\n    if (!question) {\r\n      return res.send(404);\r\n    }\r\n    if (question.user.id !== req.user._id.id) {\r\n      return res.send(401, 'Unauthorized');\r\n    }\r\n    Answer.find({ question: id }).remove(function (err) {\r\n      if (err) {\r\n        return res.send(500, {\r\n          error: err\r\n        });\r\n      }\r\n      question.remove(function (err, question) {\r\n        if (err) {\r\n          return res.send(500, {\r\n            error: err\r\n          });\r\n        }\r\n        res.jsonp(question);\r\n      });\r\n    });\r\n  });\r\n}\r\n```"}}