unknown 2012-08-28
add logic code for Issue state change and some code refactoring on Issue
@ab58bb0ebe1ab6799e2e526457702d4796f4a99d
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -13,6 +13,7 @@
 import models.enumeration.IssueState;
 import models.enumeration.StateType;
 import models.support.SearchCondition;
+import play.Logger;
 import play.data.Form;
 import play.mvc.Controller;
 import play.mvc.Http.MultipartFormData;
@@ -22,7 +23,6 @@
 import utils.Constants;
 import views.html.issue.editIssue;
 import views.html.issue.issue;
-import views.html.issue.issueError;
 import views.html.issue.issueList;
 import views.html.issue.newIssue;
 import views.html.issue.notExistingPage;
@@ -77,10 +77,9 @@
         } else {
             Issue newIssue = issueForm.get();
             newIssue.authorId = UserApp.currentUser().id;
-            newIssue.authorName = UserApp.currentUser().name;
             newIssue.project = project;
             newIssue.state = IssueState.ENROLLED;
-            newIssue.updateStatusType(newIssue.state);
+//            newIssue.updateStatusType();
             newIssue.filePath = saveFile(request());
             Issue.create(newIssue);
         }
@@ -102,15 +101,15 @@
             return badRequest(issueForm.errors().toString());
         } else {
             Issue issue = issueForm.get();
-            issue.authorId = UserApp.currentUser().id;
-            issue.authorName = UserApp.currentUser().name;
+//            issue.authorId = UserApp.currentUser().id;
             issue.id = id;
             issue.date = Issue.findById(id).date;
             issue.filePath = saveFile(request());
             issue.project = project;
-            if (issue.assigneeId != null) {
-                issue.state = IssueState.ASSIGNED;
-            }
+            issue.updateState(issue);
+
+            Logger.error("assigneeId :_"+issue.assigneeId +"// diagnoisResult:_" + issue.diagnosisResult);
+            //            issue.updateStateType();
             Issue.edit(issue);
         }
         return redirect(routes.IssueApp.issues(project.owner, project.name, StateType.ALL.name()));
@@ -135,7 +134,6 @@
             IssueComment comment = commentForm.get();
             comment.issue = Issue.findById(issueId);
             comment.authorId = UserApp.currentUser().id;
-            comment.authorName = UserApp.currentUser().name;
             comment.filePath = saveFile(request());
             IssueComment.create(comment);
             Issue.updateNumOfComments(issueId);
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -1,13 +1,11 @@
 package models;
 
 import static com.avaje.ebean.Expr.contains;
-import static models.enumeration.IssueState.ASSIGNED;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -93,522 +91,583 @@
  */
 @Entity
 public class Issue extends Model {
-	private static final long serialVersionUID = 1L;
-	private static Finder<Long, Issue> find = new Finder<Long, Issue>(
-			Long.class, Issue.class);
+    private static final long serialVersionUID = 1L;
+    private static Finder<Long, Issue> find = 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";
+    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;
+    @Id
+    public Long id;
 
-	@Constraints.Required
-	public String title;
+    @Constraints.Required
+    public String title;
 
-	@Constraints.Required
-	public String body;
+    @Constraints.Required
+    public String body;
 
-	@Formats.DateTime(pattern = "yyyy-MM-dd")
-	public Date date;
+    @Formats.DateTime(pattern = "yyyy-MM-dd")
+    public Date date;
 
-	public Long milestoneId;
-	public Long assigneeId;
-	public Long authorId;
-	public String authorName;
-	public IssueState state;
-	public StateType stateType;
-	public String issueType;
-	public String componentName;
-	// TODO 첨부 파일이 여러개인경우는?
-	public String filePath;
-	public String osType;
-	public String browserType;
-	public String dbmsType;
-	public String importance;
-	public String diagnosisResult;
+    public int numOfComments;
+    public Long milestoneId;
+    public Long assigneeId;
+    public Long authorId;
+    public IssueState state;
+    public StateType stateType;
+    public String issueType;
+    public String componentName;
+    // TODO 첨부 파일이 여러개인경우는?
+    public String filePath;
+    public String osType;
+    public String browserType;
+    public String dbmsType;
+    public String importance;
+    public String diagnosisResult;
 
-	@ManyToOne
-	public Project project;
+    @ManyToOne
+    public Project project;
 
-	@OneToMany(mappedBy = "issue", cascade = CascadeType.ALL)
-	public List<IssueComment> comments = new ArrayList<IssueComment>();
+    @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL)
+    public List<IssueComment> comments = new ArrayList<IssueComment>();
 
