Yi EungJun 2013-06-10
Label: Improve Project Label (Tag)
* Rename Tag to Label.
* Improve UI to edit Project Label.
    * Add scrollbar to view window and make edit window
      auto-resizable(#156).
    * Allow users to input category and name each separately(#157).
    * Show License and Language even if they have no label(#155).
    * Make the Edit button togglable.
    * Add appropriate auto-focus on the input box.
@4d68065bade1ba4f00324f7ccf7574e7c8b68f06
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -1873,6 +1873,8 @@
         &.project-info {
             width: 390px; /*350px;*/
             .infos {
+                overflow-y: auto;
+                height: 130px;
                 list-style: none;
                 margin: 0;
                 padding: 10px;
@@ -1887,9 +1889,11 @@
                         padding-bottom: 0;
                         border-bottom: 0 none;
                     }
-                    span:not(.label) {
-                        &:not(:last-child):after {
-                            content: ", ";
+                    .label-list {
+                        span:not(.label) {
+                            &:not(:last-child):after {
+                                content: ", ";
+                            }
                         }
                     }
                 }
 
app/controllers/LabelApp.java (added)
+++ app/controllers/LabelApp.java
@@ -0,0 +1,112 @@
+package controllers;
+
+import com.avaje.ebean.Ebean;
+import com.avaje.ebean.ExpressionList;
+import com.avaje.ebean.SqlQuery;
+import com.avaje.ebean.SqlRow;
+import models.Label;
+import play.mvc.Controller;
+import play.mvc.Http;
+import play.mvc.Result;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.avaje.ebean.Expr.*;
+import static play.libs.Json.toJson;
+
+public class LabelApp extends Controller {
+    private static final int MAX_FETCH_LABELS = 1000;
+
+    /**
+     * 태그 목록 요청에 대해 응답한다.
+     *
+     * when: 프로젝트 Overview 페이지에서 사용자가 태그를 추가하려고 할 때, 이름 자동완성을 위해 사용한다.
+     *
+     * 주어진 {@code query}로 태그를 검색하여, 그 목록을 json 형식으로 돌려준다.
+     *
+     * 돌려줄 태그 목록의 갯수가, {@link LabelApp#MAX_FETCH_LABELS}와 주어진 {@code limit}중에서 가장 작은 값보다
+     * 크다면, 그 값에 의해 제한된다. 만약 제한이 되었다면, 응답의 @{code Content-Range}헤더로 어떻게 제한이
+     * 되었는지에 대한 정보를 클라이언트에게 전달하게 된다. 예를 들어 10개 중에 8개만을 보내게 되었다면
+     * @{code Content-Range: 8/10}이 된다. 자세한 것은 label-typeahead.md 문서의 "Content-Range 헤더" 문단을
+     * 참조하라.
+     *
+     * 다음의 경우에는 {@code 406 Not Acceptable}로 응답한다.
+     * 클라이언트가 {@code application/json}을 받아들일 수 없는 경우. 태그 목록 요청에 대한 성공적인 응답에서,
+     * 엔터티 본문의 미디어 타입은 언제나 {@code application/json}이기 때문이다.
+     *
+     * @param query 태그에 대한 검색어 질의
+     * @param limit 가져올 태그의 최대 갯수
+     * @return 태그 목록 요청에 대한 응답
+     * @see <a href="https://github.com/nforge/hive/blob/master/docs/technical/label-typeahead
+     * .md>label-typeahead.md</a>
+     */
+    public static Result labels(String query, String category, Integer limit) {
+        if (!request().accepts("application/json")) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        ExpressionList<Label> el =
+                Label.find.where().and(icontains("category", category), icontains("name", query));
+
+        int total = el.findRowCount();
+
+        if (total > limit) {
+            el.setMaxRows(limit);
+            response().setHeader("Content-Range", "items " + limit + "/" + total);
+        }
+
+        ArrayList<String> labels = new ArrayList<String>();
+
+        for (Label label: el.findList()) {
+            labels.add(label.name);
+        }
+
+        return ok(toJson(labels));
+    }
+
+    public static Result categories(String query, Integer limit) {
+        if (!request().accepts("application/json")) {
+            return status(Http.Status.NOT_ACCEPTABLE);
+        }
+
+        SqlQuery sqlQuery;
+        SqlQuery sqlCountQuery;
+
+        if (query != null && query.length() > 0) {
+            String sqlString =
+                    "SELECT DISTINCT category FROM label WHERE lower(category) LIKE :category";
+            sqlQuery = Ebean
+                    .createSqlQuery(sqlString)
+                    .setParameter("category", "%" + query.toLowerCase() + "%");
+            sqlCountQuery = Ebean
+                    .createSqlQuery("SELECT COUNT(*) AS cnt FROM (" + sqlString + ")")
+                    .setParameter("category", "%" + query.toLowerCase() + "%");
+        } else {
+            String sqlString =
+                    "SELECT DISTINCT category FROM label";
+            sqlQuery = Ebean
+                    .createSqlQuery(sqlString);
+            sqlCountQuery = Ebean
+                    .createSqlQuery("SELECT COUNT(*) AS cnt FROM (" + sqlString + ")");
+        }
+
+        int cnt = sqlCountQuery.findUnique().getInteger("cnt");
+
+        if (limit > MAX_FETCH_LABELS) {
+            limit = MAX_FETCH_LABELS;
+        }
+
+        if (cnt > limit) {
+            sqlQuery.setMaxRows(limit);
+            response().setHeader("Content-Range", "items " + limit + "/" + cnt);
+        }
+
+        List<String> categories = new ArrayList<String>();
+        for (SqlRow row: sqlQuery.findList()) {
+            categories.add(row.getString("category"));
+        }
+
+        return ok(toJson(categories));
+    }
+}
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -566,7 +566,7 @@
      * @param projectName the project name
      * @return 프로젝트 태그 JSON 데이터
      */
-    public static Result tags(String owner, String projectName) {
+    public static Result labels(String owner, String projectName) {
         Project project = Project.findByOwnerAndProjectName(owner, projectName);
 
         if (project == null) {
@@ -581,12 +581,15 @@
             return status(Http.Status.NOT_ACCEPTABLE);
         }
 
-        Map<Long, String> tags = new HashMap<Long, String>();
-        for (Tag tag: project.tags) {
-            tags.put(tag.id, tag.toString());
+        Map<Long, Map<String, String>> labels = new HashMap<Long, Map<String, String>>();
+        for (Label label: project.labels) {
+            Map<String, String> tagMap = new HashMap<String, String>();
+            tagMap.put("category", label.category);
+            tagMap.put("name", label.name);
+            labels.put(label.id, tagMap);
         }
 
-        return ok(toJson(tags));
+        return ok(toJson(labels));
     }
 
     /**
@@ -601,14 +604,14 @@
      * @param projectName the project name
      * @return the result
      */
-    public static Result tag(String ownerName, String projectName) {
+    public static Result attachLabel(String ownerName, String projectName) {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
 
         if (project == null) {
             return notFound();
         }
 
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)) {
             return forbidden();
         }
 
@@ -617,43 +620,47 @@
         String category = HttpUtil.getFirstValueFromQuery(data, "category");
         String name = HttpUtil.getFirstValueFromQuery(data, "name");
         if (name == null || name.length() == 0) {
-            // A tag must have its name.
-            return badRequest("Tag name is missing.");
+            // A label must have its name.
+            return badRequest("Label name is missing.");
         }
 
-        Tag tag = Tag.find
+        Label label = Label.find
             .where().eq("category", category).eq("name", name).findUnique();
 
         boolean isCreated = false;
-        if (tag == null) {
-            // Create new tag if there is no tag which has the given name.
-            tag = new Tag(category, name);
-            tag.save();
+        if (label == null) {
+            // Create new label if there is no label which has the given name.
+            label = new Label(category, name);
+            label.save();
             isCreated = true;
         }
 
-        Boolean isAttached = project.tag(tag);
+        Boolean isAttached = project.attachLabel(label);
 
         if (!isCreated && !isAttached) {
             // Something is wrong. This case is not possible.
             play.Logger.warn(
-                    "A tag '" + tag + "' is created but failed to attach to project '"
+                    "A label '" + label + "' is created but failed to attach to project '"
                     + project + "'.");
         }
 
         if (isAttached) {
-            // Return the attached tag. The return type is Map<Long, String>
-            // even if there is only one tag, to unify the return type with
-            // ProjectApp.tags().
-            Map<Long, String> tags = new HashMap<Long, String>();
-            tags.put(tag.id, tag.toString());
+            // Return the attached label. The return type is Map<Long, String>
+            // even if there is only one label, to unify the return type with
+            // ProjectApp.labels().
+            Map<Long, Map<String, String>> labels = new HashMap<Long, Map<String, String>>();
+            Map<String, String> labelMap = new HashMap<String, String>();
+            labelMap.put("category", label.category);
+            labelMap.put("name", label.name);
+            labels.put(label.id, labelMap);
+
             if (isCreated) {
-                return created(toJson(tags));
+                return created(toJson(labels));
             } else {
-                return ok(toJson(tags));
+                return ok(toJson(labels));
             }
         } else {
-            // Return 204 No Content if the tag is already attached.
+            // Return 204 No Content if the label is already attached.
             return status(Http.Status.NO_CONTENT);
         }
     }
@@ -669,14 +676,14 @@
      * @param id the id
      * @return the result
      */
-    public static Result untag(String ownerName, String projectName, Long id) {
+    public static Result detachLabel(String ownerName, String projectName, Long id) {
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
 
         if (project == null) {
             return notFound();
         }
 
-        if (!AccessControl.isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)) {
+        if (!AccessControl.isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)) {
             return forbidden();
         }
 
@@ -687,13 +694,13 @@
             return badRequest("_method must be 'delete'.");
         }
 
-        Tag tag = Tag.find.byId(id);
+        Label tag = Label.find.byId(id);
 
         if (tag == null) {
             return notFound();
         }
 
-        project.untag(tag);
+        project.detachLabel(tag);
 
         return status(Http.Status.NO_CONTENT);
     }
 
app/controllers/TagApp.java (deleted)
--- app/controllers/TagApp.java
@@ -1,211 +0,0 @@
-package controllers;
-
-import com.avaje.ebean.ExpressionList;
-import models.Project;
-import models.Tag;
-import play.mvc.Controller;
-import play.mvc.Http;
-import play.mvc.Result;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.avaje.ebean.Expr.*;
-import static play.libs.Json.toJson;
-
-public class TagApp extends Controller {
-    private static final int MAX_FETCH_TAGS = 1000;
-
-    public enum Context {
-        PROJECT_TAGGING_TYPEAHEAD, DEFAULT
-    }
-
-    /**
-     * 태그 목록 요청에 대해 응답한다.
-     *
-     * when: 프로젝트 Overview 페이지에서 사용자가 태그를 추가하려고 할 때, 이름 자동완성을 위해 사용한다.
-     *
-     * 주어진 {@code query}로 태그를 검색하여, 그 목록을 json 형식으로 돌려준다. 이 때 {@code contextAsString}로
-     * 주어진 컨텍스트를 고려한다. 예를 들어 {@code "PROJECT_TAGGING_TYPEAHEAD"}가 주어진 경우에는, 프로젝트에
-     * 태그를 붙이려고 하는 상황에서 자동완성을 위해 태그의 목록을 요청한 경우이다. 이 때 만약 그 프로젝트에 라이선스
-     * 태그가 하나도 붙어있지 않다면, 사용자가 라이선스를 붙이는 것을 유도하기 위해 목록에서 라이선스가 앞에 나오도록
-     * 순서를 조절한다.
-     *
-     * 돌려줄 태그 목록의 갯수가, {@link TagApp#MAX_FETCH_TAGS}와 주어진 {@code limit}중에서 가장 작은 값보다
-     * 크다면, 그 값에 의해 제한된다. 만약 제한이 되었다면, 응답의 @{code Content-Range}헤더로 어떻게 제한이
-     * 되었는지에 대한 정보를 클라이언트에게 전달하게 된다. 예를 들어 10개 중에 8개만을 보내게 되었다면
-     * @{code Content-Range: 8/10}이 된다. 자세한 것은 tag-typeahead.md 문서의 "Content-Range 헤더" 문단을
-     * 참조하라.
-     *
-     * 다음중 하나의 경우에는 {@code 400 Bad Request}로 응답한다.
-     * 1. 요청에 서버가 이해할 수 없는 context 값이 들어있다.
-     * 2. 요청에 {@code context}가 요구하는 파라메터의 값이 없거나 잘못되어있다. 예를 들어,
-     * {@link Context#PROJECT_TAGGING_TYPEAHEAD}는 정수로 된 프로젝트 아이디를 요구한다.
-     *
-     * 다음의 경우에는 {@code 406 Not Acceptable}로 응답한다.
-     * 클라이언트가 {@code application/json}을 받아들일 수 없는 경우. 태그 목록 요청에 대한 성공적인 응답에서,
-     * 엔터티 본문의 미디어 타입은 언제나 {@code application/json}이기 때문이다.
-     *
-     * @param query 태그에 대한 검색어 질의
-     * @param contextAsString 어떤 상황에서 요청한 것인지에 대한 컨텍스트 정보
-     * @param limit 가져올 태그의 최대 갯수
-     * @return 태그 목록 요청에 대한 응답
-     * @see <a href="https://github.com/nforge/hive/blob/master/docs/technical/tag-typeahead.md>tag-typeahead.md</a>
-     */
-    public static Result tags(String query, String contextAsString, Integer limit) {
-        if (!request().accepts("application/json")) {
-            return status(Http.Status.NOT_ACCEPTABLE);
-        }
-
-        List<String> tags;
-
-        Context context = null;
-
-        if (contextAsString == null || contextAsString.isEmpty()) {
-            context = Context.DEFAULT;
-        } else {
-            try {
-                context = Context.valueOf(contextAsString);
-            } catch (IllegalArgumentException e) {
-                return badRequest("Invalid context '" + contextAsString + "'");
-            }
-        }
-
-        if (limit == null || limit > MAX_FETCH_TAGS) {
-            limit = MAX_FETCH_TAGS;
-        }
-
-        switch(context) {
-            case PROJECT_TAGGING_TYPEAHEAD:
-                try {
-                    Long projectId  = Long.valueOf(request().getQueryString("project_id"));
-                    Project project = Project.find.byId(projectId);
-                    if (project == null) {
-                        return badRequest("No project matches given project_id '" + projectId + "'");
-                    }
-                    tags = tagsForProjectTagging(query, limit, project);
-                } catch (IllegalArgumentException e) {
-                    return badRequest("In " + Context.PROJECT_TAGGING_TYPEAHEAD + " context, " +
-                            "the query string should have a project_id field in integer type.");
-                }
-                break;
-            default:
-                tags = tagsForDefault(query, limit);
-                break;
-        }
-
-        return ok(toJson(tags));
-    }
-
-    /**
-     * 태그 목록을 반환한다.
-     *
-     * @param el 태그의 목록에 대한 질의
-     * @return 태그 목록
-     */
-    private static List<String> tags(ExpressionList<Tag> el) {
-        ArrayList<String> tags = new ArrayList<String>();
-
-        for (Tag tag: el.findList()) {
-            tags.add(tag.toString());
-        }
-
-        return tags;
-    }
-
-    /**
-     * {@link Context#DEFAULT} 컨텍스트에서의 태그의 목록을 반환하며, Content-Range 헤더를 알맞게 설정한다.
-     *
-     * when: 컨텍스트 없이 태그의 목록을 가져오는 경우 사용된다.
-     *
-     * {@link Tag#category}혹은 {@link Tag#name}가 문자열 {@code query}을 포함하고 있는 모든 {@link Tag}의
-     * 목록을 반환한다. 단 그 갯수가 {@code limit}보다 큰 경우 그 값에 의해 제한되며, 제한이 된 경우에는 응답의
-     * @{code Content-Range} 헤더에 그 사실이 기술된다. 예를 들어 10개 중에 8개만을 보내게 되었다면
-     * @{code Content-Range: 8/10}이 된다. 자세한 것은 tag-typeahead.md 문서의 "Content-Range 헤더" 문단을
-     * 참조하라.
-     *
-     * @param query 태그에 대한 검색어 질의
-     * @param limit 목록의 최대 갯수
-     * @return 태그 목록
-     */
-    private static List<String> tagsForDefault(String query, int limit) {
-        ExpressionList<Tag> el =
-                Tag.find.where().or(icontains("category", query), icontains("name", query));
-
-        int total = el.findRowCount();
-
-        if (total > limit) {
-            el.setMaxRows(limit);
-            response().setHeader("Content-Range", "items " + limit + "/" + total);
-        }
-
-        return tags(el);
-    }
-
-    /**
-     * {@link Context#PROJECT_TAGGING_TYPEAHEAD} 컨텍스트에서의 태그의 목록을 반환하며, Content-Range 헤더를
-     * 알맞게 설정한다.
-     *
-     * when: 프로젝트 Overview 페이지에서 사용자가 태그를 추가하려고 할 때, {@link TagApp#tags}에 의해 호출된다.
-     *
-     * 1. 돌려줄 태그의 갯수가 많지 않아 {@code limit}으로 제한할 필요가 없거나, 그 프로젝트에 라이선스 태그가 하나
-     * 이상 붙어있다면  {@link TagApp#tagsForDefault(String, int)}를 호출하여 컨텍스트가 주어지지 않았을 때와
-     * 같은 목록을 반환한다.
-     * 2. 만약 제한을 해야 하는 상황이고 그 프로젝트에 라이선스 태그가 하나도 붙어있지 않다면 라이선스 태그들을 우선한다.
-     * 사용자가 프로젝트에 라이선스 태그를 붙이는 것을 유도하기 위함이다.
-     *
-     * {@code Content-Range} 헤더의 설정에 대한 것은 {@link TagApp#tagsForDefault(String, int)} )}와 같다.
-     *
-     * @param query
-     * @param limit
-     * @param project
-     * @return
-     */
-    private static List<String> tagsForProjectTagging(String query, int limit, Project project) {
-        ExpressionList<Tag> el =
-                Tag.find.where().or(icontains("category", query), icontains("name", query));
-
-        int total = el.findRowCount();
-
-        // If the limit is bigger than the total number of resulting tags, juts return all of them.
-        if (limit > total) {
-            return tagsForDefault(query, limit);
-        }
-
-        // If the project has no License tag, list License tags first to
-        // recommend add one of them.
-        boolean hasLicenseTags =
-                Tag.find.where().eq("projects.id", project.id).eq("category", "License")
-                        .findRowCount() > 0;
-
-        if (hasLicenseTags) {
-            return tagsForDefault(query, limit);
-        }
-
-        ExpressionList<Tag> elLicense =
-                Tag.find.where().and(eq("category", "License"), icontains("name", query));
-        elLicense.setMaxRows(limit);
-        List<String> tags = tags(elLicense);
-
-        // If every license tags are listed but quota still remains, then add
-        // any other tags not in License category to the list.
-        if (elLicense.findRowCount() < limit) {
-            ExpressionList<Tag> elExceptLicense =
-                    Tag.find.where().and(
-                            ne("category", "License"),
-                            or(icontains("category", query), icontains("name", query)));
-
-            elExceptLicense.setMaxRows(limit - elLicense.findRowCount());
-
-            for (Tag tag: elExceptLicense.findList()) {
-                tags.add(tag.toString());
-            }
-        }
-
-        if (tags.size() < total) {
-            response().setHeader("Content-Range", "items " + tags.size() + "/" + total);
-        }
-
-        return tags;
-    }
-}
app/models/Attachment.java
--- app/models/Attachment.java
+++ app/models/Attachment.java
@@ -10,10 +10,7 @@
 import java.util.Formatter;
 import java.util.List;
 
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.Id;
+import javax.persistence.*;
 
 import models.resource.Resource;
 import org.apache.commons.io.FileUtils;
@@ -211,6 +208,7 @@
      * @throws IOException
      * @throws NoSuchAlgorithmException
      */
+    @Transient
     public boolean store(File file, String name, Resource container) throws IOException, NoSuchAlgorithmException {
         // Store the file as its SHA1 hash in filesystem, and record its
         // metadata - projectId, containerType, containerId, size and hash - in Database.
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -183,17 +183,17 @@
         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.addCell(new jxl.write.Label(i, 0, labalArr[i], cf1));
             sheet.setColumnView(i, 20);
         }
         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));
-            sheet.addCell(new Label(colcnt++, i, issue.title, cf2));
-            sheet.addCell(new Label(colcnt++, i, getAssigneeName(issue.assignee), cf2));
-            sheet.addCell(new Label(colcnt++, i, issue.createdDate.toString(), cf2));
+            sheet.addCell(new jxl.write.Label(colcnt++, i, issue.id.toString(), cf2));
+            sheet.addCell(new jxl.write.Label(colcnt++, i, issue.state.toString(), cf2));
+            sheet.addCell(new jxl.write.Label(colcnt++, i, issue.title, cf2));
+            sheet.addCell(new jxl.write.Label(colcnt++, i, getAssigneeName(issue.assignee), cf2));
+            sheet.addCell(new jxl.write.Label(colcnt++, i, issue.createdDate.toString(), cf2));
         }
         workbook.write();
 
 
app/models/Label.java (added)
+++ app/models/Label.java
@@ -0,0 +1,120 @@
+package models;
+
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+import play.data.validation.Constraints.Required;
+import play.db.ebean.Model;
+
+import javax.persistence.*;
+import java.util.Set;
+
+/**
+ * 프로젝트에 붙일 수 있는 라벨
+ */
+@Entity
+@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"category", "name"}))
+public class Label extends Model {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = -35487506476718498L;
+    public static Finder<Long, Label> find = new Finder<Long, Label>(Long.class, Label.class);
+
+    @Id
+    public Long id;
+
+    @Required
+    public String category;
+
+    @Required
+    public String name;
+
+    @ManyToMany(mappedBy="labels")
+    public Set<Project> projects;
+
+    /**
+     * 주어진 {@code category}와 {@code name}으로 라벨를 생성한다.
+     * @param category 이 라벨가 속한 분류
+     * @param name 이 라벨의 이름
+     */
+    public Label(String category, String name) {
+        if (category == null) {
+            category = "Label";
+        }
+        this.category = category;
+        this.name = name;
+    }
+
+    /**
+     * 현재 인스턴스와 같은 라벨가 있는지의 여부를 반환한다.
+     *
+     * when: 사용자가 라벨를 추가하려고 했을 때, 중복 여부를 검사하고자 할 때 사용하고 있다.
+     *
+     * 이 인스턴스의 {@link Label#category}와 {@link Label#name}가 모두 같은 라벨가 DB에 존재하는지 확인한다.
+     *
+     * @return 같은 것이 존재하면 {@code true}, 아니면 {@code false}
+     */
+    @Transient
+    public boolean exists() {
+        return find.where().eq("category", category).eq("name", name)
+            .findRowCount() > 0;
+    }
+
+    /**
+     * 라벨를 삭제한다.
+     *
+     * 모든 프로젝트에서 이 라벨를 제거한 뒤, 라벨를 삭제한다.
+     */
+    @Override
+    public void delete() {
+        for(Project project: projects) {
+            project.labels.remove(this);
+            project.update();
+        }
+        super.delete();
+    }
+
+
+    /**
+     * 라벨를 문자열로 변환하여 반환한다.
+     *
+     * {@link Label#category}와 {@link Label#name}을 " - "로 연결한 문자열을 반환한다.
+     *
+     * @return "{@link Label#category} - {@link Label#name}" 형식의 문자열
+     */
+    @Override
+    public String toString() {
+        return category + " - " + name;
+    }
+
+    /**
+     * 라벨를 {@link Resource} 형식으로 반환한다.
+     *
+     * when: 이 라벨에 대해 접근권한이 있는지 검사하기 위해 {@link utils.AccessControl}에서 사용한다.
+     *
+     * @return {@link Resource}로서의 라벨
+     */
+    public Resource asResource() {
+        return new Resource() {
+            @Override
+            public Long getId() {
+                return id;
+            }
+
+            @Override
+            public Project getProject() {
+                return null;
+            }
+
+            @Override
+            public ResourceType getType() {
+                return ResourceType.LABEL;
+            }
+        };
+    }
+
+    public void delete(Project project) {
+        this.projects.remove(project);
+    }
+}
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -80,7 +80,7 @@
     private long lastPostingNumber;
 
     @ManyToMany
