[Notice] Announcing the End of Demo Server [Read me]
Jihan Kim 2013-03-07
update styles. recoding fileUploader javascript
uploader.js replaced with fileUploader.js.
@65c6d9920dc50904f3a08dd939b366809b3e4678
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -238,7 +238,7 @@
         &:hover { color:@orange; } 
     }
     
-    .page { padding:40px 39px 30px 39px; }
+    .page { padding:40px 39px 60px 39px; }
 }
 
 .page-wrap {
@@ -292,9 +292,8 @@
 }
 
 .page {
-    padding: 0 20px;
-    padding-top:6px;
-    
+    padding:6px 20px 60px 20px;
+
     /*margin-bottom: 40px;*/
     border: 1px solid @gray-ea;
     border-top:none;
@@ -917,7 +916,7 @@
 //-- new project
 .form-wrap {
     &.new-project {
-        margin: 30px 19px;
+        margin: 30px 19px 0px 19px;
         
         .text {
             width: 866px;
@@ -1555,21 +1554,19 @@
                     width: 128px; /*147px;*/
                 }
                 .btns {
+                    text-transform: capitalize;
                     margin-top: 6px;
+                    .n-btn { 
+                        /*width:54px; font-size:12px; letter-spacing: -1px;*/
+                        width:68px; padding:3px; padding-top:5px; font-size:12px; 
+                    }
                 }
                 p {
                     word-wrap: break-word;
                     margin-bottom: 2px;
                 }
-                .uname {
-                    font-size: 20px;
-                }
-                .name {
-                    font-size: 11px;
-                }
-                .btns {
-                    text-transform: capitalize;
-                }
+                .uname { font-size: 20px; }
+                .name  { font-size: 11px; }
             }
         }
     }
@@ -1729,7 +1726,7 @@
                     width: 584px;
                 }
                 .textarea {
-                    height: 354px;
+                    height: 305px;
                     resize: vertical;
                 }
                 .actions {
@@ -2051,12 +2048,28 @@
 
 }
 
+div.markdown-preview {
+    font-size: 12px; 
+    margin:0px; margin-bottom:9px; 
+    vertical-align : middle;
+    border:1px solid #ccc;
+    .border-radius(2px);
+    .inline-block;
+}
+
 .write-comment-box {
     padding: 17px 20px 15px;
     .write-comment-wrap {
         font-size: 0;
         margin-bottom: 10px;
+        
+        div.markdown-preview {
+            /*width : 727px !important;
+            min-height: 88px !important;*/ 
+            .border-radius(2px 0 0 2px);
+        }
     }
+    
     .comment-btn {
         vertical-align:top;
         background-color: @white;
@@ -2065,16 +2078,16 @@
         font-weight: bold;
         border: 1px solid @gray-cc;
         border-left: 0 none;
-        .border-radius(0 4px 4px 0);
+        .border-radius(0 2px 2px 0);
         width: 110px;
         height: 90px;
     }
     .comment {
         width: 715px;
         height: 80px;
-        margin: 0;
+        margin: 0; margin-bottom:9px;
         resize:vertical;
-        .border-radius(4px 0 0 4px);
+        .border-radius(2px 0 0 2px);
         .box-shadow(none);
         &:focus {
             border: 1px solid @gray-cc;
@@ -2097,6 +2110,7 @@
         margin-left: 5px;
     }
 }
+.board-actrow { margin:20px 0; }
 
 .attach-wrap {
     > div {
@@ -2109,6 +2123,10 @@
     .attach-info-wrap {
         width: 715px;
         font-size: 11px;
+        /*opacity:0;
+        -webkit-transition-duration:0.3s;
+        -webkit-transition-property:opacity;*/
+        
         .sp-line {
             border-left: 1px solid @white;
             .box-shadow(inset 1px 0 0 rgba(0, 0, 0, 0.15));
@@ -2145,9 +2163,14 @@
         .attached-file {
             color: #999;
             font-size: 11px;
+            cursor: pointer;
+            -webkit-transition-property: color;
+            -webkit-transition-duration: 0.5s;
+            &:hover { color: @orange; }
         }
         .attached-delete {
             margin-left: 10px;
+            cursor:pointer;
         }
     }
 }
@@ -2181,6 +2204,7 @@
             color:#959595; font-weight:bold;
             margin-right:5px;
         }
+        .n-alert .n-inner { font-weight:normal !important; }
     }
 }
 .content-wrap {    
@@ -2336,7 +2360,7 @@
         font-weight:bold;
         
         strong { 
-            color:#fff; .border-radius(3px); 
+            color:#fff; .border-radius(2px); 
             font-size: 11px; padding: 4px;
             text-shadow: -1px -1px rgba(0,0,0,0.3);
             margin-right: 5px;
@@ -2675,6 +2699,7 @@
 
 .user-box {
     margin: 15px 0 25px;
+    margin-bottom:0px;
     overflow: hidden;
 }
 
@@ -3001,3 +3026,4 @@
 hr.dark-gray { border-top:1px solid #d4d4d4; }
 */
 .alert { position:absolute; width:910px; top:76px; margin-left:auto; }
+form { margin:0 0 2px; }
(No newline at end of file)
app/views/board/newPost.scala.html
--- app/views/board/newPost.scala.html
+++ app/views/board/newPost.scala.html
@@ -7,23 +7,29 @@
 <div class="page">
   @views.html.prjmenu(project, utils.MenuType.BOARD, "main-menu-only")
   
-  <div class="content-wrap">
+  <div class="content-wrap frm-wrap">
     
     @helper.form(action=routes.BoardApp.newPost(project.owner, project.name), 'enctype -> "multipart/form-data", 'class->"nm"){
-      <div id="warning" class="n-alert hide">
-        <div class="n-inner">
-          <span class="msg">제목과 본문에 빈칸이 있습니다.</span>
-          <a href="#" class="ico btn-delete"></a>
-        </div>
-      </div>
-      <label for="title">
-        @helper.inputText(form("title"), 'class->"text title", 'placeholder -> Messages("post.new.title"), 'maxlength -> "250", 'tabindex -> 1)
-      </label>
-      <label for="content">
-        @helper.textarea(form("contents"), 'markdown -> true, 'class->"text content", 'tabindex -> 2)
-      </label>
-      @if(UserApp.currentUser() != UserApp.anonymous) {
-      <div id="upload" resourceType=@ResourceType.BOARD_POST></div>
+    
+    <dl>
+    	<dt>
+	      <div id="warning" class="n-alert hide">
+	        <div class="n-inner">
+	          <span class="msg">제목과 본문에 빈칸이 있습니다.</span>
+	          <a href="#" class="ico btn-delete"></a>
+	        </div>
+	      </div>
+	      <label for="title">@Messages("post.new.title")</label>
+    	</dt>
+    	<dd>
+    		@helper.inputText(form("title"), 'class->"text title", 'maxlength -> "250", 'tabindex -> 1)
+    	</dd>
+    </dl>
+        
+	@helper.textarea(form("contents"), 'markdown -> true, 'class->"text content", 'tabindex -> 2)
+
+    @if(UserApp.currentUser() != UserApp.anonymous) {
+      <div id="upload" resourceType="@ResourceType.BOARD_POST"></div>
       }
       <!--
       <div class="content-footer">
@@ -61,16 +67,17 @@
       </div>-->
       <div class="actions">
         <button class="btn-transparent n-btn orange med">SAVE</button>
-        <a href="javascript:history.back();return false;" class="n-btn gray med cancel">CANCEL</a>
+        <a href="javascript:history.back(); return false;" class="n-btn gray med cancel">CANCEL</a>
       </div>
     }
   </div>
 </div>
-<script>nforge.require("board.vaildate");</script>
 
 @views.html.markdown()
+
 <script type="text/javascript">
-nforge.require('shortcut.submit');
-nforge.require('board.new', "@routes.AttachmentApp.newFile");
+	nforge.require("board.vaildate");
+	nforge.require('shortcut.submit');
+	nforge.require('board.new', "@routes.AttachmentApp.newFile");
 </script>
 }
app/views/board/post.scala.html
--- app/views/board/post.scala.html
+++ app/views/board/post.scala.html
@@ -6,14 +6,18 @@
 @implicitField = @{ helper.FieldConstructor(simpleForm) }
 
 @main("상세보기", project, utils.MenuType.BOARD){
+
 <div class="page board-view">
   @views.html.prjmenu(project, utils.MenuType.BOARD, "main-menu-only")
   
+  @** Post Info **@
   <div class="board-header">
     <div class="board-id div">@post.id</div>
     <h1 class="title div">@post.title</h1>
     <div class="date div">@utils.TemplateHelper.agoString(post.ago())</div>
   </div>
+  
+  @** Content body **@
   <div class="board-body">
     <div class="author-info">
       <a href="@routes.UserApp.userInfo(post.authorLoginId)" class="pull-left img-rounded">
@@ -23,21 +27,35 @@
           <a href="@routes.UserApp.userInfo(post.authorLoginId)"><strong>@post.authorLoginId</strong></a> <!--<span class="name">(Loren Brichter)</span>-->
         </p>
         <p class="status">
-          <!--Hit <strong class="num">777</strong> -->Comment <strong class="num">@post.commentCount</strong><!-- Like <i class="ico ico-like-small"></i> <strong class="num">522</strong>-->
+          <!--Hit <strong class="num">777</strong> 
+          -->Comment <strong class="num">@post.commentCount</strong><!-- 
+          Like <i class="ico ico-like-small"></i> <strong class="num">522</strong>-->
         </p>
-          </div>
       </div>
-      <div class="content" markdown>@post.contents</div>
-      <div class="attachments" resourceType=@ResourceType.BOARD_POST resourceId=@post.id></div>
-      <!--
+    </div>
+    <div class="content" markdown="true">@post.contents</div>
+    <div class="attachments" resourceType="@ResourceType.BOARD_POST" resourceId="@post.id"></div>
+    <!--
       <ul class="attaches wm">
         <li class="attach"><i class="ico ico-clip"></i>K23.png (11KB)</li>
         <li class="attach"><i class="ico ico-clip"></i>K23.png (11KB)</li>
         <li class="attach"><i class="ico ico-clip"></i>K23.png (11KB)</li>
       </ul>-->
   </div>
-  <div class="board-comment-wrap">
+  <div class="board-footer board-actrow">
+  @isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){
+	<a href="@routes.BoardApp.editPostForm(project.owner, project.name, post.id)" class="n-btn orange med">@Messages("button.edit")</a>
+  }
+  @isAllowed(UserApp.currentUser(), post.asResource(), Operation.DELETE)){
+   	<a href="#deleteConfirm" data-toggle="modal" class="n-btn light-gray med">@Messages("button.delete")</a>
+  }
+    <a href="@routes.BoardApp.posts(project.owner, project.name)" class="n-btn gray med">@Messages("button.list")</a>
+  </div>
+
+@** Comment **@
+<div class="board-comment-wrap">
     <div class="comment-header"><strong>Comment</strong> <strong class="num">@post.comments.size()</strong></div>
+    
     <ul class="comments">
       @for(comment <-post.comments){
       <li class="comment">
@@ -61,64 +79,100 @@
       </li>
       }
     </ul>
-      @if(isCreatable(User.findByLoginId(session.get("loginId")), project, models.enumeration.ResourceType.BOARD_POST)){
-      <div class="write-comment-box">
+    
+@if(isCreatable(User.findByLoginId(session.get("loginId")), project, models.enumeration.ResourceType.BOARD_POST)){
+    <div class="write-comment-box">
         @helper.form(routes.BoardApp.newComment(project.owner, project.name, post.id), 'class->"nm", 'enctype -> "multipart/form-data"){
-          <div class="write-comment-wrap">
-            <!-- when the user signed in..
-            <textarea class="text comment" name="comment></textarea>
-            -->
-            <style>
-            .write-comment-wrap div[div=preview] {
-              font-size: 12px;
-               width : 729px; 
-               min-height: 80px; 
-               margin:0px; 
-               display: inline-block;
-               vertical-align : middle;
-             }
-            .write-comment-wrap div{font-size: 10px;}
-            </style>
-            @helper.textarea(commentForm("contents"), 'class->"text comment", 'markdown->true)
-            <button class="comment-btn">COMMENT</button>
+		<div class="write-comment-wrap">
+			@helper.textarea(commentForm("contents"), 'class->"text comment", 'markdown->true)
+			<button class="comment-btn">COMMENT</button>
+		</div>
+        
+		<div class="attach-wrap">
+			<div class="thumb-wrap">
+			@if(UserApp.currentUser() != UserApp.anonymous) {
+				<img src="@User.findByLoginId(session.get("loginId")).avatarUrl" class="img-rounded" width="32" height="32" alt="avatar">
+			} else {
+				<img src="@routes.Assets.at("images/default-avatar-34.png")" class="img-rounded" width="32" height="32" alt="avatar">
+			}
+			</div>
+			
+			@** fileUploader **@
             @if(UserApp.currentUser() != UserApp.anonymous) {
-            <div id="upload" resourceType=@ResourceType.BOARD_COMMENT></div>
-            }
-          </div>
-        }
-      </div>
-    }
-  </div>
-  <div class="board-footer">
-      <!--<a href="/add-notification" class="add-btn"><i class="ico ico-plus-blue"></i>자동알림추가</a>-->
-      <a href="@routes.BoardApp.posts(project.owner, project.name)" class="n-btn orange small">@Messages("button.list")</a>
-      @isAllowed(UserApp.currentUser(), post.asResource(), Operation.DELETE)){
-      <a data-toggle="modal" href="#deleteConfirm" class="n-btn black small">@Messages("button.delete")</a>
-      }
-      @isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){
-      <a href="@routes.BoardApp.editPostForm(project.owner, project.name, post.id)" class="n-btn blue small">@Messages("button.edit")</a>
-      }
-  </div>
 
-  <!--삭제확인상자-->
-  <div class="modal hide fade" id="deleteConfirm">
-    <div class="modal-header">
-      <button type="button" class="close" data-dismiss="modal">×</button>
-      <h3>확인</h3>
-    </div>
-    <div class="modal-body">
-      <p>게시글이 삭제되며 영원히 복구할수 없습니다.</p>
-      <p>그래도 삭제하시겠습니까?</p>
-    </div>
-    <div class="modal-footer">
-      <a class="n-btn red small" href="@routes.BoardApp.deletePost(project.owner, project.name, post.id)">예</a>
-      <a href="#" class="n-btn blue small" data-dismiss="modal">아니오</a>
-    </div>
+			<div id="upload" class="attach-info-wrap" resourceType="@ResourceType.BOARD_COMMENT">
+				<div>
+					<span class="progress-num">0%</span> <span class="sp-line">&nbsp;</span>
+					<strong>total</strong> <span class="total-num">0MB</span>
+				</div>
+				<div class="progress-wrap">
+					<div class="progress n4">
+						<div class="bar orange" style="width: 0%;"></div>
+					</div>
+				</div>
+				<!-- <a href="#!/cancel"><i class="ico btn-cancel"></i></a> -->
+			</div>
+			
+			<div class="btn-wrap">
+				<div class="ns-btn fake-file-wrap">
+					<i class="ico ico-plus-blue"></i>UPLOAD <input type="file" class="file" name="filePath">
+				</div>
+			</div>
+			}
+			@** end of fileUploader **@
+        
+		</div>
+		} @** end of comment form **@
+		<div class="attached-files-wrap">
+           <ul class="attached-files"></ul>
+		</div>
   </div>
+  <script type="text/template" id="tplAttachedFile"><!--
+	--><li class="attached-file" data-name="${fileName}" data-href="${fileHref}" data-mime="${mimeType}" data-size="${fileSize}">
+		<strong>${fileName}(${fileSizeReadable})</strong><!--
+		--><a class="attached-delete"><i class="ico btn-delete"></i></a>
+	</li>
+  </script>
+} @** end of write-comment-box **@
+  
 </div>
-@views.html.markdown()
-<script type="text/javascript">
-  nforge.require('shortcut.submit');
-  nforge.require('board.view', "@routes.AttachmentApp.newFile");
-</script>
+
+<div class="board-footer">
+
+@isAllowed(UserApp.currentUser(), post.asResource(), Operation.UPDATE)){
+	<a href="@routes.BoardApp.editPostForm(project.owner, project.name, post.id)" class="n-btn orange med">@Messages("button.edit")</a>
 }