-	public int numOfComments;
-
-	public Issue() {
-		this.date = JodaDateUtil.now();
-	}
-
-	public String issueTypeLabel() {
-		return issueTypes().get(issueType);
-	}
-
-	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 static Map<String, String> osTypes() {
-        return new Options(
-                "issue.new.environment.osType.windows",
-                "issue.new.environment.osType.Mac",
-                "issue.new.environment.osType.Linux");
+    public Issue() {
+        this.date = JodaDateUtil.now();
     }
-	
-	public static Map<String, String> browserTypes() {
-        return new Options(
-                "issue.new.environment.browserType.ie",
+
+    public String issueTypeLabel() {
+        return issueTypes().get(issueType);
+    }
+
+    public Duration ago() {
+        return JodaDateUtil.ago(this.date);
+    }
+
+    public String reporterName() {
+        return User.findNameById(this.authorId);
+    }
+
+    /**
+     * issueList, issue view에서 assignee의 이름을 출력해준다. 아래의 getAssigneeName과 합쳐질 수
+     * 있을듯.
+     */
+    public String assigneeName() {
+
+        return (this.assigneeId != null ? User.findNameById(this.assigneeId) : "issue.noAssignee");
+    }
+
+    /**
+     * 이슈의 오픈 상태를 확인한다.
+     * 
+     * @return boolean
+     */
+    public boolean isOpen() {
+        return StateType.OPEN.equals(this.stateType);
+    }
+
+    /**
+     * View에서 스트링값으로 변환하도록 한다.
+     * 
+     * @return
+     */
+    public String state() {
+        if (this.state.equals(IssueState.ASSIGNED)) {
+            return IssueState.ASSIGNED.state();
+        } else if (this.state.equals(IssueState.SOLVED)) {
+            return IssueState.SOLVED.state();
+        } else if (this.state.equals(IssueState.FINISHED)) {
+            return IssueState.FINISHED.state();
+        } else
+            return IssueState.ENROLLED.state();
+    }
+
+    /**
+     * 해당 이슈의 상태(state) 따라서 탭 기능에서 구분 짖는(stateType) 것이 해결인지 미해결인지 값을 결정해준다.
+     * diagnosisResult가 2인 경우는 현 진단결과 선택 란에서 "수정완료"를 의미하므로, 이슈 정상 해결을 의미한다.
+     * 
+     * @param state
+     */
+    public void updateStateType(Issue issue) {
+
+        if (this.state == null || this.state.equals(IssueState.ASSIGNED)
+                || this.state.equals(IssueState.ENROLLED)) {
+            this.stateType = StateType.OPEN;
+        } else {
+            this.stateType = StateType.CLOSED;
+        }
+    }
+
+    /**
+     * 이슈의 담당자(assignee) 배정과 이슈의 진단결과(diagnosisResult)에 따라 이슈의 상태를 정해진 로직에 따라
+     * 변경한다.
+     * 
+     * @param issue
+     */
+    public void updateState(Issue issue) {
+
+        if (isEnrolled(issue)) {
+            this.state = IssueState.ENROLLED;
+        } else if (isFinished(issue)) {
+            this.state = IssueState.FINISHED;
+        } else if (isAssigned(issue)) {
+            this.state = IssueState.ASSIGNED;
+        } else if (isSolved(issue)) {
+            this.state = IssueState.SOLVED;
+        } else
+            this.state = IssueState.ENROLLED;
+
+        updateStateType(issue);
+    }
+
+    /**
+     * 이슈가 담당자와 진단결과가 없는 경우에는 "등록" 상태임을 확인해준다.
+     * 
+     * @param issue
+     * @return boolean
+     */
+    public boolean isEnrolled(Issue issue) {
+
+        if (issue.assigneeId == null && issue.diagnosisResult.equals("")) {
+            return true;
+        } else
+            return false;
+    }
+
+    /**
+     * 이슈가 담당자는 배정받고, 진단결과가 없는 경우에는 "진행중" 상태임을 확인해준다.
+     * 
+     * @param issue
+     * @return
+     */
+    public boolean isAssigned(Issue issue) {
+
+        if (issue.assigneeId != null && issue.diagnosisResult.equals("")) {
+            return true;
+        } else
+            return false;
+    }
+
+    /**
+     * 이슈가 담당자를 배정받고 진단결과가 2번째 수정완료(추후 변경 가능) 인 경우에는 "해결" 상태임을 확인해준다.
+     * 
+     * @param issue
+     * @return
+     */
+    public boolean isSolved(Issue issue) {
+
+        if (issue.assigneeId != null && issue.diagnosisResult.equals("2")) {
+            return true;
+        } else
+            return false;
+    }
+
+    /**
+     * 이슈가 담당자의 유무와 상관 없이, 진단결과가 2번째 수정완료가 아닌 다른 사유가 존재한 경우에는 "닫힘" 상태임을 확인해준다.
+     * 
+     * @param issue
+     * @return
+     */
+
+    public boolean isFinished(Issue issue) {
+
+        if (!issue.diagnosisResult.equals("2") && !issue.diagnosisResult.equals("")) {
+            return true;
+        } else
+            return false;
+    }
+
+    /**
+     * 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");
+    }
+
+    /**
+     * 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");
     }
-	
-	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에서 사용할 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");
     }
-	
-	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> 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");
     }
-	
-	public static Map<String, String> diagnosisResults() {
-        return new Options(
-                "issue.new.result.diagnosisResult.bug",
+
+    /**
+     * 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"
-                );
+                "issue.new.result.diagnosisResult.works4me");
     }
-	
 
-	/**
-	 * View에서 스트링값으로 변환하도록 한다.
-	 * 
-	 * @return
-	 */
-	public String state() {
-		if (this.state == ASSIGNED) {
-			return IssueState.ASSIGNED.state();
-		} else if (this.state == IssueState.SOLVED) {
-			return IssueState.SOLVED.state();
-		} else if (this.state == IssueState.FINISHED) {
-			return IssueState.FINISHED.state();
-		} else
-			return IssueState.ENROLLED.state();
-	}
+    /**
+     * 이슈 id로 이슈를 찾아준다.
+     * 
+     * @param id
+     * @return
+     */
+    public static Issue findById(Long id) {
+        return find.byId(id);
+    }
 
-	/**
-	 * 해당 이슈의 상태(state) 따라서 탭 기능에서 구분 짖는(stateType) 것이 해결인지 미해결인지 값을 결정해준다.
-	 * 
-	 * @param state
-	 */
+    /**
+     * 이슈를 생성한다.
+     * 
+     * @param issue
+     * @return
+     */
+    public static Long create(Issue issue) {
+        issue.save();
+        if (issue.milestoneId != null) {
+            Milestone milestone = Milestone.findById(issue.milestoneId);
+            milestone.add(issue);
+        }
+        issue.updateStateType(issue);
+        return issue.id;
+    }
 
-	public void updateStatusType(IssueState state) {
-		if (this.state == ASSIGNED || this.state == IssueState.ENROLLED) {
-			this.stateType = StateType.OPEN;
-		} else if (this.state == IssueState.SOLVED
-				|| this.state == IssueState.FINISHED) {
-			this.stateType = StateType.CLOSED;
-		}
-	}
+    /**
+     * 이슈를 삭제한다.
+     * 
+     * @param id
+     */
+    public static void delete(Long id) {
+        Issue issue = find.byId(id);
+        if (!issue.milestoneId.equals(0l) || issue.milestoneId != null) {
+            Milestone milestone = Milestone.findById(issue.milestoneId);
+            milestone.delete(issue);
+        }
+        issue.delete();
+    }
 
-	/**
-	 * 이슈 id로 이슈를 찾아준다.
-	 * 
-	 * @param id
-	 * @return
-	 */
-	public static Issue findById(Long id) {
-		return find.byId(id);
-	}
+    /**
+     * 미해결 탭을 눌렀을 때, open 상태의 이슈들을 찾아준다..
+     * 
+     * @param projectName
+     * @return
+     */
+    public static Page<Issue> findOpenIssues(String projectName) {
+        return Issue.findIssues(projectName, StateType.OPEN);
+    }
 
-	/**
-	 * 이슈를 생성한다.
-	 * 
-	 * @param issue
-	 * @return
-	 */
-	public static Long create(Issue issue) {
-		issue.save();
-		if (issue.milestoneId != null) {
-			Milestone milestone = Milestone.findById(issue.milestoneId);
-			milestone.add(issue);
-		}
-		return issue.id;
-	}
+    /**
+     * 해결 탭을 눌렀을 때, closed 상태의 이슈들을 찾아준다.
+     * 
+     * @param projectName
+     * @return
+     */
+    public static Page<Issue> findClosedIssues(String projectName) {
+        return Issue.findIssues(projectName, StateType.CLOSED);
+    }
 
-	/**
-	 * 이슈를 삭제한다.
-	 * 
-	 * @param id
-	 */
-	public static void delete(Long id) {
-		Issue issue = find.byId(id);
-		if (!issue.milestoneId.equals(0l) || issue.milestoneId != null) {
-			Milestone milestone = Milestone.findById(issue.milestoneId);
-			milestone.delete(issue);
-		}
-		issue.delete();
-	}
+    /**
+     * 해당 프로젝트의 State 외의 것들은 기본값들로 이뤄진 이슈들을 찾아준다.
+     * 
+     * @param projectName
+     * @param state
+     * @return
+     */
+    public static Page<Issue> findIssues(String projectName, StateType state) {
+        return findIssues(projectName, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC,
+                "", null, false, false);
+    }
 
-	/**
-	 * 미해결 탭을 눌렀을 때, open 상태의 이슈들을 찾아준다..
-	 * 
-	 * @param projectName
-	 * @return
-	 */
-	public static Page<Issue> findOpenIssues(String projectName) {
-		return Issue.findIssues(projectName, StateType.OPEN);
-	}
+    /**
+     * 검색창에서 제공된 query(filter)와 댓글과 파일첨부된 이슈만 찾아주는 체크박스의 값에 따라 필터링된 이슈들을 찾아준다.
+     * 
+     * @param projectName
+     * @param filter
+     * @param state
+     * @param commentedCheck
+     * @param fileAttachedCheck
+     * @return
+     */
+    public static Page<Issue> findFilteredIssues(String projectName, String filter,
+            StateType state, boolean commentedCheck, boolean fileAttachedCheck) {
+        return findIssues(projectName, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC,
+                filter, null, commentedCheck, fileAttachedCheck);
+    }
 
-	/**
-	 * 해결 탭을 눌렀을 때, closed 상태의 이슈들을 찾아준다.
-	 * 
-	 * @param projectName
-	 * @return
-	 */
-	public static Page<Issue> findClosedIssues(String projectName) {
-		return Issue.findIssues(projectName, StateType.CLOSED);
-	}
+    /**
+     * 댓글이 달린 이슈들만 찾아준다.
+     * 
+     * @param projectName
+     * @param filter
+     * @return
+     */
+    public static Page<Issue> findCommentedIssues(String projectName, String filter) {
+        return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER,
+                Direction.DESC, filter, null, true, false);
+    }
 
-	/**
-	 * 해당 프로젝트의 State 외의 것들은 기본값들로 이뤄진 이슈들을 찾아준다.
-	 * 
-	 * @param projectName
-	 * @param state
-	 * @return
-	 */
-	public static Page<Issue> findIssues(String projectName, StateType state) {
-		return findIssues(projectName, FIRST_PAGE_NUMBER, state,
-				DEFAULT_SORTER, Direction.DESC, "", null, false, false);
-	}
+    /**
+     * 파일이 첨부된 이슈들만 찾아준다.
+     * 
+     * @param projectName
+     * @param filter
+     * @return
+     */
 
-	/**
-	 * 검색창에서 제공된 query(filter)와 댓글과 파일첨부된 이슈만 찾아주는 체크박스의 값에 따라 필터링된 이슈들을 찾아준다.
-	 * 
-	 * @param projectName
-	 * @param filter
-	 * @param state
-	 * @param commentedCheck
-	 * @param fileAttachedCheck
-	 * @return
-	 */
-	public static Page<Issue> findFilteredIssues(String projectName,
-			String filter, StateType state, boolean commentedCheck,
-			boolean fileAttachedCheck) {
-		return findIssues(projectName, FIRST_PAGE_NUMBER, state,
-				DEFAULT_SORTER, Direction.DESC, filter, null, commentedCheck,
-				fileAttachedCheck);
-	}
+    public static Page<Issue> findFileAttachedIssues(String projectName, String filter) {
+        return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER,
+                Direction.DESC, filter, null, false, true);
+    }
 
-	/**
-	 * 댓글이 달린 이슈들만 찾아준다.
-	 * 
-	 * @param projectName
-	 * @param filter
-	 * @return
-	 */
-	public static Page<Issue> findCommentedIssues(String projectName,
-			String filter) {
-		return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL,
-				DEFAULT_SORTER, Direction.DESC, filter, null, true, false);
-	}
+    /**
+     * 마일스톤 Id에 의거해서 해당 마일스톤에 속한 이슈들을 찾아준다.
+     * 
+     * @param projectName
+     * @param milestoneId
+     * @return
+     */
+    public static Page<Issue> findIssuesByMilestoneId(String projectName, Long milestoneId) {
+        return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER,
+                Direction.DESC, "", milestoneId, false, false);
+    }
 