-    public Set<Tag> tags;
+    public Set<Label> labels;
 
     @ManyToOne
     public Project originalProject;
@@ -386,11 +386,11 @@
     }
 
     /**
-     * Tags as resource.
+     * Labels as resource.
      *
      * @return the resource
      */
-    public Resource tagsAsResource() {
+    public Resource labelsAsResource() {
         return new Resource() {
 
             @Override
@@ -405,7 +405,7 @@
 
             @Override
             public ResourceType getType() {
-                return ResourceType.PROJECT_TAGS;
+                return ResourceType.PROJECT_LABELS;
             }
 
         };
@@ -448,41 +448,41 @@
     }
 
     /**
-     * 프로젝트 태그를 추가하고 성공여부를 반환한다.
+     * 프로젝트 라벨를 추가하고 성공여부를 반환한다.
      *
-     * 태그가 이미 있을경우 false를 반환한다.
-     * 태그가 없으면 추가하고 true를 반환한다.
+     * 라벨가 이미 있을경우 false를 반환한다.
+     * 라벨가 없으면 추가하고 true를 반환한다.
      *
-     * @param tag 신규 태그
-     * @return 이미 태그가 있을 경우 false / 없으면 추가하고 true 반환
+     * @param label 신규 라벨
+     * @return 이미 라벨가 있을 경우 false / 없으면 추가하고 true 반환
      */
-    public Boolean tag(Tag tag) {
-        if (tags.contains(tag)) {
-            // Return false if the tag has been already attached.
+    public Boolean attachLabel(Label label) {
+        if (labels.contains(label)) {
+            // Return false if the label has been already attached.
             return false;
         }
 
-        // Attach new tag.
-        tags.add(tag);
+        // Attach new label.
+        labels.add(label);
         update();
 
         return true;
     }
 
     /**
-     * 태그를 제거한다.
+     * 라벨를 제거한다.
      *
-     * 태그를 참조하고 있는 프로젝트가 없으면 해당 태그를 삭제하고
-     * 참조하는 프로젝트가 있으면 태그 매핑정보를 업데이트한다.
+     * 라벨를 참조하고 있는 프로젝트가 없으면 해당 라벨를 삭제하고
+     * 참조하는 프로젝트가 있으면 라벨 매핑정보를 업데이트한다.
      *
-     * @param tag 삭제할 태그
+     * @param label 삭제할 라벨
      */
-    public void untag(Tag tag) {
-        tag.projects.remove(this);
-        if (tag.projects.size() == 0) {
-            tag.delete();
+    public void detachLabel(Label label) {
+        label.projects.remove(this);
+        if (label.projects.size() == 0) {
+            label.delete();
         } else {
-            tag.update();
+            label.update();
         }
     }
 
@@ -650,9 +650,9 @@
             assignee.delete();
         }
 
-        for (Tag tag : tags) {
-            tag.delete(this);
-            tag.update();
+        for (Label label : labels) {
+            label.delete(this);
+            label.update();
         }
 
         for(IssueLabel label : IssueLabel.findByProject(this)) {
 
app/models/Tag.java (deleted)
--- app/models/Tag.java
@@ -1,121 +0,0 @@
-package models;
-
-import models.enumeration.ResourceType;
-import models.resource.Resource;
-import play.data.validation.Constraints.Required;
-import play.db.ebean.Model;
-
-import javax.persistence.*;
-import java.util.List;
-import java.util.Set;
-
-/**
- * 프로젝트에 붙일 수 있는 태그
- */
-@Entity
-@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"category", "name"}))
-public class Tag extends Model {
-
-    /**
-     *
-     */
-    private static final long serialVersionUID = -35487506476718498L;
-    public static Finder<Long, Tag> find = new Finder<Long, Tag>(Long.class, Tag.class);
-
-    @Id
-    public Long id;
-
-    @Required
-    public String category;
-
-    @Required
-    public String name;
-
-    @ManyToMany(mappedBy="tags")
-    public Set<Project> projects;
-
-    /**
-     * 주어진 {@code category}와 {@code name}으로 태그를 생성한다.
-     * @param category 이 태그가 속한 분류
-     * @param name 이 태그의 이름
-     */
-    public Tag(String category, String name) {
-        if (category == null) {
-            category = "Tag";
-        }
-        this.category = category;
-        this.name = name;
-    }
-
-    /**
-     * 현재 인스턴스와 같은 태그가 있는지의 여부를 반환한다.
-     *
-     * when: 사용자가 태그를 추가하려고 했을 때, 중복 여부를 검사하고자 할 때 사용하고 있다.
-     *
-     * 이 인스턴스의 {@link Tag#category}와 {@link Tag#name}가 모두 같은 태그가 DB에 존재하는지 확인한다.
-     *
-     * @return 같은 것이 존재하면 {@code true}, 아니면 {@code false}
-     */
-    @Transient
-    public boolean exists() {
-        return find.where().eq("category", category).eq("name", name)
-            .findRowCount() > 0;
-    }
-
-    /**
-     * 태그를 삭제한다.
-     *
-     * 모든 프로젝트에서 이 태그를 제거한 뒤, 태그를 삭제한다.
-     */
-    @Override
-    public void delete() {
-        for(Project project: projects) {
-            project.tags.remove(this);
-            project.update();
-        }
-        super.delete();
-    }
-
-
-    /**
-     * 태그를 문자열로 변환하여 반환한다.
-     *
-     * {@link Tag#category}와 {@link Tag#name}을 " - "로 연결한 문자열을 반환한다.
-     *
-     * @return "{@link Tag#category} - {@link Tag#name}" 형식의 문자열
-     */
-    @Override
-    public String toString() {
-        return category + " - " + name;
-    }
-
-    /**
-     * 태그를 {@link Resource} 형식으로 반환한다.
-     *
-     * when: 이 태그에 대해 접근권한이 있는지 검사하기 위해 {@link utils.AccessControl}에서 사용한다.
-     *
-     * @return {@link Resource}로서의 태그
-     */
-    public Resource asResource() {
-        return new Resource() {
-            @Override
-            public Long getId() {
-                return id;
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
-            }
-
-            @Override
-            public ResourceType getType() {
-                return ResourceType.TAG;
-            }
-        };
-    }
-
-    public void delete(Project project) {
-        this.projects.remove(project);
-    }
-}
app/models/enumeration/ResourceType.java
--- app/models/enumeration/ResourceType.java
+++ app/models/enumeration/ResourceType.java
@@ -22,8 +22,8 @@
     ATTACHMENT("attachment"),
     ISSUE_COMMENT("issue_comment"),
     NONISSUE_COMMENT("nonissue_comment"),
-    TAG("tag"),
-    PROJECT_TAGS("project_tags"),
+    LABEL("label"),
+    PROJECT_LABELS("project_labels"),
     FORK("fork");
 
     private String resource;
app/utils/ReservedWordsValidator.java
--- app/utils/ReservedWordsValidator.java
+++ app/utils/ReservedWordsValidator.java
@@ -75,4 +75,4 @@
         }
         return false;
     }
-}
(No newline at end of file)
+}
app/views/project/list.scala.html
--- app/views/project/list.scala.html
+++ app/views/project/list.scala.html
@@ -38,13 +38,18 @@
                     <a href="@routes.UserApp.userInfo(project.owner)">
                         <img src="@User.findByLoginId(project.owner).avatarUrl" alt="@User.findByLoginId(project.owner).name">
                     </a>&nbsp;