+
+@isAllowed(UserApp.currentUser(), post.asResource(), Operation.DELETE)){
+   	<a href="#deleteConfirm" data-toggle="modal" class="n-btn light-gray med">@Messages("button.delete")</a>
+}
+
+    <a href="@routes.BoardApp.posts(project.owner, project.name)" class="n-btn gray med">@Messages("button.list")</a>
+
+</div>
+
+@** Confirm to delete post **@
+@** TODO: 메시지 다국어 처리할 것 **@
+<div id="deleteConfirm" class="modal hide fade">
+	<div class="modal-header">
+		<button type="button" class="close" data-dismiss="modal">×</button>
+		<h3>확인</h3>
+	</div>
+	<div class="modal-body">
+		<p>게시글이 삭제되며 영원히 복구할수 없습니다.</p>
+		<p>그래도 삭제하시겠습니까?</p>
+	</div>
+	<div class="modal-footer">
+		<a class="btn btn-danger med"
+			href="@routes.BoardApp.deletePost(project.owner, project.name, post.id)">예</a>
+		<a href="#" class="btn med" data-dismiss="modal">아니오</a>
+	</div>
+</div>
+
+@views.html.markdown()
+
+<script type="text/javascript">
+	nforge.require('shortcut.submit');
+	nforge.require('board.view', "@routes.AttachmentApp.newFile");
+</script>
+}
(No newline at end of file)
app/views/board/postList.scala.html
--- app/views/board/postList.scala.html
+++ app/views/board/postList.scala.html
@@ -6,7 +6,7 @@
 @import utils.AccessControl._
 @import scala.collection.immutable.Map
 
-@header(label:String, key:String) = {
+@** header(label:String, key:String) = {
   <th>
     <a key="@key" href="@routes.BoardApp.posts(project.owner, project.name)">@label</a>
     @if(key == param.key){
@@ -18,6 +18,7 @@
     }
   </th>
 }
+**@
 
 @main(title, project, utils.MenuType.BOARD) {
 <div class="page">
@@ -37,26 +38,27 @@
   </div>
 
   @if(page.getTotalRowCount == 0){
+  
+	<div class="error-wrap">
+			<i class="ico ico-err1"></i>
+			<p>@Messages("post.is.empty")</p>
+		</div>  
     <div>
-        <p class="emptyMessage">@Messages("post.is.empty")</p>
-    </div>
+
   } else {
+
   <div class="filter-wrap board">
       <div class="filters" id="order">
-          <a href="#" key="date" class="filter underConstruction">
-              @if(param.key == "date"){
-                  <i class="ico btn-gray-arrow @if(param.order == "desc"){down}"></i>
-              }
-                  날짜순
+          <a href="#" key="date" class="filter underConstruction @if(param.key == "date"){ active }">
+			<i class="ico btn-gray-arrow @if(param.key == "date" && param.order == "desc"){down}"></i>날짜순
           </a>
-          <a href="#" key="commentCount" class="filter active underConstruction">
-              @if(param.key == "commentCount"){
-                  <i class="ico btn-gray-arrow @if(param.order == "desc"){down}"></i>
-              }
-                  댓글순
+          
+          <a href="#" key="commentCount" class="filter underConstruction @if(param.key == "commentCount"){ active }">
+            <i class="ico btn-gray-arrow @if(param.key == "commentCount" && param.order == "desc"){down}"></i>댓글순
           </a>
       </div>
   </div>
+  
   <ul class="board-list">
     @for(post <- page.getList()){
         <li class="board">
@@ -77,20 +79,24 @@
     }
   </ul>
   }
+  
   @if(isCreatable(User.findByLoginId(session.get("loginId")), project, models.enumeration.ResourceType.BOARD_POST)){
   <div class="write-btn-wrap">
     <a href="@routes.BoardApp.newPostForm(project.owner, project.name)" class="n-btn orange med">@Messages("post.write")</a>
   </div>
   }
+  
   <div id="pagination">
     <!-- pagination.js will fill here. -->
   </div>
 </div>
-  <script src="@getJSLink("pagination")" type="text/javascript"></script>
-  <script type="text/javascript">
+
+<script src="@getJSLink("pagination")" type="text/javascript"></script>
+<script type="text/javascript">
   nforge.require('board.list');
   $(document).ready(function() {
     Pagination.update($('#pagination'), @page.getTotalPageCount);
   });
-  </script>
+</script>
+
 }
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -32,8 +32,9 @@
     <script src="@getJSLink("humanize")" type="text/javascript"></script>
     <script src="@getJSLink("validate")" type="text/javascript"></script>
     <script src="@getJSLink("jquery.zclip.min")" type="text/javascript"></script>
-    <script src="@getJSLink("uploader")" type="text/javascript"></script>
+    <script src="@getJSLink("fileUploader")" type="text/javascript"></script>
     <script src="@getJSLink("jquery.placeholder.min")" type="text/javascript"></script>
+    <script src="@getJSLink("jquery.tmpl")" type="text/javascript"></script>
     <script src="@routes.Application.jsMessages()" type="text/javascript"></script>
 </head>
 
@@ -55,5 +56,6 @@
             $(".underConstruction").tooltip({placement: 'left', title: 'Sorry! Under construction...'});
         });
     </script>