-	/**
-	 * 파일이 첨부된 이슈들만 찾아준다.
-	 * 
-	 * @param projectName
-	 * @param filter
-	 * @return
-	 */
+    /**
+     * 이슈들을 아래의 parameter들의 조건에 의거하여 Page형태로 반환한다.
+     * 
+     * @param projectName
+     *            project ID to find 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, 댓글이 존재하는 이슈만 필터링
+     * @param fileAttachedCheck
+     *            filter applied on the fileAttachedCheck column, 파일이 업로드된 이슈만
+     *            필터링
+     * @return 위의 조건에 따라 필터링된 이슈들을 Page로 반환.
+     */
+    public static Page<Issue> findIssues(String projectName, int pageNumber, StateType state,
+            String sortBy, Direction order, String filter, Long milestoneId,
+            boolean commentedCheck, boolean fileAttachedCheck) {
 
-	public static Page<Issue> findFileAttachedIssues(String projectName,
-			String filter) {
-		return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL,
-				DEFAULT_SORTER, Direction.DESC, filter, null, false, true);
-	}
+        OrderParams orderParams = new OrderParams().add(sortBy, order);
+        SearchParams searchParams = new SearchParams().add("project.name", projectName,
+                Matching.EQUALS);
 
-	/**
-	 * 마일스톤 Id에 의거해서 해당 마일스톤에 속한 이슈들을 찾아준다.
-	 * 
-	 * @param projectName
-	 * @param milestoneId
-	 * @return
-	 */
-	public static Page<Issue> findIssuesByMilestoneId(String projectName,
-			Long milestoneId) {
-		return findIssues(projectName, FIRST_PAGE_NUMBER, StateType.ALL,
-				DEFAULT_SORTER, Direction.DESC, "", milestoneId, false, false);
-	}
+        if (filter != null && !filter.isEmpty()) {
+            searchParams.add("title", filter, Matching.CONTAINS);
+        }
+        if (milestoneId != null) {
+            searchParams.add("milestoneId", milestoneId, Matching.EQUALS);
+        }
+        if (commentedCheck) {
+            searchParams.add("numOfComments", NUMBER_OF_ONE_MORE_COMMENTS, Matching.GE);
+        }
+        if (fileAttachedCheck) {
+            searchParams.add("filePath", "", Matching.NOT_EQUALS);
+        }
+        if (state == null) {
+            state = StateType.ALL;
+        }
+        switch (state)
+            {
+            case OPEN:
+                searchParams.add("stateType", StateType.OPEN, Matching.EQUALS);
+                break;
+            case CLOSED:
+                searchParams.add("stateType", StateType.CLOSED, Matching.EQUALS);
+                break;
+            default:
+            }
+        return FinderTemplate.getPage(orderParams, searchParams, find, ISSUE_COUNT_PER_PAGE,
+                pageNumber);
+    }
 
