Yi EungJun 2013-03-07
Refactor Issue and Post.
* Rename Post to Posting.
* Rename Comment to PostingComment.
* Remove duplication between Issue and Posting.
@be6519d7c229d728a9c5c17fbae1913bee2fd7dc
app/Global.java
--- app/Global.java
+++ app/Global.java
@@ -19,28 +19,32 @@
 
     private static void insertDefaults() {
         if (Ebean.find(User.class).findRowCount() == 0) {
+            String initFileName = "initial-data.yml";
+
             @SuppressWarnings("unchecked")
             Map<String, List<Object>> all = (Map<String, List<Object>>) Yaml
-                    .load("initial-data.yml");
+                    .load(initFileName);
 
-            Ebean.save(all.get("users"));
-            Ebean.save(all.get("projects"));
-            Ebean.save(all.get("milestones"));
-            Ebean.save(all.get("issues"));
-            Ebean.save(all.get("issueComments"));
-            Ebean.save(all.get("posts"));
-            Ebean.save(all.get("comments"));
+            String[] entityNames = {
+                "users", "projects", "milestones",
+                "issues", "issueComments",
+                "postings", "postingComments",
+                "roles", "projectUsers",
+                "taskBoards", "lines", "cards", "labels", "checkLists",
+                "siteAdmins"
+            };
 
-            Ebean.save(all.get("roles"));
-            Ebean.save(all.get("projectUsers"));
+            // Check whether every entities exist.
+            for (String entityName : entityNames) {
+                if (all.get(entityName) == null) {
+                    throw new RuntimeException("Failed to find the '" + entityName
+                            + "' entity in '" + initFileName + "'");
+                }
+            }
 
-            Ebean.save(all.get("taskBoards"));
-            Ebean.save(all.get("lines"));
-            Ebean.save(all.get("cards"));
-            Ebean.save(all.get("labels"));
-            Ebean.save(all.get("checkLists"));
-
-            Ebean.save(all.get("siteAdmins"));
+            for (String entityName : entityNames) {
+                Ebean.save(all.get(entityName));
+            }
         }
     }
     
 
app/controllers/AbstractPostingApp.java (added)
+++ app/controllers/AbstractPostingApp.java
@@ -0,0 +1,116 @@
+package controllers;
+
+import models.AbstractPosting;
+import models.Comment;
+import models.Project;
+import models.Attachment;
+import models.enumeration.Direction;
+import models.enumeration.Matching;
+import models.enumeration.Operation;
+
+import models.support.OrderParams;
+import models.support.SearchParams;
+import play.data.Form;
+import play.mvc.Call;
+import play.mvc.Content;
+import utils.AccessControl;
+
+import play.mvc.Controller;
+import play.mvc.Result;
+import utils.Callback;
+import utils.Constants;
+
+import java.io.IOException;
+
+public class AbstractPostingApp extends Controller {
+    public static final int ITEMS_PER_PAGE = 15;
+
+    public static class SearchCondition {
+        public String orderBy;
+        public String orderDir;
+        public String filter;
+        public int pageNum;
+
+        public SearchCondition() {
+            this.orderDir = Direction.DESC.direction();
+            this.orderBy = "id";
+            this.filter = "";
+            this.pageNum = 1;
+        }
+    }
+
+    public static Result newComment(Comment comment, Form<? extends Comment> commentForm, Call redirectTo, Callback updateCommentContainer) throws IOException {
+        if (session(UserApp.SESSION_USERID) == null) {
+            flash(Constants.WARNING, "user.login.alert");
+            return redirect(redirectTo);
+        }
+
+        if (commentForm.hasErrors()) {
+            flash(Constants.WARNING, "board.comment.empty");
+            return redirect(redirectTo);
+        }
+
+        comment.setAuthor(UserApp.currentUser());
+        updateCommentContainer.run(); // this updates comment.issue or comment.posting;
+        Project project = comment.asResource().getProject();
+        comment.save();
+
+        // Attach all of the files in the current user's temporary storage.
+        Attachment.attachFiles(UserApp.currentUser().id, project.id, comment.asResource().getType(), comment.id);
+
+        return redirect(redirectTo);
+    }
+
+    public static Result deletePosting(AbstractPosting posting, Call redirectTo) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), posting.asResource(), Operation.DELETE)) {
+            return forbidden();
+        }
+
+        posting.delete();
+
+        Attachment.deleteAll(posting.asResource().getType(), posting.id);
+
+        return redirect(redirectTo);
+    }
+
+    public static Result deleteComment(Comment comment, Call redirectTo) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)) {
+            return forbidden();
+        }
+
+        comment.delete();
+
+        Attachment.deleteAll(comment.asResource().getType(), comment.id);
+
+        return redirect(redirectTo);
+    }
+
+    protected static Result editPosting(AbstractPosting original, AbstractPosting posting, Form<? extends AbstractPosting> postingForm, Call redirectTo, Callback updatePosting) {
+        if (postingForm.hasErrors()) {
+            return badRequest(postingForm.errors().toString());
+        }
+
+        posting.id = original.id;
+        posting.date = original.date;
+        posting.authorId = original.authorId;
+        posting.authorLoginId = original.authorLoginId;
+        posting.authorName = original.authorName;
+        posting.project = original.project;
+        updatePosting.run();
+        original.update();
+
+        // Attach the files in the current user's temporary storage.
+        Attachment.attachFiles(UserApp.currentUser().id, original.project.id, original.asResource().getType(), original.id);
+
+        return redirect(redirectTo);
+    }
+
+    public static Result newPostingForm(Project project, Content content) {
+        if (UserApp.currentUser() == UserApp.anonymous) {
+            return unauthorized(views.html.project.unauthorized.render(project));
+        }
+
+        return ok(content);
+    }
+
+}
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -6,85 +6,84 @@
 
 import com.avaje.ebean.Page;
 
-import models.Attachment;
-import models.Comment;
-import models.Post;
-import models.Project;
-import models.User;
-import models.enumeration.Direction;
+import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
-import play.data.Form;
-import play.mvc.Controller;
-import play.mvc.Result;
-import utils.AccessControl;
-import utils.Constants;
+
+import models.support.FinderTemplate;
+import models.support.OrderParams;
+import models.support.SearchParams;
+import models.enumeration.Direction;
+import models.enumeration.Matching;
 import views.html.board.editPost;
 import views.html.board.newPost;
 import views.html.board.postList;
 