+                    <a href="@routes.UserApp.userInfo(project.owner)" class="project-name">@project.owner</a> <span class="project-name">/</span> <a href="@routes.ProjectApp.project(project.owner, project.name)" class="black">@project.name</a>
+                    @if(!project.isPublic){ <i class="ico ico-lock"></i> }
+                    @for(label <- project.labels) {
+                        <span class="project-label @label.category.toLowerCase()">@label.name</span>
+                    }
                 </div>
                 <div style="float:left">
                     <div class="header">
                         <a href="@routes.ProjectApp.project(project.owner, project.name)" class="black">@project.name</a>
                         @if(!project.isPublic){ <i class="ico ico-lock"></i> }
-                        @for(tag <- project.tags) {
-                            <span class="project-label @tag.category.toLowerCase()">@tag.name</span>
+                        @for(label <- project.labels) {
+                            <span class="project-label @label.category.toLowerCase()">@label.name</span>
                         }
                     </div>
                     <div class="desc">
app/views/project/overview.scala.html
--- app/views/project/overview.scala.html
+++ app/views/project/overview.scala.html
@@ -53,7 +53,7 @@
 
     <div class="bubble-wrap dark-gray project-home">
         <div class="inner logo" style="background-image:url('@projectLogoImage');">
-            @if(isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)){
+            @if(isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)){
             <div class="edit">
                 <a href="@routes.ProjectApp.settingForm(project.owner, project.name)"><i class="icon-edit icon-white"></i> @Messages("project.setting")</a>
             </div>
@@ -62,22 +62,47 @@
         <div class="inner project-info">
             <header>
                 <h3>@Messages("project.info")</h3>
-                @if(isAllowed(UserApp.currentUser(), project.tagsAsResource(), Operation.UPDATE)){
-                <button type="button" class="nbtn small white" data-toggle="button" id="tag-editor-toggle">@Messages("button.edit")</button>
+                @if(isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)){
+                <button type="button" class="nbtn small white" data-toggle="button" id="label-editor-toggle">@Messages("button.edit")</button>
                 }
             </header>
-            <ul class="infos" id="tags">
-                <!-- tags are added here by hive.project.Home.js -->
+            <ul class="infos" id="label-board">
+                <!-- labels are added here by hive.project.Home.js -->
             </ul>
-            <ul>
-                <input name="newTag" type="text" class="text hidden" style="margin-bottom:0px;" data-provider="typeahead" autocomplete="off"/>
-                <button id="addTag" type="button" class="nbtn medium hidden">@Messages("button.add")</button>
-            </ul>
+
+            <script id="label-delete-button-template" type="text/x-jquery-tmpl">
+                <a href="javascript:void(0)">&times;</a>
+            </script>
+
+            <script id="label-template" type="text/x-jquery-tmpl">
+                <span class="label">${name}</span>
+            </script>
+
+            <script id="category-template" type="text/x-jquery-tmpl">
+                <li class="info" data-category="${category}">
+                    <strong>${category} : </strong>
+                    <span class="label-list"></span>
+                </li>
+            </script>
+
+            <script id="plus-button-template" type="text/x-jquery-tmpl">
+                <button class="nbtn small white">
+                    <i class="ico ico-plus-blue"></i>
+                </button>
+            </script>
+
+            <script id="label-input-template" type="text/x-jquery-tmpl">
+                <input type="text" autocomplete="off" class="text" style="margin-bottom: 0px;">
+            </script>
+
+            <script id="label-submit-template" type="text/x-jquery-tmpl">
+                <button type="button" class="nbtn medium">@Messages("button.add")</button>
+            </script>
         </div>
         <div class="inner member-info">
             <header>
                 <h3>@Messages("project.members")</h3>
-                @if(isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)){
+                @if(isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)){
                 <a href="@routes.ProjectApp.members(project.owner, project.name)" class="nbtn small white" id="member-add-link">@Messages("button.add")</a>
                 }
             </header>