-	/**
-	 * 이슈들을 아래의 parameter들의 조건에 의거하여 Page형태로 반환한다.
-	 * 
-	 * @param projectName
-	 *            project ID to find 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, 댓글이 존재하는 이슈만 필터링
-	 * @param fileAttachedCheck
-	 *            filter applied on the fileAttachedCheck column, 파일이 업로드된 이슈만
-	 *            필터링
-	 * @return 위의 조건에 따라 필터링된 이슈들을 Page로 반환.
-	 */
-	public static Page<Issue> findIssues(String projectName, int pageNumber,
-			StateType state, String sortBy, Direction order, String filter,
-			Long milestoneId, boolean commentedCheck, boolean fileAttachedCheck) {
+    public static Long findAssigneeIdByIssueId(String projectName, Long issueId) {
+        return find.byId(issueId).assigneeId;
+    }
 
-		OrderParams orderParams = new OrderParams().add(sortBy, order);
-		SearchParams searchParams = new SearchParams().add("project.name",
-				projectName, Matching.EQUALS);
+    /**
+     * 해당 마일스톤아이디로 관련 이슈를 검색한다.
+     * 
+     * @param milestoneId
+     * @return
+     */
+    public static List<Issue> findByMilestoneId(Long milestoneId) {
+        SearchParams searchParams = new SearchParams().add("milestoneId", milestoneId,
+                Matching.EQUALS);
+        return FinderTemplate.findBy(null, searchParams, find);
+    }
 
-		if (filter != null && !filter.isEmpty()) {
-			searchParams.add("title", filter, Matching.CONTAINS);
-		}
-		if (milestoneId != null) {
-			searchParams.add("milestoneId", milestoneId, Matching.EQUALS);
-		}
-		if (commentedCheck) {
-			searchParams.add("numOfComments", NUMBER_OF_ONE_MORE_COMMENTS,
-					Matching.GE);
-		}
-		if (fileAttachedCheck) {
-			searchParams.add("filePath", "", Matching.NOT_EQUALS);
-		}
-		if (state == null) {
-			state = StateType.ALL;
-		}
-		switch (state) {
-		case OPEN:
-			searchParams.add("stateType", StateType.OPEN, Matching.EQUALS);
-			break;
-		case CLOSED:
-			searchParams.add("stateType", StateType.CLOSED, Matching.EQUALS);
-			break;
-		}
-		return FinderTemplate.getPage(orderParams, searchParams, find,
-				ISSUE_COUNT_PER_PAGE, pageNumber);
-	}
+    public static void edit(Issue issue) {
+        Issue previousIssue = findById(issue.id);
+        if (issue.filePath == null) {
+            issue.filePath = previousIssue.filePath;
+        }
+        issue.updateStateType(issue);
+        issue.update();
+    }
 
