Changsung Kim 2014-04-09
Merge remote-tracking branch 'yobi/feature/organization' into feature/organization-on-master
@1300d451b7406f6115869479029704d6766c0a29
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -1817,7 +1817,7 @@
     }
 }
 .cu-label {
-    width: 80px;
+    width: 160px;
     padding-right: 45px;
     .inline-block;
     vertical-align: top;
app/controllers/ImportApp.java
--- app/controllers/ImportApp.java
+++ app/controllers/ImportApp.java
@@ -1,23 +1,32 @@
 package controllers;
 
+import models.Organization;
+import models.OrganizationUser;
 import models.Project;
 import models.ProjectUser;
+import models.User;
 import models.enumeration.RoleType;
 import play.data.Form;
 import play.db.ebean.Transactional;
 import play.mvc.Controller;
 import play.mvc.Result;
+import play.mvc.With;
 import playRepository.GitRepository;
 import utils.AccessControl;
-import utils.Constants;
+import utils.ErrorViews;
 import utils.FileUtil;
+import utils.ValidationResult;
+import views.html.project.create;
 import views.html.project.importing;
 
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jgit.api.errors.*;
-import java.io.IOException;
 
+import actions.AnonymousCheckAction;
+
+import java.io.IOException;
 import java.io.File;
+import java.util.List;
 
 import static play.data.Form.form;
 
@@ -28,13 +37,10 @@
      *
      * @return
      */
+    @With(AnonymousCheckAction.class)
     public static Result importForm() {
-        if (UserApp.currentUser().isAnonymous()) {
-            flash(Constants.WARNING, "user.login.alert");
-            return redirect(routes.UserApp.loginForm());
-        } else {
-            return ok(importing.render("title.newProject", form(Project.class)));
-        }
+        List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
+        return ok(importing.render("title.newProject", form(Project.class), orgUserList));
     }
 
     /**
@@ -48,34 +54,30 @@
         if( !AccessControl.isGlobalResourceCreatable(UserApp.currentUser()) ){
             return forbidden("'" + UserApp.currentUser().name + "' has no permission");
         }
-
         Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest();
+        String owner = filledNewProjectForm.field("owner").value();
+        Organization organization = Organization.findByName(owner);
+        User user = User.findByLoginId(owner);
 
-        String gitUrl = StringUtils.trim(filledNewProjectForm.data().get("url"));
-        if(StringUtils.isBlank(gitUrl)) {
-            flash(Constants.WARNING, "project.import.error.empty.url");
-            return badRequest(importing.render("title.newProject", filledNewProjectForm));
+        ValidationResult result = validateForm(filledNewProjectForm, organization, user);
+        if (result.hasError()) {
+            return result.getResult();
         }
 
-        if (Project.exists(UserApp.currentUser().loginId, filledNewProjectForm.field("name").value())) {
-            flash(Constants.WARNING, "project.name.duplicate");
-            filledNewProjectForm.reject("name");
-            return badRequest(importing.render("title.newProject", filledNewProjectForm));
-        }
-
-        if (filledNewProjectForm.hasErrors()) {
-            filledNewProjectForm.reject("name");
-            flash(Constants.WARNING, "project.name.alert");
-            return badRequest(importing.render("title.newProject", filledNewProjectForm));
-        }
-
+        String gitUrl = filledNewProjectForm.data().get("url");
         Project project = filledNewProjectForm.get();
-        project.owner = UserApp.currentUser().loginId;
+
+        if (Organization.isNameExist(owner)) {
+            project.organization = organization;
+        }
         String errorMessageKey = null;
         try {
             GitRepository.cloneRepository(gitUrl, project);
             Long projectId = Project.create(project);
-            ProjectUser.assignRole(UserApp.currentUser().id, projectId, RoleType.MANAGER);
+
+            if (User.isLoginIdExist(owner)) {
+                ProjectUser.assignRole(UserApp.currentUser().id, projectId, RoleType.MANAGER);
+            }
         } catch (InvalidRemoteException e) {
             // It is not an url.
             errorMessageKey = "project.import.error.wrong.url";
@@ -87,12 +89,62 @@
         }
 
         if (errorMessageKey != null) {
-            flash(Constants.WARNING, errorMessageKey);
+            List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
+            filledNewProjectForm.reject("url", errorMessageKey);
             FileUtil.rm_rf(new File(GitRepository.getGitDirectory(project)));
-            return badRequest(importing.render("title.newProject", filledNewProjectForm));
+            return badRequest(importing.render("title.newProject", filledNewProjectForm, orgUserList));
         } else {
             return redirect(routes.ProjectApp.project(project.owner, project.name));
         }
     }
 
+    private static ValidationResult validateForm(Form<Project> newProjectForm, Organization organization, User user) {
+        boolean hasError = false;
+        Result result = null;
+
+        List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
+
+        String owner = newProjectForm.field("owner").value();
+        String name = newProjectForm.field("name").value();
+        boolean ownerIsUser = User.isLoginIdExist(owner);
+        boolean ownerIsOrganization = Organization.isNameExist(owner);
+
+        if (!ownerIsUser && !ownerIsOrganization) {
+            newProjectForm.reject("owner", "project.owner.invalidate");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (ownerIsUser && UserApp.currentUser().id != user.id) {
+            newProjectForm.reject("owner", "project.owner.invalidate");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (ownerIsOrganization && !OrganizationUser.isAdmin(organization.id, UserApp.currentUser().id)) {
+            hasError = true;
+            result = forbidden(ErrorViews.Forbidden.render("'" + UserApp.currentUser().name + "' has no permission"));
+        }
+
+        if (Project.exists(owner, name)) {
+            newProjectForm.reject("name", "project.name.duplicate");
+            hasError = true;
+            result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        String gitUrl = StringUtils.trim(newProjectForm.data().get("url"));
+        if (StringUtils.isBlank(gitUrl)) {
+            newProjectForm.reject("url", "project.import.error.empty.url");
+            hasError = true;
+            result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (newProjectForm.hasErrors()) {
+            newProjectForm.reject("name", "project.name.alert");
+            hasError = true;
+            result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        return new ValidationResult(result, hasError);
+    }
 }
 
app/controllers/OrganizationApp.java (added)
+++ app/controllers/OrganizationApp.java
@@ -0,0 +1,392 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package controllers;
+
+import actions.AnonymousCheckAction;
+import models.Organization;
+import models.User;
+import models.enumeration.Operation;
+import org.codehaus.jackson.node.ObjectNode;
+import play.data.Form;
+import play.data.validation.Validation;
+import play.db.ebean.Transactional;
+import play.libs.Json;
+import play.mvc.Controller;
+import play.mvc.Http;
+import play.mvc.Result;
+import play.mvc.With;
+import utils.AccessControl;
+import utils.Constants;
+import utils.ErrorViews;
+import models.*;
+import models.enumeration.RoleType;
+import views.html.organization.create;
+import views.html.organization.view;
+import views.html.organization.setting;
+
+import javax.validation.ConstraintViolation;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.Set;
+
+import static play.data.Form.form;
+import static utils.LogoUtil.*;
+
+/**
+ * @author Keeun Baik
+ */
+public class OrganizationApp extends Controller {
+
+    @With(AnonymousCheckAction.class)
+    public static Result newForm() {
+        return ok(create.render("title.newOrganization", new Form<>(Organization.class)));
+    }
+
+    @With(AnonymousCheckAction.class)
+    public static Result newOrganization() throws Exception {
+        Form<Organization> newOrgForm = form(Organization.class).bindFromRequest();
+        validate(newOrgForm);
+        if (newOrgForm.hasErrors()) {
+            flash(Constants.WARNING, newOrgForm.error("name").message());
+            return badRequest(create.render("title.newOrganization", newOrgForm));
+        } else {
+            Organization org = newOrgForm.get();
+            org.created = new Date();
+            org.save();
+
+            UserApp.currentUser().createOrganization(org);
+            return redirect(routes.OrganizationApp.organization(org.name));
+        }
+    }
+
+    public static Result organization(String name) {
+        Organization org = Organization.findByName(name);
+        if(org == null) {
+            return notFound(ErrorViews.NotFound.render("error.notfound.organization"));
+        }
+        return ok(view.render(org));
+    }
+
+    private static void validate(Form<Organization> newOrgForm) {
+        // 조직 이름 패턴을 검사한다.
+        Set<ConstraintViolation<Organization>> results = Validation.getValidator().validate(newOrgForm.get());
+        if(!results.isEmpty()) {
+            newOrgForm.reject("name", "organization.name.alert");
+        }
+
+        String name = newOrgForm.field("name").value();
+        // 중복된 loginId로 가입할 수 없다.
+        if (User.isLoginIdExist(name)) {
+            newOrgForm.reject("name", "organization.name.duplicate");
+        }
+
+        // 같은 이름의 조직을 만들 수 없다.
+        if (Organization.isNameExist(name)) {
+            newOrgForm.reject("name", "organization.name.duplicate");
+        }
+    }
+
+    /**
+     * 그룹에 멤버를 추가한다.
+     *
+     * @param organizationName
+     * @return
+     */
+    @Transactional
+    public static Result addMember(String organizationName) {
+        Form<User> addMemberForm = form(User.class).bindFromRequest();
+        Result result = validateForAddMember(addMemberForm, organizationName);
+        if (result != null) {
+            return result;
+        }
+
+        User user = User.findByLoginId(addMemberForm.get().loginId);
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        OrganizationUser.assignRole(user.id, organization.id, RoleType.ORG_MEMBER.roleType());
+
+        return redirect(routes.OrganizationApp.members(organizationName));
+    }
+
+    /**
+     * {@link #addMember(String)}를 위해 사용되는 변수의 유효성 검사를 한다.
+     *
+     * @param addMemberForm
+     * @param organizationName
+     * @return
+     */
+    private static Result validateForAddMember(Form<User> addMemberForm, String organizationName) {
+        String userLoginId = addMemberForm.get().loginId;
+        User userToBeAdded = User.findByLoginId(userLoginId);
+
+        if (addMemberForm.hasErrors() || userToBeAdded.isAnonymous()) {
+            flash(Constants.WARNING, "organization.member.unknownUser");
+            return redirect(routes.OrganizationApp.members(organizationName));
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        if (organization == null) {
+            flash(Constants.WARNING, "organization.member.unknownOrganization");
+            return redirect(routes.OrganizationApp.members(organizationName));
+        }
+
+        User currentUser = UserApp.currentUser();
+        if (!OrganizationUser.isAdmin(organization.id, currentUser.id)) {
+            flash(Constants.WARNING, "organization.member.needManagerRole");
+            return redirect(routes.OrganizationApp.members(organizationName));
+        }
+
+        if (OrganizationUser.exist(organization.id, userToBeAdded.id)) {
+            flash(Constants.WARNING, "organization.member.alreadyMember");
+            return redirect(routes.OrganizationApp.members(organizationName));
+        }
+
+        return null;
+    }
+
+    /**
+     * 그룹에서 멤버를 삭제한다.
+     *
+     * @param organizationName
+     * @param userId
+     * @return
+     */
+    @Transactional
+    public static Result deleteMember(String organizationName, Long userId) {
+        Result result = validateForDeleteMember(organizationName, userId);
+        if (result != null) {
+            return result;
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        OrganizationUser.delete(organization.id, userId);
+
+        if (UserApp.currentUser().id.equals(userId)) {
+            return okWithLocation(routes.OrganizationApp.organization(organizationName).url());
+        } else {
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+    }
+
+    /**
+     * {@link #deleteMember(String, Long)}를 위해 사용되는 변수의 유효성 검사를 한다.
+     *
+     * @param organizationName
+     * @param userId
+     * @return
+     */
+    private static Result validateForDeleteMember(String organizationName, Long userId) {
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        if (organization == null) {
+            return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization));
+        }
+
+        if (!OrganizationUser.exist(organization.id, userId)) {
+            flash(Constants.WARNING, "organization.member.isNotAMember");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        User currentUser = UserApp.currentUser();
+        if (!AccessControl.isAllowed(currentUser, organization.asResource(), Operation.UPDATE)
+                && !currentUser.id.equals(userId)) {
+            flash(Constants.WARNING, "organization.member.needManagerRole");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        if (OrganizationUser.isAdmin(organization.id, userId) && organization.getAdmins().size() == 1) {
+            flash(Constants.WARNING, "organization.member.atLeastOneAdmin");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        return null;
+    }
+
+    /**
+     * 그룹 멤버의 권한을 수정한다.
+     *
+     * @param organizationName
+     * @param userId
+     * @return
+     */
+    @Transactional
+    public static Result editMember(String organizationName, Long userId) {
+        Form<Role> roleForm = form(Role.class).bindFromRequest();
+        Result result = validateForEditMember(roleForm, organizationName, userId);
+        if (result != null) {
+            return result;
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        OrganizationUser.assignRole(userId, organization.id, roleForm.get().id);
+
+        return status(Http.Status.NO_CONTENT);
+    }
+
+    /**
+     * {@link #editMember(String, Long)}를 위해 사용되는 변수의 유효성 검사를 한다.
+     *
+     * @param roleForm
+     * @param organizationName
+     * @param userId
+     * @return
+     */
+    private static Result validateForEditMember(Form<Role> roleForm, String organizationName, Long userId) {
+        if (roleForm.hasErrors()) {
+            flash(Constants.WARNING, "organization.member.unknownRole");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        if (organization == null) {
+            return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization));
+        }
+
+        if (!OrganizationUser.exist(organization.id, userId)) {
+            flash(Constants.WARNING, "organization.member.isNotAMember");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        User currentUser = UserApp.currentUser();
+        if (!AccessControl.isAllowed(currentUser, organization.asResource(), Operation.UPDATE)) {
+            flash(Constants.WARNING, "organization.member.needManagerRole");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+        if (OrganizationUser.isAdmin(organization.id, userId) && organization.getAdmins().size() == 1) {
+            flash(Constants.WARNING, "organization.member.atLeastOneAdmin");
+            return okWithLocation(routes.OrganizationApp.members(organizationName).url());
+        }
+
+        return null;
+    }
+
+    /**
+     * 그룹 페이지 안에있는 멤버 관리 페이지로 이동한다.
+     *
+     * @param organizationName
+     * @return
+     */
+    public static Result members(String organizationName) {
+        Result result = validateForSetting(organizationName);
+        if (result != null) {
+            return result;
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+
+        return ok(views.html.organization.members.render(organization, Role.findOrganizationRoles()));
+    }
+
+    /**
+     * 그룹 페이지 안에있는 그룹 관리 페이지로 이동한다.
+     *
+     * @param organizationName
+     * @return
+     */
+    public static Result settingForm(String organizationName) {
+        Result result = validateForSetting(organizationName);
+        if (result != null) {
+            return result;
+        }
+
+        Organization organization = Organization.findByOrganizationName(organizationName);
+
+        return ok(views.html.organization.setting.render(organization));
+    }
+
+    /**
+     * {@link #members(String)}를 위해 사용되는 변수의 유효성 검사를 한다.
+     *
+     * @param organizationName
+     * @return
+     */
+    private static Result validateForSetting(String organizationName) {
+        Organization organization = Organization.findByOrganizationName(organizationName);
+        if (organization == null) {
+            return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization));
+        }
+
+        User currentUser = UserApp.currentUser();
+        if (!OrganizationUser.isAdmin(organization.id, currentUser.id)) {
+            return forbidden(ErrorViews.Forbidden.render("error.forbidden", organization));
+        }
+
+        return null;
+    }
+
+    /**
+     * {@code location}을 JSON 형태로 저장하여 ok와 함께 리턴한다.
+     *
+     * Ajax 요청에 대해 redirect를 리턴하면 정상 작동하지 않음으로 ok에 redirect loation을 포함하여 리턴한다.
+     * 클라이언트에서 {@code location}을 확인하여 redirect 시킨다.
+     *
+     * @param location
+     * @return
+     */
+    private static Result okWithLocation(String location) {
+        ObjectNode result = Json.newObject();
+        result.put("location", location);
+
+        return ok(result);
+    }
+
+    private static Result validateForupdateOrganizationInfo(String organizationName) {
+        Result result = validateForSetting(organizationName);
+
+        if (result == null) {
+            Form<Organization> organizationForm = form(Organization.class).bindFromRequest();
+            if (organizationForm.hasErrors()) {
+                Organization organization = Organization.findByOrganizationName(organizationName);
+                return badRequest(setting.render(organization));
+            }
+        }
+
+        return result;
+    }
+
+    public static Result updateOrganizationInfo(String organizationName) throws IOException, NoSuchAlgorithmException {
+        Result result = validateForupdateOrganizationInfo(organizationName);
+        if (result != null) {
+            return result;
+        }
+
+        Form<Organization> organizationForm = form(Organization.class).bindFromRequest();
+        Organization organization = organizationForm.get();
+        Http.MultipartFormData body = request().body().asMultipartFormData();
+        Http.MultipartFormData.FilePart filePart = body.getFile("logoPath");
+
+        if (!isEmptyFilePart(filePart)) {
+            if(!isImageFile(filePart.getFilename())) {
+                flash(Constants.WARNING, "project.logo.alert");
+                organizationForm.reject("logoPath");
+            } else if (filePart.getFile().length() > LOGO_FILE_LIMIT_SIZE) {
+                flash(Constants.WARNING, "project.logo.fileSizeAlert");
+                organizationForm.reject("logoPath");
+            } else {
+                Attachment.deleteAll(organization.asResource());
+                new Attachment().store(filePart.getFile(), filePart.getFilename(), organization.asResource());
+            }
+        }
+
+        organization.update();
+
+        return redirect(routes.OrganizationApp.settingForm(organizationName));
+    }
+}
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -8,6 +8,7 @@
 import com.avaje.ebean.Page;
 
 import controllers.annotation.IsAllowed;
+import info.schleichardt.play2.mailplugin.Mailer;
 import models.*;
 import models.Project.State;
 import models.enumeration.Operation;
@@ -16,16 +17,21 @@
 import models.enumeration.RoleType;
 
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.mail.HtmlEmail;
 import org.codehaus.jackson.JsonNode;
 import org.codehaus.jackson.node.ObjectNode;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.NoHeadException;
+import org.jsoup.Jsoup;
 import org.tmatesoft.svn.core.SVNException;
 
+import play.Logger;
 import play.data.Form;
 import play.data.validation.ValidationError;
 import play.db.ebean.Transactional;
+import play.i18n.Messages;
 import play.libs.Json;
 import play.mvc.Controller;
 import play.mvc.Http;
@@ -43,6 +49,7 @@
 import views.html.project.delete;
 import views.html.project.home;
 import views.html.project.setting;
+import views.html.project.transfer;
 
 import javax.servlet.ServletException;
 
@@ -52,6 +59,7 @@
 
 import static play.data.Form.form;
 import static play.libs.Json.toJson;
+import static utils.LogoUtil.*;
 
 
 /**
@@ -61,11 +69,6 @@
 public class ProjectApp extends Controller {
 
     private static final int ISSUE_MENTION_SHOW_LIMIT = 1000;
-
-    private static final int LOGO_FILE_LIMIT_SIZE = 1024*1000*5; //5M
-
-    /** 프로젝트 로고로 사용할 수 있는 이미지 확장자 */
-    public static final String[] LOGO_TYPE = {"jpg", "jpeg", "png", "gif", "bmp"};
 
     /** 자동완성에서 보여줄 최대 프로젝트 개수 */
     private static final int MAX_FETCH_PROJECTS = 1000;
@@ -85,8 +88,6 @@
     private static final String HTML = "text/html";
 
     private static final String JSON = "application/json";
-
-
 
     /**
      * getProject
@@ -202,7 +203,8 @@
             flash(Constants.WARNING, "user.login.alert");
             return redirect(routes.UserApp.loginForm());
         } else {
-            return ok(create.render("title.newProject", form(Project.class)));
+            List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
+            return ok(create.render("title.newProject", form(Project.class), orgUserList));
         }
     }
 
@@ -241,25 +243,65 @@
         }
         Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest();
 
-        if (Project.exists(UserApp.currentUser().loginId, filledNewProjectForm.field("name").value())) {
-            flash(Constants.WARNING, "project.name.duplicate");
-            filledNewProjectForm.reject("name");
-            return badRequest(create.render("title.newProject", filledNewProjectForm));
-        } else if (filledNewProjectForm.hasErrors()) {
-            ValidationError error = filledNewProjectForm.error("name");
-            flash(Constants.WARNING, RestrictedValidator.message.equals(error.message()) ?
-                    "project.name.reserved.alert" : "project.name.alert");
-            filledNewProjectForm.reject("name");
-            return badRequest(create.render("title.newProject", filledNewProjectForm));
-        } else {
-            Project project = filledNewProjectForm.get();
-            project.owner = UserApp.currentUser().loginId;
-            ProjectUser.assignRole(UserApp.currentUser().id, Project.create(project), RoleType.MANAGER);
+        String owner = filledNewProjectForm.field("owner").value();
+        Organization organization = Organization.findByName(owner);
+        User user = User.findByLoginId(owner);
 
-            RepositoryService.createRepository(project);
-
-            return redirect(routes.ProjectApp.project(project.owner, project.name));
+        ValidationResult validation = validateForm(filledNewProjectForm, organization, user);
+        if (validation.hasError()) {
+            return validation.getResult();
         }
+
+        Project project = filledNewProjectForm.get();
+        if (Organization.isNameExist(owner)) {
+            project.organization = organization;
+        }
+        ProjectUser.assignRole(UserApp.currentUser().id, Project.create(project), RoleType.MANAGER);
+        RepositoryService.createRepository(project);
+        return redirect(routes.ProjectApp.project(project.owner, project.name));
+    }
+
+    private static ValidationResult validateForm(Form<Project> newProjectForm, Organization organization, User user) {
+        Result result = null;
+        boolean hasError = false;
+        List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
+
+        String owner = newProjectForm.field("owner").value();
+        String name = newProjectForm.field("name").value();
+        boolean ownerIsUser = User.isLoginIdExist(owner);
+        boolean ownerIsOrganization = Organization.isNameExist(owner);
+
+        if (!ownerIsUser && !ownerIsOrganization) {
+            newProjectForm.reject("owner", "project.owner.invalidate");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (ownerIsUser && UserApp.currentUser().id != user.id) {
+            newProjectForm.reject("owner", "project.owner.invalidate");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (ownerIsOrganization && !OrganizationUser.isAdmin(organization.id, UserApp.currentUser().id)) {
+            hasError = true;
+            result = forbidden(ErrorViews.Forbidden.render("'" + UserApp.currentUser().name + "' has no permission"));
+        }
+
+        if (Project.exists(owner, name)) {
+            newProjectForm.reject("name", "project.name.duplicate");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+
+        if (newProjectForm.hasErrors()) {
+            ValidationError error = newProjectForm.error("name");
+            newProjectForm.reject("name", RestrictedValidator.message.equals(error.message()) ?
+                    "project.name.reserved.alert" : "project.name.alert");
+            hasError = true;
+            result = badRequest(create.render("title.newProject", newProjectForm, orgUserList));
+        }
+        return new ValidationResult(result, hasError);
     }
 
     /**
@@ -330,37 +372,14 @@
             repository.setDefaultBranch(defaultBranch);
         }
 
-        if (!repository.renameTo(updatedProject.name)) {
-            throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name);
+        if (!project.name.equals(updatedProject.name)) {
+            if (!repository.renameTo(updatedProject.name)) {
+                throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name);
+            }
         }
-
+        
         updatedProject.update();
         return redirect(routes.ProjectApp.settingForm(loginId, updatedProject.name));
-    }
-
-    /**
-     * {@code filePart} 정보가 비어있는지 확인한다.<p />
-     * @param filePart
-     * @return {@code filePart}가 null이면 true, {@code filename}이 null이면 true, {@code fileLength}가 0 이하이면 true
-     */
-    private static boolean isEmptyFilePart(FilePart filePart) {
-        return filePart == null || filePart.getFilename() == null || filePart.getFilename().length() <= 0;
-    }
-
-    /**
-     * {@code filename}의 확장자를 체크하여 이미지인지 확인한다.<p />
-     *
-     * 이미지 확장자는 {@link controllers.ProjectApp#LOGO_TYPE} 에 정의한다.
-     * @param filename the filename
-     * @return true, if is image file
-     */
-    public static boolean isImageFile(String filename) {
-        boolean isImageFile = false;
-        for(String suffix : LOGO_TYPE) {
-            if(filename.toLowerCase().endsWith(suffix))
-                isImageFile = true;
-        }
-        return isImageFile;
     }
 
     /**
@@ -602,6 +621,141 @@
         Collections.reverse(userList);
     }
 
+    @IsAllowed(Operation.DELETE)
+    public static Result transferForm(String loginId, String projectName) {
+        Project project = Project.findByOwnerAndProjectName(loginId, projectName);
+        Form<Project> projectForm = form(Project.class).fill(project);
+        return ok(transfer.render("title.projectTransfer", projectForm, project));
+    }
+
+    @Transactional
+    @IsAllowed(Operation.DELETE)
+    public static Result transferProject(String loginId, String projectName) throws Exception {
+        Project project = Project.findByOwnerAndProjectName(loginId, projectName);
+        String destination = request().getQueryString("owner");
+
+        User destOwner = User.findByLoginId(destination);
+        Organization destOrg = Organization.findByName(destination);
+        if(destOwner.isAnonymous() && destOrg == null) {
+            return badRequest(ErrorViews.BadRequest.render());
+        }
+
+        ProjectTransfer pt = null;
+        // make a request to move to an user
+        if(!destOwner.isAnonymous()) {
+            pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOwner.loginId);
+        }
+        // make a request to move to an group
+        if(destOrg != null) {
+            pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOrg.name);
+        }
+        sendTransferRequestMail(pt);
+
+        // if the request is sent by XHR, response with 204 204 No Content and Location header.
+        String url = routes.ProjectApp.project(loginId, projectName).url();
+        if(HttpUtil.isRequestedWithXHR(request())){
+            response().setHeader("Location", url);
+            return status(204);
+        }
+
+        return redirect(url);
+    }
+
+    @Transactional
+    @With(AnonymousCheckAction.class)
+    public static synchronized Result acceptTransfer(Long id, String confirmKey) throws IOException, ServletException {
+        ProjectTransfer pt = ProjectTransfer.findValidOne(id);
+        if(pt == null) {
+            return notFound(ErrorViews.NotFound.render());
+        }
+        if(confirmKey == null || !pt.confirmKey.equals(confirmKey)) {
+            return badRequest(ErrorViews.BadRequest.render());
+        }
+
+        if(!AccessControl.isAllowed(UserApp.currentUser(), pt.asResource(), Operation.ACCEPT)) {
+            return forbidden(ErrorViews.Forbidden.render());
+        }
+
+        Project project = pt.project;
+
+        // Change the project's name and move the repository.
+        String newProjectName = Project.newProjectName(pt.destination, project.name);
+        PlayRepository repository = RepositoryService.getRepository(project);
+        repository.move(pt.sender.loginId, project.name, pt.destination, newProjectName);
+
+        User newOwnerUser = User.findByLoginId(pt.destination);
+        Organization newOwnerOrg = Organization.findByName(pt.destination);
+
+        // Change the project's information.
+        project.owner = pt.destination;
+        project.name = newProjectName;
+        if(newOwnerOrg != null) {
+            project.organization = newOwnerOrg;
+        }
+        project.update();
+
+        // Change roles.
+        if(!newOwnerUser.isAnonymous()) {
+            ProjectUser.assignRole(newOwnerUser.id, project.id, RoleType.MANAGER);
+        }
+        if(ProjectUser.isManager(pt.sender.id, project.id)) {
+            ProjectUser.assignRole(pt.sender.id, project.id, RoleType.MEMBER);
+        }
+
+        // Change the tranfer's status to be accepted.
+        pt.newProjectName = newProjectName;
+        pt.accepted = true;
+        pt.update();
+
+        // If the opposite request is exists, delete it.
+        ProjectTransfer.deleteExisting(project, pt.sender, pt.destination);
+
+        return redirect(routes.ProjectApp.project(project.owner, project.name));
+    }
+
+    private static void sendTransferRequestMail(ProjectTransfer pt) {
+        HtmlEmail email = new HtmlEmail();
+        try {
+            String acceptUrl = pt.getAcceptUrl();
+            String message = Messages.get("transfer.message.hello", pt.destination) + "\n\n"
+                    + Messages.get("transfer.message.detail", pt.project.name, pt.newProjectName, pt.project.owner, pt.destination) + "\n"
+                    + Messages.get("transfer.message.link") + "\n\n"
+                    + acceptUrl + "\n\n"
+                    + Messages.get("transfer.message.deadline") + "\n\n"
+                    + Messages.get("transfer.message.thank");
+
+            email.setFrom(Config.getEmailFromSmtp(), pt.sender.name);
+            email.addTo(Config.getEmailFromSmtp(), "Yobi");
+
+            User to = User.findByLoginId(pt.destination);
+            if(!to.isAnonymous()) {
+                email.addBcc(to.email, to.name);
+            }
+
+            Organization org = Organization.findByName(pt.destination);
+            if(org != null) {
+                List<OrganizationUser> admins = OrganizationUser.findAdminsOf(org);
+                for(OrganizationUser admin : admins) {
+                    email.addBcc(admin.user.email, admin.user.name);
+                }
+            }
+
+            email.setSubject(String.format("[%s] @%s wants to transfer project", pt.project.name, pt.sender.loginId));
+            email.setHtmlMsg(Markdown.render(message));
+            email.setTextMsg(message);
+            email.setCharset("utf-8");
+            email.addHeader("References", "<" + acceptUrl + "@" + Config.getHostname() + ">");
+            email.setSentDate(pt.requested);
+            Mailer.send(email);
+            String escapedTitle = email.getSubject().replace("\"", "\\\"");
+            String logEntry = String.format("\"%s\" %s", escapedTitle, email.getBccAddresses());
+            play.Logger.of("mail").info(logEntry);
+        } catch (Exception e) {
+            Logger.warn("Failed to send a notification: "
+                    + email + "\n" + ExceptionUtils.getStackTrace(e));
+        }
+    }
+
     private static void addCodeCommenters(String commitId, Long projectId, List<User> userList) {
         Project project = Project.find.byId(projectId);
 
app/controllers/UserApp.java
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
@@ -432,7 +432,10 @@
     }
 
     /**
-     * 사용자 정보 조회
+     * 사용자 또는 그룹 정보 조회
+     *
+     * {@code loginId}에 해당하는 그룹이 있을 때는 그룹을 보여주고 해당하는
+     * 그룹이 없을 경우에는 {@code loginId}에 해당하는 사용자 페이지를 보여준다.
      *
      * when: 사용자 로그인 아이디나 아바타를 클릭할 때 사용한다.
      *
@@ -444,6 +447,11 @@
      * @return
      */
     public static Result userInfo(String loginId, String groups, int daysAgo, String selected) {
+        Organization org = Organization.findByName(loginId);
+        if(org != null) {
+            return redirect(routes.OrganizationApp.organization(org.name));
+        }
+
         if (daysAgo == UNDEFINED) {
             Cookie cookie = request().cookie(DAYS_AGO_COOKIE);
             if (cookie != null) {
 
app/models/Organization.java (added)
+++ app/models/Organization.java
@@ -0,0 +1,135 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keesun Baik, Wansoon Park, ChangSung Kim
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package models;
+
+import models.enumeration.ProjectScope;
+import models.enumeration.ResourceType;
+import models.resource.GlobalResource;
+import models.resource.Resource;
+import play.data.format.Formats;
+import play.data.validation.Constraints;
+import play.db.ebean.Model;
+import utils.ReservedWordsValidator;
+
+import javax.persistence.*;
+import java.util.*;
+
+@Entity
+public class Organization extends Model {
+
+    private static final long serialVersionUID = -1L;
+
+    public static final Finder<Long, Organization> find = new Finder<>(Long.class, Organization.class);
+
+    @Id
+    public Long id;
+
+    @Constraints.Pattern(value = "^" + User.LOGIN_ID_PATTERN + "$", message = "user.wrongloginId.alert")
+    @Constraints.Required
+    @Constraints.ValidateWith(ReservedWordsValidator.class)
+    public String name;
+
+    @Formats.DateTime(pattern = "yyyy-MM-dd")
+    public Date created;
+
+    @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
+    public List<Project> projects;
+
+    @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
+    public List<OrganizationUser> users;
+
+    public String descr;
+
+    public void add(OrganizationUser ou) {
+        this.users.add(ou);
+    }
+
+    public static Organization findByName(String name) {
+        return find.where().eq("name", name).findUnique();
+    }
+
+    public static boolean isNameExist(String name) {
+        int findRowCount = find.where().ieq("name", name).findRowCount();
+        return (findRowCount != 0);
+    }
+
+    public List<Project> getVisiableProjects(User user) {
+        List<Project> result = new ArrayList<>();
+        if(OrganizationUser.isAdmin(this.id, user.id)) {
+            // 모든 프로젝트
+            result.addAll(this.projects);
+        } else if(OrganizationUser.isMember(this.id, user.id)) {
+            // private 프로젝트를 제외한 모든 프로젝트와 자신이 멤버로 속한 프로젝트
+            for(Project p : this.projects) {
+                if(p.projectScope != ProjectScope.PRIVATE || ProjectUser.isMember(user.id, p.id)) {
+                    result.add(p);
+                }
+            }
+        } else {
+            // public 프로젝트와 자신이 멤버로 속한 프로젝트
+            for(Project p : this.projects) {
+                if(p.projectScope == ProjectScope.PUBLIC || ProjectUser.isMember(user.id, p.id)) {
+                    result.add(p);
+                }
+            }
+        }
+
+        // 정렬
+        Collections.sort(result, new Comparator<Project>() {
+            @Override
+            public int compare(Project p1, Project p2) {
+                return p1.name.compareTo(p2.name);
+            }
+        });
+
+        return result;
+    }
+
+    public static Organization findByOrganizationName(String organizationName) {
+        return find.where().ieq("name", organizationName).findUnique();
+    }
+
+    /**
+     * As resource.
+     *
+     * @return the resource
+     */
+    public Resource asResource() {
+        return new GlobalResource() {
+
+            @Override
+            public String getId() {
+                return id.toString();
+            }
+
+            @Override
+            public ResourceType getType() {
+                return ResourceType.ORGANIZATION;
+            }
+
+        };
+    }
+
+    public List<OrganizationUser> getAdmins() {
+        return OrganizationUser.findAdminsOf(this);
+    }
+}
+
 
app/models/OrganizationUser.java (added)
+++ app/models/OrganizationUser.java
@@ -0,0 +1,108 @@
+package models;
+
+import java.util.List;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+
+import models.enumeration.RoleType;
+import play.db.ebean.Model;
+
+/**
+ * @author Keeun Baik
+ */
+@Entity
+public class OrganizationUser extends Model {
+
+    private static final long serialVersionUID = -1L;
+
+    public static final Finder<Long, OrganizationUser> find = new Finder<>(Long.class, OrganizationUser.class);
+
+    @Id
+    public Long id;
+
+    @ManyToOne
+    public User user;
+
+    @ManyToOne
+    public Organization organization;
+
+    @ManyToOne
+    public Role role;
+
+    public static List<OrganizationUser> findAdminsOf(Organization organization) {
+        return find.where()
+                .eq("organization", organization)
+                .eq("role", Role.findByName("org_admin"))
+                .findList();
+    }
+    public static List<OrganizationUser> findByAdmin(Long userId) {
+        return find.where().eq("role", Role.findByRoleType(RoleType.ORG_ADMIN))
+                    .eq("user.id", userId)
+                    .findList();
+    }
+    public static boolean isAdmin(Long organizationId, Long userId) {
+        return contains(organizationId, userId, RoleType.ORG_ADMIN);
+    }
+
+    public static boolean isMember(Long organizationId, Long userId) {
+        return contains(organizationId, userId, RoleType.ORG_MEMBER);
+    }
+
+    private static boolean contains(Long organizationId, Long userId, RoleType roleType) {
+        int rowCount = find.where().eq("organization.id", organizationId)
+                .eq("user.id", userId)
+                .eq("role.id", Role.findByRoleType(roleType).id)
+                .findRowCount();
+        return rowCount > 0;
+    }
+
+    public static void assignRole(Long userId, Long organizationId, Long roleId) {
+        OrganizationUser organizationUser = OrganizationUser.findByOrganizationIdAndUserId(organizationId, userId);
+
+        if (organizationUser == null) {
+            OrganizationUser.create(userId, organizationId, roleId);
+        } else {
+            Role role = Role.findById(roleId);
+
+            if (role != null) {
+                organizationUser.role = role;
+                organizationUser.update();
+            }
+        }
+    }
+
+    public static OrganizationUser findByOrganizationIdAndUserId(Long organizationId, Long userId) {
+        return find.where().eq("user.id", userId)
+                .eq("organization.id", organizationId)
+                .findUnique();
+    }
+
+    public static void create(Long userId, Long organizationId, Long roleId) {
+        OrganizationUser organizationUser = new OrganizationUser();
+        organizationUser.user = User.find.byId(userId);
+        organizationUser.organization = Organization.find.byId(organizationId);
+        organizationUser.role = Role.findById(roleId);
+        organizationUser.save();
+    }
+
+    public static void delete(Long organizationId, Long userId) {
+        OrganizationUser organizationUser = OrganizationUser.findByOrganizationIdAndUserId(organizationId, userId);
+
+        if (organizationUser != null) {
+            organizationUser.delete();
+        }
+    }
+
+    public static boolean exist(Long organizationId, Long userId) {
+        return findByOrganizationIdAndUserId(organizationId, userId) != null;
+    }
+
+    public static List<OrganizationUser> findByUser(User user, int size) {
+        return find.where().eq("user", user)
+                .order().asc("organization.name")
+                .setMaxRows(size)
+                .findList();
+    }
+}
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -3,7 +3,7 @@
 import com.avaje.ebean.Ebean;
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
-import controllers.routes;
+import models.enumeration.ProjectScope;
 import models.enumeration.RequestState;
 import models.enumeration.ResourceType;
 import models.enumeration.RoleType;
@@ -119,6 +119,12 @@
     public Integer defaultReviewerCount = 1;
 
     public boolean isUsingReviewerCount;
+
+    @ManyToOne
+    public Organization organization;
+
+    @Enumerated(EnumType.STRING)
+    public ProjectScope projectScope;
 
     /**
      * 신규 프로젝트를 생성한다.
@@ -733,6 +739,7 @@
     @Override
     public void delete() {
         deleteProjectVisitations();
+        deleteProjectTransfer();
         deleteFork();
         deletePullRequests();
 
@@ -770,8 +777,15 @@
 
     private void deleteProjectVisitations() {
         List<ProjectVisitation> pvs = ProjectVisitation.findByProject(this);
-        for(ProjectVisitation pv : pvs) {
+        for (ProjectVisitation pv : pvs) {
             pv.delete();
+        }
+    }
+
+    private void deleteProjectTransfer() {
+        List<ProjectTransfer> pts = ProjectTransfer.findByProject(this);
+        for(ProjectTransfer pt : pts) {
+            pt.delete();
         }
     }
 
@@ -806,7 +820,7 @@
      * @param projectName
      * @return
      */
-    private static String newProjectName(String loginId, String projectName) {
+    public static String newProjectName(String loginId, String projectName) {
         Project project = Project.findByOwnerAndProjectName(loginId, projectName);
         if(project == null) {
             return projectName;
@@ -924,4 +938,8 @@
         return Watch.countBy(resource.getType(), resource.getId());
     }
 
+    public boolean hasGroup() {
+        return this.organization != null;
+    }
+
 }
 
app/models/ProjectTransfer.java (added)
+++ app/models/ProjectTransfer.java
@@ -0,0 +1,158 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2013 NAVER Corp.
+ * http://yobi.io
+ *
+ * @Author Keeun Baik
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package models;
+
+import controllers.routes;
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+import models.resource.ResourceConvertible;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.joda.time.DateTime;
+import play.db.ebean.Model;
+import utils.Url;
+
+import javax.persistence.*;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 프로젝트 이관 정보
+ *
+ * 이관 요청을 할 때 새로운 프로젝트 이관 정보를 생성하고, 이관이 완려되면 {@code accepted}가 true로 바뀐다.
+ * 새로운 이관 요청을 만들때 받을 사용자의 프로젝트 이름을 확인하고 중복되는 프로젝트가 있을 경우에는 프로젝트 fork와 동일한 정책으로 프로젝트 이름을 변경한다.
+ * 동일한 프로젝트를 동일한 사용자에서 동일한 사용자로 여러번 이관 요청을 할 경우에는 이전 요청의 날짜와 확인키를 변경한다.
+ * 요청이 수락되면 이전 이관 요청 중에서 동일한 프로젝트를 반대로 주고받은 요청을 삭제한다.
+ */
+@Entity
+public class ProjectTransfer extends Model implements ResourceConvertible {
+
+    private static final long serialVersionUID = 1L;
+
+    public static Finder<Long, ProjectTransfer> find = new Finder<>(Long.class, ProjectTransfer.class);
+
+    @Id
+    public Long id;
+
+    // who requested this transfer.
+    @ManyToOne
+    public User sender;
+
+    /**
+     * Destination can be either an user or an organization.
+     * If you want to transfer to an user, then destination have to be set with the user's loginId.
+     * If you want to transfer to an organization, then destination have to be set with the organization's name.
+     */
+    public String destination;
+
+    @ManyToOne
+    public Project project;
+
+    @Temporal(TemporalType.TIMESTAMP)
+    public Date requested;
+
+    public String confirmKey;
+
+    public boolean accepted;
+
+    public String newProjectName;
+
+    public static ProjectTransfer requestNewTransfer(Project project, User sender, String destination) {
+        ProjectTransfer pt = find.where()
+                .eq("project", project)
+                .eq("sender", sender)
+                .eq("destination", destination)
+                .findUnique();
+
+        if(pt != null) {
+            pt.requested = new Date();
+            pt.confirmKey = RandomStringUtils.randomAlphanumeric(50);
+            pt.update();
+        } else {
+            pt = new ProjectTransfer();
+            pt.project = project;
+            pt.sender = sender;
+            pt.destination = destination;
+            pt.requested = new Date();
+            pt.confirmKey = RandomStringUtils.randomAlphanumeric(50);
+            pt.newProjectName = Project.newProjectName(destination, project.name);
+            pt.save();
+        }
+
+        return pt;
+    }
+
+    public String getAcceptUrl() {
+        return Url.create(routes.ProjectApp.acceptTransfer(id, confirmKey).url());
+    }
+
+    /**
+     * 현재 시간 기준으로 하루 안에 생성되었고 수락하지 않은 {@code ProjectTransfer} 요청을 찾습니다.
+     * @param id
+     * @return
+     */
+    public static ProjectTransfer findValidOne(Long id) {
+        Date now = new Date();
+        DateTime oneDayBefore = new DateTime(now).minusDays(1);
+
+        return find.where()
+                .eq("id", id)
+                .eq("accepted", false)
+                .between("requested", oneDayBefore, now)
+                .findUnique();
+    }
+
+    public static void deleteExisting(Project project, User sender, String destination) {
+        ProjectTransfer pt = find.where()
+                .eq("project", project)
+                .eq("sender", sender)
+                .eq("destination", destination)
+                .findUnique();
+
+        if(pt != null) {
+            pt.delete();
+        }
+    }
+
+    public Resource asResource() {
+        return new Resource() {
+            @Override
+            public String getId() {
+                return id.toString();
+            }
+
+            @Override
+            public Project getProject() {
+                return project;
+            }
+
+            @Override
+            public ResourceType getType() {
+                return ResourceType.PROJECT_TRANSFER;
+            }
+        };
+    }
+
+    public static List<ProjectTransfer> findByProject(Project project) {
+        return find.where()
+                .eq("project", project)
+                .findList();
+    }
+}
app/models/Role.java
--- app/models/Role.java
+++ app/models/Role.java
@@ -65,4 +65,19 @@
                 .in("id", projectRoleIds)
                 .findList();
     }
+
+    /**
+     * 그룹과 관련된 롤들의 목록을 반환합니다.
+     *
+     * @return
+     */
+    public static List<Role> findOrganizationRoles() {
+        List<Long> organizationRoleIds = new ArrayList<>();
+        organizationRoleIds.add(RoleType.ORG_ADMIN.roleType());
+        organizationRoleIds.add(RoleType.ORG_MEMBER.roleType());
+
+        return find.where()
+                .in("id", organizationRoleIds)
+                .findList();
+    }
 }
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -152,6 +152,9 @@
      */
     public String lang;
 
+    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
+    public List<OrganizationUser> organizationUsers;
+
     public User() {
     }
 
@@ -604,4 +607,31 @@
 
         return this.recentlyVisitedProjects.findRecentlyVisitedProjects(size);
     }
+
+    public List<Organization> getOrganizations(int size) {
+        if(size < 1) {
+            throw new IllegalArgumentException("the size should be bigger then 0");
+        }
+        List<Organization> orgs = new ArrayList<>();
+        for(OrganizationUser ou : OrganizationUser.findByUser(this, size)) {
+            orgs.add(ou.organization);
+        }
+        return orgs;
+    }
+
+    public void createOrganization(Organization organization) {
+        OrganizationUser ou = new OrganizationUser();
+        ou.user = this;
+        ou.organization = organization;
+        ou.role = Role.findByRoleType(RoleType.ORG_ADMIN);
+        ou.save();
+
+        this.add(ou);
+        organization.add(ou);
+        this.update();
+    }
+
+    private void add(OrganizationUser ou) {
+        this.organizationUsers.add(ou);
+    }
 }
 
app/models/enumeration/ProjectScope.java (added)
+++ app/models/enumeration/ProjectScope.java
@@ -0,0 +1,9 @@
+package models.enumeration;
+
+/**
+ * @author Keeun Baik
+ */
+public enum ProjectScope {
+
+    PRIVATE, PROTECTED, PUBLIC;
+}
app/models/enumeration/ResourceType.java
--- app/models/enumeration/ResourceType.java
+++ app/models/enumeration/ResourceType.java
@@ -30,6 +30,8 @@
     COMMIT("commit"),
     COMMENT_THREAD("comment_thread"),
     REVIEW_COMMENT("review_comment"),
+    ORGANIZATION("organization"),
+    PROJECT_TRANSFER("project_transfer"),
     NOT_A_RESOURCE("");
 
     private String resource;
app/models/enumeration/RoleType.java
--- app/models/enumeration/RoleType.java
+++ app/models/enumeration/RoleType.java
@@ -1,7 +1,7 @@
 package models.enumeration;
 
 public enum RoleType {
-    MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l);
+    MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l), ORG_ADMIN(6l), ORG_MEMBER(7l);
 
     private Long roleType;
 
app/models/resource/Resource.java
--- app/models/resource/Resource.java
+++ app/models/resource/Resource.java
@@ -1,7 +1,6 @@
 package models.resource;
 
 import actions.support.PathParser;
-import controllers.routes;
 import models.*;
 import models.enumeration.ResourceType;
 import play.db.ebean.Model;
@@ -53,6 +52,9 @@
                 break;
             case REVIEW_COMMENT:
                 finder = ReviewComment.find;
+                break;
+            case ORGANIZATION:
+                finder = Organization.find;
                 break;
             case COMMIT:
                 try {
@@ -124,6 +126,9 @@
                 return User.find.byId(longId).avatarAsResource();
             case REVIEW_COMMENT:
                 return ReviewComment.find.byId(longId).asResource();
+            case ORGANIZATION:
+                resource = Organization.find.byId(longId).asResource();
+                break;
             default:
                 throw new IllegalArgumentException(getInvalidResourceTypeMessage(resourceType));
         }
app/playRepository/GitRepository.java
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
@@ -9,7 +9,6 @@
 import models.enumeration.ResourceType;
 import models.resource.Resource;
 import models.support.ModelLock;
-
 import org.apache.commons.collections.IteratorUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -40,7 +39,6 @@
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.io.NullOutputStream;
 import org.tmatesoft.svn.core.SVNException;
-
 import play.Logger;
 import play.libs.Json;
 import utils.FileUtil;
@@ -1928,16 +1926,7 @@
      */
     @Override
     public boolean renameTo(String projectName) {
-
-        repository.close();
-        WindowCacheConfig config = new WindowCacheConfig();
-        config.install();
-        File src = new File(getGitDirectory(this.ownerName, this.projectName));
-        File dest = new File(getGitDirectory(this.ownerName, projectName));
-
-        src.setWritable(true);
-
-        return src.renameTo(dest);
+        return move(this.ownerName, this.projectName, this.ownerName, projectName);
     }
 
     @Override
@@ -2015,4 +2004,26 @@
         RevWalk revWalk = new RevWalk(repository);
         return revWalk.parseCommit(objectId);
     }
+
+    public boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName) {
+        repository.close();
+        WindowCacheConfig config = new WindowCacheConfig();
+        config.install();
+
+        File srcGitDirectory = new File(getGitDirectory(srcProjectOwner, srcProjectName));
+        File destGitDirectory = new File(getGitDirectory(desrProjectOwner, destProjectName));
+        File srcGitDirectoryForMerging = new File(getDirectoryForMerging(srcProjectOwner, srcProjectName));
+        File destGitDirectoryForMerging = new File(getDirectoryForMerging(desrProjectOwner, destProjectName));
+        srcGitDirectory.setWritable(true);
+        srcGitDirectoryForMerging.setWritable(true);
+
+        try {
+            org.apache.commons.io.FileUtils.moveDirectory(srcGitDirectory, destGitDirectory);
+            org.apache.commons.io.FileUtils.moveDirectory(srcGitDirectoryForMerging, destGitDirectoryForMerging);
+            return true;
+        } catch (IOException e) {
+            play.Logger.error("Move Failed", e);
+            return false;
+        }
+    }
 }
app/playRepository/PlayRepository.java
--- app/playRepository/PlayRepository.java
+++ app/playRepository/PlayRepository.java
@@ -156,4 +156,15 @@
      * @return 저장소가 비어있을시 true / 비어있지 않을시 false
      */
     boolean isEmpty();
+
+    /**
+     * 저장소를 옮긴다.
+     *
+     * @param srcProjectOwner
+     * @param srcProjectName
+     * @param desrProjectOwner
+     * @param destProjectName
+     * @return
+     */
+    boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName);
 }