@@ -158,8 +183,11 @@
 <script type="text/javascript">
 $(document).ready(function(){
 	$hive.loadModule("project.Home", {
-        "sURLProjectTags": "@routes.ProjectApp.tags(project.owner, project.name)",
-        "sURLTags"       : "@routes.TagApp.tags()",
+        "sURLProjectLabels": "@routes.ProjectApp.labels(project.owner, project.name)",
+        "sURLLabels"       : "@routes.LabelApp.labels()",
+        "sURLLabelCategories": "@routes.LabelApp.categories()",
+        "welLabelBoard": $('#label-board'),
+        "welLabelEditorToggle": $('#label-editor-toggle'),
         "nProjectId"     : @project.id
     });
 });
app/views/user/partial_projectlist.scala.html
--- app/views/user/partial_projectlist.scala.html
+++ app/views/user/partial_projectlist.scala.html
@@ -23,8 +23,8 @@
             </div>
             }
             <div class="desc tag">
-            @for(tag <- project.tags) {
-                <span class="project-label @tag.category.toLowerCase()">@tag.name</span>
+            @for(label <- project.labels) {
+                <span class="project-label @label.category.toLowerCase()">@label.name</span>
             }
             </div>
             <div class="desc">@project.overview</div>
@@ -79,4 +79,4 @@
 </script>
 @loginUser() = {
     @User.findByLoginId(session.get("loginId"))
-}
(No newline at end of file)
+}
 