-	public static Long findAssigneeIdByIssueId(String projectName, Long issueId) {
-		return find.byId(issueId).assigneeId;
-	}
+    /**
+     * JXL 라이브러리를 이용하여 엑셀 파일로 저장하며, 해당 파일이 저장된 주소를 반환한다.
+     * 
+     * @param resultList
+     *            엑셀로 저장하고자 하는 리스트
+     * @param pageName
+     *            엑셀로 저장하고자 하는 목록의 페이지(내용, ex 이슈, 게시물 등) 이름
+     * @return
+     * @throws Exception
+     */
+    public static String excelSave(List<Issue> resultList, String pageName) throws Exception {
+        String excelFile = pageName + "_" + JodaDateUtil.today().getTime() + ".xls";
+        String fullPath = "public/uploadFiles/" + excelFile;
+        WritableWorkbook workbook = null;
+        WritableSheet sheet = null;
 
-	/**
-	 * 이슈의 오픈 상태를 확인한다.
-	 * 
-	 * @return boolean
-	 */
-	public boolean isOpen() {
-		return StateType.OPEN.equals(this.stateType);
-	}
+        try {
+            WritableFont wf1 = new WritableFont(WritableFont.TIMES, 13, WritableFont.BOLD, false,
+                    UnderlineStyle.SINGLE, Colour.BLUE_GREY, ScriptStyle.NORMAL_SCRIPT);
+            WritableCellFormat cf1 = new WritableCellFormat(wf1);
+            cf1.setBorder(Border.ALL, BorderLineStyle.DOUBLE);
+            cf1.setAlignment(Alignment.CENTRE);
 
-	/**
-	 * 해당 마일스톤아이디로 관련 이슈를 검색한다.
-	 * 
-	 * @param milestoneId
-	 * @return
-	 */
-	public static List<Issue> findByMilestoneId(Long milestoneId) {
-		SearchParams searchParams = new SearchParams().add("milestoneId",
-				milestoneId, Matching.EQUALS);
-		return FinderTemplate.findBy(null, searchParams, find);
-	}
+            WritableFont wf2 = new WritableFont(WritableFont.TAHOMA, 11, WritableFont.NO_BOLD,
+                    false, UnderlineStyle.NO_UNDERLINE, Colour.BLACK, ScriptStyle.NORMAL_SCRIPT);
+            WritableCellFormat cf2 = new WritableCellFormat(wf2);
+            cf2.setShrinkToFit(true);
+            cf2.setBorder(Border.ALL, BorderLineStyle.THIN);
+            cf2.setAlignment(Alignment.CENTRE);
 
-	public static void edit(Issue issue) {
-		Issue previousIssue = findById(issue.id);
-		if (issue.filePath == null) {
-			issue.filePath = previousIssue.filePath;
-		}
-		issue.update();
-	}
+            workbook = Workbook.createWorkbook(new File(fullPath));
+            sheet = workbook.createSheet(String.valueOf(JodaDateUtil.today().getTime()), 0);
 
-	/**
-	 * JXL 라이브러리를 이용하여 엑셀 파일로 저장하며, 해당 파일이 저장된 주소를 반환한다.
-	 * 
-	 * @param resultList
-	 *            엑셀로 저장하고자 하는 리스트
-	 * @param pageName
-	 *            엑셀로 저장하고자 하는 목록의 페이지(내용, ex 이슈, 게시물 등) 이름
-	 * @return
-	 * @throws Exception
-	 */
-	public static String excelSave(List<Issue> resultList, String pageName)
-			throws Exception {
-		String excelFile = pageName + "_" + JodaDateUtil.today().getTime()
-				+ ".xls";
-		String fullPath = "public/uploadFiles/" + excelFile;
-		WritableWorkbook workbook = null;
-		WritableSheet sheet = null;
+            String[] labalArr = { "ID", "STATE", "TITLE", "ASSIGNEE", "DATE" };
 
-		try {
-			WritableFont wf1 = new WritableFont(WritableFont.TIMES, 13,
-					WritableFont.BOLD, false, UnderlineStyle.SINGLE,
-					Colour.BLUE_GREY, ScriptStyle.NORMAL_SCRIPT);
-			WritableCellFormat cf1 = new WritableCellFormat(wf1);
+            for (int i = 0; i < labalArr.length; i++) {
+                sheet.addCell(new Label(i, 0, labalArr[i], cf1));
+                sheet.setColumnView(i, 20);
+            }
+            for (int i = 1; i < resultList.size() + 1; i++) {
+                Issue issue = (Issue) resultList.get(i - 1);
+                int colcnt = 0;
+                sheet.addCell(new Label(colcnt++, i, issue.id.toString(), cf2));
+                sheet.addCell(new Label(colcnt++, i, issue.state.toString(), cf2));
+                sheet.addCell(new Label(colcnt++, i, issue.title, cf2));
+                sheet.addCell(new Label(colcnt++, i, getAssigneeName(issue.assigneeId), cf2));
+                sheet.addCell(new Label(colcnt++, i, issue.date.toString(), cf2));
+            }
+            workbook.write();
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw e;
+        } finally {
+            try {
+                if (workbook != null)
+                    workbook.close();
+            } catch (WriteException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return excelFile;
+    }
 
-			cf1.setBorder(Border.ALL, BorderLineStyle.DOUBLE);
+    /**
+     * excelSave에서 assignee를 리턴해준다.
+     * 
+     * @param uId
+     * @return
+     */
+    private static String getAssigneeName(Long uId) {
+        return (uId != null ? User.findNameById(uId) : TO_BE_ASSIGNED);
+    }
 
-			cf1.setAlignment(Alignment.CENTRE);
+    // FIXME 이것이 없이 테스트는 잘 작동하나, view에서 댓글이 달린 이슈들을 필터링하는 라디오버튼을 작동시에 이 메쏘드에서
+    // 시행하는 동기화 작업 없이는 작동을 하지 않는다.
 
-			WritableFont wf2 = new WritableFont(WritableFont.TAHOMA, 11,
-					WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE,
-					Colour.BLACK, ScriptStyle.NORMAL_SCRIPT);
-			WritableCellFormat cf2 = new WritableCellFormat(wf2);
-			cf2.setShrinkToFit(true);
-			cf2.setBorder(Border.ALL, BorderLineStyle.THIN);
-			cf2.setAlignment(Alignment.CENTRE);
+    /**
+     * comment가 delete되거나 create될 때, numOfComment와 comment.size()를 동기화 시켜준다.
+     * 
+     * @param id
+     */
+    public static void updateNumOfComments(Long id) {
 
-			workbook = Workbook.createWorkbook(new File(fullPath));
-			sheet = workbook.createSheet(
-					String.valueOf(JodaDateUtil.today().getTime()), 0);
+        Issue issue = Issue.findById(id);
+        issue.numOfComments = issue.comments.size();
+        issue.update();
+    }
 
-			String[] labalArr = { "ID", "STATE", "TITLE", "ASSIGNEE", "DATE" };
-
-			for (int i = 0; i < labalArr.length; i++) {
-				sheet.addCell(new Label(i, 0, labalArr[i], cf1));
-				sheet.setColumnView(i, 20);
-			}
-			for (int i = 1; i < resultList.size() + 1; i++) {
-				Issue issue = (Issue) resultList.get(i - 1);
-				int colcnt = 0;
-				sheet.addCell(new Label(colcnt++, i, issue.id.toString(), cf2));
-				sheet.addCell(new Label(colcnt++, i, issue.state.toString(),
-						cf2));
-				sheet.addCell(new Label(colcnt++, i, issue.title, cf2));
-				sheet.addCell(new Label(colcnt++, i,
-						getAssigneeName(issue.assigneeId), cf2));
-				sheet.addCell(new Label(colcnt++, i, issue.date.toString(), cf2));
-			}
-			workbook.write();
-		} catch (Exception e) {
-			e.printStackTrace();
-			throw e;
-		} finally {
-			try {
-				if (workbook != null)
-					workbook.close();
-			} catch (WriteException e) {
-				e.printStackTrace();
-			} catch (IOException e) {
-				e.printStackTrace();
-			}
-		}
-
-		return excelFile;
-	}
-
-	public String reporterName() {
-	    return this.authorName;
-		//return User.findNameById(this.authorId);
-	}
-
-	/**
-	 * issueList, issue view에서 assignee의 이름을 출력해준다. 아래의 getAssigneeName과 합쳐질 수
-	 * 있을듯.
-	 */
-	public String assigneeName() {
-
-		return (this.assigneeId != null ? User.findNameById(this.assigneeId)
-				: "issue.noAssignee");
-	}
-
-	/**
-	 * excelSave에서 assignee를 리턴해준다.
-	 * 
-	 * @param uId
-	 * @return
-	 */
-	private static String getAssigneeName(Long uId) {
-		return (uId != null ? User.findNameById(uId) : TO_BE_ASSIGNED);
-	}
-
-	// 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 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);
-	}
-
-	/**
-	 * 전체 컨텐츠 검색할 때 제목과 내용에 condition.filter를 포함하고 있는 이슈를 검색한다.
-	 * 
-	 * @param project
-	 * @param condition
-	 * @return
-	 */
-	public static Page<Issue> findIssues(Project project,
-			SearchApp.ContentSearchCondition condition) {
-		String filter = condition.filter;
-		return find.where().eq("project.id", project.id)
-				.or(contains("title", filter), contains("body", filter))
-				.findPagingList(condition.pageSize).getPage(condition.page - 1);
-	}
-	// =======
-	// public boolean isAuthor(Long currentUserId, Long objectId, String
-	// projectName) {
-	//
-	// boolean authorIs;
-	// if (currentUserId == findById(objectId).reporterId
-	// || RoleCheck.roleCheck(currentUserId, project.id,
-	// PermissionResource.PROJECT.resource(),
-	// PermissionOperation.SETTING.operation())) {
-	// authorIs = true;
-	// } else {
-	//
-	// }
-	//
-	// return authorIs;
-	//
-	// }
-	// >>>>>>> Stashed changes
+    /**
+     * 전체 컨텐츠 검색할 때 제목과 내용에 condition.filter를 포함하고 있는 이슈를 검색한다.
+     * 
+     * @param project
+     * @param condition
+     * @return
+     */
+    public static Page<Issue> findIssues(Project project, SearchApp.ContentSearchCondition condition) {
+        String filter = condition.filter;
+        return find.where().eq("project.id", project.id)
+                .or(contains("title", filter), contains("body", filter))
+                .findPagingList(condition.pageSize).getPage(condition.page - 1);
+    }
 }
