[Notice] Announcing the End of Demo Server [Read me]
Jihan Kim 2013-05-03
Merge branch 'master' of https://github.com/nforge/hive
@e5e1aab368beed3ca920c2ef10be5f53d39991ac
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -1241,6 +1241,7 @@
             float: left;
             .header {
                 font-size: 20px;
+                font-weight: bold;
                 margin-bottom: 15px;
                 .project-name {
                     color: #888;
@@ -2110,8 +2111,6 @@
         color: #222;
         font-size: 14px;
         line-height: 40px;
-        border-bottom: 1px solid @white;
-        .box-shadow(inset 0 -1px 0 rgba(0, 0, 0, 0.15));
     }
     .comments {
         margin: 0;
@@ -2119,8 +2118,6 @@
         list-style: none;
         .comment {
             padding: 20px;
-            border-bottom: 1px solid @white;
-            .box-shadow(inset 0 -1px 0 rgba(0, 0, 0, 0.15));
             .media-body {
                 padding-left: 17px;
                 .commenter {
@@ -2220,7 +2217,6 @@
     margin:20px 0;
     
     .labels-wrap {
-        width: 650px;
         line-height: 30px;
         padding-right: 30px;
         text-align: left;
@@ -3277,4 +3273,4 @@
 /*
 .alert { position:absolute; width:830px; top:0px; margin-left:auto; z-index:999; display:none; }
 form { margin:0 0 2px; }
-*/
(파일 끝에 줄바꿈 문자 없음)
+*/
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -138,15 +138,17 @@
     public static Result editPost(String userName, String projectName, Long postId) {
         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);
+        final Posting post = postForm.get();
+        final Posting original = Posting.finder.byId(postId);
         Call redirectTo = routes.BoardApp.posts(project.owner, project.name, 1);
-        Callback doNothing = new Callback() {
+        Callback updatePostingBeforeUpdate = new Callback() {
             @Override
-            public void run() { }
+            public void run() {
+                post.comments = original.comments;
+            }
         };
 
-        return editPosting(original, post, postForm, redirectTo, doNothing);
+        return editPosting(original, post, postForm, redirectTo, updatePostingBeforeUpdate);
     }
 
     public static Result deletePost(String userName, String projectName, Long postingId) {
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -25,16 +25,13 @@
 import com.avaje.ebean.Page;
 import com.avaje.ebean.ExpressionList;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Map;
 
-import static com.avaje.ebean.Expr.contains;
 import static com.avaje.ebean.Expr.icontains;
 
 public class IssueApp extends AbstractPostingApp {
@@ -149,14 +146,14 @@
 
     public static Result issuesAsExcel(ExpressionList<Issue> el, Project project)
             throws WriteException, IOException, UnsupportedEncodingException {
-        File excelFile = Issue.excelSave(el.findList(), project.name + "_issues");
+        byte[] excelData = Issue.excelFrom(el.findList());
+        String filename = HttpUtil.encodeContentDisposition(
+                project.name + "_issues_" + JodaDateUtil.today().getTime() + ".xls");
 
-        String filename = HttpUtil.encodeContentDisposition(excelFile.getName());
-
-        response().setHeader("Content-Type", new Tika().detect(excelFile));
+        response().setHeader("Content-Type", new Tika().detect(filename));
         response().setHeader("Content-Disposition", "attachment; " + filename);
 
-        return ok(excelFile);
+        return ok(excelData);
     }
 
     public static Result issue(String userName, String projectName, Long issueId) {
@@ -246,7 +243,7 @@
         Callback updateIssueBeforeSave = new Callback() {
             @Override
             public void run() {
-                issue.project = project;
+                issue.comments = originalIssue.comments;
                 addLabels(issue.labels, request());
             }
         };
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -1,6 +1,7 @@
 package controllers;
 
 import com.avaje.ebean.Page;
+import com.avaje.ebean.ExpressionList;
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.RoleType;
@@ -32,12 +33,15 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.ArrayList;
 
 import static play.data.Form.form;
 import static play.libs.Json.toJson;
+import static com.avaje.ebean.Expr.contains;
 
 public class ProjectApp extends Controller {
-	public static final String[] LOGO_TYPE = {"jpg", "png", "gif", "bmp"};
+    public static final String[] LOGO_TYPE = {"jpg", "jpeg", "png", "gif", "bmp"};
+    private static final int MAX_FETCH_PROJECTS = 1000;
 
     public static Project getProject(String userName, String projectName) {
         return Project.findByNameAndOwner(userName, projectName);
@@ -247,6 +251,30 @@
     }
 
     public static Result projects(String filter, String state, int pageNum) {
+        final String HTML = "text/html";
+        final String JSON = "application/json";
+        String prefer = HttpUtil.getPreferType(request(), JSON, HTML);
+
+        if (prefer == null) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        if (prefer.equals(JSON)) {
+            String query = request().getQueryString("query");
+            List<String> projectNames = new ArrayList<String>();
+            ExpressionList<Project> el = Project.find.where().or(contains("name", query), contains("owner", query));
+            int total = el.findRowCount();
+            if (total > MAX_FETCH_PROJECTS) {
+                el.setMaxRows(MAX_FETCH_PROJECTS);
+                response().setHeader("Content-Range", "items " + MAX_FETCH_PROJECTS + "/" + total);
+            }
+            for (Project project: el.findList()) {
+                projectNames.add(project.owner + "/" + project.name);
+            }
+
+            return ok(toJson(projectNames));
+        }
+
         OrderParams orderParams = new OrderParams();
         SearchParams searchParams = new SearchParams();
 
app/controllers/SiteApp.java
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
@@ -1,28 +1,23 @@
 package controllers;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.commons.mail.*;
-
-import models.Project;
-import models.User;
+import com.avaje.ebean.Page;
+import info.schleichardt.play2.mailplugin.Mailer;
+import models.*;
+import org.apache.commons.mail.EmailException;
 import org.apache.commons.mail.SimpleEmail;
 import play.Configuration;
 import play.Logger;
+import play.i18n.Messages;
 import play.mvc.Controller;
+import play.mvc.Http;
 import play.mvc.Result;
 import utils.Constants;
+import views.html.site.*;
 
-import views.html.site.setting;
-import views.html.site.mail;
-import views.html.site.userList;
-import views.html.site.projectList;
+import java.util.*;
 
-import com.avaje.ebean.Page;
 import static play.data.Form.form;
-import info.schleichardt.play2.mailplugin.Mailer;
+import static play.libs.Json.toJson;
 
 public class SiteApp extends Controller {
     public static Result sendMail() throws EmailException{
@@ -59,19 +54,44 @@
         return ok(mail.render("title.sendMail", notConfiguredItems, sender, errorMessage, sended));
     }
 
+    public static Result massMail() {
+        Configuration config = play.Play.application().configuration();
+        List<String> notConfiguredItems = new ArrayList<String>();
+        String[] requiredItems = {"smtp.host", "smtp.user", "smtp.password"};
+        for(String key : requiredItems) {
+            if (config.getString(key) == null) {
+                notConfiguredItems.add(key);
+            }
+        }
+
+        String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain");
+
+        return ok(massMail.render("title.massMail", notConfiguredItems, sender));
+    }
+
     public static Result setting() {
         return ok(setting.render("title.siteSetting"));
     }
-    
+
     public static Result userList(int pageNum, String loginId) {
         return ok(userList.render("title.siteSetting", User.findUsers(pageNum, loginId)));
     }
-    
+
+    public static Result postList(int pageNum) {
+        Page<Posting> page = Posting.finder.order("createdDate DESC").findPagingList(30).getPage(pageNum - 1);
+        return ok(postList.render("title.siteSetting", page));
+    }
+
+    public static Result issueList(int pageNum) {
+        Page<Issue> page = Issue.finder.order("createdDate DESC").findPagingList(30).getPage(pageNum - 1);
+        return ok(issueList.render("title.siteSetting", page));
+    }
+
     public static Result searchUser() {
         String loginId = form(User.class).bindFromRequest().get().loginId;
         return redirect(routes.SiteApp.userList(0, loginId));
     }
-    
+
     public static Result deleteUser(Long userId) {
         if( User.findByLoginId(session().get("loginId")).isSiteManager() ){
             if(Project.isOnlyManager(userId).size() == 0)
@@ -84,12 +104,12 @@
 
         return redirect(routes.SiteApp.userList(0, null));
     }
-        
+
     public static Result projectList(String filter) {
         Page<Project> projects = Project.findByName(filter, 25, 0);
         return ok(projectList.render("title.projectList", projects, filter));
     }
-    
+
     public static Result deleteProject(Long projectId){
         if( User.findByLoginId(session().get("loginId")).isSiteManager() ){
             Project.find.byId(projectId).delete();
@@ -98,7 +118,7 @@
         }
         return redirect(routes.SiteApp.projectList(""));
     }
-    
+
     public static Result softwareMap() {
         return TODO;
     }
@@ -117,4 +137,43 @@
         flash(Constants.WARNING, "auth.unauthorized.waringMessage");
         return redirect(routes.Application.index());
     }
+
+    public static Result mailList() {
+        Set<String> emails = new HashSet<String>();
+        Map<String, String[]> projects = request().body().asFormUrlEncoded();
+
+        if(!UserApp.currentUser().isSiteManager()) {
+            return forbidden(Messages.get("auth.unauthorized.waringMessage"));
+        }
+
+        if (!request().accepts("application/json")) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        if (projects == null) {
+            return ok(toJson(new HashSet<String>()));
+        }
+
+        if (projects.containsKey("all")) {
+            if (projects.get("all")[0].equals("true")) {
+                for(User user : User.find.findList()) {
+                    emails.add(user.email);
+                }
+            }
+        } else {
+            for(String[] projectNames : projects.values()) {
+                String projectName = projectNames[0];
+                String[] parts = projectName.split("/");
+                String owner = parts[0];
+                String name = parts[1];
+                Project project = Project.findByNameAndOwner(owner, name);
+                for (ProjectUser projectUser : ProjectUser.findMemberListByProject(project.id)) {
+                    Logger.debug(projectUser.user.email);
+                    emails.add(projectUser.user.email);
+                }
+            }
+        }
+
+        return ok(toJson(emails));
+    }
 }
app/controllers/TagApp.java
--- app/controllers/TagApp.java
+++ app/controllers/TagApp.java
@@ -31,9 +31,9 @@
             response().setHeader("Content-Range", "items " + MAX_FETCH_TAGS + "/" + total);
         }
 
-        Map<Long, String> tags = new HashMap<Long, String>();
+        List<String> tags = new ArrayList<String>();
         for (Tag tag: el.findList()) {
-            tags.put(tag.id, tag.name);
+            tags.add(tag.name);
         }
 
         return ok(toJson(tags));
app/models/AbstractPosting.java
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
@@ -13,6 +13,7 @@
 import javax.persistence.*;
 import javax.validation.constraints.Size;
 import java.util.Date;
+import java.util.List;
 
 import static com.avaje.ebean.Expr.contains;
 
@@ -127,7 +128,12 @@
         authorName = user.name;
     }
 
+    abstract public List<? extends Comment> getComments();
+
     public void delete() {
+        for (Comment comment: getComments()) {
+            comment.delete();
+        }
         Attachment.deleteAll(asResource().getType(), id);
         super.delete();
     }
app/models/Attachment.java
--- app/models/Attachment.java
+++ app/models/Attachment.java
@@ -88,6 +88,12 @@
                 .eq("containerId", containerId).findList();
     }
 
+    public static int countByContainer(ResourceType containerType, Long containerId) {
+        return find.where()
+                .eq("containerType", containerType)
+                .eq("containerId", containerId).findRowCount();
+    }
+
     public static List<Attachment> findByContainer(Resource resource) {
         return findByContainer(resource.getType(), resource.getId());
     }
app/models/Comment.java
--- app/models/Comment.java
+++ app/models/Comment.java
@@ -53,6 +53,7 @@
     }
 
     public void delete() {
+        Attachment.deleteAll(asResource().getType(), id);
         super.delete();
         getParent().save();
     }
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -104,18 +104,16 @@
     }
 
     /**
-     * JXL 라이브러리를 이용하여 엑셀 파일로 저장하며, 해당 파일이 저장된 주소를 반환한다.
+     * Generate a Microsoft Excel file in byte array from the given issue list,
+     * using JXL.
      *
-     * @param resultList 엑셀로 저장하고자 하는 리스트
-     * @param pageName   엑셀로 저장하고자 하는 목록의 페이지(내용, ex 이슈, 게시물 등) 이름
+     * @param issueList 엑셀로 저장하고자 하는 리스트
      * @return
      * @throws WriteException
      * @throws IOException
      * @throws Exception
      */
-    public static File excelSave(List<Issue> resultList, String pageName) throws WriteException, IOException {
-        String excelFile = pageName + "_" + JodaDateUtil.today().getTime() + ".xls";
-        String fullPath = "public/uploadFiles/" + excelFile;
+    public static byte[] excelFrom(List<Issue> issueList) throws WriteException, IOException {
         WritableWorkbook workbook = null;
         WritableSheet sheet = null;
 
@@ -131,7 +129,8 @@
         cf2.setBorder(Border.ALL, BorderLineStyle.THIN);
         cf2.setAlignment(Alignment.CENTRE);
 
-        workbook = Workbook.createWorkbook(new File(fullPath));
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        workbook = Workbook.createWorkbook(bos);
         sheet = workbook.createSheet(String.valueOf(JodaDateUtil.today().getTime()), 0);
 
         String[] labalArr = {"ID", "STATE", "TITLE", "ASSIGNEE", "DATE"};
@@ -140,8 +139,8 @@
             sheet.addCell(new Label(i, 0, labalArr[i], cf1));
             sheet.setColumnView(i, 20);
         }
-        for (int i = 1; i < resultList.size() + 1; i++) {
-            Issue issue = resultList.get(i - 1);
+        for (int i = 1; i < issueList.size() + 1; i++) {
+            Issue issue = issueList.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));
@@ -159,7 +158,8 @@
         } catch (IOException e) {
             e.printStackTrace();
         }
-        return new File(fullPath);
+
+        return bos.toByteArray();
     }
 
     /**
@@ -224,5 +224,11 @@
                 .findPagingList(size).getPage(0)
                 .getList();
     }
+
+    @Transient
+    public List<? extends Comment> getComments() {
+        return comments;
+    }
+
 }
 
app/models/Milestone.java
--- app/models/Milestone.java
+++ app/models/Milestone.java
@@ -58,11 +58,11 @@
     }
 
     public int getNumClosedIssues() {
-        return Issue.finder.where().eq("milestone", this).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("milestone", this).eq("state", State.OPEN).findRowCount();
+    	return Issue.finder.where().eq("milestone", this).eq("state", State.OPEN).findRowCount();
     }
 
     public int getNumTotalIssues() {
@@ -139,13 +139,38 @@
      * @return
      */
     public static List<Milestone> findMilestones(Long projectId,
-                                                 State state, String sort, Direction direction) {
-        OrderParams orderParams = new OrderParams().add(sort, direction);
+                                                 State state, String sort, final Direction direction) {
+    	
+    	OrderParams orderParams = new OrderParams();
+    	
+    	if(!"completionRate".equals(sort)) {
+    		orderParams.add(sort, direction);	
+    	}
+    	    	
         SearchParams searchParams = new SearchParams().add("project.id", projectId, Matching.EQUALS);
-        if (state != null && state != State.ALL) {
+        if(state != null && state != State.ALL) {
             searchParams.add("state", state, Matching.EQUALS);
         }
-        return FinderTemplate.findBy(orderParams, searchParams, find);
+        
+        List<Milestone> milestones = FinderTemplate.findBy(orderParams, searchParams, find);
+
+        if("completionRate".equals(sort)) {
+        	Collections.sort(milestones, new Comparator<Milestone>() {
+				@Override
+				public int compare(Milestone o1, Milestone o2) {
+					int o1CompletionRate = o1.getCompletionRate();
+					int o2CompletionRate = o2.getCompletionRate();
+					
+					if(direction == Direction.ASC) {
+				        return (o1CompletionRate < o2CompletionRate ? -1 : (o1CompletionRate == o2CompletionRate ? 0 : 1));
+					} else {
+						return (o1CompletionRate < o2CompletionRate ? 1 : (o1CompletionRate == o2CompletionRate ? 0 : -1));
+					}
+				}
+			});
+        }
+        
+        return milestones;
     }
 
     public void updateWith(Milestone newMilestone) {
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -61,4 +61,9 @@
                 .findPagingList(size).getPage(0)
                 .getList();
     }
+
+    @Transient
+    public List<? extends Comment> getComments() {
+        return comments;
+    }
 }
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -1,6 +1,7 @@
 package models;
 
 import java.io.IOException;
+import java.text.SimpleDateFormat;
 import java.util.*;
 
 import javax.persistence.*;
@@ -77,6 +78,11 @@
     @ManyToMany
     public Set<Tag> tags;
 
+    public String getCreatedDate() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
+        return sdf.format(this.createdDate);
+    }
+
     public static Long create(Project newProject) {
 		newProject.siteurl = "http://localhost:9000/" + newProject.name;
         newProject.createdDate = new Date();
app/utils/HttpUtil.java
--- app/utils/HttpUtil.java
+++ app/utils/HttpUtil.java
@@ -1,7 +1,12 @@
 package utils;
 
+import play.api.http.MediaRange;
+import play.mvc.Http;
+import scala.Option;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.List;
 import java.util.Map;
 
 public class HttpUtil {
@@ -25,4 +30,16 @@
         filename = "filename*=UTF-8''" + filename;
         return filename;
     }
+
+    public static String getPreferType(Http.Request request, String ... types) {
+        // acceptedTypes is sorted by preference.
+        for(MediaRange range : request.acceptedTypes()) {
+            for(String type : types) {
+                if (range.accepts(type)) {
+                    return type;
+                }
+            }
+        }
+        return null;
+    }
 }
app/views/board/editPost.scala.html
--- app/views/board/editPost.scala.html
+++ app/views/board/editPost.scala.html
@@ -46,7 +46,7 @@
 			<div class="avatar-wrap">
 				<img src="@User.findByLoginId(session.get("loginId")).avatarUrl" class="img-rounded" width="32" height="32" alt="avatar">
 			</div>
-			<div id="upload" class="attach-info-wrap" resourceType="@ResourceType.BOARD_POST">
+			<div id="upload" class="attach-info-wrap" resourceType="@ResourceType.BOARD_POST" resourceId="@postId">
 				<div>
 					<span class="progress-num">0%</span> <span class="sp-line">&nbsp;</span>
 					<strong>total</strong> <span class="total-num">0MB</span>
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -9,7 +9,7 @@
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.ico")">
-<link rel="stylesheet" type="text/css" media="screen" href="@getCSSLink("bootstrap.min")">
+<link rel="stylesheet" type="text/css" media="screen" href="@getCSSLink("bootstrap")">
 <link rel="stylesheet" type="text/css" media="screen" href="@getCSSLink("nforge")">
 <script type="text/javascript" src="@getJSLink("lib/jquery/jquery-1.9.0")"></script>
 <script type="text/javascript" src="@getJSLink("common/hive.Common")"></script>
@@ -76,5 +76,17 @@
 <!-- HIVE -->
 @uservoice("196721")
 
+    <script type="text/javascript">
+            var _gaq = _gaq || [];
+            _gaq.push(['_setAccount', 'UA-40528193-1']);
+            _gaq.push(['_trackPageview']);
+
+            (function() {
+            var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+            ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+            var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+            })();
+
+    </script>
 </body>
 </html>
(파일 끝에 줄바꿈 문자 없음)
app/views/login.scala.html
--- app/views/login.scala.html
+++ app/views/login.scala.html
@@ -26,7 +26,7 @@
             	</dl>
 
                 <div class="act-row right-txt">
-                    <input type="checkbox" class="checkbox" autocomplete="off" id="remember-me" name="remeber-me"><label for="remember-me" class="bg-checkbox">@Messages("title.rememberMe")</label>
+                    <input type="checkbox" class="checkbox" autocomplete="off" id="remember-me" name="rememberMe"><label for="remember-me" class="bg-checkbox">@Messages("title.rememberMe")</label>
                     <a href="@routes.PasswordResetApp.lostPassword()" class="forgot-password"><span class="label ">@Messages("title.forgotpassword")</span></a>
                 </div>
                 
app/views/milestone/list.scala.html
--- app/views/milestone/list.scala.html
+++ app/views/milestone/list.scala.html
@@ -65,9 +65,9 @@
                <a href="@makeSortLink("completionRate")" class="filter active"><i class="ico ico-sort @sortMark("completionRate")"></i>@Messages("order.completionRate")</a>
            </div>
        	<div class="legend">
-       		<i class="orange"></i>완료
-       		<i class="blue"></i>진행중
-       		<i class="gray"></i>미할당
+       		<i class="gray"></i>@Messages("issue.state.open")
+            <i class="blue"></i>@Messages("issue.state.assigned")
+            <i class="orange"></i>@Messages("issue.state.closed")
        	</div>
     </div>
 
@@ -102,7 +102,7 @@
                     <strong class="version"></strong>
                     <span class="title">@milestone.title</span>
                     <span class="sp">|</span>
-                    <span class="due-date">완료일 <strong>@milestone.getDueDateString</strong></span>
+                    <span class="due-date">@Messages("label.dueDate") <strong>@milestone.getDueDateString</strong></span>
                 </div>
                 
                 <div class="progress-wrap">
@@ -115,15 +115,10 @@
 					<div class="progress-label">
 						@** 0% 일 수도 있어서 최소한의 텍스트 영역 확보 **@
 						<div class="pull-left" style="width: @milestone.getCompletionRate%; min-width:100px;">
-							<a href="@makeIssuesLink(milestone.id,"closed")">완료 <strong>@milestone.getCompletionRate%</strong></a>
+							<a href="@makeIssuesLink(milestone.id,"closed")">@Messages("milestone.state.closed") <strong>@milestone.getCompletionRate%</strong></a>
 						</div>
-						<!-- 
-						<div class="pull-left center-txt" style="width: 30%;">
-							진행중 <strong>30%</strong>
-						</div> 
-						-->
 						<div class="pull-right">
-							<a href="@makeIssuesLink(milestone.id,"open")">미완료 <strong>@{100 - milestone.getCompletionRate}%</strong></a>
+							<a href="@makeIssuesLink(milestone.id,"open")">@Messages("issue.state.open") <strong>@{100 - milestone.getCompletionRate}%</strong></a>
 						</div>
 					</div>
 				</div>
app/views/milestone/manage.scala.html
--- app/views/milestone/manage.scala.html
+++ app/views/milestone/manage.scala.html
@@ -55,7 +55,7 @@
 				<td><a href="@makeSortLink("title")">@Messages("label.title") @sortMark("title")</a></td>
 				<td>@Messages("label.contents")</td>
 				<td><a href="@makeSortLink("dueDate")">@Messages("label.dueDate") @sortMark("dueDate")</a></td>
-				<td><a href="@makeSortLink("completionRate")">@Messages("label.state") @sortMark("completionRate")</a></td>
+				<td><a href="@makeSortLink("state")">@Messages("label.state") @sortMark("state")</a></td>
 				<td>&nbsp;</td>
 			</tr>
 		</thead>
app/views/project/projectList.scala.html
--- app/views/project/projectList.scala.html
+++ app/views/project/projectList.scala.html
@@ -1,4 +1,5 @@
 @(message: String, currentPage: com.avaje.ebean.Page[Project], filter:String, listState:String)
+@import utils.TemplateHelper._
 
 @home(message, utils.MenuType.PROJECTS) {
 <div class="page">
@@ -18,7 +19,7 @@
 					<input name="state" type="hidden" value="@listState">
 					<input name="filter" class="text" type="text" placeholder="@Messages("site.project.filter")" value="@filter"><!-- 
 				 --><button type="submit" class="btn search-btn">@Messages("issue.search")</button><!-- 
-				 	<a href="#" class="btn-advanced"><i class="ico"></i>Advanced Search</a> -->
+					<a href="#" class=tn-advanced"><i class="ico"></i>Advanced Search</a> -->
                      
 					<div class="srch-advanced">
 	                 	<!-- TODO: fill this form -->
@@ -42,7 +43,7 @@
                   <div class="desc">
                       @project.overview
                   </div>
-                  <p class="name-tag">by <a href="@routes.UserApp.userInfo(project.owner)">@project.owner</a> <!--<span>20 days ago.</span>--></p>
+                  <p class="name-tag">by <a href="@routes.UserApp.userInfo(project.owner)"><b>@project.owner</b></a> since @agoString(project.ago)</p>
               </div>
               <div class="stats-wrap">
                   <div class="like">
 
app/views/site/issueList.scala.html (added)
+++ app/views/site/issueList.scala.html
@@ -0,0 +1,40 @@
+@(message: String, currentPage: com.avaje.ebean.Page[Issue])
+
+@siteMngMain(message) {
+    <h2>@Messages("site.sidebar.issueList")</h2>
+
+    <div class="row-fluid">
+        <table class="table table-striped table-condensed">
+            <thead>
+                <tr>
+                    <th>@Messages("project.name")</th>
+                    <th>@Messages("issue.author")</th>
+                    <th>@Messages("issue.title")</th>
+                    <th>@Messages("issue.createdDate")</th>
+                    <th>@Messages("issue.attachment")</th>
+                    <th>@Messages("issue.numOfComments")</th>
+                </tr>
+            </thead>
+            <tbody>
+                @for(issue <- currentPage.getList()) {
+                    @issuelisting(issue)
+                }
+            </tbody>
+        </table>
+    </div>
+
+    <div class="row-fluid">
+        <center>@pagination(currentPage, 5, "pagination", "/sites/issueList")</center>
+    </div>
+}
+
+@issuelisting(issue: models.Issue) = {
+    <tr>
+        <td>@issue.project.name</td>
+        <td>@issue.authorName</td>
+        <td><a href="@routes.BoardApp.post(issue.project.owner, issue.project.name, issue.id)">@issue.title</a></td>
+        <td>@utils.TemplateHelper.agoString(issue.ago())</td>
+        <td>@models.Attachment.countByContainer(models.enumeration.ResourceType.ISSUE_POST, issue.id)</td>
+        <td>@issue.numOfComments</td>
+    </tr>
+}(파일 끝에 줄바꿈 문자 없음)
 
app/views/site/massMail.scala.html (added)
+++ app/views/site/massMail.scala.html
@@ -0,0 +1,42 @@
+@(message: String, notConfiguredItems: List[String], sender: String)
+
+@siteMngMain(message) {
+<div class="page-header">
+  <h3>@Messages(message)</h3>
+</div>
+
+<div>
+  <p>
+  <label class="radio">
+    <input type="radio" name="mailingType" value="all">
+    @Messages("site.massMail.toAll")
+  </label>
+  </p>
+  <p>
+  <label class="radio">
+    <input type="radio" name="mailingType" value="projects" checked>@Messages("site.massMail.toProjects"):
+    <span id="selected-projects"><!-- project names are added here by hive.site.MassMail.js --></span>
+    <input type="text" class="text uname"
+            id="input-project"
+            data-provider="typeahead" autocomplete="off"
+            placeholder="@Messages("project.name")"/>
+    <button type="submit" class="btn btn-primary" id="select-project" data-loading-text="@Messages("site.massMail.loading")">
+      <strong>@Messages("button.add")</strong>
+    </button>
+  </label>
+  </p>
+</div>
+
+<button type="submit" class="btn btn-primary" id="write-email">
+  <strong>@Messages("mail.write")</strong>
+</button>
+
+<script type="text/javascript">
+  $(document).ready(function(){
+    $hive.loadModule("site.MassMail", {
+        "sURLMailList": "@routes.SiteApp.mailList()",
+        "sURLProjects": "@routes.ProjectApp.projects()"
+    });
+  });
+</script>
+}
 
app/views/site/pagination.scala.html (added)
+++ app/views/site/pagination.scala.html
@@ -0,0 +1,46 @@
+@(page:com.avaje.ebean.Page[_ <: play.db.ebean.Model], pageNum:Int, divId:String, listUrl:String)
+
+@{
+    var currentPageNum = page.getPageIndex + 1
+    var lastPageNum = page.getTotalPageCount()
+    var str = ""
+
+    if(currentPageNum <= pageNum/2) {
+        currentPageNum = pageNum/2 +1
+    } else if(currentPageNum > lastPageNum - pageNum/2) {
+        currentPageNum = lastPageNum - pageNum/2 - 1
+    }
+    makeList(currentPageNum)
+}
+@makeList(currentPageNum:Int) = {
+    <div class='pagination' id="@divId"><ul>
+    @if(page.hasPrev){
+        @makeLink("Prev", page.getPageIndex)
+    } else {
+        <li class="disabled"><a>Prev</a></li>
+    }
+
+    @if(page.getTotalPageCount() < pageNum) {
+        @for(x <- (1 to page.getTotalPageCount())){
+            @makeLink(x + "", x)
+        }
+    } else {
+        @for( x <- (currentPageNum - pageNum/2 to currentPageNum + pageNum/2)){
+            @makeLink(x + "", x)
+        }
+    }
+
+
+    @if(page.hasNext) {
+        @makeLink("Next",page.getPageIndex + 2)
+    } else {
+        <li class="disabled"><a>Next</a></li>
+    }
+
+    </ul></div>
+}
+@makeLink(title:String, index:Int) = {
+    <li class="@if((page.getPageIndex + 1).equals(index)){active}">
+      <a href="@(listUrl + "?pageNum=" + index)" pageNum="@index">@title</a>
+    </li>
+}(파일 끝에 줄바꿈 문자 없음)
 
app/views/site/postList.scala.html (added)
+++ app/views/site/postList.scala.html
@@ -0,0 +1,40 @@
+@(message: String, currentPage: com.avaje.ebean.Page[Posting])
+
+@siteMngMain(message) {
+    <h2>@Messages("site.sidebar.postList")</h2>
+
+    <div class="row-fluid">
+        <table class="table table-striped table-condensed">
+            <thead>
+                <tr>
+                    <th>@Messages("project.name")</th>
+                    <th>@Messages("post.author")</th>
+                    <th>@Messages("post.title")</th>
+                    <th>@Messages("post.createdDate")</th>
+                    <th>@Messages("post.attachment")</th>
+                    <th>@Messages("post.numOfComments")</th>
+                </tr>
+            </thead>
+            <tbody>
+                @for(post <- currentPage.getList()) {
+                    @postlisting(post)
+                }
+            </tbody>
+        </table>
+    </div>
+
+    <div class="row-fluid">
+        <center>@pagination(currentPage, 5, "pagination", "/sites/postList")</center>
+    </div>
+}
+
+@postlisting(post: models.Posting) = {
+    <tr>
+        <td>@post.project.name</td>
+        <td>@post.authorName</td>
+        <td><a href="@routes.BoardApp.post(post.project.owner, post.project.name, post.id)">@post.title</a></td>
+        <td>@utils.TemplateHelper.agoString(post.ago())</td>
+        <td>@models.Attachment.countByContainer(models.enumeration.ResourceType.BOARD_POST, post.id)</td>
+        <td>@post.numOfComments</td>
+    </tr>
+}(파일 끝에 줄바꿈 문자 없음)
app/views/site/projectList.scala.html
--- app/views/site/projectList.scala.html
+++ app/views/site/projectList.scala.html
@@ -7,7 +7,7 @@
       <button type="submit" class="btn">검색</button>
     </form>
   </div>
-  
+
   <div class="row-fluid">
     <table class="table table-striped table-condensed">
       <thead>
@@ -24,9 +24,9 @@
             <td>@project.owner</td>
             <td>
               <a class="btn btn-danger" data-toggle="modal" href="#alertDeletion@project.id">@Messages("button.delete")</a>
-            </td> 
+            </td>
           </tr>
-          
+
           <div class="modal hide" id="alertDeletion@project.id">
               <div class="modal-header">
                   <button type="button" class="close" data-dismiss="modal">×</button>
@@ -44,7 +44,7 @@
       </tbody>
     </table>
   </div>
-  
+
   <div class="row-fluid">
     <center>@paginationForUserList(currentPage, 5, "pagination")</center>
   </div>
app/views/site/sidebar.scala.html
--- app/views/site/sidebar.scala.html
+++ app/views/site/sidebar.scala.html
@@ -17,6 +17,12 @@
         <li class="@isActiveMenu(routes.SiteApp.userList())">
             <a href="@routes.SiteApp.userList()"><i class="icon-user"></i>@Messages("site.sidebar.userList")</a>
         </li>
+        <li class="@isActiveMenu(routes.SiteApp.postList())">
+            <a href="@routes.SiteApp.postList()"><i class="icon-pencil"></i>@Messages("site.sidebar.postList")</a>
+        </li>
+        <li class="@isActiveMenu(routes.SiteApp.issueList())">
+            <a href="@routes.SiteApp.issueList()"><i class="icon-check"></i>@Messages("site.sidebar.issueList")</a>
+        </li>
         <li class="@isActiveMenu(routes.SiteApp.projectList())">
             <a href="@routes.SiteApp.projectList()"><i class="icon-folder-open"></i>@Messages("site.sidebar.projectList")</a>
         </li>
@@ -27,5 +33,8 @@
         <li class="@isActiveMenu(routes.Application.index())">
             <a href="@routes.SiteApp.writeMail()"><i class="icon-envelope"></i>@Messages("site.sidebar.mailSend")</a>
         </li>
+        <li class="@isActiveMenu(routes.Application.index())">
+            <a href="@routes.SiteApp.massMail()"><i class="icon-envelope"></i>@Messages("site.sidebar.massMail")</a>
+        </li>
     </ul>
 </div> <!-- /well -->
app/views/user/info.scala.html
--- app/views/user/info.scala.html
+++ app/views/user/info.scala.html
@@ -86,12 +86,6 @@
 							</div>
 						</div>
 					</div>
-					<div class="filter-wrap user-setting">
-						<div class="filters">
-							<a href="#" class="filter"><i class="ico btn-gray-arrow"></i>@Messages("order.all")</a>
-							<a href="#" class="filter active"><i class="ico btn-gray-arrow down"></i>@Messages("order.state")</a>
-						</div>
-					</div>
 				</div>
 				<ul class="user-streams all-projects">
 					@for(project <- user.myProjects()){
 
conf/logger.xml (deleted)
--- conf/logger.xml
@@ -1,23 +0,0 @@
-<configuration>
-
-  <conversionRule conversionWord="coloredLevel" converterClass="play.api.Logger$ColoredLevel" />
-
-  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder>
-      <pattern>%date - [%level] - from %logger in %thread %n%message%n%xException%n</pattern>
-    </encoder>
-  </appender>
-
-  <logger name="play" level="INFO" />
-  <logger name="application" level="INFO" />
-
-  <!-- Off these ones as they are annoying, and anyway we manage configuration ourself -->
-  <logger name="com.avaje.ebean.config.PropertyMapLoader" level="OFF" />
-  <logger name="com.avaje.ebeaninternal.server.core.XmlConfigLoader" level="OFF" />
-  <logger name="com.avaje.ebeaninternal.server.lib.BackgroundThread" level="OFF" />
-
-  <root level="ERROR">
-    <appender-ref ref="STDOUT" />
-  </root>
-</configuration>
-  (파일 끝에 줄바꿈 문자 없음)
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -62,12 +62,14 @@
 label.confirm.delete = Are you Sure?
 label.select = Select Label
 label.error.duplicated = Failed to create new label. It might be same label exists already.
-label.error.creationFailed = Failed to create new label. It might be server error or your request is invalid. 
+label.error.creationFailed = Failed to create new label. It might be server error or your request is invalid.
 
 order.all = All
 order.date = Date
 order.state = State
 order.comments = Comments
+order.completionRate = Completion Rate
+order.dueDate = Due Date
 
 button.save = Save
 button.confirm = Confirm
@@ -116,7 +118,7 @@
 milestone.form.dueDate = Choose due date
 milestone.error.title = Title is required
 milestone.error.content = Description is required
-milestone.error.duedateFormat = Invalid format of due date (YYYY-MM-DD) 
+milestone.error.duedateFormat = Invalid format of due date (YYYY-MM-DD)
 
 #Issue
 issue.state.unit = issues
@@ -192,7 +194,11 @@
 issue.downloadAsExcel = Download as Excel
 issue.search = SEARCH
 issue.error.emptyTitle = Title of this issue is required
-issue.error.emptyBody = Description about this issue is required 
+issue.error.emptyBody = Description about this issue is required
+issue.title = Title
+issue.createdDate = Created Date
+issue.attachment = Attachment
+issue.numOfComments = Comment Count
 
 #Post
 post.new = New
@@ -210,6 +216,10 @@
 post.popup.fileAttach.contents = Please, select file to attach
 post.edit.rejectNotAuthor = You don't have permisstion to access.
 post.update.error = Errors on Input Value
+post.title = Title
+post.createdDate = Created Date
+post.attachment = Attachment
+post.numOfComments = Comment Count
 
 #Project
 project.myproject = MY PROJECTS
@@ -278,9 +288,12 @@
 site.sidebar = Site Management
 site.sidebar.setting = Site Setting
 site.sidebar.userList = Users
+site.sidebar.postList = Posts
+site.sidebar.issueList = Issues
 site.sidebar.projectList = Projects
 site.sidebar.softwareMap = Software Map
 site.sidebar.mailSend = Send a Email
+site.sidebar.massMail = Send Mass Emails
 site.project.filter = Find Project by Project Name
 site.userList.search = Find a user by his/her login ID...
 site.userList.isActive = Active Status
@@ -291,6 +304,9 @@
 site.project.deleteConfirm = Do you want to delete this project?
 site.userlist.unlocked = Active User List
 site.userlist.locked = Account Locked User List
+site.massMail.toAll = To All
+site.massMail.toProjects = To members of specific projects
+site.massMail.loading = Loading...
 
 #User
 user.loginId = Login ID
@@ -359,7 +375,7 @@
 code.commits = Commits
 code.newer = Newer
 code.older = Older
-code.repoUrl = REPOSITORY URL
+code.repoUrl = URL
 
 #mail
 mail.password = Password
@@ -384,6 +400,7 @@
 mail.subject = Subject
 mail.body = Body
 mail.send = Send
+mail.write = Write
 mail.fail = Failed to send a mail.
 mail.sended = Mail is sent.
 
@@ -393,7 +410,7 @@
 none = None
 comment = Comment
 noAssignee = No Assignee
-noMilestone = No Milestone 
+noMilestone = No Milestone
 
 #validation
 validation.tooShortPassword = The password field must be at least 4 characters in length.
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -22,6 +22,7 @@
 title.commitHistory = 커밋 히스토리
 title.mailSetting = 메일 설정
 title.sendMail = 메일 발송
+title.massMail = 대량 메일 발송
 title.projectList = 프로젝트 목록
 title.help = 도움말
 title.search = 검색
@@ -63,7 +64,7 @@
 label.confirm.delete = 라벨을 삭제하면 이슈에 지정한 라벨도 함께 제거됩니다. 정말 삭제하시겠습니까?
 label.select = 라벨 선택
 label.error.duplicated = 라벨 생성에 실패했습니다.\n이미 동일한 라벨이 존재할지도 모릅니다.
-label.error.creationFailed = 라벨 생성에 실패했습니다.\n서버에 문제가 있거나 올바른 요청이 아닐 수 있습니다. 
+label.error.creationFailed = 라벨 생성에 실패했습니다.\n서버에 문제가 있거나 올바른 요청이 아닐 수 있습니다.
 
 order.all = 전체
 order.date = 날짜순
@@ -94,8 +95,8 @@
 button.apply = 적용
 button.back = 돌아가기
 button.upload = 파일 올리기
-button.user.makeAccountUnlock.true = 잠김해제
-button.user.makeAccountUnlock.false = 계정잠그기
+button.user.makeAccountUnlock.true = 잠김해제
+button.user.makeAccountUnlock.false = 계정잠그기
 
 checkbox.commented = 댓글
 checkbox.fileAttached = 첨부파일
@@ -120,7 +121,7 @@
 milestone.form.dueDate = 완료일을 선택하세요
 milestone.error.title = 마일스톤 제목을 입력해주세요
 milestone.error.content = 마일스톤 내용을 입력해주세요
-milestone.error.duedateFormat = 완료일 형식이 잘못되었습니다. YYYY-MM-DD 형식으로 입력해주세요. 
+milestone.error.duedateFormat = 완료일 형식이 잘못되었습니다. YYYY-MM-DD 형식으로 입력해주세요.
 
 #Issue
 issue.state.unit = 이슈
@@ -197,7 +198,11 @@
 issue.downloadAsExcel = 엑셀파일로 다운받기
 issue.search = 검색
 issue.error.emptyTitle = 이슈 제목을 입력해주세요
-issue.error.emptyBody = 이슈 내용을 입력해주세요  
+issue.error.emptyBody = 이슈 내용을 입력해주세요
+issue.title = 제목
+issue.createdDate = 작성일
+issue.attachment = 첨부파일
+issue.numOfComments = 댓글개수
 
 #Post
 post.new = 새글
@@ -215,6 +220,10 @@
 post.popup.fileAttach.contents = 첨부할 파일을 선택해주세요.
 post.edit.rejectNotAuthor = 글쓴이가 아닙니다.
 post.update.error = 입력값 오류
+post.title = 제목
+post.createdDate = 작성일
+post.attachment = 첨부파일
+post.numOfComments = 댓글개수
 
 #Project
 project.myproject = 내 프로젝트
@@ -249,7 +258,7 @@
 project.member.delete = 프로젝트 유저 삭제
 project.member.notExist = 존재하지 않는 유저입니다.
 project.member.ownerCannotLeave = 프로젝트 소유자는 탈퇴할 수 없습니다.
-project.member.ownerMustBeAManager = 프로젝트 소유자는 관리 권한을 가져야 합니다.
+project.member.ownerMustBeAManager = 프로젝트 소유자는 관리 권한을 가져야 합니다.
 project.sidebar = 프로젝트 설정
 project.setting = 설정
 project.member = 멤버
@@ -283,10 +292,13 @@
 #Site
 site.sidebar = 사이트 관리
 site.sidebar.setting = 설정
-site.sidebar.userList = 유저 관리
+site.sidebar.userList = 유저
+site.sidebar.postList = 게시물
+site.sidebar.issueList = 이슈
 site.sidebar.projectList = 프로젝트 설정
 site.sidebar.softwareMap = 소프트웨어 맵
 site.sidebar.mailSend = 메일 발송
+site.sidebar.massMail = 대량 메일 발송
 site.project.filter = 찾으려는 프로젝트 이름을 입력하세요
 site.userList.search = 찾으려는 사용자의 ID를 입력하세요
 site.userList.isActive = 활성 상태
@@ -295,8 +307,12 @@
 site.user.deleteConfirm = 정말로 해당 유저를 사이트에서 탈퇴시키겠습니까?
 site.project.delete = 프로젝트 삭제
 site.project.deleteConfirm = 정말로 해당 프로젝트를 사이트에서 삭제하겠습니까?
-site.userlist.unlocked = 활성화된 유저 목록
-site.userlist.locked = 계정이 잠긴 유저 목록
+site.userlist.unlocked = 활성화된 유저 목록
+site.userlist.locked = 계정이 잠긴 유저 목록
+site.massMail.toAll = 모두에게
+site.massMail.toProjects = 특정 프로젝트의 멤버들에게
+site.massMail.loading = 불러오는중...
+
 
 #User
 user.loginId = 아이디
@@ -320,9 +336,9 @@
 user.signupBtn = 참여하기
 user.loginWithNewPassword = 새로 설정한 비밀번호로 로그인 하세요
 user.notExists.name = 존재하지 않는 유저입니다.
-user.locked = 잠긴 사용자 계정입니다.
-user.isLocked = 잠김여부
-user.signup.requested = 하이브 가입이 요청되었습니다. 사이트 관리자가 검토/승인 후 사용가능합니다. 감사합니다.
+user.locked = 잠긴 사용자 계정입니다.
+user.isLocked = 잠김여부
+user.signup.requested = 하이브 가입이 요청되었습니다. 사이트 관리자가 검토/승인 후 사용가능합니다. 감사합니다.
 
 #Role
 role.manager = 관리자
@@ -390,6 +406,7 @@
 mail.subject = 제목
 mail.body = 본문
 mail.send = 발송
+mail.write = 메일 쓰기
 mail.fail = 메일 발송에 실패했습니다.
 mail.sended = 메일을 발송하였습니다.
 
@@ -399,7 +416,7 @@
 none = 없음
 comment = 댓글
 noAssignee = 담당자 없음
-noMilestone = 마일스톤 없음 
+noMilestone = 마일스톤 없음
 
 #validation
 validation.tooShortPassword = 비밀번호를 4자 이상으로 만들어 주세요!
conf/routes
--- conf/routes
+++ conf/routes
@@ -35,6 +35,8 @@
 GET     /sites/setting                                  controllers.SiteApp.setting()
 GET     /sites/mail                                     controllers.SiteApp.writeMail(errorMessage:String ?= "", sended:Boolean ?= false)
 POST    /sites/mail                                     controllers.SiteApp.sendMail()
+GET     /sites/massmail                                 controllers.SiteApp.massMail()
+POST    /sites/mailList                                 controllers.SiteApp.mailList()
 GET     /sites/userList                                 controllers.SiteApp.userList(pageNum: Int ?= 0, loginId: String ?= null)
 POST    /sites/userList                                 controllers.SiteApp.searchUser()
 GET     /sites/user/delete                              controllers.SiteApp.deleteUser(userId: Long)
@@ -46,6 +48,9 @@
 POST    /lostPassword                                   controllers.PasswordResetApp.requestResetPasswordEmail()
 GET     /resetPassword                                  controllers.PasswordResetApp.resetPasswordForm(s:String)
 POST    /resetPassword                                  controllers.PasswordResetApp.resetPassword()
+GET     /sites/postList                                 controllers.SiteApp.postList(pageNum: Int ?= 1)
+GET     /sites/issueList                                controllers.SiteApp.issueList(pageNum: Int ?= 1)
+
 
 # Attachments
 GET     /files                                          controllers.AttachmentApp.getFileList()
public/javascripts/common/hive.Common.js
--- public/javascripts/common/hive.Common.js
+++ public/javascripts/common/hive.Common.js
@@ -69,7 +69,7 @@
 		// 그렇지 않으면 스크립트 파일 불러온 뒤 초기화 시도
 		if(registerModule(sName, htOptions) === false){
 			htVar.htTryLoad = htVar.htTryLoad || {};
-			htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (htVar.htTryLoad[sName]++);
+			htVar.htTryLoad[sName] = (typeof htVar.htTryLoad[sName] == "undefined") ? 1 : (++htVar.htTryLoad[sName]);
 			
 			if(htVar.htTryLoad[sName] > 3){
 				console.log("[HIVE] fail to load module " + sName);
@@ -226,7 +226,7 @@
 		welForm.ajaxForm({
 			"success" : htOptions.fOnLoad  || function(){},
 			"error"   : htOptions.fOnError || function(){},
-			"datatype": htOptions.sDataType || null
+			"dataType": htOptions.sDataType || null
 		});
 		welForm.submit();
 		
@@ -286,4 +286,4 @@
 		
 		$hive.loadModule(sModuleName, htOptions);
 	}
-};
(파일 끝에 줄바꿈 문자 없음)
+};
public/javascripts/common/hive.FileUploader.js
--- public/javascripts/common/hive.FileUploader.js
+++ public/javascripts/common/hive.FileUploader.js
@@ -22,9 +22,13 @@
 	function _init(htOptions){
 		htOptions = htOptions || {};
 		
-		_initVar(htOptions);
 		_initElement(htOptions);
+		_initVar(htOptions);
 		_attachEvent();
+		
+		if(htVar.sMode == "edit") {
+			_requestList();			
+		}
 	}
 	
 	/**
@@ -42,6 +46,10 @@
 			"beforeSubmit"  : _onBeforeSubmitForm,
 			"uploadProgress": _onUploadProgressForm
 		};
+		
+		htVar.sMode = htOptions.sMode;
+		htVar.sResourceId = htElements.welTarget.attr('resourceId');
+		htVar.sResourceType = htElements.welTarget.attr('resourceType');		
 	}
 
 	/**
@@ -66,6 +74,60 @@
 		htElements.welInputFile.click(function(){
 			_setProgressBar(0);
 		});
+	}
+	
+	/**
+	 * request attached file list
+	 */
+	function _requestList(){
+		var htData = _getRequestData();
+
+		$hive.sendForm({
+			"sURL"     : htVar.sAction,
+			"htData"   : htData,
+			"htOptForm": {"method":"get"},
+			"fOnLoad"  : _onLoadRequest
+		});		
+	}
+	
+	/**
+	 * get request parameters
+	 * @return {Hash Table}
+	 */
+	function _getRequestData(){
+		var htData = {};
+		
+		if(typeof htVar.sResourceType !== "undefined"){
+			htData.containerType = htVar.sResourceType;
+		}
+		
+		if(typeof htVar.sResourceId !== "undefined"){
+			htData.containerId = htVar.sResourceId;
+		}
+		
+		return htData;
+	}
+	
+	function _onLoadRequest(oRes) {
+
+		var aItems = [];
+		var aFiles = oRes.attachments;
+
+		if(aFiles == null || aFiles.length === 0){
+			return;
+		}
+		
+		var totalFileSize = 0;
+		aFiles.forEach(function(oFile) {
+			var welItem = _createFileItem(oFile);
+			welItem.click(_onClickListItem);
+			htElements.welFileList.append(welItem);
+			totalFileSize = totalFileSize + parseInt(oFile.size);
+		});
+		
+		_setProgressBar(100);
+		_updateTotalFilesize(totalFileSize);
+		
 	}
 	
 	/**
@@ -132,7 +194,7 @@
 
 		// create list item
 		var welItem = _createFileItem(oRes);
-			welItem.click(_onClickListItem);
+		welItem.click(_onClickListItem);
 		htElements.welFileList.append(welItem);
 		
 		_setProgressBar(100);
public/javascripts/common/hive.ui.Typeahead.js
--- public/javascripts/common/hive.ui.Typeahead.js
+++ public/javascripts/common/hive.ui.Typeahead.js
@@ -60,6 +60,7 @@
             		"sURL"		: htVar.sActionURL,
             		"htOptForm"	: {"method":"get"},
             		"htData"	: {"query": sQuery},
+                    "sDataType" : "json",
             		"fOnLoad"	: function(oData, oStatus, oXHR){
             			var sContentRange = oXHR.getResponseHeader('Content-Range');
             			
@@ -78,7 +79,7 @@
          * Return whether the given content range is an entire range for items.
          * e.g) "items 10/10"
          *
-         * @param {String} sContentRange the vaule of Content-Range header from response
+         * @param {String} sContentRange the value of Content-Range header from response
          * @return {Boolean}
          */
          function _isEntireRange(sContentRange){
@@ -89,4 +90,4 @@
 		_init(sQuery, htOptions || {});
 	};
 	
-})("hive.ui.Typeahead");
(파일 끝에 줄바꿈 문자 없음)
+})("hive.ui.Typeahead");
public/javascripts/lib/bootstrap.js
--- public/javascripts/lib/bootstrap.js
+++ public/javascripts/lib/bootstrap.js
@@ -1,5 +1,5 @@
 /* ===================================================
- * bootstrap-transition.js v2.2.2
+ * bootstrap-transition.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#transitions
  * ===================================================
  * Copyright 2012 Twitter, Inc.
@@ -58,7 +58,7 @@
   })
 
 }(window.jQuery);/* ==========================================================
- * bootstrap-alert.js v2.2.2
+ * bootstrap-alert.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#alerts
  * ==========================================================
  * Copyright 2012 Twitter, Inc.
@@ -156,7 +156,7 @@
   $(document).on('click.alert.data-api', dismiss, Alert.prototype.close)
 
 }(window.jQuery);/* ============================================================
- * bootstrap-button.js v2.2.2
+ * bootstrap-button.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#buttons
  * ============================================================
  * Copyright 2012 Twitter, Inc.
@@ -260,7 +260,7 @@
   })
 
 }(window.jQuery);/* ==========================================================
- * bootstrap-carousel.js v2.2.2
+ * bootstrap-carousel.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#carousel
  * ==========================================================
  * Copyright 2012 Twitter, Inc.
@@ -289,6 +289,7 @@
 
   var Carousel = function (element, options) {
     this.$element = $(element)
+    this.$indicators = this.$element.find('.carousel-indicators')
     this.options = options
     this.options.pause == 'hover' && this.$element
       .on('mouseenter', $.proxy(this.pause, this))
@@ -299,19 +300,24 @@
 
     cycle: function (e) {
       if (!e) this.paused = false
+      if (this.interval) clearInterval(this.interval);
       this.options.interval
         && !this.paused
         && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
       return this
     }
 
+  , getActiveIndex: function () {
+      this.$active = this.$element.find('.item.active')
+      this.$items = this.$active.parent().children()
+      return this.$items.index(this.$active)
+    }
+
   , to: function (pos) {
-      var $active = this.$element.find('.item.active')
-        , children = $active.parent().children()
-        , activePos = children.index($active)
+      var activeIndex = this.getActiveIndex()
         , that = this
 
-      if (pos > (children.length - 1) || pos < 0) return
+      if (pos > (this.$items.length - 1) || pos < 0) return
 
       if (this.sliding) {
         return this.$element.one('slid', function () {
@@ -319,18 +325,18 @@
         })
       }
 
-      if (activePos == pos) {
+      if (activeIndex == pos) {
         return this.pause().cycle()
       }
 
-      return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos]))
+      return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
     }
 
   , pause: function (e) {
       if (!e) this.paused = true
       if (this.$element.find('.next, .prev').length && $.support.transition.end) {
         this.$element.trigger($.support.transition.end)
-        this.cycle()
+        this.cycle(true)
       }
       clearInterval(this.interval)
       this.interval = null
@@ -364,9 +370,18 @@
 
       e = $.Event('slide', {
         relatedTarget: $next[0]
+      , direction: direction
       })
 
       if ($next.hasClass('active')) return
+
+      if (this.$indicators.length) {
+        this.$indicators.find('.active').removeClass('active')
+        this.$element.one('slid', function () {
+          var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
+          $nextIndicator && $nextIndicator.addClass('active')
+        })
+      }
 
       if ($.support.transition && this.$element.hasClass('slide')) {
         this.$element.trigger(e)
@@ -412,7 +427,7 @@
       if (!data) $this.data('carousel', (data = new Carousel(this, options)))
       if (typeof option == 'number') data.to(option)
       else if (action) data[action]()
-      else if (options.interval) data.cycle()
+      else if (options.interval) data.pause().cycle()
     })
   }
 
@@ -435,16 +450,23 @@
  /* CAROUSEL DATA-API
   * ================= */
 
-  $(document).on('click.carousel.data-api', '[data-slide]', function (e) {
+  $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
     var $this = $(this), href
       , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
       , options = $.extend({}, $target.data(), $this.data())
+      , slideIndex
+
     $target.carousel(options)
+
+    if (slideIndex = $this.attr('data-slide-to')) {
+      $target.data('carousel').pause().to(slideIndex).cycle()
+    }
+
     e.preventDefault()
   })
 
 }(window.jQuery);/* =============================================================
- * bootstrap-collapse.js v2.2.2
+ * bootstrap-collapse.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#collapse
  * =============================================================
  * Copyright 2012 Twitter, Inc.
@@ -497,7 +519,7 @@
         , actives
         , hasData
 
-      if (this.transitioning) return
+      if (this.transitioning || this.$element.hasClass('in')) return
 
       dimension = this.dimension()
       scroll = $.camelCase(['scroll', dimension].join('-'))
@@ -517,7 +539,7 @@
 
   , hide: function () {
       var dimension
-      if (this.transitioning) return
+      if (this.transitioning || !this.$element.hasClass('in')) return
       dimension = this.dimension()
       this.reset(this.$element[dimension]())
       this.transition('removeClass', $.Event('hide'), 'hidden')
@@ -574,7 +596,7 @@
     return this.each(function () {
       var $this = $(this)
         , data = $this.data('collapse')
-        , options = typeof option == 'object' && option
+        , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option)
       if (!data) $this.data('collapse', (data = new Collapse(this, options)))
       if (typeof option == 'string') data[option]()
     })
@@ -610,7 +632,7 @@
   })
 
 }(window.jQuery);/* ============================================================
- * bootstrap-dropdown.js v2.2.2
+ * bootstrap-dropdown.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#dropdowns
  * ============================================================
  * Copyright 2012 Twitter, Inc.
@@ -692,7 +714,10 @@
 
       isActive = $parent.hasClass('open')
 
-      if (!isActive || (isActive && e.keyCode == 27)) return $this.click()
+      if (!isActive || (isActive && e.keyCode == 27)) {
+        if (e.which == 27) $parent.find(toggle).focus()
+        return $this.click()
+      }
 
       $items = $('[role=menu] li:not(.divider):visible a', $parent)
 
@@ -726,8 +751,9 @@
       selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
     }
 
-    $parent = $(selector)
-    $parent.length || ($parent = $this.parent())
+    $parent = selector && $(selector)
+
+    if (!$parent || !$parent.length) $parent = $this.parent()
 
     return $parent
   }
@@ -763,14 +789,15 @@
    * =================================== */
 
   $(document)