conf/evolutions/default/15.sql (added)
+++ conf/evolutions/default/15.sql
@@ -0,0 +1,13 @@
+# --- !Ups
+
+DROP TABLE IF EXISTS label;
+
+# --- !Downs
+
+CREATE TABLE label (
+  id                        bigint not null,
+  name                      varchar(255),
+  color                     varchar(255),
+  task_board_id             bigint,
+  constraint pk_label primary key (id))
+;
 
conf/evolutions/default/16.sql (added)
+++ conf/evolutions/default/16.sql
@@ -0,0 +1,33 @@
+# --- !Ups
+
+ALTER TABLE tag DROP CONSTRAINT IF EXISTS uq_tag_category_name;
+ALTER TABLE tag DROP CONSTRAINT IF EXISTS uq_label_category_name;
+ALTER TABLE tag ADD CONSTRAINT uq_label_category_name UNIQUE (category, name);
+ALTER TABLE tag DROP CONSTRAINT IF EXISTS pk_tag;
+ALTER TABLE tag DROP CONSTRAINT IF EXISTS pk_label;
+ALTER TABLE tag ADD CONSTRAINT pk_label PRIMARY KEY (ID);
+ALTER TABLE tag RENAME TO label;
+
+ALTER TABLE project_tag ALTER COLUMN tag_id RENAME TO label_id;
+ALTER TABLE project_tag RENAME TO project_label;
+
+DROP SEQUENCE IF EXISTS label_seq;
+CREATE SEQUENCE label_seq START WITH tag_seq.nextval;
+DROP SEQUENCE IF EXISTS tag_seq;
+
+# --- !Downs
+
+ALTER TABLE label DROP CONSTRAINT IF EXISTS uq_label_category_name;
+ALTER TABLE label DROP CONSTRAINT IF EXISTS uq_tag_category_name;
+ALTER TABLE label ADD CONSTRAINT uq_tag_category_name UNIQUE (category, name);
+ALTER TABLE label DROP CONSTRAINT IF EXISTS pk_label;
+ALTER TABLE label DROP CONSTRAINT IF EXISTS pk_tag;
+ALTER TABLE label ADD CONSTRAINT pk_tag PRIMARY KEY (ID);
+ALTER TABLE label RENAME TO tag;
+
+ALTER TABLE project_label ALTER COLUMN label_id RENAME TO tag_id;
+ALTER TABLE project_label RENAME TO project_tag;
+
+DROP SEQUENCE IF EXISTS tag_seq;
+CREATE SEQUENCE tag_seq START WITH label_seq.nextval;
+DROP SEQUENCE IF EXISTS label_seq;
conf/initial-data.yml
--- conf/initial-data.yml
+++ conf/initial-data.yml
@@ -23,467 +23,467 @@
         name:           member
         active:         true
 