app/playRepository/SVNRepository.java
--- app/playRepository/SVNRepository.java
+++ app/playRepository/SVNRepository.java
@@ -1,36 +1,35 @@
 package playRepository;
 
-import java.io.*;
-import java.util.*;
-
-import javax.servlet.*;
-
+import controllers.ProjectApp;
 import controllers.UserApp;
 import controllers.routes;
 import models.Project;
 import models.User;
 import models.enumeration.ResourceType;
 import models.resource.Resource;
-
+import org.apache.commons.io.FileUtils;
 import org.apache.tika.Tika;
 import org.codehaus.jackson.node.ObjectNode;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.errors.AmbiguousObjectException;
-import org.tigris.subversion.javahl.*;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.tigris.subversion.javahl.ClientException;
 import org.tmatesoft.svn.core.*;
 import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
 import org.tmatesoft.svn.core.wc.SVNClientManager;
 import org.tmatesoft.svn.core.wc.SVNDiffClient;
 import org.tmatesoft.svn.core.wc.SVNRevision;
-
-import controllers.ProjectApp;
 import play.libs.Json;
 import utils.FileUtil;
 import utils.GravatarUtil;
 
-import org.joda.time.format.*;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 public class SVNRepository implements PlayRepository {
 
@@ -387,13 +386,7 @@
      */
     @Override
     public boolean renameTo(String projectName) {
-
-        File src = new File(getRepoPrefix() + this.ownerName + "/" + this.projectName);
-        File dest = new File(getRepoPrefix() + this.ownerName + "/" + projectName);
-        src.setWritable(true);
-
-        return src.renameTo(dest);
-
+        return move(this.ownerName, this.projectName, this.ownerName, projectName);
     }
 
     @Override
@@ -437,4 +430,18 @@
             }
         }
     }