-public class BoardApp extends Controller {
+import utils.AccessControl;
+import utils.Callback;
+import utils.Constants;
+import utils.JodaDateUtil;
 
-    //TODO 이 클래스는 원래 따로 존재해야 함.
-    public static class SearchCondition{
-        public final static String ORDERING_KEY_ID = "id";
-        public final static String ORDERING_KEY_TITLE = "title";
-        public final static String ORDERING_KEY_AGE = "date";
-        public final static String ORDERING_KEY_AUTHOR = "authorName";
+import play.data.Form;
+import play.mvc.Call;
+import play.mvc.Result;
 
-        public SearchCondition() {
-            this.order = Direction.DESC.direction();
-            this.key = ORDERING_KEY_ID;
-            this.filter = "";
-            this.pageNum = 1;
+import java.io.IOException;
+
+public class BoardApp extends AbstractPostingApp {
+    public static class SearchCondition extends AbstractPostingApp.SearchCondition {
+        OrderParams getOrderParams() {
+            return new OrderParams().add(
+                        orderBy, Direction.getValue(orderDir));
         }
 
-        public String order;
-        public String key;
-        public String filter;
-        public int pageNum;
+        SearchParams getSearchParam(Project project) {
+            return new SearchParams()
+                    .add("project.owner", project.owner, Matching.EQUALS)
+                    .add("project.name", project.name, Matching.EQUALS)
+                    .add("body", filter, Matching.CONTAINS);
+        }
     }
-
 
     public static Result posts(String userName, String projectName, int pageNum) {
         Form<SearchCondition> postParamForm = new Form<SearchCondition>(SearchCondition.class);
-        SearchCondition postSearchCondition = postParamForm.bindFromRequest().get();
-        postSearchCondition.pageNum = pageNum - 1;
+        SearchCondition searchCondition = postParamForm.bindFromRequest().get();
+        searchCondition.pageNum = pageNum - 1;
         Project project = ProjectApp.getProject(userName, projectName);
 
         if (!AccessControl.isCreatable(User.findByLoginId(session().get("loginId")), project, ResourceType.BOARD_POST)) {
             return unauthorized(views.html.project.unauthorized.render(project));
         }
 
-        Page<Post> posts = Post.findOnePage(project.owner, project.name, postSearchCondition.pageNum,
-                        Direction.getValue(postSearchCondition.order), postSearchCondition.key, postSearchCondition.filter);
-        return ok(postList.render("menu.board", project, posts, postSearchCondition));
+        SearchParams searchParam = searchCondition.getSearchParam(project);
+        OrderParams orderParams = searchCondition.getOrderParams();
+
+        Page<Posting> posts = FinderTemplate.getPage(
+                orderParams, searchParam, Posting.finder, ITEMS_PER_PAGE, searchCondition.pageNum);
+
+        return ok(postList.render("menu.board", project, posts, searchCondition));
     }
 
     public static Result newPostForm(String userName, String projectName) {
         Project project = ProjectApp.getProject(userName, projectName);
-        if (UserApp.currentUser() == UserApp.anonymous) {
-            return unauthorized(views.html.project.unauthorized.render(project));
-        }
-
-        return ok(newPost.render("board.post.new", new Form<Post>(Post.class), project));
+        return newPostingForm(project,
+                newPost.render("board.post.new", new Form<Posting>(Posting.class), project));
     }
 
     public static Result newPost(String userName, String projectName) {
-        Form<Post> postForm = new Form<Post>(Post.class).bindFromRequest();
+        Form<Posting> postForm = new Form<Posting>(Posting.class).bindFromRequest();
         Project project = ProjectApp.getProject(userName, projectName);
         if (postForm.hasErrors()) {
             flash(Constants.WARNING, "board.post.empty");
 
             return redirect(routes.BoardApp.newPost(userName, projectName));
         } else {
-            Post post = postForm.get();
-            post.authorId = UserApp.currentUser().id;
-            post.authorLoginId = UserApp.currentUser().loginId;
-            post.authorName = UserApp.currentUser().name;
-            post.commentCount = 0;
+            Posting post = postForm.get();
+            post.date = JodaDateUtil.now();
+            post.setAuthor(UserApp.currentUser());
             post.project = project;
-            Post.write(post);
+
+            post.save();
 
             // Attach all of the files in the current user's temporary storage.
             Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.BOARD_POST, post.id);
@@ -94,8 +93,8 @@
     }
 
     public static Result post(String userName, String projectName, Long postId) {
-        Post post = Post.findById(postId);
         Project project = ProjectApp.getProject(userName, projectName);
+        Posting post = Posting.finder.byId(postId);
         if (!AccessControl.isCreatable(User.findByLoginId(session().get("loginId")), project, ResourceType.BOARD_POST)) {
             return unauthorized(views.html.project.unauthorized.render(project));
         }
@@ -104,45 +103,39 @@
             flash(Constants.WARNING, "board.post.notExist");
             return redirect(routes.BoardApp.posts(project.owner, project.name, 1));
         } else {
-            Form<Comment> commentForm = new Form<Comment>(Comment.class);
+            Form<PostingComment> commentForm = new Form<PostingComment>(PostingComment.class);
             return ok(views.html.board.post.render(post, commentForm, project));
         }
     }
 
-    public static Result newComment(String userName, String projectName, Long postId) {
-        Form<Comment> commentForm = new Form<Comment>(Comment.class).bindFromRequest();
+    public static Result newComment(String userName, String projectName, Long postId) throws IOException {
+        final Posting post = Posting.finder.byId(postId);
+        Project project = post.project;
+        Call redirectTo = routes.BoardApp.post(project.owner, project.name, postId);
+        Form<PostingComment> commentForm = new Form<PostingComment>(PostingComment.class)
+                .bindFromRequest();
 
-        Project project = ProjectApp.getProject(userName, projectName);
-        if (commentForm.hasErrors()) {
-            flash(Constants.WARNING, "board.comment.empty");
-            return redirect(routes.BoardApp.post(project.owner, project.name, postId));
-        } else {
-            Comment comment = commentForm.get();
-            comment.post = Post.findById(postId);
-            comment.authorId = UserApp.currentUser().id;
-            comment.authorLoginId = UserApp.currentUser().loginId;
-            comment.authorName = UserApp.currentUser().name;
+        final PostingComment comment = commentForm.get();
 
-            Comment.write(comment);
-            Post.countUpCommentCounter(postId);
-
-            // Attach all of the files in the current user's temporary storage.
-            Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.BOARD_COMMENT, comment.id);
-
-            return redirect(routes.BoardApp.post(project.owner, project.name, postId));
-        }
+        return newComment(comment, commentForm, redirectTo, new Callback() {
+            @Override
+            public void run() {
+                comment.posting = post;
+            }
+        });
     }
 
     public static Result deletePost(String userName, String projectName, Long postId) {
-        Project project = ProjectApp.getProject(userName, projectName);
-        Post.delete(postId);
-        Attachment.deleteAll(ResourceType.BOARD_POST, postId);
-        return redirect(routes.BoardApp.posts(project.owner, project.name, 1));
+        Posting posting = Posting.finder.byId(postId);
+        Project project = posting.project;
+
+        return deletePosting(posting,
+                routes.BoardApp.posts(project.owner, project.name, 1));
     }
 
     public static Result editPostForm(String userName, String projectName, Long postId) {
-        Post existPost = Post.findById(postId);
-        Form<Post> editForm = new Form<Post>(Post.class).fill(existPost);
+        Posting existPost = Posting.finder.byId(postId);
+        Form<Posting> editForm = new Form<Posting>(Posting.class).fill(existPost);
         Project project = ProjectApp.getProject(userName, projectName);
 
         if (AccessControl.isAllowed(UserApp.currentUser(), existPost.asResource(), Operation.UPDATE)) {
@@ -154,35 +147,25 @@
     }
 
     public static Result editPost(String userName, String projectName, Long postId) {
-        Form<Post> postForm = new Form<Post>(Post.class).bindFromRequest();
+        Form<Posting> postForm = new Form<Posting>(Posting.class).bindFromRequest();
         Project project = ProjectApp.getProject(userName, projectName);
+        Posting post = postForm.get();
+        Posting original = Posting.finder.byId(postId);
+        Call redirectTo = routes.BoardApp.posts(project.owner, project.name, 1);
+        Callback doNothing = new Callback() {
+            @Override
+            public void run() { }
+        };
 
-        if (postForm.hasErrors()) {
-            flash(Constants.WARNING, "board.post.empty");
-            return redirect(routes.BoardApp.editPost(userName, projectName, postId));
-        } else {
-
-            Post post = postForm.get();
-            post.authorId = UserApp.currentUser().id;
-            post.authorName = UserApp.currentUser().name;
-            post.id = postId;
-            post.project = project;
-
-            Post.edit(post);
-
-            // Attach the files in the current user's temporary storage.
-            Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.BOARD_POST, post.id);
-        }
-
-        return redirect(routes.BoardApp.posts(project.owner, project.name, 1));
-
+        return editPosting(original, post, postForm, redirectTo, doNothing);
     }
 
-    public static Result deleteComment(String userName, String projectName, Long postId, Long commentId) {
-        Comment.delete(commentId);
-        Post.countDownCommentCounter(postId);
-        Attachment.deleteAll(ResourceType.BOARD_COMMENT, commentId);
-        return redirect(routes.BoardApp.post(userName, projectName, postId));
-    }
+    public static Result deleteComment(String userName, String projectName, Long postId,
+            Long commentId) {
+        Comment comment = PostingComment.find.byId(commentId);
+        Project project = comment.asResource().getProject();
 
+        return deleteComment(comment,
+                routes.BoardApp.post(project.owner, project.name, comment.getParent().id));
+    }
 }
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -4,46 +4,107 @@
 
 package controllers;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.List;
-
-import models.enumeration.ResourceType;
-
-import jxl.write.WriteException;
-
-import models.Assignee;
-import models.Attachment;
-import models.Issue;
-import models.IssueComment;
-import models.IssueLabel;
-import models.Project;
-import models.enumeration.Direction;
-import models.enumeration.Operation;
-import models.enumeration.State;
+import models.*;
+import models.enumeration.*;
 import models.support.FinderTemplate;
 import models.support.OrderParams;
-import models.support.SearchCondition;
+import models.support.SearchParams;
 
-import org.apache.tika.Tika;
-
-import play.data.Form;
-import play.mvc.Controller;
-import play.mvc.Result;
-import utils.AccessControl;
-import utils.Constants;
-import utils.HttpUtil;
-import utils.JodaDateUtil;
+import play.mvc.Http;
 import views.html.issue.editIssue;
 import views.html.issue.issue;
 import views.html.issue.issueList;
 import views.html.issue.newIssue;
 import views.html.issue.notExistingPage;
 
+import utils.AccessControl;
+import utils.Callback;
+import utils.JodaDateUtil;
+import utils.HttpUtil;
+
+import play.data.Form;
+import play.mvc.Call;
+import play.mvc.Result;
+
+import jxl.write.WriteException;
+import org.apache.tika.Tika;
 import com.avaje.ebean.Page;
 
-public class IssueApp extends Controller {
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class IssueApp extends AbstractPostingApp {
+    public static class SearchCondition extends AbstractPostingApp.SearchCondition {
+        public String state;
+        public Boolean commentedCheck;
+        public Long milestoneId;
+        public Set<Long> labelIds;
+        public String authorLoginId;
+        public Long assigneeId;
+
+        public SearchCondition() {
+            super();
+            milestoneId = null;
+            state = State.OPEN.name();
+            commentedCheck = false;
+        }
+        public OrderParams getOrderParams() {
+            return new OrderParams().add(orderBy, Direction.getValue(orderDir));
+        }
+
+        public SearchParams getSearchParam(Project project) {
+            SearchParams searchParams = new SearchParams();
+
+            searchParams.add("project.id", project.id, Matching.EQUALS);
+
+            if (authorLoginId != null && !authorLoginId.isEmpty()) {
+                User user = User.findByLoginId(authorLoginId);
+                if (user != null) {
+                    searchParams.add("authorId", user.id, Matching.EQUALS);
+                } else {
+                    List<Long> ids = new ArrayList<Long>();
+                    for (User u : User.find.where().contains("loginId", authorLoginId).findList()) {
+                        ids.add(u.id);
+                    }
+                    searchParams.add("authorId", ids, Matching.IN);
+                }
+            }
+
+            if (assigneeId != null) {
+                searchParams.add("assignee.user.id", assigneeId, Matching.EQUALS);
+                searchParams.add("assignee.project.id", project.id, Matching.EQUALS);
+            }
+
+            if (filter != null && !filter.isEmpty()) {
+                searchParams.add("title", filter, Matching.CONTAINS);
+            }
+
+            if (milestoneId != null) {
+                searchParams.add("milestone.id", milestoneId, Matching.EQUALS);
+            }
+
+            if (labelIds != null) {
+                for (Long labelId : labelIds) {
+                    searchParams.add("labels.id", labelId, Matching.EQUALS);
+                }
+            }
+
+            if (commentedCheck) {
+                searchParams.add("numOfComments", AbstractPosting.NUMBER_OF_ONE_MORE_COMMENTS, Matching.GE);
+            }
+
+            State st = State.getValue(state);
+            if (st.equals(State.OPEN) || st.equals(State.CLOSED)) {
+                searchParams.add("state", st, Matching.EQUALS);
+            }
+
+            return searchParams;
+        }
+    }
 
     /**
      * 페이지 처리된 이슈들의 리스트를 보여준다.
@@ -65,8 +126,6 @@
 
         Form<SearchCondition> issueParamForm = new Form<SearchCondition>(SearchCondition.class);
         SearchCondition issueParam = issueParamForm.bindFromRequest().get();
-        OrderParams orderParams = new OrderParams().add(issueParam.sortBy,
-                Direction.getValue(issueParam.orderBy));
         issueParam.state = state;
         issueParam.pageNum = pageNum - 1;
 
@@ -78,19 +137,20 @@
         }
 
         if (format.equals("xls")) {
-            return issuesAsExcel(issueParam, orderParams, project, state);
+            return issuesAsExcel(issueParam, issueParam.getOrderParams(), project, state);
         } else {
-            Page<Issue> issues = FinderTemplate.getPage(orderParams, issueParam.asSearchParam(project),
-                Issue.finder, Issue.ISSUE_COUNT_PER_PAGE, issueParam.pageNum);
+            Page<Issue> issues = FinderTemplate.getPage(
+                issueParam.getOrderParams(), issueParam.getSearchParam(project),
+                Issue.finder, ITEMS_PER_PAGE, issueParam.pageNum);
             return ok(issueList.render("title.issueList", issues, issueParam, project));
         }
     }
 
     public static Result issuesAsExcel(SearchCondition issueParam, OrderParams orderParams, Project project,
             String state) throws WriteException, IOException, UnsupportedEncodingException {
-        List<Issue> issues = FinderTemplate.findBy(orderParams, issueParam.asSearchParam(project), Issue.finder);
+        List<Issue> issues = FinderTemplate.findBy(orderParams, issueParam.getSearchParam(project), Issue.finder);
         File excelFile = Issue.excelSave(issues, project.name + "_" + state + "_filter_"
-                + issueParam.filter + "_milestone_" + issueParam.milestone);
+                + issueParam.filter + "_milestone_" + issueParam.milestoneId);
 
         String filename = HttpUtil.encodeContentDisposition(excelFile.getName());
 
@@ -103,15 +163,20 @@
 
     public static Result issue(String userName, String projectName, Long issueId) {
         Project project = ProjectApp.getProject(userName, projectName);
-        Issue issueInfo = Issue.findById(issueId);
+        Issue issueInfo = Issue.finder.byId(issueId);
+
+        if (!AccessControl.isCreatable(User.findByLoginId(session().get("loginId")), project, ResourceType.ISSUE_POST)) {
+            return unauthorized(views.html.project.unauthorized.render(project));
+        }
+
         if (issueInfo == null) {
             return ok(notExistingPage.render("title.post.notExistingPage", project));
         } else {
             for (IssueLabel label: issueInfo.labels) {
               label.refresh();
             }
-            Form<IssueComment> commentForm = new Form<IssueComment>(IssueComment.class);
-            Issue targetIssue = Issue.findById(issueId);
+            Form<Comment> commentForm = new Form<Comment>(Comment.class);
+            Issue targetIssue = Issue.finder.byId(issueId);
             Form<Issue> editForm = new Form<Issue>(Issue.class).fill(targetIssue);
             return ok(issue.render("title.issueDetail", issueInfo, editForm, commentForm, project));
         }
@@ -119,11 +184,8 @@
 
     public static Result newIssueForm(String userName, String projectName) {
         Project project = ProjectApp.getProject(userName, projectName);
-        if (UserApp.currentUser() == UserApp.anonymous) {
-            return unauthorized(views.html.project.unauthorized.render(project));
-        }
-
-        return ok(newIssue.render("title.newIssue", new Form<Issue>(Issue.class), project));
+        return newPostingForm(
+                project, newIssue.render("title.newIssue", new Form<Issue>(Issue.class), project));
     }
 
     public static Result newIssue(String ownerName, String projectName) throws IOException {
@@ -134,35 +196,24 @@
         } else {
             Issue newIssue = issueForm.get();
             newIssue.date = JodaDateUtil.now();
-            newIssue.authorId = UserApp.currentUser().id;
-            newIssue.authorLoginId = UserApp.currentUser().loginId;
-            newIssue.authorName = UserApp.currentUser().name;
+            newIssue.setAuthor(UserApp.currentUser());
             newIssue.project = project;
-            newIssue.state = State.OPEN;
-            if (newIssue.assignee.user.id != null) {
-                newIssue.assignee = Assignee.add(newIssue.assignee.user.id, project.id);
-            } else {
-                newIssue.assignee = null;
-            }
-            String[] labelIds = request().body().asMultipartFormData().asFormUrlEncoded()
-                    .get("labelIds");
-            if (labelIds != null) {
-                for (String labelId : labelIds) {
-                    newIssue.labels.add(IssueLabel.findById(Long.parseLong(labelId)));
-                }
-            }
 
-            Long issueId = Issue.create(newIssue);
+            newIssue.state = State.OPEN;
+            addLabels(newIssue.labels, request());
+
+            newIssue.save();
 
             // Attach all of the files in the current user's temporary storage.
-            Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.ISSUE_POST, issueId);
+            Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.ISSUE_POST, newIssue.id);
         }
+
         return redirect(routes.IssueApp.issues(project.owner, project.name,
                 State.OPEN.state(), "html", 1));
     }
 
     public static Result editIssueForm(String userName, String projectName, Long id) {
-        Issue targetIssue = Issue.findById(id);
+        Issue targetIssue = Issue.finder.byId(id);
         Form<Issue> editForm = new Form<Issue>(Issue.class).fill(targetIssue);
         Project project = ProjectApp.getProject(userName, projectName);
         if (!AccessControl.isAllowed(UserApp.currentUser(), targetIssue.asResource(), Operation.UPDATE)) {
@@ -172,86 +223,68 @@
         return ok(editIssue.render("title.editIssue", editForm, targetIssue, project));
     }
 
-    public static Result editIssue(String userName, String projectName, Long id) throws IOException {
-        Form<Issue> issueForm = new Form<Issue>(Issue.class).bindFromRequest();
-
-        if (issueForm.hasErrors()) {
-            return badRequest(issueForm.errors().toString());
-        }
-
-        Issue issue = issueForm.get();
-        Issue originalIssue = Issue.findById(id);
-
-        issue.id = id;
-        issue.date = originalIssue.date;
-        issue.authorId = originalIssue.authorId;
-        issue.authorLoginId = originalIssue.authorLoginId;
-        issue.authorName = originalIssue.authorName;
-        issue.project = originalIssue.project;
-        if (issue.assignee.user.id != null) {
-            issue.assignee = Assignee.add(issue.assignee.user.id, originalIssue.project.id);
-        } else {
-            issue.assignee = null;
-        }
-        String[] labelIds = request().body().asMultipartFormData().asFormUrlEncoded()
+    public static void addLabels(Set<IssueLabel> labels, Http.Request request) {
+        String[] labelIds = request.body().asMultipartFormData().asFormUrlEncoded()
                 .get("labelIds");
         if (labelIds != null) {
             for (String labelId : labelIds) {
-                issue.labels.add(IssueLabel.findById(Long.parseLong(labelId)));
+                labels.add(IssueLabel.findById(Long.parseLong(labelId)));
             }
         }
+    };
 
-        Issue.edit(issue);
+    public static Result editIssue(String userName, String projectName, Long id) throws IOException {
+        Form<Issue> issueForm = new Form<Issue>(Issue.class).bindFromRequest();
+        final Issue issue = issueForm.get();
+        final Issue originalIssue = Issue.finder.byId(id);
+        final Project project = originalIssue.project;
+        Call redirectTo =
+                routes.IssueApp.issues(project.owner, project.name, State.OPEN.name(), "html", 1);
 
-        // Attach the files in the current user's temporary storage.
-        Attachment.attachFiles(UserApp.currentUser().id, originalIssue.project.id, ResourceType.ISSUE_POST, id);
+        // updateIssueBeforeSave.run would be called just before this issue is saved.
+        // It updates some properties only for issues, such as assignee or labels, but not for non-issues.
+        Callback updateIssueBeforeSave = new Callback() {
+            @Override
+            public void run() {
+                issue.project = project;
+                addLabels(issue.labels, request());
+            }
+        };
 
-        return redirect(routes.IssueApp.issues(originalIssue.project.owner, originalIssue.project.name, State.OPEN.name(), "html", 1));
+        return editPosting(originalIssue, issue, issueForm, redirectTo, updateIssueBeforeSave);
     }
 
     public static Result deleteIssue(String userName, String projectName, Long issueId) {
-        Project project = ProjectApp.getProject(userName, projectName);
+        Issue issue = Issue.finder.byId(issueId);
+        Project project = issue.project;
 
-        Issue.delete(issueId);
-        Attachment.deleteAll(ResourceType.ISSUE_POST, issueId);
-        return redirect(routes.IssueApp.issues(project.owner, project.name,
-                State.OPEN.state(), "html", 1));
+        return deletePosting(issue, routes.IssueApp.issues(project.owner, project.name,
+                    State.OPEN.state(), "html", 1));
     }
 
     public static Result newComment(String userName, String projectName, Long issueId) throws IOException {
+        final Issue issue = Issue.finder.byId(issueId);
+        Project project = issue.project;
+        Call redirectTo = routes.IssueApp.issue(project.owner, project.name, issueId);
         Form<IssueComment> commentForm = new Form<IssueComment>(IssueComment.class)
                 .bindFromRequest();
-        Project project = ProjectApp.getProject(userName, projectName);
-        if (session(UserApp.SESSION_USERID) == null){
-            flash(Constants.WARNING, "user.login.alert");
-            return redirect(routes.IssueApp.issue(project.owner, project.name, issueId));
-        }
-        if (commentForm.hasErrors()) {
-            flash(Constants.WARNING, "board.comment.empty");
-            return redirect(routes.IssueApp.issue(project.owner, project.name, issueId));
-        } else {
-            IssueComment comment = commentForm.get();
-            comment.issue = Issue.findById(issueId);
-            comment.authorId = UserApp.currentUser().id;
-            comment.authorLoginId = UserApp.currentUser().loginId;
-            comment.authorName = UserApp.currentUser().name;
-            Long commentId = IssueComment.create(comment);
-            Issue.updateNumOfComments(issueId);
 
-            // Attach all of the files in the current user's temporary storage.
-            Attachment.attachFiles(UserApp.currentUser().id, project.id, ResourceType.ISSUE_COMMENT, commentId);
+        final IssueComment comment = commentForm.get();
 
-            return redirect(routes.IssueApp.issue(project.owner, project.name, issueId));
-        }
+        return newComment(comment, commentForm, redirectTo, new Callback() {
+            @Override
+            public void run() {
+                comment.issue = issue;
+            }
+        });
     }
 
     public static Result deleteComment(String userName, String projectName, Long issueId,
             Long commentId) {
-        Project project = ProjectApp.getProject(userName, projectName);
-        IssueComment.delete(commentId);
-        Issue.updateNumOfComments(issueId);
-        Attachment.deleteAll(ResourceType.ISSUE_COMMENT, commentId);
-        return redirect(routes.IssueApp.issue(project.owner, project.name, issueId));
-    }
+        Comment comment = IssueComment.find.byId(commentId);
+        Project project = comment.asResource().getProject();
 
+        return deleteComment(comment,
+                routes.IssueApp.issue(project.owner, project.name, comment.getParent().id));
+    }
 }
app/controllers/MilestoneApp.java
--- app/controllers/MilestoneApp.java
+++ app/controllers/MilestoneApp.java
@@ -115,7 +115,7 @@
         if(!project.id.equals(Milestone.findById(id).project.id)) {
             return internalServerError();
         }
-        Milestone.delete(Milestone.findById(id));
+        Milestone.findById(id).delete();
         return redirect(routes.MilestoneApp.manageMilestones(userName, projectName));
 
     }
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -167,7 +167,7 @@
 
         if (AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.DELETE)) {
             RepositoryService.deleteRepository(userName, projectName, project.vcs);
-            Project.delete(project.id);
+            project.delete();
             return redirect(routes.Application.index());
         } else {
             flash(Constants.WARNING, "project.member.isManager");
@@ -259,7 +259,7 @@
         }
 
         Page<Project> projects = FinderTemplate.getPage(
-                orderParams, searchParams, Project.find, 10, pageNum - 1);
+                orderParams, searchParams, Project.find, Project.PROJECT_COUNT_PER_PAGE, pageNum - 1);
 
         return ok(projectList.render("title.projectList", projects, filter, state));
     }
app/controllers/SearchApp.java
--- app/controllers/SearchApp.java
+++ app/controllers/SearchApp.java
@@ -6,6 +6,7 @@
 import com.avaje.ebean.*;
 import models.*;
 import play.mvc.*;
+
 import views.html.search.*;
 
 import static play.data.Form.form;
@@ -51,13 +52,13 @@
         }
 
         Page<Issue> resultIssues = null;
-        Page<Post> resultPosts = null;
+        Page<Posting> resultPosts = null;
 
         if(!condition.type.equals("post")) {
-            resultIssues = Issue.find(project, condition);
+            resultIssues = AbstractPosting.find(Issue.finder, project, condition);
         }
         if(!condition.type.equals("issue")) {
-            resultPosts = Post.find(project, condition);
+            resultPosts = AbstractPosting.find(Posting.finder, project, condition);
         }
 
         response().setHeader("Accept-Ranges", "pages");
app/controllers/SiteApp.java
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
@@ -94,7 +94,7 @@
     }
     
     public static Result deleteProject(Long projectId){
-        Project.delete(projectId);
+        Project.find.byId(projectId).delete();
         return redirect(routes.SiteApp.projectList(""));
     }
     
 
app/models/AbstractPosting.java (added)
+++ app/models/AbstractPosting.java
@@ -0,0 +1,123 @@
+package models;
+
+import com.avaje.ebean.Page;
+import controllers.SearchApp;
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+import org.joda.time.Duration;
+import play.data.format.Formats;
+import play.data.validation.Constraints;
+import play.db.ebean.*;
+import utils.JodaDateUtil;
+
+import javax.persistence.*;
+import javax.validation.constraints.Size;
+import java.util.Date;
+
+import static com.avaje.ebean.Expr.contains;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: nori
+ * Date: 13. 3. 4
+ * Time: 오후 6:17
+ * To change this template use File | Settings | File Templates.
+ */
+
+@MappedSuperclass
+abstract public class AbstractPosting extends Model {
+    public static final int FIRST_PAGE_NUMBER = 0;
+    public static final int NUMBER_OF_ONE_MORE_COMMENTS = 1;
+
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    public Long id;
+
+    @Constraints.Required
+    @Size(max=255)
+    public String title;
+
+    @Constraints.Required
+    @Lob
+    public String body;
+
+    @Constraints.Required
+    @Formats.DateTime(pattern = "YYYY/MM/DD/hh/mm/ss")
+    public Date date;
+
+    public Long authorId;
+    public String authorLoginId;
+    public String authorName;
+
+    @ManyToOne
+    public Project project;
+
+    // This field is only for ordering. This field should be persistent because
+    // Ebean does NOT sort entities by transient field.
+    public int numOfComments;
+
+    abstract public int computeNumOfComments();
+
+    public AbstractPosting() {
+        this.date = JodaDateUtil.now();
+    }
+
+    public void save() {
+        numOfComments = computeNumOfComments();
+        super.save();
+    }
+
+    public void update() {
+        numOfComments = computeNumOfComments();
+        super.update();
+    }
+
+    public static <T> Page<T> find(Finder<Long, T> finder, Project project, SearchApp.ContentSearchCondition condition) {
+        String filter = condition.filter;
+        return finder.where().eq("project.id", project.id)
+                .or(contains("title", filter), contains("body", filter))
+                .findPagingList(condition.pageSize).getPage(condition.page - 1);
+    }
+
+    /**
+     * 현재 글을 쓴지 얼마나 되었는지를 얻어내는 함수
+     * @return
+     */
+    public Duration ago() {
+        return JodaDateUtil.ago(this.date);
+    }
+
+    abstract public Resource asResource();
+
+    public Resource asResource(final ResourceType type) {
+        return new Resource() {
+	        @Override
+	        public Long getId() {
+	            return id;
+	        }
+
+	        @Override
+	        public Project getProject() {
+	            return project;
+	        }
+
+	        @Override
+	        public ResourceType getType() {
+                return type;
+	        }
+
+            @Override
+            public Long getAuthorId() {
+                return authorId;
+            }
+	    };
+    }
+
+    @Transient
+    public void setAuthor(User user) {
+        authorId = user.id;
+        authorLoginId = user.loginId;
+        authorName = user.name;
+    }
+}
app/models/Comment.java
--- app/models/Comment.java
+++ app/models/Comment.java
@@ -1,6 +1,5 @@
 package models;
 
-import models.enumeration.ResourceType;
 import models.resource.Resource;
 
 import org.joda.time.*;
@@ -12,10 +11,9 @@
 import javax.validation.constraints.Size;
 import java.util.*;
 
-@Entity
-public class Comment extends Model {
+@MappedSuperclass
+abstract public class Comment extends Model {
     private static final long serialVersionUID = 1L;
-    private static Finder<Long, Comment> find = new Finder<Long, Comment>(Long.class, Comment.class);
 
     @Id
     public Long id;
@@ -26,60 +24,36 @@
     @Constraints.Required
     public Date date;
 
-    public String filePath;
     public Long authorId;
     public String authorLoginId;
     public String authorName;
-    @ManyToOne
-    public Post post;
 
     public Comment() {
-        date = JodaDateUtil.now();
+        date = new Date();
     }
 
-    public static Comment findById(Long id) {
-        return find.byId(id);
-    }
-
-    public static Long write(Comment comment) {
-        comment.save();
-
-        return comment.id;
-    }
-
-    public static List<Comment> findCommentsByPostId(Long postId) {
-        return find.where().eq("post.id", postId).findList();
-    }
-
-    public static boolean isAuthor(Long currentUserId, Long id) {
-        int findRowCount = find.where().eq("authorId", currentUserId).eq("id", id).findRowCount();
-        return (findRowCount != 0) ? true : false;
-    }
-
-    public Duration ago(){
+    public Duration ago() {
         return JodaDateUtil.ago(this.date);
     }
 
-    public static void delete(Long commentId) {
-        find.byId(commentId).delete();
+    abstract public Resource asResource();
+
+    abstract public AbstractPosting getParent();
+
+    @Transient
+    public void setAuthor(User user) {
+        authorId = user.id;
+        authorLoginId = user.loginId;
+        authorName = user.name;
     }
 
-    public Resource asResource() {
-        return new Resource() {
-            @Override
-            public Long getId() {
-                return id;
-            }
+    public void save() {
+        super.save();
+        getParent().save();
+    }
 
-            @Override
-            public Project getProject() {
-                return post.project;
-            }
-
-            @Override
-            public ResourceType getType() {
-                return ResourceType.BOARD_COMMENT;
-            }
-        };
+    public void delete() {
+        super.delete();
+        getParent().save();
     }
 }
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -1,7 +1,5 @@
 package models;
 
-import com.avaje.ebean.*;
-import controllers.*;
 import jxl.*;
 import jxl.format.*;
 import jxl.format.Colour;
@@ -11,22 +9,14 @@
 import jxl.write.*;
 import models.enumeration.*;
 import models.resource.Resource;
-import models.support.*;
-import org.joda.time.*;
-import play.data.format.*;
-import play.data.validation.*;
-import play.db.ebean.*;
 import utils.*;
 
 import javax.persistence.*;
-import javax.validation.constraints.Size;
 import java.io.*;
 import java.util.*;
 
-import static com.avaje.ebean.Expr.*;
-
 @Entity
-public class Issue extends Model {
+public class Issue extends AbstractPosting {
     /**
      * @param id              이슈 ID
      * @param title           이슈 제목
@@ -35,7 +25,6 @@
      * @param date            이슈 등록 날짜
      * @param authorId        이슈 작성자 ID
      * @param project         이슈가 등록된 프로젝트
-     * @param issueType       이슈 상세정보의 유형
      * @param assigneeId      이슈에 배정된 담당자 Id
      * @param milestone       이슈가 등록된 마일스톤
      * @param importance      이슈 상세정보의 중요도
@@ -45,50 +34,25 @@
 
     public static Finder<Long, Issue> finder = new Finder<Long, Issue>(Long.class, Issue.class);
 
-    public static final int FIRST_PAGE_NUMBER = 0;
-    public static final int ISSUE_COUNT_PER_PAGE = 25;
-    public static final int NUMBER_OF_ONE_MORE_COMMENTS = 1;
     public static final String DEFAULT_SORTER = "date";
     public static final String TO_BE_ASSIGNED = "TBA";
 
-    @Id
-    public Long id;
-
-    @Constraints.Required
-    public String title;
-
-    @Lob
-    public String body;
-
-    @Formats.DateTime(pattern = "yyyy-MM-dd")
-    public Date date;
-
-    public int numOfComments;
-    public Long milestoneId;
-    public Long authorId;
-    public String authorLoginId;
-    public String authorName;
     public State state;
 
     @ManyToOne
-    public Project project;
+    public Milestone milestone;
 
-    @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL)
-    public List<IssueComment> comments;
-
-    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
+    @ManyToMany(fetch = FetchType.EAGER)
     public Set<IssueLabel> labels;
 
-    @ManyToOne(cascade = CascadeType.ALL)
+    @ManyToOne
     public Assignee assignee;
 
-    public Issue(String title) {
-        this.title = title;
-        this.date = JodaDateUtil.now();
-    }
+    @OneToMany(cascade = CascadeType.ALL)
+    public List<IssueComment> comments;
 
-    public Duration ago() {
-        return JodaDateUtil.ago(this.date);
+    public int computeNumOfComments() {
+        return comments.size();
     }
 
     /**
@@ -99,122 +63,14 @@
         return (this.assignee != null ? assignee.user.name : null);
     }
 
-    /**
-     * View에서 사용할 이슈 유형에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서
-     * 최대한 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> issueTypes() {
-        return new Options("issue.new.detailInfo.issueType.worst",
-                "issue.new.detailInfo.issueType.worse", "issue.new.detailInfo.issueType.bad",
-                "issue.new.detailInfo.issueType.enhancement",
-                "issue.new.detailInfo.issueType.recommendation");
-    }
+    public void save() {
+        if (assignee != null && assignee.user.id != null) {
+            assignee = Assignee.add(assignee.user.id, project.id);
+        } else {
+            assignee = null;
+        }
 
-    /**
-     * View에서 사용할 OS유형에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서
-     * 최대한 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> osTypes() {
-        return new Options("issue.new.environment.osType.windows",
-                "issue.new.environment.osType.Mac", "issue.new.environment.osType.Linux");
-    }
-
-    /**
-     * View에서 사용할 브라우져 유형에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서
-     * 최대한 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> browserTypes() {
-        return new Options("issue.new.environment.browserType.ie",
-                "issue.new.environment.browserType.chrome",
-                "issue.new.environment.browserType.firefox",
-                "issue.new.environment.browserType.safari",
-                "issue.new.environment.browserType.opera");
-    }
-
-    /**
-     * View에서 사용할 DBMS 유형에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서
-     * 최대한 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> dbmsTypes() {
-        return new Options("issue.new.environment.dbmsType.postgreSQL",
-                "issue.new.environment.dbmsType.CUBRID", "issue.new.environment.dbmsType.MySQL");
-    }
-
-    /**
-     * View에서 사용할 중요도에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서 최대한
-     * 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> importances() {
-        return new Options("issue.new.result.importance.highest",
-                "issue.new.result.importance.high", "issue.new.result.importance.average",
-                "issue.new.result.importance.low", "issue.new.result.importance.lowest");
-    }
-
-    /**
-     * View에서 사용할 진단 결과에 대한 옵션을 제공한다. Purpose : View에서 Select 부분에서 i18n를 사용하면서
-     * 최대한 간단하게 하기 위함.
-     *
-     * @return
-     */
-    public static Map<String, String> diagnosisResults() {
-        return new Options("issue.new.result.diagnosisResult.bug",
-                "issue.new.result.diagnosisResult.fixed",
-                "issue.new.result.diagnosisResult.willNotFixed",
-                "issue.new.result.diagnosisResult.notaBug",
-                "issue.new.result.diagnosisResult.awaitingResponse",
-                "issue.new.result.diagnosisResult.unreproducible",
-                "issue.new.result.diagnosisResult.duplicated",
-                "issue.new.result.diagnosisResult.works4me");
-    }
-
-    /**
-     * 이슈 id로 이슈를 찾아준다.
-     *
-     * @param id
-     * @return
-     */
-    public static Issue findById(Long id) {
-        return finder.byId(id);
-    }
-
-    /**
-     * 이슈를 생성한다.
-     *
-     * @param issue
-     * @return
-     */
-    public static Long create(Issue issue) {
-        issue.save();
-        return issue.id;
-    }
-
-    /**
-     * 이슈를 삭제한다.
-     *
-     * @param id
-     */
-    public static void delete(Long id) {
-        Issue issue = finder.byId(id);
-        issue.delete();
-    }
-
-    /**
-     * 이슈를 수정 & 업데이트 한다.
-     *
-     * @param issue
-     */
-    public static void edit(Issue issue) {
-        issue.update();
+        super.save();
     }
 
     public static int countIssues(Long projectId, State state) {
@@ -223,166 +79,6 @@
         } else {
             return finder.where().eq("project.id", projectId).eq("state", state).findRowCount();
         }
-    }
-
-    /**
-     * 미해결 탭을 눌렀을 때, open 상태의 이슈들을 찾아준다..
-     *
-     * @param projectId
-     * @return
-     */
-    public static Page<Issue> findOpenIssues(Long projectId) {
-        return Issue.findIssues(projectId, State.OPEN);
-    }
-
-    /**
-     * 해결 탭을 눌렀을 때, closed 상태의 이슈들을 찾아준다.
-     *
-     * @param projectId
-     * @return
-     */
-    public static Page<Issue> findClosedIssues(Long projectId) {
-        return Issue.findIssues(projectId, State.CLOSED);
-    }
-
-    /**
-     * 해당 프로젝트의 State 외의 것들은 기본값들로 이뤄진 이슈들을 찾아준다.
-     *
-     * @param projectName
-     * @param state
-     * @return
-     */
-    public static Page<Issue> findIssues(Long projectId, State state) {
-        return find(projectId, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC, "", null,
-                null, false);
-    }
-
-    /**
-     * 검색창에서 제공된 query(filter)와 댓글과 파일첨부된 이슈만 찾아주는 체크박스의 값에 따라 필터링된 이슈들을 찾아준다.
-     *
-     * @param projectId
-     * @param filter
-     * @param state
-     * @param commentedCheck
-     * @return
-     */
-    public static Page<Issue> findFilteredIssues(Long projectId, String filter, State state,
-            boolean commentedCheck) {
-        return find(projectId, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC, filter,
-                null, null, commentedCheck);
-    }
-
-    /**
-     * 댓글이 달린 이슈들만 찾아준다.
-     *
-     * @param projectId
-     * @param filter
-     * @return
-     */
-    public static Page<Issue> findCommentedIssues(Long projectId, String filter) {
-        return find(projectId, FIRST_PAGE_NUMBER, State.ALL, DEFAULT_SORTER, Direction.DESC,
-                filter, null, null, true);
-    }
-
-    /**
-     * 마일스톤 Id에 의거해서 해당 마일스톤에 속한 이슈들을 찾아준다.
-     *
-     * @param projectId
-     * @param milestoneId
-     * @return
-     */
-    public static Page<Issue> findIssuesByMilestoneId(Long projectId, Long milestoneId) {
-        return find(projectId, FIRST_PAGE_NUMBER, State.ALL, DEFAULT_SORTER, Direction.DESC, "",
-                milestoneId, null, false);
-    }
-
-    /**
-     * 이슈들을 아래의 parameter들의 조건에 의거하여 Page형태로 반환한다.
-     *
-     * @param projectId
-     *            project ID to finder issues
-     * @param pageNumber
-     *            Page to display
-     * @param state
-     *            state type of issue(OPEN or CLOSED
-     * @param sortBy
-     *            Issue property used for sorting, but, it might be fixed to
-     *            enum type
-     * @param order
-     *            Sort order(either asc or desc)
-     * @param filter
-     *            filter applied on the title column
-     * @param commentedCheck
-     *            filter applied on the commetedCheck column, 댓글이 존재하는 이슈만 필터링
-     * @return 위의 조건에 따라 필터링된 이슈들을 Page로 반환.
-     */
-    public static Page<Issue> find(Long projectId, int pageNumber, State state, String sortBy,
-            Direction order, String filter, Long milestoneId, Set<Long> labelIds,
-            boolean commentedCheck) {
-        OrderParams orderParams = new OrderParams().add(sortBy, order);
-        SearchParams searchParams = new SearchParams()
-                .add("project.id", projectId, Matching.EQUALS);
-
-        if (filter != null && !filter.isEmpty()) {
-            searchParams.add("title", filter, Matching.CONTAINS);
-        }
-        if (milestoneId != null) {
-            searchParams.add("milestoneId", milestoneId, Matching.EQUALS);
-        }
-        if (labelIds != null) {
-            // searchParams.add("labels.id", labelIds, Matching.IN);
-            for (Long labelId : labelIds) {
-                searchParams.add("labels.id", labelId, Matching.EQUALS);
-            }
-        }
-        if (commentedCheck) {
-            searchParams.add("numOfComments", NUMBER_OF_ONE_MORE_COMMENTS, Matching.GE);
-        }
-
-        if (state == null) {
-            state = State.ALL;
-        }
-        switch (state) {
-            case OPEN:
-                searchParams.add("state", State.OPEN, Matching.EQUALS);
-                break;
-            case CLOSED:
-                searchParams.add("state", State.CLOSED, Matching.EQUALS);
-                break;
-            default:
-        }
-        return FinderTemplate.getPage(orderParams, searchParams, finder, ISSUE_COUNT_PER_PAGE,
-                pageNumber);
-    }
-
-    /**
-     * 전체 컨텐츠 검색할 때 제목과 내용에 condition.filter를 포함하고 있는 이슈를 검색한다.
-     *
-     * @param project
-     * @param condition
-     * @return
-     */
-    public static Page<Issue> find(Project project, SearchApp.ContentSearchCondition condition) {
-        String filter = condition.filter;
-        return finder.where().eq("project.id", project.id)
-                .or(contains("title", filter), contains("body", filter))
-                .findPagingList(condition.pageSize).getPage(condition.page - 1);
-    }
-
-    public static Long findAssigneeIdByIssueId(Long issueId) {
-        return finder.byId(issueId).assignee.user.id;
-    }
-
-    /**
-     * 해당 마일스톤아이디로 관련 이슈를 검색한다.
-     *
-     * @param milestoneId
-     * @return
-     */
-    public static List<Issue> findByMilestoneId(Long milestoneId) {
-        SearchParams searchParams = new SearchParams().add("milestoneId", milestoneId,
-                Matching.EQUALS);
-        return FinderTemplate.findBy(null, searchParams, finder);
     }
 
     /**
@@ -447,7 +143,7 @@
     /**
      * excelSave에서 assignee를 리턴해준다.
      *
-     * @param uId
+     * @param assignee
      * @return
      */
     private static String getAssigneeName(Assignee assignee) {
@@ -457,18 +153,6 @@
     // FIXME 이것이 없이 테스트는 잘 작동하나, view에서 댓글이 달린 이슈들을 필터링하는 라디오버튼을 작동시에 이 메쏘드에서
     // 시행하는 동기화 작업 없이는 작동을 하지 않는다.
 
-    /**
-     * comment가 delete되거나 create될 때, numOfComment와 comment.size()를 동기화 시켜준다.
-     *
-     * @param id
-     */
-    public static void updateNumOfComments(Long id) {
-
-        Issue issue = Issue.findById(id);
-        issue.numOfComments = issue.comments.size();
-        issue.update();
-    }
-
 	public boolean isOpen() {
 	    return this.state == State.OPEN;
 	}
@@ -477,24 +161,9 @@
 	    return this.state == State.CLOSED;
 	}
 
-	public Resource asResource() {
-	    return new Resource() {
-	        @Override
-	        public Long getId() {
-	            return null;
-	        }
-
-	        @Override
-	        public Project getProject() {
-	            return project;
-	        }
-
-	        @Override
-	        public ResourceType getType() {
-	            return ResourceType.ISSUE_POST;
-	        }
-	    };
-	}
+    public Resource asResource() {
+        return asResource(ResourceType.ISSUE_POST);
+    }
 
     public Resource fieldAsResource(final ResourceType resourceType) {
         return new Resource() {
app/models/IssueComment.java
--- app/models/IssueComment.java
+++ app/models/IssueComment.java
@@ -1,74 +1,27 @@
-/**
- * @author Taehyun Park
- */
-
 package models;
 
 import models.enumeration.ResourceType;
 import models.resource.Resource;
 
-import org.joda.time.*;
-import play.data.validation.*;
-import play.db.ebean.*;
-import utils.*;
-
 import javax.persistence.*;
-import javax.validation.constraints.Size;
-import java.util.*;
 
 @Entity
-public class IssueComment extends Model {
+public class IssueComment extends Comment {
     private static final long serialVersionUID = 1L;
-    private static Finder<Long, IssueComment> find = new Finder<Long, IssueComment>(Long.class,
-            IssueComment.class);
-
-    @Id
-    public Long id;
-
-    @Constraints.Required @Column(length = 4000) @Size(max=4000)
-    public String contents;
-
-    public Date date;
-    public Long authorId;
-    public String authorLoginId;
-    public String authorName;
-    public String filePath;
+    public static Finder<Long, IssueComment> find = new Finder<Long, IssueComment>(Long.class, IssueComment.class);
 
     @ManyToOne
     public Issue issue;
 
     public IssueComment() {
-        date = JodaDateUtil.now();
+        super();
     }
 
-    public static IssueComment findById(Long id) {
-        return find.byId(id);
-    }
-
-    public static Long create(IssueComment issueComment) {
-        issueComment.save();
-        return issueComment.id;
-    }
-
-    public String authorName() {
-        return User.find.byId(this.authorId).name;
-    }
-
-    public static void delete(Long id) {
-        find.byId(id).delete();
-    }
-
-    public static boolean isAuthor(Long currentUserId, Long id) {
-        int findRowCount = find.where().eq("authorId", currentUserId).eq("id", id).findRowCount();
-        return (findRowCount != 0) ? true : false;
-    }
-
-    public Duration ago() {
-        return JodaDateUtil.ago(this.date);
+    public AbstractPosting getParent() {
+        return issue;
     }
 
     public Resource asResource() {
-
         return new Resource() {
             @Override
             public Long getId() {
@@ -84,7 +37,11 @@
             public ResourceType getType() {
                 return ResourceType.ISSUE_COMMENT;
             }
+
+            @Override
+            public Long getAuthorId() {
+                return authorId;
+            }
         };
     }
 }
-
app/models/IssueLabel.java
--- app/models/IssueLabel.java
+++ app/models/IssueLabel.java
@@ -50,6 +50,7 @@
         return finder.byId(id);
     }
 
+    @Transient
     public boolean exists() {
         return finder.where().eq("project.id", project.id)
                 .eq("name", name).eq("color", color).findRowCount() > 0;
app/models/Milestone.java
--- app/models/Milestone.java
+++ app/models/Milestone.java
@@ -14,7 +14,7 @@
 public class Milestone extends Model {
 
     private static final long serialVersionUID = 1L;
-    private static Finder<Long, Milestone> find = new Finder<Long, Milestone>(
+    public static Finder<Long, Milestone> find = new Finder<Long, Milestone>(
             Long.class, Milestone.class);
 
     public static String DEFAULT_SORTER = "dueDate";
@@ -38,33 +38,38 @@
     @ManyToOne
     public Project project;
 
+    @OneToMany
+    public Set<Issue> issues;
+
+    public void delete() {
+        // Set all issues' milestone to null.
+        // I don't know why Ebean does not do this by itself.
+        for(Issue issue : issues) {
+            issue.milestone = null;
+            issue.update();
+        }
+
+        super.delete();
+    }
+
     public static void create(Milestone milestone) {
         milestone.save();
     }
 
     public int getNumClosedIssues() {
-        return Issue.finder.where().eq("milestoneId", this.id).eq("state", State.CLOSED).findRowCount();
+        return Issue.finder.where().eq("milestone", this).eq("state", State.CLOSED).findRowCount();
     }
 
     public int getNumOpenIssues() {
-        return Issue.finder.where().eq("milestoneId", this.id).eq("state", State.OPEN).findRowCount();
+        return Issue.finder.where().eq("milestone", this).eq("state", State.OPEN).findRowCount();
     }
-
+    
     public int getNumTotalIssues() {
-        return Issue.findByMilestoneId(this.id).size();
+        return issues.size();
     }
 
     public int getCompletionRate() {
         return new Double(((double) getNumClosedIssues() / (double) getNumTotalIssues()) * 100).intValue();
-    }
-
-    public static void delete(Milestone milestone) {
-        List<Issue> issues = Issue.findByMilestoneId(milestone.id);
-        milestone.delete();
-        for (Issue issue : issues) {
-            issue.milestoneId = null;
-            issue.update();
-        }
     }
 
     public static Milestone findById(Long id) {
 
app/models/Post.java (deleted)
--- app/models/Post.java
@@ -1,165 +0,0 @@
-/**
- * @author Ahn Hyeok Jun
- */
-
-package models;
-
-import com.avaje.ebean.*;
-import controllers.*;
-import models.enumeration.*;
-import models.resource.Resource;
-import models.support.*;
-import org.joda.time.*;
-import play.data.format.*;
-import play.data.validation.*;
-import play.db.ebean.*;
-import utils.*;
-
-import javax.persistence.*;
-import javax.validation.constraints.Size;
-import java.util.*;
-
-import static com.avaje.ebean.Expr.*;
-import static play.data.validation.Constraints.*;
-
-@Entity
-public class Post extends Model {
-    private static final long serialVersionUID = 1L;
-    private static Finder<Long, Post> finder = new Finder<Long, Post>(Long.class, Post.class);
-
-    @Id
-    public Long id;
-
-    @Required @Size(max=255)
-    public String title;
-
-    @Required @Lob
-    public String contents;
-
-    @Required
-    @Formats.DateTime(pattern = "YYYY/MM/DD/hh/mm/ss")
-    public Date date;
-
-    public int commentCount;
-    public String filePath;
-
-    public Long authorId;
-    public String authorLoginId;
-    public String authorName;
-
-    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
-    public List<Comment> comments;
-
-    @ManyToOne
-    public Project project;
-
-    public Post() {
-        this.date = JodaDateUtil.now();
-    }
-
-    public static Post findById(Long id) {
-        return finder.byId(id);
-    }
-
-    /**
-     * @param projectName
-     *            프로젝트이름
-     * @param pageNum
-     *            페이지 번호
-     * @param direction
-     *            오름차순(asc), 내림차순(decs)
-     * @param key
-     *            오름차순과 내림차수를 결정하는 기준
-     * @return
-     */
-    public static Page<Post> findOnePage(String ownerName, String projectName, int pageNum,
-            Direction direction, String key, String filter) {
-        SearchParams searchParam = new SearchParams()
-            .add("project.owner", ownerName, Matching.EQUALS)
-            .add("project.name", projectName, Matching.EQUALS)
-            .add("contents", filter, Matching.CONTAINS);
-        OrderParams orderParams = new OrderParams().add(key, direction);
-        return FinderTemplate.getPage(orderParams, searchParam, finder, 10, pageNum);
-    }
-
-    public static Long write(Post post) {
-        post.save();
-        return post.id;
-    }
-
-    public static void delete(Long id) {
-        finder.byId(id).delete();
-    }
-
-    /**
-     * 댓글이 달릴때 체크를 하는 함수.
-     * @param id Post의 ID
-     */
-    public static void countUpCommentCounter(Long id) {
-        Post post = finder.byId(id);
-        post.commentCount++;
-        post.update();
-    }
-
-    /**
-     * 현재 글을 쓴지 얼마나 되었는지를 얻어내는 함수
-     * @return
-     */
-    public Duration ago() {
-        return JodaDateUtil.ago(this.date);
-    }
-
-    public static void edit(Post post) {
-        Post beforePost = findById(post.id);
-        post.commentCount = beforePost.commentCount;
-        if (post.filePath == null) {
-            post.filePath = beforePost.filePath;
-        }
-        post.update();
-    }
-
-    public static boolean isAuthor(Long currentUserId, Long id) {
-        int findRowCount = finder.where().eq("authorId", currentUserId).eq("id", id).findRowCount();
-        return (findRowCount != 0) ? true : false;
-    }
-
-	/**
-	 * 전체 컨텐츠 검색할 때 제목과 내용에 condition.filter를 포함하고 있는 게시글를 검색한다.
-	 * @param project
-	 * @param condition
-	 * @return
-	 */
-	public static Page<Post> find(Project project, SearchApp.ContentSearchCondition condition) {
-		String filter = condition.filter;
-		return finder.where()
-				.eq("project.id", project.id)
-				.or(contains("title", filter), contains("contents", filter))
-				.findPagingList(condition.pageSize)
-				.getPage(condition.page - 1);
-	}
-
-    public static void countDownCommentCounter(Long id) {
-        Post post = finder.byId(id);
-        post.commentCount--;
-        post.update();
-    }
-
-    public Resource asResource() {
-        return new Resource() {
-            @Override
-            public Long getId() {
-                return id;
-            }
-
-            @Override
-            public Project getProject() {
-                return project;
-            }
-
-            @Override
-            public ResourceType getType() {
-                return ResourceType.BOARD_POST;
-            }
-        };
-    }
-}
 
app/models/Posting.java (added)
+++ app/models/Posting.java
@@ -0,0 +1,32 @@
+/**
+ * @author Ahn Hyeok Jun
+ */
+
+package models;
+
+import javax.persistence.*;
+
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+
+import java.util.*;
+
+@Entity
+public class Posting extends AbstractPosting {
+    public static Finder<Long, Posting> finder = new Finder<Long, Posting>(Long.class, Posting.class);
+
+    @OneToMany(cascade = CascadeType.ALL)
+    public List<PostingComment> comments;
+
+    public int computeNumOfComments() {
+        return comments.size();
+    }
+
+    public Posting() {
+        super();
+    }
+
+    public Resource asResource() {
+        return asResource(ResourceType.BOARD_POST);
+    }
+}
 
app/models/PostingComment.java (added)
+++ app/models/PostingComment.java
@@ -0,0 +1,47 @@
+package models;
+
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+
+import javax.persistence.*;
+
+@Entity
+public class PostingComment extends Comment {
+    private static final long serialVersionUID = 1L;
+    public static Finder<Long, PostingComment> find = new Finder<Long, PostingComment>(Long.class, PostingComment.class);
+
+    @ManyToOne
+    public Posting posting;
+
+    public PostingComment() {
+        super();
+    }
+
+    public AbstractPosting getParent() {
+        return posting;
+    }
+
+    public Resource asResource() {
+        return new Resource() {
+            @Override
+            public Long getId() {
+                return id;
+            }
+
+            @Override
+            public Project getProject() {
+                return posting.project;
+            }
+
+            @Override
+            public ResourceType getType() {
+                return ResourceType.NONISSUE_COMMENT;
+            }
+
+            @Override
+            public Long getAuthorId() {
+                return authorId;
+            }
+        };
+    }
+}
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -40,6 +40,8 @@
 	public static Finder<Long, Project> find = new Finder<Long, Project>(
 			Long.class, Project.class);
 
+    public static final int PROJECT_COUNT_PER_PAGE = 15;
+
     public static Comparator sortByNameWithIgnoreCase = new SortByNameWithIgnoreCase();
     public static Comparator sortByNameWithIgnoreCaseDesc = new SortByNameWithIgnoreCaseDesc();
     public static Comparator sortByDate = new SortByDate();
@@ -65,13 +67,13 @@
 	public Date date;
 
 	@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
-	public List<Issue> issues;
+	public Set<Issue> issues;
 
 	@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
 	public List<ProjectUser> projectUser;
 
 	@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
-	public List<Post> posts;
+	public List<Posting> posts;
 
 	@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
 	public List<Milestone> milestones;
@@ -86,10 +88,6 @@
 		ProjectUser.assignRole(User.SITE_MANAGER_ID, newProject.id,
 				RoleType.SITEMANAGER);
 		return newProject.id;
-	}
-
-	public static void delete(Long id) {
-		Project.find.byId(id).delete();
 	}
 
 	public static Page<Project> findByName(String name, int pageSize,
app/models/SiteAdmin.java
--- app/models/SiteAdmin.java
+++ app/models/SiteAdmin.java
@@ -25,4 +25,4 @@
     public static boolean exists(User user) {
         return user != null && find.where().eq("admin.id", user.id).findRowCount() > 0;
     }
-}
(No newline at end of file)
+}
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -11,6 +11,7 @@
 import javax.persistence.Id;
 import javax.persistence.OneToMany;
 import javax.persistence.Table;
+import javax.persistence.Transient;
 
 import models.enumeration.Direction;
 import models.enumeration.Matching;
@@ -153,6 +154,7 @@
                 .findList();
     }
 
+    @Transient
     public Long avatarId(){
         return Attachment.findByContainer(ResourceType.USER_AVATAR, id).get(0).id;
     }
app/models/enumeration/ResourceType.java
--- app/models/enumeration/ResourceType.java
+++ app/models/enumeration/ResourceType.java
@@ -2,15 +2,13 @@
 
 public enum ResourceType {
     ISSUE_POST("issue_post"),
-    ISSUE_COMMENT("issue_comment"),
     ISSUE_ASSIGNEE("issue_assignee"),
     ISSUE_STATE("issue_state"),
     ISSUE_CATEGORY("issue_category"),
     ISSUE_MILESTONE("issue_milestone"),
-    ISSUE_NOTICE("issue_notice"),
+
     ISSUE_LABEL("issue_label"),
     BOARD_POST("board_post"),
-    BOARD_COMMENT("board_comment"),
     BOARD_CATEGORY("board_category"),
     BOARD_NOTICE("board_notice"),
     CODE("code"),
@@ -21,7 +19,9 @@
     USER("user"),
     USER_AVATAR("user_avatar"),
     PROJECT("project"),
-    ATTACHMENT("attachment");
+    ATTACHMENT("attachment"),
+    ISSUE_COMMENT("issue_comment"),
+    NONISSUE_COMMENT("nonissue_comment");
 
     private String resource;
 
@@ -41,4 +41,4 @@
         }
         return ResourceType.ISSUE_POST;
     }
-}
(No newline at end of file)
+}
app/models/resource/Resource.java
--- app/models/resource/Resource.java
+++ app/models/resource/Resource.java
@@ -8,4 +8,5 @@
     abstract public Project getProject();
     abstract public ResourceType getType();
     public Resource getContainer() { return null; };
-}
(No newline at end of file)
+    public Long getAuthorId() { return null; };
+}
 
app/models/support/SearchCondition.java (deleted)
--- app/models/support/SearchCondition.java
@@ -1,90 +0,0 @@
-package models.support;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-import models.Issue;
-import models.Project;
-import models.User;
-import models.enumeration.*;
-/**
- *
- * @author Taehyun Park
- *
- */
-public class SearchCondition {
-
-    public String filter;
-    public String sortBy;
-    public String orderBy;
-    public int pageNum;
-
-    public String state;
-    public Boolean commentedCheck;
-    public Boolean fileAttachedCheck;
-    public Long milestone;
-    public Set<Long> labelIds;
-    public String authorLoginId;
-    public Long assigneeId;
-
-    public SearchCondition() {
-        filter = "";
-        sortBy = "date";
-        orderBy = Direction.DESC.direction();
-        pageNum = 0;
-        milestone = null;
-        state = State.OPEN.name();
-        commentedCheck = false;
-        fileAttachedCheck = false;
-    }
-
-    public SearchParams asSearchParam(Project project) {
-        SearchParams searchParams = new SearchParams();
-
-        searchParams.add("project.id", project.id, Matching.EQUALS);
-
-        if (authorLoginId != null && !authorLoginId.isEmpty()) {
-            User user = User.findByLoginId(authorLoginId);
-            if (user != null) {
-                searchParams.add("authorId", user.id, Matching.EQUALS);
-            } else {
-                List<Long> ids = new ArrayList<Long>();
-                for (User u : User.find.where().contains("loginId", authorLoginId).findList()) {
-                    ids.add(u.id);
-                }
-                searchParams.add("authorId", ids, Matching.IN);
-            }
-        }
-
-        if (assigneeId != null) {
-            searchParams.add("assignee.user.id", assigneeId, Matching.EQUALS);
-            searchParams.add("assignee.project.id", project.id, Matching.EQUALS);
-        }
-
-        if (filter != null && !filter.isEmpty()) {
-            searchParams.add("title", filter, Matching.CONTAINS);
-        }
-
-        if (milestone != null) {
-            searchParams.add("milestoneId", milestone, Matching.EQUALS);
-        }
-
-        if (labelIds != null) {
-            for (Long labelId : labelIds) {
-                searchParams.add("labels.id", labelId, Matching.EQUALS);
-            }
-        }
-
-        if (commentedCheck) {
-            searchParams.add("numOfComments", Issue.NUMBER_OF_ONE_MORE_COMMENTS, Matching.GE);
-        }
-
-        State st = State.getValue(state);
-        if (st.equals(State.OPEN) || st.equals(State.CLOSED)) {
-            searchParams.add("state", st, Matching.EQUALS);
-        }
-
-        return searchParams;
-    }
-}(No newline at end of file)
app/models/task/Card.java
--- app/models/task/Card.java
+++ app/models/task/Card.java
@@ -10,6 +10,7 @@
 import javax.persistence.ManyToOne;
 import javax.persistence.OneToMany;
 import javax.persistence.OneToOne;
+import javax.persistence.Transient;
 
 import org.codehaus.jackson.JsonNode;
 import org.codehaus.jackson.node.ArrayNode;
@@ -74,6 +75,7 @@
      * checklist.save(); }
      */
 
+    @Transient
     public JsonNode toJSON() {
 
         ObjectNode json = Json.newObject();
app/models/task/TaskBoard.java
--- app/models/task/TaskBoard.java
+++ app/models/task/TaskBoard.java
@@ -3,11 +3,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.persistence.CascadeType;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.OneToMany;
-import javax.persistence.OneToOne;
+import javax.persistence.*;
 
 import models.Project;