app/views/issue/issue.scala.html
--- app/views/issue/issue.scala.html
+++ app/views/issue/issue.scala.html
@@ -125,10 +125,13 @@
         <input type="hidden" value="@issue.title" name="title"/>
         <input type="hidden" value="@issue.body" name="body"/>
         <input type="hidden" value="@issue.authorId" name="authorId"/>
-        <input type="submit" class="btn pull-right" value=@Messages("button.save") />
+        <div class = "btn pull-right">
+            <input type="submit" value=@Messages("button.save") />
+            <input type="button" id=makeFinished value="이슈 해결" />
+        </div>
     }
    }
-    
+  
 </div>
 <!-- 여기서 부터 덧글 -->
 @for(comment <- issue.comments){
app/views/issue/newIssue.scala.html
--- app/views/issue/newIssue.scala.html
+++ app/views/issue/newIssue.scala.html
@@ -15,7 +15,7 @@
 	</div>
     @form(action = routes.IssueApp.saveIssue(project.owner, project.name), 
     				'enctype -> "multipart/form-data",
-    				'class -> "form-horizontal"){
+    				'class -> "horizontal form-horizontal") {
     	<fieldset>
     	@inputText(
     		issueForm("title"), 
@@ -26,8 +26,9 @@
     		'_showConstraints -> false,
     		'_label-> Messages("label.contents"))
     	</fieldset>
+
     	<i class = "icon-file"></i>@Messages("post.new.fileAttach") <a id="fileUpload" data-toggle="modal" href="#askFilePath" class="btn">@Messages("button.selectFile")</a>
-		<div class="modal hide" id="askFilePath">
+	       <div class="modal hide" id="askFilePath">
 		    	<div class="modal-header">
 			        <button type="button" class="close" data-dismiss="modal">@Messages("button.popup.exit")</button>
 			            <h3>@Messages("post.popup.fileAttach.title")</h3>
@@ -40,11 +41,12 @@
 			        <a href="#" class="btn" data-dismiss="modal">@Messages("button.cancel")</a>
 			        <a href="#" class="btn btn-primary" data-dismiss="modal">@Messages("button.confirm")</a>
 			    </div>
-			 </div>
-		</br></Sbr>
+	       </div>
+		</br></br>
+
 		<fieldset>
             <div class="well">
-                <h4> @Messages("issue.new.environment")</h4>
+                <legend><b>@Messages("issue.new.environment")</b></legend>
                 <div class="well form-inline">
                         @selectEx(
                             issueForm("osType"), 
@@ -68,9 +70,10 @@
                 </div>
             </div>
         </fieldset>
+
 		<fieldset>
 			<div class="well">
-	    		<h4>@Messages("issue.new.detailInfo")</h4>
+	    		<legend><b>@Messages("issue.new.detailInfo")</b></legend>
 	    		<div class="well form-inline">
                 @isVisible(models.enumeration.Resource.ISSUE_CATEGORY) {		  	
 				  	@selectEx(
@@ -111,7 +114,7 @@
 	    </fieldset>
     	<fieldset>
     		<div class="well">
-    			<h4> @Messages("issue.new.result")</h4>
+    			<legend><b>@Messages("issue.new.result")</b></legend>
     			<div class="well form-inline">
                     @isVisible(models.enumeration.Resource.ISSUE_IMPORTANCE) {				    	
 				    	@selectEx(
@@ -132,7 +135,7 @@
 				</div>		
     		</div>
     	</fieldset>
-    	}
+
 	<div class="row pull-right">
 			<div class="actions">
 				<input type="submit" class="btn btn-primary" value="@Messages("button.save")"> 
@@ -140,4 +143,4 @@
 			</div>
     </div>
  }
-
+}
(No newline at end of file)
app/views/selectEx.scala.html
--- app/views/selectEx.scala.html
+++ app/views/selectEx.scala.html
@@ -1,5 +1,7 @@
 @(field: play.api.data.Field, options: Seq[(String,String)], args: (Symbol,Any)*)
 @import helper.input 
+@import helper.FieldConstructor
+@implicitFieldConstructor = @{ FieldConstructor(twitterBootstrapInput.render) } 
 
 @input(field, args:_*) { (id, name, value, htmlArgs) =>
     <select id="@id" name="@name" @toHtmlArgs(htmlArgs)>
test/models/IssueTest.java
--- test/models/IssueTest.java
+++ test/models/IssueTest.java
@@ -1,19 +1,20 @@
 package models;
 
-import com.avaje.ebean.Page;
-import controllers.SearchApp;
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.List;
+
 import models.enumeration.IssueState;
 import models.enumeration.StateType;
+
 import org.junit.Ignore;
 import org.junit.Test;
 
-import play.Logger;
 import utils.JodaDateUtil;
 
-import java.io.File;
-import java.util.List;
+import com.avaje.ebean.Page;
 
-import static org.fest.assertions.Assertions.assertThat;
+import controllers.SearchApp;
 
 public class IssueTest extends ModelTest<Issue> {
 
@@ -26,6 +27,7 @@
         issue.state = IssueState.ASSIGNED;
         issue.authorId = User.findById(1l).id;
         issue.milestoneId = 4l;
+        issue.issueType = "issue.new.detailInfo.issueType.worst";
         // When
         // Then
         assertThat(Issue.create(issue)).isNotNull();
@@ -171,22 +173,6 @@
         assertThat(Issue.findById(3l).numOfComments).isEqualTo(0);
     }
     
-    @Test
-    @Ignore
-    public void isAuthor() throws Exception {
-        // Given
-        Long currentUserId_hobi = 2l;
-        Long issueId1 = 1l;
-        Long issueId2 = 2l;
-        // When
-        boolean result1 = Issue.isAuthor(currentUserId_hobi, issueId1);
-        boolean result2 = Issue.isAuthor(currentUserId_hobi, issueId2);
-        // Then
-        assertThat(result1).isEqualTo(true);
-        assertThat(result2).isEqualTo(false);
-
-    }
-
 	@Test
 	public void findIssues() {
 		// Given
test/models/MilestoneTest.java
--- test/models/MilestoneTest.java
+++ test/models/MilestoneTest.java
@@ -245,7 +245,7 @@
     public void updateIssue() throws Exception {
         //Given
         Issue issue = new Issue();
-        issue.updateStatusType(IssueState.ENROLLED);
+        issue.updateStateType(issue);
         issue.milestoneId = 6l;
         issue.update(5l);
 
Add a comment
List