+    
 </body>
 </html>
app/views/login.scala.html
--- app/views/login.scala.html
+++ app/views/login.scala.html
@@ -11,7 +11,7 @@
             <form action="@routes.UserApp.login()" method="POST">
             	<dl>
             		<dt>
-            			<label for="email">@Messages("user.loginId")</label>
+            			<label for="loginId">@Messages("user.loginId")</label>
             		</dt>
             		<dd>
 						<input type="text" class="text email" id="loginId" autocomplete="off" name="loginId">
app/views/milestone/create.scala.html
--- app/views/milestone/create.scala.html
+++ app/views/milestone/create.scala.html
@@ -1,66 +1,104 @@
-@(title:String, form: Form[Milestone], projectInst: Project)
-
-
-@import utils.TemplateHelper._
-@project.projectMngMain(title, projectInst) {
-    @pageTitle(projectInst,"Milestone")
-    <div class="form-wrap milestone">
-        <form class="nm" action="@routes.MilestoneApp.newMilestone(projectInst.owner, projectInst.name)" method="post">
-            <div class="inner left">
-                <div class="n-alert hide" id="title_error">
-                    <div class="n-inner">
-                        <span class="msg">타이틀을 입력해주세요.</span>
-                        <a href="#!/close" class="ico btn-delete"></a>
-                    </div>
-                </div>
-                <label for="title">
-                    <input type="text" name="title" id="title" class="text" placeholder="새 마일스톤의 제목을 입력해주세요.">
-                </label>
-                <div class="n-alert hide" id="contents_error">
-                    <div class="n-inner">
-                        <span class="msg">내용을 입력해주세요.</span>
-                        <a href="#!/close" class="ico btn-delete"></a>
-                    </div>
-                </div>
-                <label for="contents">
-                    <textarea class="textarea" id="contents" name="contents" placeholder="내용을 입력해주세요."></textarea>
-                </label>
-                <div class="actions">
-                    <button type="submit" class="n-btn blue med btn-transparent save">SAVE</button>
-                    <a href="@routes.MilestoneApp.manageMilestones(projectInst.owner, projectInst.name)" class="n-btn gray med">CANCEL</a>
-                </div>
-            </div>
-            <div class="inner right">
-                <div class="cu-label">상태</div>
-                <div class="cu-desc">
-                    <input name="state" type="radio" checked class="radio-btn" value=@State.OPEN><label for="state" class="bg-radiobtn">Open</label>
-                    <input name="state" type="radio" class="radio-btn" value=@State.CLOSED><label for="state" class="bg-radiobtn">Closed</label>
-                </div>
-                <hr/>
-                <p>완료일을 선택하세요.</p>
-                <div class="n-alert hide" id="dueDate_error">
-                    <div class="n-inner">
-                        <span class="msg">완료일을 입력해주세요.</span>
-                        <a href="#!/close" class="ico btn-delete"></a>
-                    </div>
-                </div>
-                <label for="dueDate">
-                    <input type="text" name="dueDate" id="dueDate" class="validate due-date">
-                </label>
-            </div>
-        </form>
-    </div>
-
-    <script type="text/javascript">nforge.require('milestone.manage');</script>
-    <style>
-        @@IMPORT url(@getCSSLink("pikaday"));
-    </style>
-    <script src="@getJSLink("moment.min")" type="text/javascript"></script>
-    <script src="@getJSLink("pikaday/pikaday")" type="text/javascript"></script>
-    <script>
-        var picker = new Pikaday({ 
-            field: document.getElementById('dueDate'), 
-            format: 'YYYY-MM-DD',
-        });
-    </script>
-}
+@(title:String, form: Form[Milestone], projectInst: Project)
+
+
+@import utils.TemplateHelper._
+@project.projectMngMain(title, projectInst) {
+    @views.html.prjmenu(projectInst, utils.MenuType.PROJECT_SETTING, "")
+
+    <div class="form-wrap milestone frm-wrap">
+        <form class="nm" action="@routes.MilestoneApp.newMilestone(projectInst.owner, projectInst.name)" method="post">
+            <div class="inner left">
+            	<dl>
+            		<dt>
+		                <div class="n-alert hide" id="title_error">
+		                    <div class="n-inner">
+		                        <span class="msg">타이틀을 입력해주세요.</span>
+		                        <a href="#!/close" class="ico btn-delete"></a>
+		                    </div>
+		                </div>
+            			<label for="title">새 마일스톤의 제목을 입력해주세요</label>
+            		</dt>
+            		<dd>
+						<input type="text" name="title" id="title" class="text" placeholder="">
+					</dd>
+					
+					<dt>
+		                <div class="n-alert hide" id="contents_error">
+		                    <div class="n-inner">
+		                        <span class="msg">내용을 입력해주세요.</span>
+		                        <a href="#!/close" class="ico btn-delete"></a>
+		                    </div>
+		                </div>
+		                <label for="contents">내용을 입력해주세요</label>
+		            </dt>
+		            <dd>
+						<textarea class="textarea" id="contents" name="contents"></textarea>
+					</dd>
+            	</dl>
+
+                <div class="actions">
+                    <button type="submit" class="n-btn orange med btn-transparent save">SAVE</button>
+                    <a href="@routes.MilestoneApp.manageMilestones(projectInst.owner, projectInst.name)" class="n-btn gray med">CANCEL</a>
+                </div>
+            </div>
+            
+            <div class="inner right bubble-wrap dark-gray">
+                <p>마일스톤 상태</p>
+                <div>
+               		<input type="radio" name="state" value="@State.OPEN" id="milestone-open" class="radio-btn" checked="checked"><label for="milestone-open" class="bold">Open</label>
+               		&nbsp;
+               		<input type="radio" name="state" value="@State.CLOSED" id="milestone-close" class="radio-btn"><label for="milestone-close" class="bold">Closed</label>
+                </div>
+                <hr/>
+                <p>완료일을 선택하세요.</p>
+                <div class="n-alert hidden">
+                    <div class="n-inner">
+                        <span class="msg">완료일을 입력해주세요.</span>
+                        <a href="#!/close" class="ico btn-delete"></a>
+                    </div>
+                </div>
+                <label for="dueDate">
+                    <input type="text" name="dueDate" id="dueDate" class="validate due-date">
+                </label>
+                <div id="datepicker" class="date-picker"></div>
+
+            </div>
+        </form>
+    </div>
+
+    <script type="text/javascript">nforge.require('milestone.manage');</script>
+    <style type="text/css">
+        @@IMPORT url(@getCSSLink("pikaday"));
+    </style>
+    <script src="@getJSLink("moment.min")" type="text/javascript"></script>
+    <script src="@getJSLink("pikaday/pikaday")" type="text/javascript"></script>
+    
+    <script type="text/javascript">
+	$(document).ready(function(){
+		
+		// 날짜 선택 영역 설정 
+		// @@requires Pikaday (https://github.com/dbushell/Pikaday) 
+		(function(sInputId, sContainer){
+			var elInput = $(sInputId);
+			var elContainer = $(sContainer);
+			var sDateFormat = "YYYY-MM-DD";
+			
+			var oPicker = new Pikaday({
+				"format": sDateFormat,
+				"onSelect": function(oDate){
+					elInput.val(this.toString());
+				}
+			});
+			
+			elContainer.append(oPicker.el);
+			
+			elInput.attr("value", oPicker.getMoment().format(sDateFormat));
+			elInput.blur(function(){
+				oPicker.setDate(this.value);
+			});
+			
+		})("#dueDate", "#datepicker");
+		
+	});
+</script>
+}
app/views/project/memberList.scala.html
--- app/views/project/memberList.scala.html
+++ app/views/project/memberList.scala.html
@@ -16,8 +16,8 @@
 	<div class="bubble-wrap dark-gray wp">
        <div class="inner-bubble">
            <form class="nm" action="@routes.ProjectApp.newMember(project.owner, project.name)" method="post" id="addNewMember">
-               <input type="text" class="text uname" id="loginId" name="loginId" placeholder="@Messages("project.members.addMember")" pattern="[a-zA-Z0-9_][a-zA-Z0-9_]+" title="@Messages("user.wrongloginId.alert")" />
-               <button class="ns-btn" type="submit"><i class="ico ico-plus-blue"></i>@Messages("button.add")</button>
+               <input type="text" class="text uname" id="loginId" name="loginId" placeholder="@Messages("project.members.addMember")" pattern="[a-zA-Z0-9_][a-zA-Z0-9_]+" title="@Messages("user.wrongloginId.alert")" /><!-- 
+                --><button class="ns-btn" type="submit"><i class="ico ico-plus-blue"></i>@Messages("button.add")</button>
            </form>
        </div>
        