-# Tags
-tags:
-    - !!models.Tag
+# Labels
+labels:
+    - !!models.Label
         category:       OS
         name:           Windows
-    - !!models.Tag
+    - !!models.Label
         category:       OS
         name:           OSX
-    - !!models.Tag
+    - !!models.Label
         category:       OS
         name:           Linux
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ActionScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Ada
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ALGOL 58
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ALGOL 60
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ALGOL 68
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           APL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           AppleScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           AspectJ
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ASP.NET
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Assembly language
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           AutoLISP / Visual LISP
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual LISP
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           AWK
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           B
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Bash
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           BASIC
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           bc
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           BCPL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Batch (Windows/Dos)
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Bourne shell
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           BREW
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           C
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           C++
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           C#
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Clipper
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Clojure
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           COBOL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           CobolScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           CoffeeScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ColdFusion
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Common Lisp
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Component Pascal
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           csh
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Curl
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           D
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Dart
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           dBase
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Delphi
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ECMAScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Eiffel
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Emacs Lisp
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Erlang
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           F
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           F#
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Forth
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Fortran
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           FoxBase
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           FoxPro
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Go
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Go!
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Groovy
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Haskell
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Io
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           J
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Java
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           JavaScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           JScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           JavaFX Script
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ksh
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           LaTeX
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Lisp
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Logo
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Lua
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Machine code
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           make
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Mathematica
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           MATLAB
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Maya
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           MDL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Microcode
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           ML
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Modula
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Modula-2
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Modula-3
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Object Lisp
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Object Pascal
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Objective-C
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           OCaml
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Opa
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Orc
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Pascal
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Perl
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PHP
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL-11
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/0
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/B
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/C
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/I
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/M
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/P
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PL/SQL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           POP-11
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PostScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PowerBuilder
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           PowerShell
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Processing.js
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Prolog
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual Prolog
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Python
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           QBasic
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           QuakeC
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           R
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           R++
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           REXX
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Ruby
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Rust
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Scala
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Scheme
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Script.NET
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Sed
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Self
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Simula
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Simulink
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Smalltalk
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Small Basic
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Snowball
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Squeak
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Tcl
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           TeX
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           TEX
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           UNITY
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Unix shell
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           UnrealScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Vala
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           VBA
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           VBScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Verilog
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           VHDL
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual Basic
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual Basic .NET
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Microsoft Visual C++
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual C#
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual DataFlex
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual DialogScript
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual Fortran
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual FoxPro
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual J++
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Visual J#
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           Windows PowerShell
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           XQuery
-    - !!models.Tag
+    - !!models.Label
         category:       Language
         name:           XSLT
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           Apache
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           BSD
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           GPL
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           ISC
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           LGPL
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           MIT
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           MPL v2.0
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           Public Domain
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           EPL
-    - !!models.Tag
+    - !!models.Label
         category:       License
         name:           MPL v1.1
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -64,6 +64,8 @@
 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.addNewCategory = Add new category
+label.addNewLabel = Add new label
 
 order.all = All
 order.date = Date
@@ -79,6 +81,7 @@
 button.cancel = Cancel
 button.reset = Reset
 button.edit = Edit
+button.done = Done
 button.delete = Delete
 button.list = list
 button.selectFile = Select File
@@ -519,4 +522,4 @@
 fork.go = Go
 fork.already.exist = There is already forked the same project.
 fork.help.message.1 = Forking is a great way to contribute to a project even though you don't have write access.
-fork.help.message.2 = After forking a project, you can sent pull requests to contribute your code.
(No newline at end of file)
+fork.help.message.2 = After forking a project, you can sent pull requests to contribute your code.
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -64,6 +64,8 @@
 label.select = 라벨 선택
 label.error.duplicated = 라벨 생성에 실패했습니다.\n이미 동일한 라벨이 존재할지도 모릅니다.
 label.error.creationFailed = 라벨 생성에 실패했습니다.\n서버에 문제가 있거나 올바른 요청이 아닐 수 있습니다.
+label.addNewCategory = 새 분류 추가
+label.addNewLabel = 새 라벨 추가
 
 order.all = 전체
 order.date = 날짜순
@@ -79,6 +81,7 @@
 button.cancel = 취소
 button.reset = 다시쓰기
 button.edit = 수정
+button.done = 완료
 button.delete = 삭제
 button.list = 목록
 button.selectFile = 파일 선택
@@ -519,4 +522,4 @@
 fork.go = 바로가기
 fork.already.exist = 동일한 원본 프로젝트를 복사한 프로젝트가 있습니다.
 fork.help.message.1 = 원본 프로젝트에 직접 소스 코드를 추가하거나 수정할 수 없더라도, 자기가 원하는 코드를 작성하여 기여하거나 본인만의 프로젝트를 만들 수 있습니다.
-fork.help.message.2 = 프로젝트를 복사한 다음 원하는 코드를 작성하고 코드 보내기 기능으로 원본 프로젝트에 코드를 보낼 수 있습니다.
(No newline at end of file)
+fork.help.message.2 = 프로젝트를 복사한 다음 원하는 코드를 작성하고 코드 보내기 기능으로 원본 프로젝트에 코드를 보낼 수 있습니다.
conf/routes
--- conf/routes
+++ conf/routes
@@ -68,11 +68,12 @@
 POST    /:user/:project/post/:number/edit                   controllers.BoardApp.editPost(user, project, number:Long)
 GET     /:user/:project/post/:number/comment/:commentId/delete     controllers.BoardApp.deleteComment(user, project, number:Long, commentId:Long)
 