-    .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
-    .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
-    .on('touchstart.dropdown.data-api', '.dropdown-menu', function (e) { e.stopPropagation() })
-    .on('click.dropdown.data-api touchstart.dropdown.data-api'  , toggle, Dropdown.prototype.toggle)
-    .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
+    .on('click.dropdown.data-api', clearMenus)
+    .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
+    .on('click.dropdown-menu', function (e) { e.stopPropagation() })
+    .on('click.dropdown.data-api'  , toggle, Dropdown.prototype.toggle)
+    .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
 
-}(window.jQuery);/* =========================================================
- * bootstrap-modal.js v2.2.2
+}(window.jQuery);
+/* =========================================================
+ * bootstrap-modal.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#modals
  * =========================================================
  * Copyright 2012 Twitter, Inc.
@@ -831,8 +858,7 @@
             that.$element.appendTo(document.body) //don't move modals dom position
           }
 
-          that.$element
-            .show()
+          that.$element.show()
 
           if (transition) {
             that.$element[0].offsetWidth // force reflow
@@ -910,16 +936,17 @@
         })
       }
 
-    , hideModal: function (that) {
-        this.$element
-          .hide()
-          .trigger('hidden')
-
-        this.backdrop()
+    , hideModal: function () {
+        var that = this
+        this.$element.hide()
+        this.backdrop(function () {
+          that.removeBackdrop()
+          that.$element.trigger('hidden')
+        })
       }
 
     , removeBackdrop: function () {
-        this.$backdrop.remove()
+        this.$backdrop && this.$backdrop.remove()
         this.$backdrop = null
       }
 
@@ -943,6 +970,8 @@
 
           this.$backdrop.addClass('in')
 
+          if (!callback) return
+
           doAnimate ?
             this.$backdrop.one($.support.transition.end, callback) :
             callback()
@@ -951,8 +980,8 @@
           this.$backdrop.removeClass('in')
 
           $.support.transition && this.$element.hasClass('fade')?
-            this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) :
-            this.removeBackdrop()
+            this.$backdrop.one($.support.transition.end, callback) :
+            callback()
 
         } else if (callback) {
           callback()
@@ -1015,7 +1044,7 @@
 
 }(window.jQuery);
 /* ===========================================================
- * bootstrap-tooltip.js v2.2.2
+ * bootstrap-tooltip.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#tooltips
  * Inspired by the original jQuery.tipsy by Jason Frame
  * ===========================================================
@@ -1054,19 +1083,27 @@
   , init: function (type, element, options) {
       var eventIn
         , eventOut
+        , triggers
+        , trigger
+        , i
 
       this.type = type
       this.$element = $(element)
       this.options = this.getOptions(options)
       this.enabled = true
 
-      if (this.options.trigger == 'click') {
-        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
-      } else if (this.options.trigger != 'manual') {
-        eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
-        eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
-        this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
-        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+      triggers = this.options.trigger.split(' ')
+
+      for (i = triggers.length; i--;) {
+        trigger = triggers[i]
+        if (trigger == 'click') {
+          this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+        } else if (trigger != 'manual') {
+          eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
+          eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
+          this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+          this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+        }
       }
 
       this.options.selector ?
@@ -1075,7 +1112,7 @@
     }
 
   , getOptions: function (options) {
-      options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
+      options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options)
 
       if (options.delay && typeof options.delay == 'number') {
         options.delay = {
@@ -1088,7 +1125,15 @@
     }
 
   , enter: function (e) {
-      var self = $(e.currentTarget)[this.type](this._options).data(this.type)
+      var defaults = $.fn[this.type].defaults
+        , options = {}
+        , self
+
+      this._options && $.each(this._options, function (key, value) {
+        if (defaults[key] != value) options[key] = value
+      }, this)
+
+      self = $(e.currentTarget)[this.type](options).data(this.type)
 
       if (!self.options.delay || !self.options.delay.show) return self.show()
 
@@ -1113,14 +1158,16 @@
 
   , show: function () {
       var $tip
-        , inside
         , pos
         , actualWidth
         , actualHeight
         , placement
         , tp
+        , e = $.Event('show')
 
       if (this.hasContent() && this.enabled) {
+        this.$element.trigger(e)
+        if (e.isDefaultPrevented()) return
         $tip = this.tip()
         this.setContent()
 
@@ -1132,19 +1179,18 @@
           this.options.placement.call(this, $tip[0], this.$element[0]) :
           this.options.placement
 
-        inside = /in/.test(placement)
-
         $tip
           .detach()
           .css({ top: 0, left: 0, display: 'block' })
-          .insertAfter(this.$element)
 
-        pos = this.getPosition(inside)
+        this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+
+        pos = this.getPosition()
 
         actualWidth = $tip[0].offsetWidth
         actualHeight = $tip[0].offsetHeight
 
-        switch (inside ? placement.split(' ')[1] : placement) {
+        switch (placement) {
           case 'bottom':
             tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
             break
@@ -1159,11 +1205,56 @@
             break
         }
 
-        $tip
-          .offset(tp)
-          .addClass(placement)
-          .addClass('in')
+        this.applyPlacement(tp, placement)
+        this.$element.trigger('shown')
       }
+    }
+
+  , applyPlacement: function(offset, placement){
+      var $tip = this.tip()
+        , width = $tip[0].offsetWidth
+        , height = $tip[0].offsetHeight
+        , actualWidth
+        , actualHeight
+        , delta
+        , replace
+
+      $tip
+        .offset(offset)
+        .addClass(placement)
+        .addClass('in')
+
+      actualWidth = $tip[0].offsetWidth
+      actualHeight = $tip[0].offsetHeight
+
+      if (placement == 'top' && actualHeight != height) {
+        offset.top = offset.top + height - actualHeight
+        replace = true
+      }
+
+      if (placement == 'bottom' || placement == 'top') {
+        delta = 0
+
+        if (offset.left < 0){
+          delta = offset.left * -2
+          offset.left = 0
+          $tip.offset(offset)
+          actualWidth = $tip[0].offsetWidth
+          actualHeight = $tip[0].offsetHeight
+        }
+
+        this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
+      } else {
+        this.replaceArrow(actualHeight - height, actualHeight, 'top')
+      }
+
+      if (replace) $tip.offset(offset)
+    }
+
+  , replaceArrow: function(delta, dimension, position){
+      this
+        .arrow()
+        .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
     }
 
   , setContent: function () {
@@ -1177,6 +1268,10 @@
   , hide: function () {
       var that = this
         , $tip = this.tip()
+        , e = $.Event('hide')
+
+      this.$element.trigger(e)
+      if (e.isDefaultPrevented()) return
 
       $tip.removeClass('in')
 
@@ -1195,13 +1290,15 @@
         removeWithAnimation() :
         $tip.detach()
 
+      this.$element.trigger('hidden')
+
       return this
     }
 
   , fixTitle: function () {
       var $e = this.$element
       if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
-        $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
+        $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
       }
     }
 
@@ -1209,11 +1306,12 @@
       return this.getTitle()
     }
 
-  , getPosition: function (inside) {
-      return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
-        width: this.$element[0].offsetWidth
-      , height: this.$element[0].offsetHeight
-      })
+  , getPosition: function () {
+      var el = this.$element[0]
+      return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+        width: el.offsetWidth
+      , height: el.offsetHeight
+      }, this.$element.offset())
     }
 
   , getTitle: function () {
@@ -1229,6 +1327,10 @@
 
   , tip: function () {
       return this.$tip = this.$tip || $(this.options.template)
+    }
+
+  , arrow: function(){
+      return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow")
     }
 
   , validate: function () {
@@ -1252,8 +1354,8 @@
     }
 
   , toggle: function (e) {
-      var self = $(e.currentTarget)[this.type](this._options).data(this.type)
-      self[self.tip().hasClass('in') ? 'hide' : 'show']()
+      var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this
+      self.tip().hasClass('in') ? self.hide() : self.show()
     }
 
   , destroy: function () {
@@ -1285,10 +1387,11 @@
   , placement: 'top'
   , selector: false
   , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
-  , trigger: 'hover'
+  , trigger: 'hover focus'
   , title: ''
   , delay: 0
   , html: false
+  , container: false
   }
 
 
@@ -1300,8 +1403,9 @@
     return this
   }
 
-}(window.jQuery);/* ===========================================================
- * bootstrap-popover.js v2.2.2
+}(window.jQuery);
+/* ===========================================================
+ * bootstrap-popover.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#popovers
  * ===========================================================
  * Copyright 2012 Twitter, Inc.
@@ -1360,8 +1464,8 @@
         , $e = this.$element
         , o = this.options
 
-      content = $e.attr('data-content')
-        || (typeof o.content == 'function' ? o.content.call($e[0]) :  o.content)
+      content = (typeof o.content == 'function' ? o.content.call($e[0]) :  o.content)
+        || $e.attr('data-content')
 
       return content
     }
@@ -1401,7 +1505,7 @@
     placement: 'right'
   , trigger: 'click'
   , content: ''
-  , template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"></div></div></div>'
+  , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
   })
 
 
@@ -1413,8 +1517,9 @@
     return this
   }
 
-}(window.jQuery);/* =============================================================
- * bootstrap-scrollspy.js v2.2.2
+}(window.jQuery);
+/* =============================================================
+ * bootstrap-scrollspy.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#scrollspy
  * =============================================================
  * Copyright 2012 Twitter, Inc.
@@ -1474,7 +1579,7 @@
               , $href = /^#\w/.test(href) && $(href)
             return ( $href
               && $href.length
-              && [[ $href.position().top + self.$scrollElement.scrollTop(), href ]] ) || null
+              && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null
           })
           .sort(function (a, b) { return a[0] - b[0] })
           .each(function () {
@@ -1575,7 +1680,7 @@
   })
 
 }(window.jQuery);/* ========================================================
- * bootstrap-tab.js v2.2.2
+ * bootstrap-tab.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#tabs
  * ========================================================
  * Copyright 2012 Twitter, Inc.
@@ -1718,7 +1823,7 @@
   })
 
 }(window.jQuery);/* =============================================================
- * bootstrap-typeahead.js v2.2.2
+ * bootstrap-typeahead.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#typeahead
  * =============================================================
  * Copyright 2012 Twitter, Inc.
@@ -1891,6 +1996,7 @@
 
   , listen: function () {
       this.$element
+        .on('focus',    $.proxy(this.focus, this))
         .on('blur',     $.proxy(this.blur, this))
         .on('keypress', $.proxy(this.keypress, this))
         .on('keyup',    $.proxy(this.keyup, this))
@@ -1902,6 +2008,7 @@
       this.$menu
         .on('click', $.proxy(this.click, this))
         .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
+        .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
     }
 
   , eventSupported: function(eventName) {
@@ -1975,20 +2082,31 @@
       e.preventDefault()
   }
 
+  , focus: function (e) {
+      this.focused = true
+    }
+
   , blur: function (e) {
-      var that = this
-      setTimeout(function () { that.hide() }, 150)
+      this.focused = false
+      if (!this.mousedover && this.shown) this.hide()
     }
 
   , click: function (e) {
       e.stopPropagation()
       e.preventDefault()
       this.select()
+      this.$element.focus()
     }
 
   , mouseenter: function (e) {
+      this.mousedover = true
       this.$menu.find('.active').removeClass('active')
       $(e.currentTarget).addClass('active')
+    }
+
+  , mouseleave: function (e) {
+      this.mousedover = false
+      if (!this.focused && this.shown) this.hide()
     }
 
   }
@@ -2035,13 +2153,12 @@
   $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
     var $this = $(this)
     if ($this.data('typeahead')) return
-    e.preventDefault()
     $this.typeahead($this.data())
   })
 
 }(window.jQuery);
 /* ==========================================================
- * bootstrap-affix.js v2.2.2
+ * bootstrap-affix.js v2.3.1
  * http://twitter.github.com/bootstrap/javascript.html#affix
  * ==========================================================
  * Copyright 2012 Twitter, Inc.
 
public/javascripts/lib/bootstrap.min.js (deleted)
--- public/javascripts/lib/bootstrap.min.js
@@ -1,6 +0,0 @@
-/*!
-* Bootstrap.js by @fat & @mdo
-* Copyright 2012 Twitter, Inc.
-* http://www.apache.org/licenses/LICENSE-2.0.txt
-*/
-!function($){"use strict";$(function(){$.support.transition=function(){var transitionEnd=function(){var name,el=document.createElement("bootstrap"),transEndEventNames={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(name in transEndEventNames)if(void 0!==el.style[name])return transEndEventNames[name]}();return transitionEnd&&{end:transitionEnd}}()})}(window.jQuery),!function($){"use strict";var dismiss='[data-dismiss="alert"]',Alert=function(el){$(el).on("click",dismiss,this.close)};Alert.prototype.close=function(e){function removeElement(){$parent.trigger("closed").remove()}var $parent,$this=$(this),selector=$this.attr("data-target");selector||(selector=$this.attr("href"),selector=selector&&selector.replace(/.*(?=#[^\s]*$)/,"")),$parent=$(selector),e&&e.preventDefault(),$parent.length||($parent=$this.hasClass("alert")?$this:$this.parent()),$parent.trigger(e=$.Event("close")),e.isDefaultPrevented()||($parent.removeClass("in"),$.support.transition&&$parent.hasClass("fade")?$parent.on($.support.transition.end,removeElement):removeElement())};var old=$.fn.alert;$.fn.alert=function(option){return this.each(function(){var $this=$(this),data=$this.data("alert");data||$this.data("alert",data=new Alert(this)),"string"==typeof option&&data[option].call($this)})},$.fn.alert.Constructor=Alert,$.fn.alert.noConflict=function(){return $.fn.alert=old,this},$(document).on("click.alert.data-api",dismiss,Alert.prototype.close)}(window.jQuery),!function($){"use strict";var Button=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.button.defaults,options)};Button.prototype.setState=function(state){var d="disabled",$el=this.$element,data=$el.data(),val=$el.is("input")?"val":"html";state+="Text",data.resetText||$el.data("resetText",$el[val]()),$el[val](data[state]||this.options[state]),setTimeout(function(){"loadingText"==state?$el.addClass(d).attr(d,d):$el.removeClass(d).removeAttr(d)},0)},Button.prototype.toggle=function(){var $parent=this.$element.closest('[data-toggle="buttons-radio"]');$parent&&$parent.find(".active").removeClass("active"),this.$element.toggleClass("active")};var old=$.fn.button;$.fn.button=function(option){return this.each(function(){var $this=$(this),data=$this.data("button"),options="object"==typeof option&&option;data||$this.data("button",data=new Button(this,options)),"toggle"==option?data.toggle():option&&data.setState(option)})},$.fn.button.defaults={loadingText:"loading..."},$.fn.button.Constructor=Button,$.fn.button.noConflict=function(){return $.fn.button=old,this},$(document).on("click.button.data-api","[data-toggle^=button]",function(e){var $btn=$(e.target);$btn.hasClass("btn")||($btn=$btn.closest(".btn")),$btn.button("toggle")})}(window.jQuery),!function($){"use strict";var Carousel=function(element,options){this.$element=$(element),this.options=options,"hover"==this.options.pause&&this.$element.on("mouseenter",$.proxy(this.pause,this)).on("mouseleave",$.proxy(this.cycle,this))};Carousel.prototype={cycle:function(e){return e||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval($.proxy(this.next,this),this.options.interval)),this},to:function(pos){var $active=this.$element.find(".item.active"),children=$active.parent().children(),activePos=children.index($active),that=this;if(!(pos>children.length-1||0>pos))return this.sliding?this.$element.one("slid",function(){that.to(pos)}):activePos==pos?this.pause().cycle():this.slide(pos>activePos?"next":"prev",$(children[pos]))},pause:function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&$.support.transition.end&&(this.$element.trigger($.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){return this.sliding?void 0:this.slide("next")},prev:function(){return this.sliding?void 0:this.slide("prev")},slide:function(type,next){var e,$active=this.$element.find(".item.active"),$next=next||$active[type](),isCycling=this.interval,direction="next"==type?"left":"right",fallback="next"==type?"first":"last",that=this;if(this.sliding=!0,isCycling&&this.pause(),$next=$next.length?$next:this.$element.find(".item")[fallback](),e=$.Event("slide",{relatedTarget:$next[0]}),!$next.hasClass("active")){if($.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(e),e.isDefaultPrevented())return;$next.addClass(type),$next[0].offsetWidth,$active.addClass(direction),$next.addClass(direction),this.$element.one($.support.transition.end,function(){$next.removeClass([type,direction].join(" ")).addClass("active"),$active.removeClass(["active",direction].join(" ")),that.sliding=!1,setTimeout(function(){that.$element.trigger("slid")},0)})}else{if(this.$element.trigger(e),e.isDefaultPrevented())return;$active.removeClass("active"),$next.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return isCycling&&this.cycle(),this}}};var old=$.fn.carousel;$.fn.carousel=function(option){return this.each(function(){var $this=$(this),data=$this.data("carousel"),options=$.extend({},$.fn.carousel.defaults,"object"==typeof option&&option),action="string"==typeof option?option:options.slide;data||$this.data("carousel",data=new Carousel(this,options)),"number"==typeof option?data.to(option):action?data[action]():options.interval&&data.cycle()})},$.fn.carousel.defaults={interval:5e3,pause:"hover"},$.fn.carousel.Constructor=Carousel,$.fn.carousel.noConflict=function(){return $.fn.carousel=old,this},$(document).on("click.carousel.data-api","[data-slide]",function(e){var href,$this=$(this),$target=$($this.attr("data-target")||(href=$this.attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,"")),options=$.extend({},$target.data(),$this.data());$target.carousel(options),e.preventDefault()})}(window.jQuery),!function($){"use strict";var Collapse=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.collapse.defaults,options),this.options.parent&&(this.$parent=$(this.options.parent)),this.options.toggle&&this.toggle()};Collapse.prototype={constructor:Collapse,dimension:function(){var hasWidth=this.$element.hasClass("width");return hasWidth?"width":"height"},show:function(){var dimension,scroll,actives,hasData;if(!this.transitioning){if(dimension=this.dimension(),scroll=$.camelCase(["scroll",dimension].join("-")),actives=this.$parent&&this.$parent.find("> .accordion-group > .in"),actives&&actives.length){if(hasData=actives.data("collapse"),hasData&&hasData.transitioning)return;actives.collapse("hide"),hasData||actives.data("collapse",null)}this.$element[dimension](0),this.transition("addClass",$.Event("show"),"shown"),$.support.transition&&this.$element[dimension](this.$element[0][scroll])}},hide:function(){var dimension;this.transitioning||(dimension=this.dimension(),this.reset(this.$element[dimension]()),this.transition("removeClass",$.Event("hide"),"hidden"),this.$element[dimension](0))},reset:function(size){var dimension=this.dimension();return this.$element.removeClass("collapse")[dimension](size||"auto")[0].offsetWidth,this.$element[null!==size?"addClass":"removeClass"]("collapse"),this},transition:function(method,startEvent,completeEvent){var that=this,complete=function(){"show"==startEvent.type&&that.reset(),that.transitioning=0,that.$element.trigger(completeEvent)};this.$element.trigger(startEvent),startEvent.isDefaultPrevented()||(this.transitioning=1,this.$element[method]("in"),$.support.transition&&this.$element.hasClass("collapse")?this.$element.one($.support.transition.end,complete):complete())},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var old=$.fn.collapse;$.fn.collapse=function(option){return this.each(function(){var $this=$(this),data=$this.data("collapse"),options="object"==typeof option&&option;data||$this.data("collapse",data=new Collapse(this,options)),"string"==typeof option&&data[