+
+    public boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName) {
+        File src = new File(getRepoPrefix() + srcProjectOwner + "/" + srcProjectName);
+        File dest = new File(getRepoPrefix() + desrProjectOwner + "/" + destProjectName);
+        src.setWritable(true);
+
+        try {
+            FileUtils.moveDirectory(src, dest);
+            return true;
+        } catch (IOException e) {
+            play.Logger.error("Move Failed", e);
+            return false;
+        }
+    }
 }
app/utils/AccessControl.java
--- app/utils/AccessControl.java
+++ app/utils/AccessControl.java
@@ -3,6 +3,7 @@
 import models.Project;
 import models.ProjectUser;
 import models.User;
+import models.*;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
 import models.resource.GlobalResource;
@@ -39,6 +40,11 @@
         if (user.isSiteManager()) {
             return true;
         }
+
+        // project가 조직에 속하고 사용자가 Admin 이면 true
+        /*if (project.organization != null && project.organization.isAdmin(user)) {
+            return true;
+        }*/
 
         if (ProjectUser.isMember(user.id, project.id)) {
             // Project members can create anything.
@@ -135,7 +141,19 @@
         case USER_AVATAR:
             return user.id.toString().equals(resource.getId());
         case PROJECT:
-            return ProjectUser.isManager(user.id, Long.valueOf(resource.getId()));
+            // allow to managers of the project.
+            boolean isManager = ProjectUser.isManager(user.id, Long.valueOf(resource.getId()));
+            if(isManager) {
+                return true;
+            }
+            // allow to admins of the group of the project.
+            Project project = Project.find.byId(Long.valueOf(resource.getId()));
+            if(project.hasGroup()) {
+                return OrganizationUser.isAdmin(project.organization.id, user.id);
+            }
+            return false;
+        case ORGANIZATION:
+            return OrganizationUser.isAdmin(Long.valueOf(resource.getId()), user.id);
         default:
             // undefined
             return false;
@@ -155,10 +173,30 @@
      * @return true if the user has the permission
      */
     private static boolean isProjectResourceAllowed(User user, Project project, Resource resource, Operation operation) {
+        Organization org = project.organization;
+        if(org != null && OrganizationUser.isAdmin(org.id, user.id)) {
+            return true;
+        }
+
         if (user.isSiteManager()
                 || ProjectUser.isManager(user.id, project.id)
                 || isAllowedIfAuthor(user, resource)) {
             return true;
+        }
+
+        // If the resource is a project_transfer, only new owner can accept the request.
+        if(resource.getType() == ResourceType.PROJECT_TRANSFER) {
+            switch (operation) {
+                case ACCEPT:
+                    ProjectTransfer pt = ProjectTransfer.find.byId(Long.parseLong(resource.getId()));
+                    User to = User.findByLoginId(pt.destination);
+                    if(!to.isAnonymous()) {
+                        return user.loginId.equals(pt.destination);
+                    } else {
+                        Organization receivingOrg = Organization.findByName(pt.destination);
+                        return receivingOrg != null && OrganizationUser.isAdmin(receivingOrg.id, user.id);
+                    }
+            }
         }
 
         // Some resource's permission depends on their container.
@@ -167,7 +205,7 @@
             case ISSUE_ASSIGNEE:
             case ISSUE_MILESTONE:
             case ATTACHMENT:
-                switch(operation) {
+                switch (operation) {
                     case READ:
                         return isAllowed(user, resource.getContainer(), Operation.READ);
                     case UPDATE:
app/utils/ErrorViews.java
--- app/utils/ErrorViews.java
+++ app/utils/ErrorViews.java
@@ -1,5 +1,6 @@
 package utils;
 import controllers.UserApp;
+import models.Organization;
 import models.Project;
 import models.User;
 import play.api.templates.Html;
@@ -22,11 +23,16 @@
         }
 
         public Html render(String messageKey, String returnUrl) {
-            if(UserApp.currentUser() == User.anonymous){
+            if (UserApp.currentUser() == User.anonymous) {
                 return views.html.user.login.render("error.fobidden", null, returnUrl);
             } else {
                 return views.html.error.forbidden_default.render(messageKey);
             }
+        }
+
+        @Override
+        public Html render(String messageKey, Organization organization) {
+            return views.html.error.forbidden_organization.render(messageKey, organization);
         }
 
         @Deprecated
@@ -49,6 +55,12 @@
         @Override
         public Html render(String messageKey, Project project) {
             return render(messageKey, project, null);
+        }
+
+        @Override
+        public Html render(String messageKey, Organization organization) {
+            // TODO : make notfound view for organization
+            return views.html.error.notfound_default.render(messageKey);
         }
 
         @Override
@@ -78,6 +90,11 @@
         }
 
         @Override
+        public Html render(String messageKey, Organization organization) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
         public Html render(String messageKey, Project project, String target) {
             throw new UnsupportedOperationException();
         }
@@ -91,6 +108,12 @@
         @Override
         public Html render(String messageKey, Project project) {
             return views.html.error.badrequest.render(messageKey, project);
+        }
+
+        @Override
+        public Html render(String messageKey, Organization organization) {
+            // TODO : make badrequest view for organization
+            return views.html.error.badrequest_default.render(messageKey);
         }
 
         @Deprecated
@@ -139,6 +162,16 @@
 
     /**
      * 오류페이지 HTML을 레더링 한다.
+     * 메세지는 파라미터로 전달되는 messageKey를 사용하고 레이아웃은 그룹레벨이 된다.
+     *
+     * @param messageKey 메세지키
+     * @param organization 그룹 정보
+     * @return
+     */
+    public abstract Html render(String messageKey, Organization organization);
+
+    /**
+     * 오류페이지 HTML을 레더링 한다.
      * 메세지와 레이아웃은 세부타겟정보에 따라 이슈/게시판/프로젝트로 나뉘어 진다.
      *
      * @param messageKey 메세지키
 
app/utils/LogoUtil.java (added)
+++ app/utils/LogoUtil.java
@@ -0,0 +1,35 @@
+package utils;
+
+import play.mvc.Http;
+
+public class LogoUtil {
+    public static final int LOGO_FILE_LIMIT_SIZE = 1024*1000*5; //5M
+
+    /** 프로젝트 로고로 사용할 수 있는 이미지 확장자 */
+    public static final String[] LOGO_TYPE = {"jpg", "jpeg", "png", "gif", "bmp"};
+
+    /**
+     * {@code filePart} 정보가 비어있는지 확인한다.<p />
+     * @param filePart
+     * @return {@code filePart}가 null이면 true, {@code filename}이 null이면 true, {@code fileLength}가 0 이하이면 true
+     */
+    public static boolean isEmptyFilePart(Http.MultipartFormData.FilePart filePart) {
+        return filePart == null || filePart.getFilename() == null || filePart.getFilename().length() <= 0;
+    }
+
+    /**
+     * {@code filename}의 확장자를 체크하여 이미지인지 확인한다.<p />
+     *
+     * 이미지 확장자는 {@link #LOGO_TYPE} 에 정의한다.
+     * @param filename the filename
+     * @return true, if is image file
+     */
+    public static boolean isImageFile(String filename) {
+        boolean isImageFile = false;
+        for(String suffix : LOGO_TYPE) {
+            if(filename.toLowerCase().endsWith(suffix))
+                isImageFile = true;
+        }
+        return isImageFile;
+    }
+}
app/utils/TemplateHelper.scala
--- app/utils/TemplateHelper.scala
+++ app/utils/TemplateHelper.scala
@@ -5,20 +5,18 @@
 import play.i18n.Messages
 import controllers.routes
 import controllers.UserApp
-import java.security.MessageDigest
 import views.html._
 import java.net.URI
 import playRepository.DiffLine
 import playRepository.DiffLineType
 import models.CodeRange.Side
 import scala.collection.JavaConversions._
-import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
 import views.html.partial_diff_comment_on_line
 import views.html.partial_diff_line
 import views.html.git.partial_pull_request_event
+import models.Organization
 import models.PullRequestEvent
 import models.PullRequest
-import models.TimelineItem
 import models.Project
 import models.Issue
 import java.net.URLEncoder
@@ -27,7 +25,6 @@
 import play.api.i18n.Lang
 import models.CodeCommentThread
 import models.CommentThread
-import javax.swing.text.html.HTML
 
 object TemplateHelper {
 
@@ -212,10 +209,17 @@
   }
 
   def countOpenIssuesBy(project:Project, cond:java.util.Map[String,String]) = {
-    cond += ("state"->models.enumeration.State.OPEN.toString)
+    cond += ("state" -> models.enumeration.State.OPEN.toString)
     Issue.countIssuesBy(project.id, cond)
   }
 
+  def urlToOrganizationLogo(organization: Organization) = {
+    models.Attachment.findByContainer(organization.asResource) match {
+      case files if files.size > 0 => routes.AttachmentApp.getFile(files.head.id)
+      case _ => routes.Assets.at("images/bg-default-project.jpg")
+    }
+  }
+
   object DiffRenderer {
 
     def removedWord(word: String) = "<span class='remove'>" + word + "</span>"
 
app/utils/ValidationResult.java (added)
+++ app/utils/ValidationResult.java
@@ -0,0 +1,20 @@
+package utils;
+
+import play.mvc.Result;
+
+public class ValidationResult {
+    private Result result;
+    private boolean hasError;
+
+    public ValidationResult(Result result, boolean hasError) {
+        this.result = result;
+        this.hasError = hasError;
+    }
+
+    public boolean hasError(){
+        return hasError;
+    }
+    public Result getResult() {
+        return result;
+    }
+}
app/views/common/usermenu.scala.html
--- app/views/common/usermenu.scala.html
+++ app/views/common/usermenu.scala.html
@@ -10,13 +10,26 @@
         </form>
     </li>
     @if(session.contains("loginId")){
-    <li>
-        <a href="@routes.ProjectApp.newProjectForm()" data-toggle="tooltip" title="@Messages("button.newProject")" data-placement="bottom" class="ybtn ybtn-success">
-            <i class="yobicon-database-add"></i>
+    <li class="gnb-usermenu-dropdown">
+        <a href="javascript:void(0);" class="gnb-dropdown-toggle" data-toggle="dropdown">
+            <i class="yobicon-plus"></i>
+            <span class="caret"></span>
         </a>
+        <ul class="dropdown-menu flat right">
+            <li>
+                <a href="@routes.ProjectApp.newProjectForm()">
+                    @Messages("button.newProject")
+                </a>
+            </li>
+            <li>
+                <a href="@routes.OrganizationApp.newForm()">
+                    @Messages("title.newOrganization")
+                </a>
+            </li>
+        </ul>
     </li>
     @if(session.get("userId").equals("1")) {
-    <li class="gnb-usermenu-item">
+    <li class="gnb-usermenu-dropdown">
         <a href="@routes.SiteApp.userList()" data-toggle="tooltip" title="@Messages("menu.siteAdmin")" data-placement="bottom">
             <i class="yobicon-wrench"></i>
         </a>
@@ -29,7 +42,7 @@
             </span>
             <span class="caret"></span>
         </a>
-        <ul class="dropdown-menu flat right maximum">
+        <ul class="dropdown-menu flat right">
             <li class="title">
                 @User.findByLoginId(session.get("loginId")).name <span class="disabled">@{"@"}@session.get("loginId")</span>
             </li>
@@ -46,6 +59,19 @@
                     @Messages("title.logout")
                 </a>
             </li>
+            @defining(UserApp.currentUser.getOrganizations(5)) { groups =>
+                <li class="title">
+                    @Messages("title.organization")
+                    <span class="numberic">@groups.size</span>
+                </li>
+                @if(groups.length > 0) {
+                    @for(g <- groups){
+                        <li><a href="@routes.OrganizationApp.organization(g.name)"><span class="bold">@g.name</span></a></li>
+                    }
+                } else {
+                    <li class="empty">@Messages("organization.is.empty")</li>
+                }
+            }
             @defining(UserApp.currentUser.getVisitedProjects(10)){ visitedProjects =>
                 <li class="title">
                     @Messages("project.recently.visited")
 
app/views/error/forbidden_organization.scala.html (added)
+++ app/views/error/forbidden_organization.scala.html
@@ -0,0 +1,13 @@
+@(messageKey:String = "error.forbidden", organization: Organization)
+
+@siteLayout(organization.name, utils.MenuType.NONE) {
+    <div class="site-breadcrumb-outer">
+        <div class="site-breadcrumb-inner">
+            <div class="error-wrap">
+                <i class="ico ico-err2"></i>
+                <p>@Messages(messageKey)</p>
+            </div>
+        </div>
+    </div>
+}
+
 
app/views/organization/create.scala.html (added)
+++ app/views/organization/create.scala.html
@@ -0,0 +1,49 @@
+@(title:String, form: Form[Organization])
+
+@siteLayout("app.name", utils.MenuType.NONE) {
+<div class="page-wrap-outer">
+    <div class="project-page-wrap">
+        <div class="form-wrap new-project">
+            <form action="@routes.OrganizationApp.newOrganization()" method="post" name="new-org" class="frm-wrap">
+                <legend>
+                    @Messages("title.newOrganization")
+                </legend>
+                <dl>
+                    <dt>
+                        <div class="n-alert" data-errType="name">
+                            <div class="orange-txt">
+                                @if(flash.get("warning") != null) { <span class="warning">@Messages(flash.get("warning"))</span> }
+                                <span class="msg wrongName" style="display: none;">@Messages("project.wrongName")</span>
+                            </div>
+                        </div>
+                        <label for="name">@Messages("organization.name.placeholder")</label>
+                    </dt>
+                    <dd>
+                        <input id="name" type="text" name="name" class="text" placeholder="" maxlength="250" value="@form.field("name").value()" />
+                    </dd>
+
+                    <dt>
+                        <label for="descr">@Messages("organization.description.placeholder")</label>
+                    </dt>
+                    <dd>
+                        <textarea id="descr" name="descr" class="text textarea.span4" style="resize: vertical;" >@form.field("descr").value()</textarea>
+                    </dd>
+                </dl>
+                <div class="actions">
+                    <button class="ybtn ybtn-success">
+                        <i class="yobicon-friends"></i> @Messages("organization.create")
+                    </button>
+                    <a href="/" class="ybtn">@Messages("button.cancel")</a>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+<script type="text/javascript">
+    $(document).ready(function() {
+        $yobi.loadModule("organization.New", {
+            "sFormName" : "new-org"
+        });
+    });
+</script>
+}
 
app/views/organization/header.scala.html (added)
+++ app/views/organization/header.scala.html
@@ -0,0 +1,19 @@
+@(org:Organization)
+
+@import utils.TemplateHelper._
+@import utils.JodaDateUtil
+
+<div class="project-header-outer" style="background-image:url('@urlToOrganizationLogo(org)')">
+    <div class="project-header-inner">
+        <div class="project-header-wrap">
+            <div class="project-header-avatar">
+                <img src="@urlToOrganizationLogo(org)" />
+            </div>
+            <div class="project-breadcrumb-wrap">
+                <div class="project-breadcrumb">
+                    <span class="project-author"><a href="@routes.OrganizationApp.organization(org.name)">@org.name</a></span>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
 
app/views/organization/members.scala.html (added)
+++ app/views/organization/members.scala.html
@@ -0,0 +1,78 @@
+@(organization:Organization, roles: List[Role])
+
+@memberRole(userRole: String, loginId: String, userId: Long) = {
+@for(role <- roles){
+    <li data-value="@role.id" @if(role.name.equals(userRole)){ data-selected="true" class="active" }><a href="javascript:void(0)" data-action="apply" data-href="@routes.OrganizationApp.editMember(organization.name, userId)" data-loginId="@loginId">@Messages("user.role." + role.name)</a></li>
+}
+}
+
+@siteLayout(organization.name, utils.MenuType.NONE) {
+@header(organization)
+@menu(organization)
+
+<div class="page-wrap-outer">
+    <div class="project-page-wrap">
+        @partial_settingmenu(organization)
+        
+        <div class="inner-bubble">
+            <form class="nm" action="@routes.OrganizationApp.addMember(organization.name)" method="post" id="addNewMember">
+                <input type="text" class="text uname" id="loginId" name="loginId"
+                data-provider="typeahead" autocomplete="off"
+                placeholder="@Messages("project.members.addMember")"
+                pattern="^[a-zA-Z0-9-]+([_.][a-zA-Z0-9-]+)*$" title="@Messages("user.wrongloginId.alert")" /><!--
+            --><button type="submit" class="ybtn ybtn-success"><i class="yobicon-addfriend"></i> @Messages("button.add")</button>
+            </form>
+        </div>
+
+        <ul class="members project row-fluid">
+        @for(member <- organization.users){
+            @if(member.user != null){
+                <li class="member span6">
+                    <a href="@routes.UserApp.userInfo(member.user.loginId)" class="avatar-wrap mlarge pull-left mr10">
+                        <img src="@User.findByLoginId(member.user.loginId).avatarUrl" width="64" height="64">
+                    </a>
+                    <div class="member-name">@member.user.name</div>
+                    <div class="member-id">@{"@"}@member.user.loginId</div>
+                    <div class="member-setting">
+                        <div class="btn-group" data-name="roleof-@member.user.loginId">
+                            <button class="btn dropdown-toggle large" data-toggle="dropdown">
+                                <span class="d-label">@Messages("user.role." + member.role.name)</span>
+                                <span class="d-caret"><span class="caret"></span></span>
+                            </button>
+                            <ul class="dropdown-menu">@memberRole(member.role.name, member.user.loginId, member.user.id)</ul>
+                        </div>
+                        <a href="javascript:void(0)" data-action="delete" data-href="@routes.OrganizationApp.deleteMember(organization.name, member.user.id)" class="ybtn ybtn-danger ybtn-small">
+                        @Messages("button.delete")
+                        </a>
+                    </div>
+                </li>
+            }
+        }
+        </ul>
+
+        @** Confirm to delete member **@
+        <div id="alertDeletion" class="modal hide">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal">×</button>
+                <h3>@Messages("organization.member.delete")</h3>
+            </div>
+            <div class="modal-body">
+                <p>@Messages("organization.member.deleteConfirm")</p>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="ybtn ybtn-info ybtn-mini" id="deleteBtn">@Messages("button.yes")</button>
+                <button type="button" class="ybtn ybtn-mini" data-dismiss="modal">@Messages("button.no")</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<link rel="stylesheet" type="text/css" media="screen" href="/assets/javascripts/lib/mentionjs/mention.css">
+<script type="text/javascript">
+$(document).ready(function(){
+    $yobi.loadModule("organization.Member", {
+    "sActionURL": "@routes.UserApp.users()"
+    });
+});
+</script>
+}
 
app/views/organization/menu.scala.html (added)
+++ app/views/organization/menu.scala.html
@@ -0,0 +1,24 @@
+@(org:Organization)
+
+<div class="project-menu-outer">
+    <div class="project-menu-inner">
+        <ul class="project-menu-nav project-menu-gruop">
+            <li >
+                <a href="@routes.OrganizationApp.organization(org.name)">
+                    @Messages("title.organizationHome")
+                </a>
+            </li>
+        </ul>
+        <div class="project-setting">
+            <ul class="project-menu-nav">
+                <li class="">
+                    <a href="@routes.OrganizationApp.settingForm(org.name)">
+                        <i class="yobicon-cog"></i>
+                        <span class="blind">@Messages("menu.admin")</span>
+                        
+                    </a>
+                <li>
+            </ul>
+        </div>
+    </div>
+</div>
 
app/views/organization/partial_settingmenu.scala.html (added)
+++ app/views/organization/partial_settingmenu.scala.html
@@ -0,0 +1,20 @@
+@(organization:Organization)
+
+@isActiveSubMenu(calls: Call*) = @{
+    var menuState = ""
+    for(call <- calls) {
+        if(call.toString().equals(request().path().toString())) {
+            menuState = "active"
+        }
+    }
+    menuState
+}
+
+<ul class="nav nav-tabs">
+    <li class="@isActiveSubMenu(routes.OrganizationApp.settingForm(organization.name))">
+        <a href="@routes.OrganizationApp.settingForm(organization.name)">@Messages("organization.settingFrom")</a>
+    </li>
+    <li class="@isActiveSubMenu(routes.OrganizationApp.members(organization.name))">
+        <a href="@routes.OrganizationApp.members(organization.name)">@Messages("organization.member")</a>
+    </li>
+</ul>
 
app/views/organization/setting.scala.html (added)
+++ app/views/organization/setting.scala.html
@@ -0,0 +1,55 @@
+@(organization:Organization)
+
+@import utils.TemplateHelper._
+
+@siteLayout(organization.name, utils.MenuType.NONE) {
+@header(organization)
+@menu(organization)
+
+<div class="page-wrap-outer">
+    <div class="project-page-wrap">
+        @partial_settingmenu(organization)
+        <form id="saveSetting" method="post" action="@routes.OrganizationApp.updateOrganizationInfo(organization.name)" enctype="multipart/form-data" class="nm">
+            <input type="hidden" name="id" value="@organization.id">
+            <input type="hidden" name="name" value="@organization.name">
+            <div class="bubble-wrap gray">
+                <div class="box-wrap top clearfix frm-wrap" style="padding-top:20px;">
+                    <div class="setting-box left">
+                        <div class="logo-wrap" style="background-image:url('@urlToOrganizationLogo(organization)')"></div>
+                        <div class="logo-desc">
+                            <ul class="unstyled descs">
+                                <li><strong>@Messages("organization.logo")</strong></li>
+                                <li>@Messages("organization.logo.type") <span class="point">bmp, jpg, gif, png</span></li>
+                                <li>@Messages("organization.logo.maxFileSize") <span class="point">5MB</span></li>
+                                <li>
+                                    <div class="btn-wrap">
+                                        <div class="nbtn medium white fake-file-wrap">
+                                            <i class="yobicon-upload"></i> @Messages("button.upload")<input id="logoPath" type="file" class="file" name="logoPath" accept="image/*">
+                                        </div>
+                                    </div>
+                                </li>
+                            </ul>
+                        </div>
+                    </div>
+                    <dl class="setting-box right">
+                        <dt>
+                            <label for="project-desc">@Messages("organization.description.placeholder")</label>
+                        </dt>
+                        <dd>
+                            <textarea id="project-desc" name="descr" maxlength="250" class="textarea">@organization.descr</textarea>
+                        </dd>
+                    </dl>
+                </div>
+            </div>
+            <div class="box-wrap bottom">
+                <button id="save" type="submit" class="ybtn ybtn-success">@Messages("button.save")</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script language="javascript">
+    $(function() {
+        $yobi.loadModule("organization.Setting")
+    })
+</script>
+}
 
app/views/organization/view.scala.html (added)
+++ app/views/organization/view.scala.html
@@ -0,0 +1,105 @@
+@(org:Organization)
+
+@import utils.TemplateHelper._
+@import utils.JodaDateUtil
+
+@siteLayout(org.name, utils.MenuType.NONE) {
+@header(org)