-# Tags
-GET    /tags                                            controllers.TagApp.tags(query: String ?= "", context: String ?= "", limit: Integer ?= null)
-GET    /:user/:project/tags                             controllers.ProjectApp.tags(user, project)
-POST   /:user/:project/tags                             controllers.ProjectApp.tag(user, project)
-POST   /:user/:project/tags/:id                         controllers.ProjectApp.untag(user, project, id: Long)
+# Labels
+GET    /labels                                          controllers.LabelApp.labels(query: String ?= "", category: String ?= "", limit: Integer ?= null)
+GET    /categories                                      controllers.LabelApp.categories(query: String ?= "", limit: Integer ?= null)
+GET    /:user/:project/labels                           controllers.ProjectApp.labels(user, project)
+POST   /:user/:project/labels                           controllers.ProjectApp.attachLabel(user, project)
+POST   /:user/:project/labels/:id                       controllers.ProjectApp.detachLabel(user, project, id: Long)
 
 # Projects
 GET     /projectform                                    controllers.ProjectApp.newProjectForm()
docs/technical/label-typeahead.md (Renamed from docs/technical/tag-typeahead.md)
--- docs/technical/tag-typeahead.md
+++ docs/technical/label-typeahead.md
No changes
public/javascripts/common/hive.ui.Typeahead.js
--- public/javascripts/common/hive.ui.Typeahead.js
+++ public/javascripts/common/hive.ui.Typeahead.js
@@ -52,6 +52,14 @@
 			htVar.rxContentRange = /items\s+([0-9]+)\/([0-9]+)/;
             htVar.htData = htOptions.htData || {};
 		}