app/views/project/setting.scala.html
--- app/views/project/setting.scala.html
+++ app/views/project/setting.scala.html
@@ -11,7 +11,7 @@
     <div class="bubble-wrap gray">
         @form(action=routes.ProjectApp.settingProject(project.owner, project.name), 'id->"saveSetting" , 'enctype->"multipart/form-data", 'class->"nm"){
             <input type="hidden" name="id" value="@projectForm("id").value.toLong">
-            <div class="box-wrap top clearfix">
+            <div class="box-wrap top clearfix frm-wrap">
                 <div class="setting-box left">
                     <div class="logo-wrap">
                         @if(projectForm("logoPath").value != null) {
@@ -25,7 +25,7 @@
                             <li><strong>@Messages("project.logo")</strong></li>
                             <li>@Messages("project.logo.type") <span class="point">bmp, jpg, gif, png</span></li>
                             <li>@Messages("project.logo.maxFileSize") <span class="point">1MB</span></li>
-                            <li>@Messages("project.logo.size") <span class="point">234px * 168px</span></li>
+                            <li>@Messages("project.logo.size") <span class="point">234px × 168px</span></li>
                             <li>
                                 <div class="ns-btn fake-file-wrap"><i class="ico ico-plus-blue"></i>UPLOAD
                                     <input type="file" class="file" name="logoPath">
@@ -34,26 +34,33 @@
                         </ul>
                     </div>
                 </div>
-                <div class="setting-box right">
-                    <div class="n-alert hide" id="alert_msg">
-                        <div class="n-inner">
-                            <span class="msg">@Messages("project.wrongName")</span>
-                            <a href="#" class="ico btn-delete" data-dismiss="alert"></a>
-                        </div>
-                    </div>
-                    <label for="project-name">
-                        @inputText(projectForm("name"), 'class->"text", 'placeholder->Messages("project.name.placeholder"), 'maxlength -> "250")
-                    </label>
-                    <label for="project-desc">
-                        @textarea(projectForm("overview"), 'placeholder->Messages("project.description.placeholder"), 'class->"textarea")
-                    </label>
-                </div>
+                <dl class="setting-box right">
+                	<dt>
+	                    <div class="n-alert hide" id="alert_msg">
+	                        <div class="n-inner">
+	                            <span class="msg">@Messages("project.wrongName")</span>
+	                            <a href="#" class="ico btn-delete" data-dismiss="alert"></a>
+	                        </div>
+	                    </div>
+	                    <label for="project-name">@Messages("project.name.placeholder")</label>
+                    </dt>
+                    <dd>
+                        @inputText(projectForm("name"), 'class->"text", 'maxlength -> "250")
+                    </dd>
+                    
+                    <dt>
+                    	<label for="project-desc">@Messages("project.description.placeholder")</label>
+                    </dt>
+                    <dd>
+                        @textarea(projectForm("overview"), 'class->"textarea")
+                    </dd>
+                </dl>
             </div>
             <div class="box-wrap middle">
                 <div class="cu-label">@Messages("project.shareOption")</div>
                 <div class="cu-desc">
-                    <input name="share_option" type="radio" @if(project.share_option == true){checked="checked"} id="public" value="true" class="radio-btn"><label for="public" class="bg-radiobtn">@Messages("project.public")</label>
-                    <input name="share_option" type="radio" @if(project.share_option == false){checked="checked"} id="private" value="false" class="radio-btn"><label for="private" class="bg-radiobtn">@Messages("project.private")</label>
+                    <input name="share_option" type="radio" @if(project.share_option == true){checked="checked"} id="public" value="true" class="radio-btn"><label for="public" class="bg-radiobtn label-public">@Messages("project.public")</label>
+                    <input name="share_option" type="radio" @if(project.share_option == false){checked="checked"} id="private" value="false" class="radio-btn"><label for="private" class="bg-radiobtn label-private">@Messages("project.private")</label>
                     <span class="note">@Messages("project.private.notice")</span>
                 </div>
             </div>
@@ -75,7 +82,7 @@
         <div class="cu-label">@Messages("project.delete")</div>
         <div class="cu-desc">
             <p><strong class="notice">@Messages("project.delete.description")</strong></p>
-            <p><input type="checkbox" class="checkbox" autocomplete="off" id="accept"><label for="agreement" class="bg-checkbox">@Messages("project.delete.accept")</label>
+            <p><input type="checkbox" class="checkbox" autocomplete="off" id="accept"><label for="agreement" class="bg-checkbox label-agreement">@Messages("project.delete.accept")</label>
                 <a id="deletion" data-toggle="modal" href="#alertDeletion" class="n-btn small black"><i class="ico ico-delete-small"></i>@Messages("project.delete.this")</a>
             </p>
         </div>
app/views/user/info.scala.html
--- app/views/user/info.scala.html
+++ app/views/user/info.scala.html
@@ -5,18 +5,16 @@
 @home("Users Info", utils.MenuType.USER) {
     @if(user != null && session != null && user.loginId == session.get("loginId")){
 		<div class="side-menu-wrap">
-		    <ul class="side-menus ico bg-side-menu unstyled">
+		    <ul class="side-menus unstyled bg">
 		        <li class="side-menu"><a href="@routes.UserApp.editUserInfoForm()"><i class="ico ico-setting on"></i></a></li>
 		    </ul>
-		</div>    
+		</div>
     }
+
 
 <div class="page-wrap container">
     <div class="page">
-        <div>
-            <div><h1 class="page-title">@user.loginId</h1></div>
-        </div>
-        
+        <h1 class="page-title"><span class="gray">HIVE/</span>@user.loginId</h1>
         <section class="user-box">
             <div class="user-info-box">
                 <div class="whoami-wrap">
@@ -25,55 +23,76 @@
                     <img src="@user.avatarUrl" width="127" height="127" class="img-rounded">
                 </div>
                 <div class="user-location info-box">
-                    <p class="u-location"><i class="ico ico-location"></i>@user.email</p>
+                    <p class="u-location"><i class="ico ico-location"></i>@user.email<!-- Seoul, Korea, South --></p>
                 </div>
                 <hr/>
-                <div class="user-other-info info-box">
-                    <p class="since"><strong>MEMBER SINCE</strong></p>
-                    <p><span class="since">@user.getDateString</span></p>
-                    <p class="social-btns">
-                        <a href="http://twitter.com/nforge"><i class="ico btn-tw"></i></a>
-                        <a href="http://facebook.com/nforge"><i class="ico btn-fb"></i></a>
-                        <a href="http://me2.com/nforge"><i class="ico btn-m2"></i></a>
-                    </p>
-                </div>
-                <hr/>
-                <div class="btn-wrap">
-                    <a href="/projectform"><i class="ico btn-new-project"></i></a>
-                </div>
-            </div>
-            <div class="user-stream-box">
-                <ul class="nav nav-tabs user-stream-tab">
-                    <li class="active">Repositories</li>
-                    <!-- li><a href="user-activities.html">Activities</a></li-->
-                </ul>
-                <!-- div class="user-stream-wrap">
-                    <div class="filter-wrap user-setting">
-                        <div class="filters">
-                            <a href="/html/user-setting.html" class="filter"><i class="ico btn-gray-arrow"></i>전체</a>
-                            <a href="/html/user-activities.html?order=staus" class="filter active"><i class="ico btn-gray-arrow down"></i>상태순</a>
-                        </div>
-                    </div>
-                </div-->
-                <ul class="user-streams">
-                @for(project <- user.myProjects()){                
-                    <li class="user-stream">
-                        <h3 class="project-name"><a href="@routes.ProjectApp.project(project.owner, project.name)">@project.owner/@project.name</a></h3>
-                        <div class="stream-desc-wrap">
-                            <div class="stream-desc">
-                                <!-- p class="nm">최근 활동 관련 영역입니다.</p-->
-                                <p class="date">Last updated @agoString(project.ago)</p>
-                            </div>
-                            <!-- div class="project-status">
-                                <i class="ico ico-like"></i><span class="num">1</span>
-                                <i class="ico ico-activity high"></i>
-                            </div-->
-                        </div>
-                    </li>
-                }
-                </ul>
-            </div>
-        </section>
+    <div class="user-status info-box">
+        <p><span class="labels">My projects</span><span class="num">@user.myProjects().length</span></p>
+        <p><span class="labels">Followers</span><span class="num">0</span></p>
+        <p><span class="labels">Following</span><span class="num">0</span></p>
+        <p><span class="labels">Starred</span><span class="num">0</span></p>
     </div>
+    <hr/>
+    <div class="user-other-info info-box">
+        <p><strong>FOCUS</strong></p>
+        <p class="focuses">C#, JAVA, SQL, JAVASCRIPT</p>
+        <p class="since"><strong>MEMBER SINCE</strong></p>
+        <p><span class="since">@user.getDateString</span></p>
+        <p class="social-btns">
+            <a href="http://twitter.com/@user.loginId"><i class="ico btn-tw"></i></a>
+            <a href="http://facebook.com/@user.loginId"><i class="ico btn-fb"></i></a>
+            <a href="http://me2day.net/@user.loginId"><i class="ico btn-m2"></i></a>
+        </p>
+    </div>
+    <hr/>
+    <div class="btn-wrap-new-project">
+		<a href="@routes.ProjectApp.newProjectForm()"><i class="ico ico-pencil"></i> Create your project</a>
+	</div>
 </div>
+<div class="user-stream-box">
+    <ul class="nav nav-tabs user-stream-tab hive-tabs">
+        <li class="active"><a href="#">Repositories</a></li>
+        <li><a href="#">Activities</a></li>
+    </ul>
+    <div class="user-stream-wrap">
+        <div class="header-wrap">
+            <div class="search-wrap user-setting">
+                <div class="inner">
+                    <form action="/activity-search" method="get">
+                        <input name="query" class="text" type="text" placeholder="검색"><button type="submit" class="btn search-btn">SEARCH</button>
+                    </form>
+                </div>
+            </div>
+        </div>
+        <div class="filter-wrap user-setting">
+            <div class="filters">
+                <a href="/html/user-setting.html" class="filter"><i class="ico btn-gray-arrow"></i>전체</a>
+                <a href="/html/user-activities.html?order=staus" class="filter active"><i class="ico btn-gray-arrow down"></i>상태순</a>
+            </div>
+        </div>
+    </div>
+    <ul class="user-streams">
+    	@for(project <- user.myProjects()){
+    	
+        <li class="user-stream">
+            <h3 class="project-name"><a href="@routes.ProjectApp.project(project.owner, project.name)">@project.owner/@project.name</a></h3>
+            <div class="stream-desc-wrap">
+                <div class="stream-desc">
+                    <p class="nm">@project.overview</p>
+                    <p class="date">Last updated @agoString(project.ago)</p>
+                </div>
+                <div class="project-status">
+                    <i class="ico ico-like"></i><span class="num">1</span>
+                    <i class="ico ico-activity high"></i>
+                </div>
+            </div>
+        </li>
+
+		}
+    </ul>
+</div>
+</section>
+</div>
+</div>
+
 }
app/views/user/signup.scala.html
--- app/views/user/signup.scala.html
+++ app/views/user/signup.scala.html
@@ -8,23 +8,45 @@
             <p class="tag-line">Software development platform for Open Source project.</p>
         </div>
         
-        <div class="signup-form-wrap">
+        <div class="signup-form-wrap frm-wrap">
             <form action="@routes.UserApp.newUser()" method="POST" name="signup">
-				<label for="loginId">
-                    <input type="text" class="text password" id="loginId" placeholder="@Messages("user.signupId")" autocomplete="off" name="loginId">
-                </label>
-				<label for="uname">
-                    <input type="text" class="text password" id="uname" placeholder="@Messages("user.name")" autocomplete="off" name="name">
-                </label>
-                <label for="email">
-                    <input type="text" class="text password" id="email" placeholder="@Messages("user.email")" autocomplete="off" name="email">
-                </label>
-                <label for="password">
-                    <input type="password" class="text password" id="password" placeholder="@Messages("user.password")" autocomplete="off" name="password">
-                </label>
-                <label for="retypedPassword">
-                    <input type="password" class="text password" id="retypedPassword" placeholder="@Messages("validation.retypePassword")" autocomplete="off" name="retypedPassword">
-                </label>
+            	<dl>
+            		<dt>
+            			<label for="loginId">@Messages("user.signupId")</label>
+            		</dt>
+            		<dd>
+            			<input type="text" class="text password" id="loginId" placeholder="" autocomplete="off" name="loginId">
+            		</dd>
+
+					<dt>
+						<label for="uname">@Messages("user.name")</label>
+					</dt>
+					<dd>
+						<input type="text" class="text password" id="uname" placeholder="" autocomplete="off" name="name">
+					</dd>
+					
+					<dt>
+						<label for="email">@Messages("user.email")</label>
+					</dt>
+					<dd>
+						<input type="text" class="text password" id="email" placeholder="" autocomplete="off" name="email">
+					</dd>
+					
+					<dt>
+						<label for="password">@Messages("user.password")</label>
+					</dt>
+					<dd>
+						<input type="password" class="text password" id="password" placeholder="" autocomplete="off" name="password">
+					</dd>
+					
+					<dt>
+						<label for="retypedPassword">@Messages("validation.retypePassword")</label>
+					</dt>
+					<dd>
+						<input type="password" class="text password" id="retypedPassword" placeholder="" autocomplete="off" name="retypedPassword">
+					</dd>
+            	</dl>
+
                 <div class="act-row">
                     @Messages("user.isAlreadySignupUser") <a href="@routes.UserApp.loginForm()" class="go-login">@Messages("title.login")</a>
                 </div>
 
public/javascripts/fileUploader.js (added)
+++ public/javascripts/fileUploader.js
@@ -0,0 +1,355 @@
+/**
+ * getFileList
+ */
+var getFileList = function(target, urlToGetFileList, fn) {
+	var form = $('<form>').attr('method', 'get').attr('action', urlToGetFileList);
+
+	var resourceType = target.attr('resourceType');
+	if (typeof resourceType !== "undefined") {
+		form.append('<input type="hidden" name="containerType" value="' + resourceType + '">');
+	}
+
+	var resourceId = target.attr('resourceId');
+	if (typeof resourceId !== "undefined") {
+		form.append('<input type="hidden" name="containerId" value="' + resourceId + '">');
+	}
+
+	form.ajaxForm({
+		"success" : fn
+	});
+	
+	try {
+		form.submit();
+	} finally {
+		form = resourceType = resourceId = null;
+	}
+};
+
+/**
+ * fileUploader
+ */
+var fileUploader = (function() {
+	
+	var htVar = {};
+	var htElements = {};
+	
+	/**
+	 * initialize fileUploader
+	 * 파일 업로더 초기화 함수. fileUploader.init(htOptions) 으로 사용한다.
+	 * @param {Hash Table} htOptions
+	 * @param {Variant} htOptions.elTarget     첨부파일 
+	 * @param {Variant} htOptions.elTextarea   이미지 첨부파일의 경우 클릭시 이 영역에 태그를 삽입한다   
+	 * @param {String}  htOptions.sTplFileItem 첨부한 파일명을 표시할 HTML 템플릿   
+	 */
+	function _init(htOptions){
+		htOptions = htOptions || {};
+		
+		_initVar(htOptions);
+		_initElement(htOptions);
+		_attachEvent();
+	}
+	
+	/**
+	 * init variables
+	 */
+	function _initVar(htOptions){
+		htVar.nTotalSize   = 0;
+		htVar.sAction      = htOptions.sAction;
+		htVar.sTplFileItem = htOptions.sTplFileItem;
+		
+		htVar.htUploadOpts = {
+			"dataType"      : "json",
+			"error"         : _onErrorSubmitForm,
+			"success"       : _onSuccessSubmitForm,
+			"beforeSubmit"  : _onBeforeSubmitForm,
+			"uploadProgress": _onUploadProgressForm
+		};
+	}
+
+	/**
+	 * init elements
+	 */
+	function _initElement(htOptions){
+		htElements.welTarget      = $(htOptions.elTarget);
+		htElements.welTextarea    = $(htOptions.elTextarea);
+		
+		htElements.welInputFile   = $(htOptions.elInputFile   || ".file");
+		htElements.welTotalNum    = $(htOptions.elTotalNum    || ".total-num");
+		htElements.welProgressNum = $(htOptions.elProgressNum || ".progress-num");
+		htElements.welProgressBar = $(htOptions.elProgressBar || ".progress > .bar");
+		htElements.welFileList    = $(htOptions.elFileList    || "ul.attached-files");
+	}
+	
+	/**
+	 * init event handlers
+	 */
+	function _attachEvent(){
+		htElements.welInputFile.change(_onChangeFile);
+		htElements.welInputFile.click(function(){
+			_setProgressBar(0);
+		});
+	}
+	
+	/**
+	 * change event handler on <input type="file">
+	 */
+	function _onChangeFile(){
+		// Validation
+		var sFileName = _getBasename(htElements.welInputFile.val());
+		//console.log("changeFile : " + sFileName);
+		
+		if(sFileName == ""){
+			return;
+		}
+
+		// Submit
+		var welForm = $('<form method="post" enctype="multipart/form-data" style="display:none">');
+		var welInputFile = htElements.welInputFile.clone();
+		welInputFile[0].files = htElements.welInputFile[0].files;
+		welForm.attr('action', htVar.sAction);
+		welForm.append(welInputFile).appendTo(document.body);
+		welForm.ajaxForm(htVar.htUploadOpts);
+		
+		try {
+			welForm.submit();
+		} finally {
+			welInputFile.remove();
+			welForm.remove();
+			welForm = welInputFile = null;
+		}
+	}
+	
+	/**
+	 * Returns trailing name component of path
+	 * @param {String} sPath
+	 * @returns {String}  
+	 */
+	function _getBasename(sPath){
+		var sSeparator = 'fakepath';
+		var nPos = sPath.indexOf(sSeparator);		
+		return (nPos > -1) ? sPath.substring(nPos + sSeparator.length + 1) : sPath;
+	}
+	
+	function _onBeforeSubmitForm(){
+		var sFileName = _getBasename(htElements.welInputFile.val());
+		//console.log("beforeSubmit: " + sFileName);
+		
+		return !(sFileName == "");
+	}
+	
+	/**
+	 * On success to submit temporary form created in onChangeFile()
+	 * @param {Hash Table} htData
+	 * @returns
+	 */
+	function _onSuccessSubmitForm(oRes){
+		htElements.welInputFile.val("");
+		
+		// Validation
+		if(!(oRes instanceof Object) || !oRes.name || !oRes.url){
+			//console.log("Failed to upload - Server Error");
+			_setProgressBar(0);
+			return;
+		}
+
+		// create list item
+		var welItem = _createFileItem(oRes);
+			welItem.click(_onClickListItem);
+		htElements.welFileList.append(welItem);
+		
+		_setProgressBar(100);
+		_updateTotalFilesize(oRes.size);
+	}
+	
+	/**
+	 * 첨부 파일 크기 합계 표시
+	 * @param {Number} nValue 첨부 파일 크기 변화할 값
+	 */
+	function _updateTotalFilesize(nValue){
+		nValue = (nValue || 0) * 1;
+		htVar.nTotalSize += nValue;
+		htElements.welTotalNum.text(humanize.filesize(htVar.nTotalSize));
+	}
+	
+	/**
+	 * Create uploaded file item HTML element using template string
+	 * @param {Hash Table} htFile
+	 * @returns {HTMLElement} 
+	 */
+	function _createFileItem(htFile) {
+		var oItem = $.tmpl(htVar.sTplFileItem, {
+			"mimeType": htFile.mimeType,
+			"fileName": htFile.name,
+			"fileHref": htFile.url,
+			"fileSize": htFile.size,
+			"fileSizeReadable": humanize.filesize(htFile.size)
+		});
+		
+		return oItem;
+	}
+	
+	/**
+	 * On error to submit temporary form created in onChangeFile()
+	 */
+	function _onErrorSubmitForm(oRes){
+		_setProgressBar(0);
+		//console.log("errorSubmit : %o", oRes);
+	}
+	
+	/**
+	 * uploadProgress event handler 
+	 */
+	function _onUploadProgressForm(oEvent, nPos, nTotal, nPercentComplete){
+		_setProgressBar(nPercentComplete);
+	}
+
+	/**
+	 * Set Progress Bar Width 
+	 * @param nProgress
+	 */
+	function _setProgressBar(nProgress) {
+		nProgress = nProgress * 1;
+//		htElements.welTarget.css("opacity", (nProgress === 0) ? 0 : 1);
+		htElements.welProgressBar.css("width", nProgress + "%");
+		htElements.welProgressNum.text(nProgress + "%");
+	}
+
+	
+	/**
+	 * On Click attached files list
+	 */
+	function _onClickListItem(oEvt){
+		var welTarget = $(oEvt.target);
+		var welItem = $(oEvt.currentTarget);
+		
+		// 파일 아이템 전체에 이벤트 핸들러가 설정되어 있으므로
+		// 클릭이벤트 발생한 위치를 삭제버튼과 나머지 영역으로 구분하여 처리
+		if(welTarget.hasClass("btn-delete")){
+			_deleteAttachedFile(welItem);    // 첨부파일 삭제
+		} else {
+			_insertLinkToTextarea(welItem);  // <textarea>에 링크 텍스트 추가
+		}
+	}
+	
+	/**
+	 * 선택한 파일 아이템의 링크 텍스트를 <textarea>에 추가하는 함수
+	 */
+	function _insertLinkToTextarea(welItem){
+		var welTextarea = htElements.welTextarea;
+		if(!welTextarea){
+			return false;
+		}
+		
+		var nPos  = welTextarea.prop('selectionStart');
+		var sText = welTextarea.val();
+		var sLink = _getLinkText(welItem);
+		
+		welTextarea.val(sText.substring(0, nPos) + sLink + sText.substring(nPos));
+	}
+	
+	/**
+	 * 선택한 파일 아이템을 첨부 파일에서 삭제
+	 * <textarea>에서 해당 파일의 링크 텍스트도 제거함 (_clearLinkInTextarea)
+	 */
+	function _deleteAttachedFile(welItem){	
+		/**/
+		var nFileSize = welItem.attr("data-size") * 1;
+		var welForm = $('<form method="post" enctype="multipart/form-data" style="display:none">');
+		welForm.attr('action', welItem.attr("data-href"));
+		welForm.append('<input type="hidden" name="_method" value="delete">');
+		welForm.appendTo(document.body);
+		welForm.ajaxForm({
+			"success" : function() {
+				_updateTotalFilesize(nFileSize * -1);
+				_clearLinkInTextarea(welItem);
+				_setProgressBar(0);
+				welItem.remove();
+			}
+		});
+		
+		try {
+			welForm.submit();
+		} finally {
+			welForm.remove();
+			welTextarea = welForm = null;
+		}
+		/**/
+		
+		// TODO: 아래와 같은 간단한 AJAX 호출 사용 검토
+		/*
+		var sActionURL = welItem.attr("data-href");
+		$.post(sActionURL, {"_method": "delete"}, function(){
+			welItem.remove();
+			_clearLinkInTextarea(sLink);
+			_setProgressBar(0);			
+		});
+		*/
+	}
+	
+	/**
+	 * 파일 아이템으로부터 링크 텍스트를 생성하여 반환하는 함수
+	 * @param {Wrapped Element} welItem 템플릿 htVar.sTplFileItem 에 의해 생성된 첨부 파일 아이템
+	 * @return {String}
+	 */
+	function _getLinkText(welItem){
+		var sMimeType = welItem.attr("data-mime");
+		var sFileName = welItem.attr("data-name");
+		var sFilePath = welItem.attr("data-href");
+		
+		var sLinkText = '';
+		if (sMimeType.substr(0,5) == "image"){
+			sLinkText = '<img src="' + sFilePath + '">';
+		} else {
+			sLinkText = '[' + sFileName +'](' + sFilePath + ')';
+		}
+		
+		return sLinkText;
+	}
+	
+	/**
+	 * <textarea>에서 해당 파일 아이템의 링크 텍스트를 제거하는 함수
+	 * @param {Wrapped Element} welItem
+	 */
+	function _clearLinkInTextarea(welItem){
+		var welTextarea = htElements.welTextarea;
+		if(!welTextarea){
+			return false;
+		}
+		
+		var sLink = _getLinkText(welItem);		
+		welTextarea.val(welTextarea.val().split(sLink).join(''));		
+	}
+	
+	/**
+	 * 인터페이스 반환
+	 */
+	return {
+		"init": _init
+	};
+})();
+
+/**
+ * fileDownloader
+ */
+var fileDownloader = function(target, urlToGetFileList) {
+	var createFileItem = function(file) {
+		var link = $('<a>').prop('href', file.url).append(
+				$('<i>').addClass('icon-download')).append(
+				$('<div>').text(file.name).html());
+
+		return $('<li>').append(link);
+	};
+
+	var filelist = $('<ul>');
+	var addFiles = function(responseBody, statusText, xhr) {
+		var files = responseBody.attachments;
+		for ( var i = 0; i < files.length; i++) {
+			filelist.css('display', '');
+			filelist.append(createFileItem(files[i]));
+		}
+	};
+
+	getFileList(target, urlToGetFileList, addFiles);
+
+	target.append(filelist);
+};
 
public/javascripts/jquery.tmpl.js (added)
+++ public/javascripts/jquery.tmpl.js
@@ -0,0 +1,484 @@
+/*!
+ * jQuery Templates Plugin 1.0.0pre
+ * http://github.com/jquery/jquery-tmpl
+ * Requires jQuery 1.4.2
+ *
+ * Copyright 2011, Software Freedom Conservancy, Inc.
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+(function( jQuery, undefined ){
+	var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
+		newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
+
+	function newTmplItem( options, parentItem, fn, data ) {
+		// Returns a template item data structure for a new rendered instance of a template (a 'template item').
+		// The content field is a hierarchical array of strings and nested items (to be
+		// removed and replaced by nodes field of dom elements, once inserted in DOM).
+		var newItem = {
+			data: data || (data === 0 || data === false) ? data : (parentItem ? parentItem.data : {}),
+			_wrap: parentItem ? parentItem._wrap : null,
+			tmpl: null,
+			parent: parentItem || null,
+			nodes: [],
+			calls: tiCalls,
+			nest: tiNest,
+			wrap: tiWrap,
+			html: tiHtml,
+			update: tiUpdate
+		};
+		if ( options ) {
+			jQuery.extend( newItem, options, { nodes: [], parent: parentItem });
+		}
+		if ( fn ) {
+			// Build the hierarchical content to be used during insertion into DOM
+			newItem.tmpl = fn;
+			newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
+			newItem.key = ++itemKey;
+			// Keep track of new template item, until it is stored as jQuery Data on DOM element
+			(stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
+		}
+		return newItem;
+	}
+
+	// Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
+	jQuery.each({
+		appendTo: "append",
+		prependTo: "prepend",
+		insertBefore: "before",
+		insertAfter: "after",
+		replaceAll: "replaceWith"
+	}, function( name, original ) {
+		jQuery.fn[ name ] = function( selector ) {
+			var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems,
+				parent = this.length === 1 && this[0].parentNode;
+
+			appendToTmplItems = newTmplItems || {};
+			if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+				insert[ original ]( this[0] );
+				ret = this;
+			} else {
+				for ( i = 0, l = insert.length; i < l; i++ ) {
+					cloneIndex = i;
+					elems = (i > 0 ? this.clone(true) : this).get();
+					jQuery( insert[i] )[ original ]( elems );
+					ret = ret.concat( elems );
+				}
+				cloneIndex = 0;
+				ret = this.pushStack( ret, name, insert.selector );
+			}
+			tmplItems = appendToTmplItems;
+			appendToTmplItems = null;
+			jQuery.tmpl.complete( tmplItems );
+			return ret;
+		};
+	});
+
+	jQuery.fn.extend({
+		// Use first wrapped element as template markup.
+		// Return wrapped set of template items, obtained by rendering template against data.
+		tmpl: function( data, options, parentItem ) {
+			return jQuery.tmpl( this[0], data, options, parentItem );
+		},
+
+		// Find which rendered template item the first wrapped DOM element belongs to
+		tmplItem: function() {
+			return jQuery.tmplItem( this[0] );
+		},
+
+		// Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
+		template: function( name ) {
+			return jQuery.template( name, this[0] );
+		},
+
+		domManip: function( args, table, callback, options ) {
+			if ( args[0] && jQuery.isArray( args[0] )) {
+				var dmArgs = jQuery.makeArray( arguments ), elems = args[0], elemsLength = elems.length, i = 0, tmplItem;
+				while ( i < elemsLength && !(tmplItem = jQuery.data( elems[i++], "tmplItem" ))) {}
+				if ( tmplItem && cloneIndex ) {
+					dmArgs[2] = function( fragClone ) {
+						// Handler called by oldManip when rendered template has been inserted into DOM.
+						jQuery.tmpl.afterManip( this, fragClone, callback );
+					};
+				}
+				oldManip.apply( this, dmArgs );
+			} else {
+				oldManip.apply( this, arguments );
+			}
+			cloneIndex = 0;
+			if ( !appendToTmplItems ) {
+				jQuery.tmpl.complete( newTmplItems );
+			}
+			return this;
+		}
+	});
+
+	jQuery.extend({
+		// Return wrapped set of template items, obtained by rendering template against data.
+		tmpl: function( tmpl, data, options, parentItem ) {
+			var ret, topLevel = !parentItem;
+			if ( topLevel ) {
+				// This is a top-level tmpl call (not from a nested template using {{tmpl}})
+				parentItem = topTmplItem;
+				tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );
+				wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
+			} else if ( !tmpl ) {
+				// The template item is already associated with DOM - this is a refresh.
+				// Re-evaluate rendered template for the parentItem
+				tmpl = parentItem.tmpl;
+				newTmplItems[parentItem.key] = parentItem;
+				parentItem.nodes = [];
+				if ( parentItem.wrapped ) {
+					updateWrapped( parentItem, parentItem.wrapped );
+				}
+				// Rebuild, without creating a new template item
+				return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
+			}
+			if ( !tmpl ) {
+				return []; // Could throw...
+			}
+			if ( typeof data === "function" ) {
+				data = data.call( parentItem || {} );
+			}
+			if ( options && options.wrapped ) {
+				updateWrapped( options, options.wrapped );
+			}
+			ret = jQuery.isArray( data ) ?
+				jQuery.map( data, function( dataItem ) {
+					return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
+				}) :
+				[ newTmplItem( options, parentItem, tmpl, data ) ];
+			return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
+		},
+
+		// Return rendered template item for an element.
+		tmplItem: function( elem ) {
+			var tmplItem;
+			if ( elem instanceof jQuery ) {
+				elem = elem[0];
+			}
+			while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
+			return tmplItem || topTmplItem;
+		},
+
+		// Set:
+		// Use $.template( name, tmpl ) to cache a named template,
+		// where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
+		// Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
+
+		// Get:
+		// Use $.template( name ) to access a cached template.
+		// Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
+		// will return the compiled template, without adding a name reference.
+		// If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
+		// to $.template( null, templateString )
+		template: function( name, tmpl ) {
+			if (tmpl) {
+				// Compile template and associate with name
+				if ( typeof tmpl === "string" ) {
+					// This is an HTML string being passed directly in.
+					tmpl = buildTmplFn( tmpl );
+				} else if ( tmpl instanceof jQuery ) {
+					tmpl = tmpl[0] || {};
+				}
+				if ( tmpl.nodeType ) {
+					// If this is a template block, use cached copy, or generate tmpl function and cache.
+					tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
+					// Issue: In IE, if the container element is not a script block, the innerHTML will remove quotes from attribute values whenever the value does not include white space.
+					// This means that foo="${x}" will not work if the value of x includes white space: foo="${x}" -> foo=value of x.
+					// To correct this, include space in tag: foo="${ x }" -> foo="value of x"
+				}
+				return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;
+			}
+			// Return named compiled template
+			return name ? (typeof name !== "string" ? jQuery.template( null, name ):
+				(jQuery.template[name] ||
+					// If not in map, and not containing at least on HTML tag, treat as a selector.
+					// (If integrated with core, use quickExpr.exec)
+					jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;
+		},
+
+		encode: function( text ) {
+			// Do HTML encoding replacing < > & and ' and " by corresponding entities.
+			return ("" + text).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
+		}
+	});
+
+	jQuery.extend( jQuery.tmpl, {
+		tag: {
+			"tmpl": {
+				_default: { $2: "null" },
+				open: "if($notnull_1){__=__.concat($item.nest($1,$2));}"
+				// tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
+				// This means that {{tmpl foo}} treats foo as a template (which IS a function).
+				// Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
+			},
+			"wrap": {
+				_default: { $2: "null" },
+				open: "$item.calls(__,$1,$2);__=[];",
+				close: "call=$item.calls();__=call._.concat($item.wrap(call,__));"
+			},
+			"each": {
+				_default: { $2: "$index, $value" },
+				open: "if($notnull_1){$.each($1a,function($2){with(this){",
+				close: "}});}"
+			},
+			"if": {
+				open: "if(($notnull_1) && $1a){",
+				close: "}"
+			},
+			"else": {
+				_default: { $1: "true" },
+				open: "}else if(($notnull_1) && $1a){"
+			},
+			"html": {
+				// Unecoded expression evaluation.
+				open: "if($notnull_1){__.push($1a);}"
+			},
+			"=": {
+				// Encoded expression evaluation. Abbreviated form is ${}.
+				_default: { $1: "$data" },
+				open: "if($notnull_1){__.push($.encode($1a));}"
+			},
+			"!": {
+				// Comment tag. Skipped by parser
+				open: ""
+			}
+		},
+
+		// This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
+		complete: function( items ) {
+			newTmplItems = {};
+		},
+
+		// Call this from code which overrides domManip, or equivalent
+		// Manage cloning/storing template items etc.
+		afterManip: function afterManip( elem, fragClone, callback ) {
+			// Provides cloned fragment ready for fixup prior to and after insertion into DOM
+			var content = fragClone.nodeType === 11 ?
+				jQuery.makeArray(fragClone.childNodes) :
+				fragClone.nodeType === 1 ? [fragClone] : [];
+
+			// Return fragment to original caller (e.g. append) for DOM insertion
+			callback.call( elem, fragClone );
+
+			// Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
+			storeTmplItems( content );
+			cloneIndex++;
+		}
+	});
+
+	//========================== Private helper functions, used by code above ==========================
+
+	function build( tmplItem, nested, content ) {
+		// Convert hierarchical content into flat string array
+		// and finally return array of fragments ready for DOM insertion
+		var frag, ret = content ? jQuery.map( content, function( item ) {
+			return (typeof item === "string") ?
+				// Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
+				(tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) :
+				// This is a child template item. Build nested template.
+				build( item, tmplItem, item._ctnt );
+		}) :
+		// If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
+		tmplItem;
+		if ( nested ) {
+			return ret;
+		}
+
+		// top-level template
+		ret = ret.join("");
+
+		// Support templates which have initial or final text nodes, or consist only of text
+		// Also support HTML entities within the HTML markup.
+		ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
+			frag = jQuery( middle ).get();
+
+			storeTmplItems( frag );
+			if ( before ) {
+				frag = unencode( before ).concat(frag);
+			}
+			if ( after ) {
+				frag = frag.concat(unencode( after ));
+			}
+		});
+		return frag ? frag : unencode( ret );
+	}
+
+	function unencode( text ) {
+		// Use createElement, since createTextNode will not render HTML entities correctly
+		var el = document.createElement( "div" );
+		el.innerHTML = text;
+		return jQuery.makeArray(el.childNodes);
+	}
+
+	// Generate a reusable function that will serve to render a template against data
+	function buildTmplFn( markup ) {
+		return new Function("jQuery","$item",
+			// Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
+			"var $=jQuery,call,__=[],$data=$item.data;" +
+
+			// Introduce the data as local variables using with(){}
+			"with($data){__.push('" +
+
+			// Convert the template into pure JavaScript
+			jQuery.trim(markup)
+				.replace( /([\\'])/g, "\\$1" )
+				.replace( /[\r\t\n]/g, " " )
+				.replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
+				.replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
+				function( all, slash, type, fnargs, target, parens, args ) {
+					var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
+					if ( !tag ) {
+						throw "Unknown template tag: " + type;
+					}
+					def = tag._default || [];
+					if ( parens && !/\w$/.test(target)) {
+						target += parens;
+						parens = "";
+					}
+					if ( target ) {
+						target = unescape( target );
+						args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
+						// Support for target being things like a.toLowerCase();
+						// In that case don't call with template item as 'this' pointer. Just evaluate...
+						expr = parens ? (target.indexOf(".") > -1 ? target + unescape( parens ) : ("(" + target + ").call($item" + args)) : target;
+						exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
+					} else {
+						exprAutoFnDetect = expr = def.$1 || "null";
+					}
+					fnargs = unescape( fnargs );
+					return "');" +
+						tag[ slash ? "close" : "open" ]
+							.split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
+							.split( "$1a" ).join( exprAutoFnDetect )
+							.split( "$1" ).join( expr )
+							.split( "$2" ).join( fnargs || def.$2 || "" ) +
+						"__.push('";
+				}) +
+			"');}return __;"
+		);
+	}
+	function updateWrapped( options, wrapped ) {
+		// Build the wrapped content.
+		options._wrap = build( options, true,
+			// Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
+			jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()]
+		).join("");
+	}
+
+	function unescape( args ) {
+		return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
+	}
+	function outerHtml( elem ) {
+		var div = document.createElement("div");
+		div.appendChild( elem.cloneNode(true) );
+		return div.innerHTML;
+	}
+
+	// Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
+	function storeTmplItems( content ) {
+		var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
+		for ( i = 0, l = content.length; i < l; i++ ) {
+			if ( (elem = content[i]).nodeType !== 1 ) {
+				continue;
+			}
+			elems = elem.getElementsByTagName("*");
+			for ( m = elems.length - 1; m >= 0; m-- ) {
+				processItemKey( elems[m] );
+			}
+			processItemKey( elem );
+		}
+		function processItemKey( el ) {
+			var pntKey, pntNode = el, pntItem, tmplItem, key;
+			// Ensure that each rendered template inserted into the DOM has its own template item,
+			if ( (key = el.getAttribute( tmplItmAtt ))) {
+				while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
+				if ( pntKey !== key ) {
+					// The next ancestor with a _tmplitem expando is on a different key than this one.
+					// So this is a top-level element within this template item
+					// Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
+					pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
+					if ( !(tmplItem = newTmplItems[key]) ) {
+						// The item is for wrapped content, and was copied from the temporary parent wrappedItem.
+						tmplItem = wrappedItems[key];
+						tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode] );
+						tmplItem.key = ++itemKey;
+						newTmplItems[itemKey] = tmplItem;
+					}
+					if ( cloneIndex ) {
+						cloneTmplItem( key );
+					}
+				}
+				el.removeAttribute( tmplItmAtt );
+			} else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
+				// This was a rendered element, cloned during append or appendTo etc.
+				// TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
+				cloneTmplItem( tmplItem.key );
+				newTmplItems[tmplItem.key] = tmplItem;
+				pntNode = jQuery.data( el.parentNode, "tmplItem" );
+				pntNode = pntNode ? pntNode.key : 0;
+			}
+			if ( tmplItem ) {
+				pntItem = tmplItem;
+				// Find the template item of the parent element.
+				// (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
+				while ( pntItem && pntItem.key != pntNode ) {
+					// Add this element as a top-level node for this rendered template item, as well as for any
+					// ancestor items between this item and the item of its parent element
+					pntItem.nodes.push( el );
+					pntItem = pntItem.parent;
+				}
+				// Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
+				delete tmplItem._ctnt;
+				delete tmplItem._wrap;
+				// Store template item as jQuery data on the element
+				jQuery.data( el, "tmplItem", tmplItem );
+			}
+			function cloneTmplItem( key ) {
+				key = key + keySuffix;
+				tmplItem = newClonedItems[key] =
+					(newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent ));
+			}
+		}
+	}
+
+	//---- Helper functions for template item ----
+
+	function tiCalls( content, tmpl, data, options ) {
+		if ( !content ) {
+			return stack.pop();
+		}
+		stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options });
+	}
+
+	function tiNest( tmpl, data, options ) {
+		// nested template, using {{tmpl}} tag
+		return jQuery.tmpl( jQuery.template( tmpl ), data, options, this );
+	}
+
+	function tiWrap( call, wrapped ) {
+		// nested template, using {{wrap}} tag
+		var options = call.options || {};
+		options.wrapped = wrapped;
+		// Apply the template, which may incorporate wrapped content,
+		return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item );
+	}
+
+	function tiHtml( filter, textOnly ) {
+		var wrapped = this._wrap;
+		return jQuery.map(
+			jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ),
+			function(e) {
+				return textOnly ?
+					e.innerText || e.textContent :
+					e.outerHTML || outerHtml(e);
+			});
+	}
+
+	function tiUpdate() {
+		var coll = this.nodes;
+		jQuery.tmpl( null, null, null, this).insertBefore( coll[0] );
+		jQuery( coll ).remove();
+	}
+})( jQuery );
public/javascripts/modules/board.js
--- public/javascripts/modules/board.js
+++ public/javascripts/modules/board.js
@@ -1,19 +1,27 @@
+/**
+ * nforge.board.js
+ */
 nforge.namespace("board");
+
+/**
+ * PostList
+ */
 
 nforge.board.list = function() {
     var that = {
-        init : function() {
+        "init" : function() {
             that.setUpEventListener();
         },
 
-        setUpEventListener : function() {
+        "setUpEventListener" : function() {
             var $headers = $("#order a");
             $headers.click(that.onHeader);
+            
             var $pagination = $("#pagination a");
             $pagination.click(that.onPager);
         },
 
-        onHeader : function() {
+        "onHeader" : function() {
             var key = $(this).attr("key");
             var $input = $("#option_form input[name=key]");
             if (key !== $input.val()) {
@@ -21,28 +29,29 @@
             } else {
                 $input = $("#option_form input[name=order]");
                 if ($input.val() === "desc"){
-                  $input.val("asc");
-                }
-                else if ($input.val() === "asc") {
-                  $input.val("desc");
+                	$input.val("asc");
+                } else if ($input.val() === "asc") {
+                	$input.val("desc");
                 }
             }
             $("#option_form").submit();
             return false;
         },
 
-        onPager : function() {
+        "onPager" : function() {
             var $input = $("#option_form input[name=pageNum]");
             $input.val($(this).attr("pageNum"));
             $("#option_form").submit();
             return false;
         }
     };
+    
     return that;
 };
+
 nforge.board.vaildate = function() {
     var that = {
-        init : function() {
+        "init" : function() {
             $("form").submit(function() {
                 if ($("input#title").val() === "" || $("textarea#contents").val() === "") {
                     $("#warning button").click(function(){
@@ -50,23 +59,31 @@
                     });
                     $('#warning').show();
                     return false;
-                } else {
-                    return true;
                 }
+                return true;
             });
         }
     };
     return that;
 };
 
+/**
+ * PostView
+ */
 nforge.board.view = function() {
     var that = {
-        init : function(filesUrl) {
+        "init" : function(filesUrl) {
             var attachments;
 
-            fileUploader($('#upload'), $('#contents'), filesUrl);
+            fileUploader.init({
+            	"elTarget"    : $('#upload'),   // upload area
+            	"elTextarea"  : $('#contents'), // textarea
+            	"sTplFileItem": $('#tplAttachedFile').text(),
+            	"sAction"     : filesUrl
+            });
+            
             attachments = $('.attachments');
-            for (var i = 0; i < attachments.length; i++) {
+            for (var i = 0, nLength = attachments.length; i < nLength; i++) {
                 fileDownloader($(attachments[i]), filesUrl);
             }
         }
@@ -75,16 +92,24 @@
     return that;
 };
 
+/**
+ * newPost
+ */
 nforge.board.new = function() {
-  var that;
+	var that = {
+		"init": function(filesUrl) {
+            fileUploader.init({
+            	"elTarget"    : $('#upload'),   // upload area
+            	"elTextarea"  : $('#contents'), // textarea
+            	"sTplFileItem": $('#tplAttachedFile').text(),
+            	"sAction"     : filesUrl
+            });
+		}
+	};
 
-  that = {
-    init: function(filesUrl) {
-      fileUploader($('#upload'), $('#contents'), filesUrl);
-    }
-  }
-
-  return that;
+	return that;
 };
 
+
+// Alias
 nforge.board.edit = nforge.issue.new;
public/javascripts/modules/markdown.js
--- public/javascripts/modules/markdown.js
+++ public/javascripts/modules/markdown.js
@@ -1,63 +1,78 @@
+/**
+ * nforge.markdown.js
+ */
+
 nforge.namespace('markdown');
 
-var renderMarkdown = function(text) {
-  text = text
-    .replace(/```(\w+)(?:\r\n|\r|\n)((\r|\n|.)*?)(\r|\n)```/gm, function(match, p1, p2) {
-    try {
-      return '<pre><code class="' + p1 + '">' + hljs(p2, p1).value + '</code></pre>';
-    } catch (e) {
-      return '<pre><code>' + hljs(p2).value + '</code></pre>';
-    }
-  });
+(function(){
+	var rxMarkdown = /```(\w+)(?:\r\n|\r|\n)((\r|\n|.)*?)(\r|\n)```/gm;
 
-  return new Showdown.converter().makeHtml(text);
-};
+	var renderMarkdown = function(text) {
+	  text = text.replace(rxMarkdown, function(match, p1, p2) {
+	    try {
+	      return '<pre><code class="' + p1 + '">' + hljs(p2, p1).value + '</code></pre>';
+	    } catch (e) {
+	      return '<pre><code>' + hljs(p2).value + '</code></pre>';
+	    }
+	  });
 
-var editor = function (textarea) {
-  var previewDiv, previewSwitch;
+	  return new Showdown.converter().makeHtml(text);
+	};
 
-  previewDiv = $('<div>');
-  previewDiv.attr('div', 'preview');
-  previewDiv.css('display', 'none');
+	var sPreviewSwitchHTML = '\
+<input type="radio" name="edit-mode" id="edit-mode" value="edit" checked="checked" class="radio-btn" />\
+<label for="edit-mode" style="margin-right:3px;">Edit</label>\
+<input type="radio" name="edit-mode" id="preview-mode" value="preview" class="radio-btn" />\
+<label for="preview-mode">Preview</label>';
 
-  previewSwitch = $('<div id="mode-select">');
-  previewSwitch.append($('<input type="radio" name="edit-mode" id="edit-mode" value="edit" checked>Edit</input>'));
-  previewSwitch.append($('<input type="radio" name="edit-mode" id="preview-mode" value="preview">Preview</input>'));
-  previewSwitch.change(function() {
-    var val = $('input:radio[name=edit-mode]:checked').val();
-    if (val == 'preview') {
-      previewDiv.html(renderMarkdown(textarea.val()));
-      textarea.css('display', 'none');
-      previewDiv.css('display', '');
-    } else {
-      textarea.css('display', '');
-      previewDiv.css('display', 'none');
-    }
-  });
+	var editor = function (textarea) {
+		var previewDiv, previewSwitch;
 
-  textarea.before(previewSwitch);
-  textarea.before(previewDiv);
-};
+		previewDiv = $('<div class="markdown-preview">');
+		previewDiv.css('display', 'none');
+		previewDiv.css('width', (textarea.width()) + 'px');
+		previewDiv.css('min-height', textarea.height() + 'px');
+		previewDiv.css('padding', textarea.css("padding"));
 
-var viewer = function (target) {
-  target.html(renderMarkdown(target.text()));
-};
+		previewSwitch = $('<div id="mode-select">');
+		previewSwitch.append($(sPreviewSwitchHTML));
 
-nforge.markdown.enable = function() {
-  var that = {
-    init: function(targets) {
-      for(var i = 0; i < targets.length; i++) {
-        var target = targets[i];
-        var tagname = target.tagName.toLowerCase();
-        if (tagname == 'textarea' || tagname == 'input'
-                || target.contentEditable == 'true') {
-          editor($(target));
-        } else {
-          viewer($(target));
-        }
-      }
-    }
-  };
+		previewSwitch.change(function() {
+			var val = $('input:radio[name=edit-mode]:checked').val();
+			if (val == 'preview') {
+				previewDiv.html(renderMarkdown(textarea.val()));
+				textarea.css('display', 'none');
+				previewDiv.css('display', '');
+			} else {
+				textarea.css('display', '');
+				previewDiv.css('display', 'none');
+			}
+		});
 
-  return that;
-}
+		textarea.before(previewSwitch);
+		textarea.before(previewDiv);
+	};
+
+	var viewer = function (target) {
+		target.html(renderMarkdown(target.text()));
+	};
+
+	nforge.markdown.enable = function() {
+		var that = {
+			"init" : function(targets) {
+				var nLength = targets.length;
+				for ( var i = 0; i < nLength; i++) {
+					var target = targets[i];
+					var tagname = target.tagName.toLowerCase();
+					if (tagname == 'textarea' || tagname == 'input' || target.contentEditable == 'true') {
+						editor($(target));
+					} else {
+						viewer($(target));
+					}
+				}
+			}
+		};
+
+		return that;
+	};	
+})();
public/javascripts/uploader.js
--- public/javascripts/uploader.js
+++ public/javascripts/uploader.js
@@ -1,262 +1,270 @@
+/**
+ * getFileList
+ */
 var getFileList = function(target, urlToGetFileList, fn) {
-  var form, resourceType, resourceId;
+	var form = $('<form>').attr('method', 'get').attr('action', urlToGetFileList);
 
-  form = $('<form>')
-    .attr('method', 'get')
-    .attr('action', urlToGetFileList);
+	var resourceType = target.attr('resourceType');
+	if (typeof resourceType !== "undefined") {
+		form.append('<input type="hidden" name="containerType" value="' + resourceType + '">');
+	}
 
-  resourceType = target.attr('resourceType');
-  if (resourceType !== undefined) {
-    form.append('<input type="hidden" name="containerType" value="' + resourceType + '">');
-  }
+	var resourceId = target.attr('resourceId');
+	if (typeof resourceId !== "undefined") {
+		form.append('<input type="hidden" name="containerId" value="' + resourceId + '">');
+	}
 
-  resourceId = target.attr('resourceId');
-  if (resourceId !== undefined) {
-    form.append('<input type="hidden" name="containerId" value="' + resourceId + '">');
-  }
-
-  form.ajaxForm({ success: fn });
-  form.submit();
+	form.ajaxForm({
+		"success" : fn
+	});
+	
+	try {
+		form.submit();
+	} finally {
+		form = resourceType = resourceId = null;
+	}
 };
 
-var fileUploader = function (target, textarea, action) {
-  var setProgressBar = function(value) {
-    progressbar.css("width", value + "%");
-    progressbar.text(value + "%");
-  };
+/**
+ * fileUploader
+ */
+var fileUploader = function(htOptions) {
+	var target = htOptions.target;
+	var textarea = htOptions.textarea;
+	var action = htOptions.action;
+	
+	var setProgressBar = function(value) {
+		progressbar.css("width", value + "%");
+		progressbar.text(value + "%");
+	};
 
-  var createFileItem = function(file, link) {
-    var fileitem, filelink, filesize, insertButton, deleteButton;
+	var createFileItem = function(file, link) {
+		var fileitem, filelink, filesize, insertButton, deleteButton;
 
-    fileitem = $('<li>');
-    fileitem.attr('tabindex', 0);
+		fileitem = $('<li>');
+		fileitem.attr('tabindex', 0);