+
+        function data(key, value) {
+            if (value !== undefined) {
+                htVar.htData[key] = value;
+            } else {
+                return htVar.htData[key];
+            }
+        }
 		
 		/**
 		 * 엘리먼트 초기화
@@ -60,11 +68,11 @@
 		 */
 		function _initElement(sQuery){
 			htElement.welInput = $(sQuery);
-			htElement.welInput.typeahead({
-                "minLength": 0,
-                "source"   : _onTypeAhead,
-                "items"    : htVar.htData.limit || 8
-            });
+			htElement.welInput.typeahead({ minLength: 0 });
+            htData = htElement.welInput.data('typeahead');
+			htData.minLength = 0;
+			htData.items = htVar.htData.limit || 8;
+			htData.source = _onTypeAhead;
 		}
 		
         /**
@@ -73,8 +81,8 @@
         * For more information, See "source" option at
         * http://twitter.github.io/bootstrap/javascript.html#typeahead
         *
-        * @param {String} sQuery
-        * @param {Function} fProcess
+
+        * @param {Function} frocess
         */
         function _onTypeAhead(sQuery, fProcess) {
             if (sQuery.match(htVar.sLastQuery) && htVar.bIsLastRangeEntire) {
public/javascripts/service/hive.project.Home.js
--- public/javascripts/service/hive.project.Home.js
+++ public/javascripts/service/hive.project.Home.js
@@ -20,27 +20,59 @@
 			_initVar(htOpt);
 			_initElement(htOptions);
 			_attachEvent();			
-            _initTags();
+            _initLabels();
 		}
 
         function _initVar(htOptions){
-            htVar.sURLProjectTags = htOptions.sURLProjectTags;
-            htVar.sURLTags = htOptions.sURLTags;
+            htVar.sURLProjectLabels = htOptions.sURLProjectLabels;
+            htVar.sURLLabels = htOptions.sURLLabels;
+            htVar.sURLLabelCategories = htOptions.sURLLabelCategories;
             htVar.nProjectId = htOptions.nProjectId;
 		}
 
 		/**
 		 * initialize element
 		 */
-		function _initElement(htOptions){
+		function _initElement(htOptions) {
+            var welBtnPlus = $('#plus-button-template').tmpl();
+
 			htElement.welRepoURL = $("#repositoryURL");
 
-            // tags
-            htElement.welInputAddTag = $('input[name="newTag"]');
-            htElement.welTags = $('#tags');
-            htElement.welBtnAddTag = $('#addTag');
-            htElement.welTagEditorToggle = $('#tag-editor-toggle');
-            htElement.waTag = $();
+            // project label
+            htElement.welLabelBoard = htOptions.welLabelBoard;
+            htElement.welLabelEditorToggle = htOptions.welLabelEditorToggle;
+
+            htElement.welInputCategory = $('#label-input-template').tmpl();
+            htElement.welSubmitCategory = $('#label-submit-template').tmpl();
+            htElement.welInputCategory.attr('placeholder', Messages('label.addNewCategory'));
+            htElement.welInputCategory.keypress(_onKeyPressNewCategory);
+            htElement.welSubmitCategory.click(_onClickNewCategory);
+
+            htElement.welInputLabel = $('#label-input-template').tmpl();
+            htElement.welSubmitLabel = $('#label-submit-template').tmpl();
+            htElement.welInputLabel.keypress(_onKeyPressNewLabel);
+            htElement.welInputLabel.attr('placeholder', Messages('label.addNewLabel'));
+            htElement.welSubmitLabel.click(_submitLabel);
+
+            htElement.welInputLabelBox = $('<p>')
+                .append(htElement.welInputLabel)
+                .append(htElement.welSubmitLabel);
+
+            htElement.welInputCategoryBox = $('<p>')
+                .append(htElement.welInputCategory)
+                .append(htElement.welSubmitCategory);
+
+            htElement.welBtnPlusLabel = welBtnPlus.clone();
+            htElement.welBtnPlusCategory = welBtnPlus.clone();
+
+            htElement.aLabel = [];
+            htElement.htCategory = {};
+            htElement.aBtnPlusLabel = [];
+
+            htElement.welNewCategory = $('<li>')
+                .append(htElement.welBtnPlusCategory);
+
+            htElement.welLabelBoard.append(htElement.welNewCategory);
 		}
 		
 		/**
@@ -48,26 +80,29 @@
 		 */
 		function _attachEvent(){
 			htElement.welRepoURL.click(_onClickRepoURL);
-            htElement.welInputAddTag.keypress(_onKeyPressNewTag);
-            htElement.welBtnAddTag.click(_submitTag);
-            htElement.welTagEditorToggle.on('click', function() {
+            htElement.welLabelEditorToggle.on('click', function() {
                 if ($(this).hasClass('active')) {
                     // Now inactive
-                    _hideTagEditor();
+                    $(this).removeClass('active');
+                    $(this).text(Messages("button.edit"));
+                    _hideLabelEditor();
                 } else {
                     // Now active
-                    _showTagEditor();
+                    $(this).addClass('active');
+                    $(this).text(Messages("button.done"));
+                    _showLabelEditor();
                 }
             });
-            
-            new hive.ui.Typeahead(htElement.welInputAddTag, {
-            	"sActionURL": htVar.sURLTags,
+
+            new hive.ui.Typeahead(htElement.welInputCategory, {
+                "sActionURL": htVar.sURLLabelCategories,
                 "htData": {
-                    "context": "PROJECT_TAGGING_TYPEAHEAD",
                     "project_id": htVar.nProjectId,
                     "limit": 8
                 }
             });
+
+            htElement.welBtnPlusCategory.click(_onClickPlusCategory);
 		}
 
 		function _onClickRepoURL(){
@@ -75,145 +110,280 @@
 		}
 
         /**
-        * Add a tag, which user types in htElement.welInputAddTag, into #tags div.
+        * When any key is pressed on input box in any Category line.
         *
         * @param {Object} oEvent
         */
-        function _onKeyPressNewTag(oEvent) {
+        function _onKeyPressNewLabel(oEvent) {
             if (oEvent.keyCode == 13) {
-                _submitTag();
-                htElement.welInputAddTag.val("");
+                _submitLabel();
                 return false;
             }
         }
 
-        function _parseTag(sTag) {
-            var sSeparator = ' - ';
-            var aPart =
-                jQuery.map(sTag.split(sSeparator), function(s) { return s.trim(); });
-            var htTag;
-
-            if (aPart.length > 2) {
-                aPart = [aPart.shift(), aPart.join(sSeparator)];
+        /**
+         * When any key is pressed on input box in New Category line.
+         *
+         * @param {Object} oEvent
+         */
+        function _onKeyPressNewCategory(oEvent) {
+            if (oEvent.keyCode == 13) {
+                _onClickNewCategory();
+                return false;
             }
+        }
 
-            if (aPart.length > 1) {
-                htTag = {"category": aPart[0], "name": aPart[1]};
-            } else if (aPart.length == 1) {
-                htTag = {"category": "Tag", "name": aPart[0]};
-            } else {
-                return null;
-            }
-
-            return htTag;
+        /**
+         * Read data to create a label from input box.
+         */
+        function _labelFromInput() {
+            return {
+                "category": htElement.welInputLabel.data('category'),
+                "name": htElement.welInputLabel.val()
+            };
         }
 
         /**
         * Submit new tag to add that.
         */
-        function _submitTag () {
-            var htTag = _parseTag(htElement.welInputAddTag.val());
+        function _submitLabel() {
+            var htLabel = _labelFromInput();
 
-            if (htTag == null) {
+            if (htLabel == null) {
                 return;
             }
 
-		$hive.sendForm({
-			"sURL"   : htVar.sURLProjectTags,
-			"htData" : htTag,
-			"fOnLoad": _appendTags
-		});
+            htElement.welInputLabel.val("");
+
+            $hive.sendForm({
+                "sURL"   : htVar.sURLProjectLabels,
+                "htData" : htLabel,
+                "fOnLoad": _appendLabels
+            });
+        }
+
+        var aRequired = ["Language", "License"];
+
+        function isRequired(sCategory) {
+            return aRequired.indexOf(sCategory) < 0;
         }
 
         /**
         * Get list of tags from the server and show them in #tags div.
         */
-        function _initTags() {
-		$hive.sendForm({
-			"sURL"     : htVar.sURLProjectTags,
-			"htOptForm": {"method":"get"},
-			"fOnLoad"  : function(data) {
-                    _appendTags(data);
-                    _hideTagEditor();
-                }
-		});
+        function _initLabels() {
+            $hive.sendForm({
+                "sURL"     : htVar.sURLProjectLabels,
+                "htOptForm": {"method":"get"},
+                "fOnLoad"  : function(data) {
+                        _appendLabels(data);
+
+                        for (var i = 0; i < aRequired.length; i++) {
+                            var sCategory = aRequired[i];
+                            if (!htElement.htCategory.hasOwnProperty(sCategory)) {
+                                __addCategory(sCategory);
+                            }
+                        }
+
+                        _hideLabelEditor();
+                    }
+            });
         }
 
         /**
-        * Make a tag element by given instance id and name.
+        * Make a tag element by given instance id and name.)
         *
         * @param {String} sInstanceId
         * @param {String} sName
         */
-        function _createTag(sInstanceId, sName) {
+        function _createLabel(sInstanceId, sName) {
             // If someone clicks a delete button, remove the tag which contains
             // the button, and also hide its category in .project-info div if
             // the category becomes to have no tag.
-            var fOnClickDelete = function() {
-		$hive.sendForm({
-			"sURL"   : htVar.sURLProjectTags + '/' + sInstanceId,
-			"htData" : {"_method":"DELETE"},
-			"fOnLoad": function(){
-                        var welCategory = welTag.parent();
-				welTag.remove();
-                        if (welCategory.children('span').length == 0) {
-                            welCategory.remove();
-                        }
-			}
-		});
+            var fOnLoadAfterDeleteLabel = function() {
+                var welCategory = welLabel.parent().parent();
+                var sCategory = welCategory.data('category');
+                welLabel.remove();
+                if (welCategory.children('.label-list').children().length == 0
+                    && isRequired(sCategory)
+                    && htElement.welInputLabel.data('category') != sCategory) {
+                    delete htElement.htCategory[sCategory];
+                    welCategory.remove();
+                }
             };
 
-            var welDeleteButton = $('<a href="javascript:void(0)">&times;</a>').click(fOnClickDelete);
-            var welTag = $('<span class="label">' + sName + '</span>').append(welDeleteButton);
+            var fOnClickDelete = function() {
+                $hive.sendForm({
+                    "sURL"   : htVar.sURLProjectLabels + '/' + sInstanceId,
+                    "htData" : {"_method":"DELETE"},
+                    "fOnLoad": fOnLoadAfterDeleteLabel
+                });
+            };
 
-            welTag.setRemovability = function(bFlag) {
+            var welDeleteButton = $('#label-delete-button-template')
+                .tmpl()
+                .click(fOnClickDelete);
+
+            var welLabel = $('#label-template')
+                .tmpl({'name': sName})
+                .append(welDeleteButton);
+
+            welLabel.setRemovability = function(bFlag) {
                 if (bFlag === true) {
-                    welTag.addClass('label');
+                    welLabel.addClass('label');
                     welDeleteButton.show();
                 } else {
-                    welTag.removeClass('label');
+                    welLabel.removeClass('label');
                     welDeleteButton.hide();
                 }
             }
 
-            htElement.waTag.push(welTag);
+            htElement.aLabel.push(welLabel);
 
-            return welTag;
+            return welLabel;
         }
 
         /**
         * Append the given tags on #tags div to show them.
         *
-        * @param {Object} htTags
+        * @param {Object} htLabels
         */
-        function _appendTags(htTags) {
-            for(var sInstanceId in htTags) {
-                var waChildren, newCategory;
-                var htTag = _parseTag(htTags[sInstanceId]);
+        function _appendLabels(htLabels) {
+            for(var sInstanceId in htLabels) {
+                var waCategory, welCategory;
+                var htLabel = htLabels[sInstanceId];
 
-                waChildren =
-                    htElement.welTags.children("[category=" + htTag.category + "]");
+                waCategory = htElement.welLabelBoard
+                    .children("[data-category=" + htLabel.category + "]");
 
-                if (waChildren.length > 0) {
-                    waChildren.append(_createTag(sInstanceId, htTag.name));
+                if (waCategory.length > 0) {
+                    waCategory
+                        .children(".label-list")
+                        .append(_createLabel(sInstanceId, htLabel.name));
                 } else {
-                    newCategory = $('<li class="info">')
-                        .attr('category', htTag.category)
-                        .append($('<strong>').text(htTag.category + ' : '))
-                        .append(_createTag(sInstanceId, htTag.name));
-                    htElement.welTags.append(newCategory);
+                    __addCategory(htLabel.category)
+                        .children(".label-list")
+                        .append(_createLabel(sInstanceId, htLabel.name));
                 }
             }
         }
 
         /**
-        * Show all delete buttons for all tags if bFlag is true, and hide if
+         * Create a category consists with category name, labels belong