블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

'Study Frameworks/BroadLeaf eCommerce'에 해당되는 글 4

  1. 2014.12.15 [BroadLeaf] Product 화면 RWD 적용하기 - 4
  2. 2014.12.14 [BroadLeaf] Carousel 적용하기 - 3
  3. 2014.12.03 [BroadLeaf] Twitter Bootstrap 적용하기 - 2
  4. 2014.12.03 [BroadLeaf] eCommerce 시작하기 - 1

배너 밑으로는 선전하고 싶은 상품 목록 4개를 열거한다. 이때 반응형 웹 디자인의 그리드(Grid)를 적용해 본다. 




화면 분할


  - WEB-INF/templates/layout/home.html 템플릿의 내용이 많아지면 partial html로 분리하여 include하는 방식을 취한다

  - 우선, Homepage Banner Ad Carousel 부분을 <th:include> 처리하고 Product 목록 부분도 <th:include> 처리해 관리한다. 

  - 기존 화면 

   


  - 변경 화면

    + 박스 테두리 CSS를 적용한다

    + 각 박스 테두리별로 반응형 그리드를 적용한다  

   

  - WEB-INF/templates/layout/home.html 내역중 Carousel에 대한 부분과 product 목록에 대한 부분을 WEB-INF/templates 폴더 밑에 main/partials 폴더를 새롭게 만들고 banner-caruosel.html 과 product-list.html 을 생성하고 기존 내역을 옮긴다. 

  - content의 contentType인 "Homepage Featured Products Title" 인 목록을 뿌려주는 것이다. 

  - layout 폴더에서 main 폴더로 옮기는 이유는 해당 main 폴더 영역에 있는 부분이 계속 변경되기 때문이다. 즉, 다른 페이지로 이동해도 nav와 footer는 고정되어 있다. 

// home.html의 과거 내역 


<blc:content contentType="Homepage Featured Products Title" />    

<div th:if="${contentItem !=null and contentItem['messageText'] !=null}" class="title_bar" th:text="${contentItem['messageText']}"></div>


<ul id="products" class="group">

    <li th:each="product : ${products}" th:object="${product}" th:include="catalog/partials/productListItem" class="product_container"></li>

</ul>



// 신규 layout/home.html 일부 내역 


    <div th:include="main/partials/banner-carousel" />

    

    <div class="section">

        <div th:include="main/partials/featured-products" />

    </div>



// main/partials/featured-products.html


    <div class="container">

   

          <div class="row">

          <blc:content contentType="Homepage Featured Products Title" />  

             <div th:if="${contentItem !=null and contentItem['messageText'] !=null}" 

                  class="col-lg-12 col-md-12 col-sm-12 col-xs-12 featured-title" 

                  th:text="${contentItem['messageText']}"></div>

       

              <div th:each="product : ${products}" 

                   th:object="${product}" 

                   th:include="catalog/partials/productListItem" 

                   class="col-lg-3 col-md-3 col-sm-6 col-xs-6">

              </div>

          </div>

          

    </div>


  - class="col-lg-3 col-md-3 col-sm-6 col-xs-6" 을 적용해서 데스크톱에서는 4개씩 상품이 보이고 테블릿 이하는 2개씩 상품이 보임




Product Item 그리드 구성 


  - th:include="catalog/partials/productListItem" 에서 기존 productListItem.html 의 CSS를 하기와 같이 다시 적용한다 

    + max-width: 320px; 를 주어 박스의 최대 크기를 지정한다. 

// /webapp/css/style.css 일부 내역 


/************************************/

/* featured product list  */

/************************************/

.thumbnail {

max-width: 320px;

display: block;

padding: 5px;

margin-bottom: 10px;

line-height: 1.5;

background-color: #fff;

border: 1px solid #ddd;

border-radius: 4px;

-webkit-transition: border .2s ease-in-out;

-o-transition: border .2s ease-in-out;

transition: border .2s ease-in-out;

}


.thumbnail .content {

padding: 3px;

font: 13px/18px 'BryantLGRegular';

color: #655c5a;

overflow: hidden;

}


.thumbnail .content .title {

font: 15px/18px 'BryantLGMedium';

margin-bottom: 7px;

color: #222222;

height: 36px;

max-height: 36px;

}


.thumbnail .content .desc {

padding: 3px;

font: 13px/18px 'BryantLGLight';

color: #655c5a;

overflow: hidden;

height: 72px;

max-height: 72px;

}


.thumbnail .new_badge {

width: 60px;

height: 60px;

background: url('../img/badge-new.png') no-repeat;

text-indent: -9999px;

position: absolute;

top: -10px;

left: -1px;

z-index: 20;

}


.thumbnail .image {

margin-bottom: 10px;

overflow: hidden;

position: relative;

z-index: 10;

}


.thumbnail .image .price {

position: absolute;

background-color: black;

color: white;

opacity: 0.8;

padding: 10px 20px;

font: 16px/16px 'BryantLGMedium';

font-weight: 600;

bottom: 5px;

right: 5px;

}


  - 적용한 productListItem.html 의 내용은 하기와 같다.

<div class="thumbnail">

<div th:if="*{featuredProduct}" class="new_badge">New!</div>


<div class="image">

<a th:href="@{*{url}}">

    <img th:if="*{media['primary']}" blc:src="@{*{media['primary'].url} + '?browse'}" th:alt="*{name}" />

    <div class="price" th:if="${#object instanceof T(org.broadleafcommerce.core.catalog.domain.ProductBundle)}">

        <div blc:price="*{salePrice}" th:if="*{onSale}" th:classappend="*{defaultSku.onSale}? 'sale'"></div>

        <div blc:price="*{retailPrice}" th:classappend="*{onSale}? 'has-sale'"></div>

    </div>

    <div class="price" th:unless="${#object instanceof T(org.broadleafcommerce.core.catalog.domain.ProductBundle)}">

        <div blc:price="*{defaultSku.salePrice}" th:if="*{defaultSku.onSale}" th:classappend="*{defaultSku.onSale}? 'sale'"></div>

        <div blc:price="*{defaultSku.retailPrice}" th:classappend="*{defaultSku.onSale}? 'has-sale'"></div>

    </div>

    </a>

    </div>

    

<div class="content">

  <div class="title" th:text="*{name}">Product Name</div>

<p class="desc" th:utext="*{longDescription}">description</p>

<p> 

<div th:class="*{'productActions productActions' + id}"

    th:with="checkInventory=*{defaultSku.inventoryType?.type == 'CHECK_QUANTITY'},

              availableInventory=${checkInventory ? #object.defaultSku.quantityAvailable != null and #object.defaultSku.quantityAvailable != 0 : true},

              inCart=${cart.containsSku(#object.defaultSku) and #lists.isEmpty(product.productOptions)}">

    <div th:if="${checkInventory and !availableInventory}" class="out_of_stock">

        <a disabled="disabled" class="inCart">Out of Stock</a>

    </div>

    <div class="btn btn-default" th:classappend="${!inCart}? ' hidden'" th:if="${#lists.isEmpty(product.productOptions)}">

        <a class="modalcart inCart" th:href="@{/cart}"><span th:text="#{product.inCart}">In Cart!</span></a>

    </div>

    <div class="add_to_cart" th:classappend="${inCart or !availableInventory}? ' hidden'">

        <blc:form method="POST" th:action="@{/cart/add}">

            <input type="hidden" name="productId" th:value="*{id}" />

            <input type="hidden" name="quantity" value="1" />

            <input type="hidden" name="hasProductOptions" th:value="*{!#lists.isEmpty(productOptions)}" />

            <input type="submit" class="btn btn-primary" th:value="#{product.buyNow}"/>

        </blc:form>

    </div>

</div>

</p>

    </div>

    

</div>


<div style="display: none;" th:id="*{'productOptions' + id}" class="product-options modal">

    <h3 th:text="*{name}"></h3>

    <div class="product-options" th:substituteby="catalog/partials/productOptions"/>

    <input type="button" class="addToCart" th:value="#{product.buyNow}" />

</div>


화면 구성이 되었다면 다음으로 2단 메뉴 구성을 해보자 

posted by 윤영식

DemoSite에서 제공하는 랜딩 페이지의 중앙에는 마케팅 제품 한개만을 디스플레이한다. 이를 여러개의 마케팅 제품을 Carousel로 변경하는 작업을 해본다. 




Carousel HTML 추가


  - 원래 화면으로 SHIRT SPECIAL 이미지가 고정이다.

    


  - 변경된 화면으로 Carousel을 적용한다. 

    + Twitter Bootstrap의 carousel을 적용

    + 기존 제품을 두번째에 배치

    


  - HTML에서 메인 마케팅 제품의 <blc> 태그 내역음 하기와 같다

    + <blc:content> 태그의 contentType="Homepage Banner Ad"의 값이 키가 되어 content의 데이터를 MySQL에서 찾음

    + 반환값인 contentItem 객체를 통해 Thymeleaf 템플릿엔진은 HTML을 생성해줌

    + Twitter Bootstrap의 carousel을 추가하고 첫번째에 배너를 넣고 다음 아이템(보라색) 두번째에 틀린 배너를 넣음

       contentType인 Homepage Banner Ad2 라는 이름임 

// 원본 소스 

<nav th:substituteby="layout/partials/nav" />

            

<blc:content contentType="Homepage Banner Ad" />   

<div id="banners" th:if="${contentItem !=null and contentItem['targetUrl'] != null and contentItem['imageUrl'] != null}">

    <a th:href="@{${contentItem['targetUrl']}}"><img blc:src="@{${contentItem['imageUrl']}}" /></a>       

</div>



// 수정 소스 

<div th:include="layout/partials/nav" />

       

<div id="myCarousel" class="carousel slide">

    <!-- Indicators -->

    <ol class="carousel-indicators">

        <li data-target="#myCarousel" data-slide-to="0" class="active"></li>

        <li data-target="#myCarousel" data-slide-to="1"></li>

        <li data-target="#myCarousel" data-slide-to="2"></li>

    </ol>


    <!-- Wrapper for slides -->

    <div class="carousel-inner">

        <div class="item active">

            <blc:content contentType="Homepage Banner Ad" />

            <div class="fill" th:if="${contentItem !=null and contentItem['targetUrl'] != null and contentItem['imageUrl'] != null}" >

              <a th:href="@{${contentItem['targetUrl']}}"><img style="height:100%; width:100%" blc:src="@{${contentItem['imageUrl']}}" /></a>  

            </div>

            <div class="carousel-caption">

                <h1></h1>

            </div>

        </div>

        <div class="item">

            <blc:content contentType="Homepage Banner Ad2" />

            <div class="fill" th:if="${contentItem !=null and contentItem['targetUrl'] != null and contentItem['imageUrl'] != null}" >

              <a th:href="@{${contentItem['targetUrl']}}"><img style="height:100%; width:100%" blc:src="@{${contentItem['imageUrl']}}" /></a>  

            </div>

            <div class="carousel-caption">

                <h1>This is Homepage Banner Ad2 Test</h1>

            </div>

        </div>

        <div class="item">

            <div class="fill" style=""></div>

            <div class="carousel-caption">

                <h1>Additional Layout Options at <a href="http://startbootstrap.com">http://startbootstrap.com</a>

                </h1>

            </div>

        </div>

    </div>


    <!-- Controls -->

    <a class="left carousel-control" href="#myCarousel" data-slide="prev">

        <span class="icon-prev"></span>

    </a>

    <a class="right carousel-control" href="#myCarousel" data-slide="next">

        <span class="icon-next"></span>

    </a>

</div>


  - Carousel을 구동하는 자바스크립트를 추가함 

    + WEB-INF/templates/layout/partials/footer.html안에 main.js를 추가함. 메인 페이지 하단에 자바스크립트를 추가해줌 

    + webapp/js 폴더 밑에 main.js 를 추가하고 carousel 자바스크립트를 초기화하고 화면간 이동시간을 설정함

// footer.html 

 <blc:bundle name="heatclinic.js" 

                mapping-prefix="/js/"

                files="main.js,

                       BLC.js,

                       heatClinic.js,

                       cartOperations.js,

                       checkoutOperations.js,

                       globalOnReady.js,

                       manageAccountOperations.js,

                       reviewOperations.js" />



// js/main.js

$('.carousel').carousel({

  interval: 3000

})




Homepage Banner Ad2 추가하기


  - Homepage Banner Ad가 추가된 위치를 Admin에서 화인할 수 있다.

    + DemoSite에 보면 admin sub-module이 있고 Ant를 통해서 실행할 수 있다. 

    + http://localhost:8081/admin 으로 접속해서 id:admin, pwd:admin으로 접속한다.

    + 관리페이지의 Content -> Content Items를 보면 아이템 목록이 나오고 "Content Type"이 Homepage Banner Ad가 언어별로 등록됨

    + 즉, <blc:content> 태그의 contentType이 데이터 조회를 위한 파라미터 키이고 Homepage Banner Ad가 값임.

    


  - 최초에 DemoSite를 수행할 때 src/main/resources/runtime-properties/development.properties는 하기와 같다 

    + create-drop 설정은 DemoSite 서버(jetty)를 기동할 때 기존 테이블을 전부 drop 하고 새롭게 생성하고 데이터를 넣어줌 

    + 한번 생성된 이후 다시 drop하지 않고 DemoSite를 기동하고 싶으면 update로 변경함 

// 기존 설정 

blPU.hibernate.hbm2ddl.auto=create-drop


// 변경 설정

blPU.hibernate.hbm2ddl.auto=update


  - 결국 DemoSite 기동할 때 insert 구문을 통해 Homepage Banner Ad를 넣고 있으므로 Homepage Banner Ad2 값을 넣을 수 있음

    + sql 파일은 core sub-module의 src/main/resrouces/sql밑에 존재함 

    + 관련 sql은 load_content_structure.sql 과 load_content_data.sql 이다 

    + content_structure는 원화 기준, 언어 기준, 화면 템플릿의 논리적 그룹핑 명칭 등록 및 4단계를 거쳐 structure를 만들어준다 

// load_content_structure.sql 안에 보라색 내역 Homepage Banner Ad2를 추가함


---------------------------------------------------------------------------------------------------------------------------

-- Structured Content Step 4:   Create Types (These represent areas on a page or general types:  e.g 'Homepage Banner Ad')

---------------------------------------------------------------------------------------------------------------------------

INSERT INTO BLC_SC_TYPE (SC_TYPE_ID, NAME, DESCRIPTION, SC_FLD_TMPLT_ID) VALUES (1, 'Homepage Banner Ad', NULL, 1);

INSERT INTO BLC_SC_TYPE (SC_TYPE_ID, NAME, DESCRIPTION, SC_FLD_TMPLT_ID) VALUES (2, 'Homepage Middle Promo Snippet', NULL, 2);

INSERT INTO BLC_SC_TYPE (SC_TYPE_ID, NAME, DESCRIPTION, SC_FLD_TMPLT_ID) VALUES (3, 'Homepage Featured Products Title', NULL, 3);

INSERT INTO BLC_SC_TYPE (SC_TYPE_ID, NAME, DESCRIPTION, SC_FLD_TMPLT_ID) VALUES (4, 'Right Hand Side Banner Ad', NULL, 1);


-- by ysyun

INSERT INTO BLC_SC_TYPE (SC_TYPE_ID, NAME, DESCRIPTION, SC_FLD_TMPLT_ID) VALUES (5, 'Homepage Banner Ad2', NULL, 1);


    + content_data는 컨텐트의 url, image 등의 세부 내역이 담겨져 있다. 

    + 원본은 3개의 Banner가 우선순위가 있게 등록되어 있는데 Homepage Banner Ad2를 새로운 배너 우선순위 1로 해서 등록한다.

    + 기존 베너에 있던 Buy One Get One을 Homepage Banner Ad2에 배정한다. 

    + 세번째 Carousel을 등록하고 싶다면 Homepage Banner Ad3를 structure에 등록해서 하기 과정에 sql을 추가하면 끝! 

    + 이미지는 webapp/img/banners 폴더에 존재함 

// load_content_data.sql 에서 보라색 부분을 추가한다.


---------------------------------------------------

-- HOME PAGE BANNER

---------------------------------------------------

-- Content Item

INSERT INTO BLC_SC (SC_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, CONTENT_NAME, OFFLINE_FLAG, PRIORITY, LOCALE_CODE, SC_TYPE_ID) VALUES (100, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 'Buy One Get One - Twice the Burn', FALSE, 5, 'en', 1);

INSERT INTO BLC_SC (SC_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, CONTENT_NAME, OFFLINE_FLAG, PRIORITY, LOCALE_CODE, SC_TYPE_ID) VALUES (101, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 'Shirt Special - 20% off all shirts', FALSE, 1, 'en', 1);

INSERT INTO BLC_SC (SC_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, CONTENT_NAME, OFFLINE_FLAG, PRIORITY, LOCALE_CODE, SC_TYPE_ID) VALUES (102, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 'Member Special - $10 off next order over $50', FALSE, 5, 'en', 1);


-- by ysyun

INSERT INTO BLC_SC (SC_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, CONTENT_NAME, OFFLINE_FLAG, PRIORITY, LOCALE_CODE, SC_TYPE_ID) VALUES (103, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 'Buy One Get One - Twice the Burn', FALSE, 1, 'en', 5);


-- Fields

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (1, CURRENT_TIMESTAMP, 'imageUrl', 1, '/img/banners/buy-one-get-one-home-banner.jpg');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (2, CURRENT_TIMESTAMP, 'targetUrl', 1, '/hot-sauces');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (3, CURRENT_TIMESTAMP, 'imageUrl', 1, '/img/banners/shirt-special.jpg');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (4, CURRENT_TIMESTAMP, 'targetUrl', 1, '/merchandise');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (5, CURRENT_TIMESTAMP, 'imageUrl', 1, '/img/banners/member-special.jpg');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (6, CURRENT_TIMESTAMP, 'targetUrl', 1, '/register');


-- by ysyun

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (7, CURRENT_TIMESTAMP, 'imageUrl', 1, '/img/banners/buy-one-get-one-home-banner.jpg');

INSERT INTO BLC_SC_FLD (SC_FLD_ID, DATE_CREATED, FLD_KEY, CREATED_BY, VALUE) VALUES (8, CURRENT_TIMESTAMP, 'targetUrl', 1, '/hot-sauces');


-- Field XREF

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (100, 1, 'imageUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (100, 2, 'targetUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (101, 3, 'imageUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (101, 4, 'targetUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (102, 5, 'imageUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (102, 6, 'targetUrl');


-- by ysyun

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (103, 7, 'imageUrl');

INSERT INTO BLC_SC_FLD_MAP (SC_ID, SC_FLD_ID, MAP_KEY) VALUES (103, 8, 'targetUrl');


  - sql 추가하고 create-drop으로 놓은 다음 DemoSite를 제구동하면 Admin에서 하기와 같이 나온다. 

  


베너를 Carousel로 변환하여 볼 수 있을 것이다. 다음은 2 level Menu로 변경하는 과정을 살펴보자. 



참조 


  - Broadleaf DemoSite 소스

posted by 윤영식

BroadleafThymeleaf이라는 서버 템플릿 엔진을 사용하고 있다. Broadleaf은 Tymeleaf을 상속받아 확장한 <blc:XXX> 태그를 xxxProcessor 클래스에 정의하고 있다. 따라서 템플릿에는 Tymeleaf 태그와 Broadleaf이 정의한 <blc:XXX> 태그가 함께 사용된다. 이에 대해 간략히 알아보고 Twitter Bootstrap을 템플릿에 적용해 보자 




Broadleaf에서 Thymeleaf 역할 


  - Thymeleaf은 레이아웃 템플릿 보다 텍스트 템플릿에 가깝고 객체값을 받아서 화면에 표현해 주는 제어구문들을 가지고 있다.

  - AngularJS의 Directive와 서버 템플릿의 Taglib로 사용자 정의한 태그와 백앤드/프론트앤드만 틀리고 유사하다고 하겠다. 

  - 그럼 데이터는 어떻게 주고 받아서 표현할까?

     + Thymeleaf은 HTML을 해석하고 맵핑된 객체를 통해 Value가 설정된 HTML을 최종 클라이언트에 전송한다. 

     + 이때 객체 정보는 Broadlead의 <blc:XXX> 태그에서 Broadleaf core소스에서 정의한 XXXProcessor 가 YYYService를 통해서 Thymleaf에 사용할 객체를 조회/설정/전달한다. 


  - core/broadlleaf-framework : Hibernate와 연결된 DAO와 업무 도메인 Service가 존재한다. 

  - core/broadleaf-framework-web : web이므로 프론트단에 필요한 폼, 필터, 태그처리 프로세스(Processor)등이 존재한다. 

    + DemoSite의 메인 페이지 템플릿은 /site/src/main/webapp/WEB-INF/templates/layout/home.html 해당 파일이다. 

    + home.html을 열어보면 맨위에 하기와 같이 <head>가 설정되어 있다. 

    + <head> 태그를 core/broadleaf-framework-web의 org.broadleafcommerce.core.web.processor.HeadProcessor 가 처리한다.

    + HeadProcessor는 pageTitle 값을 처리하는 클래스이다. 

    + 즉, XxxYyyZzzProcessor 가 있고 태그로 <xxx-yyy-zzz> 태그를 사용하면 XxxYyyZzzProcessor 가 동작하는 것이다. 

// home.html 일부 내역


<head th:include="/layout/partials/head (pageTitle='Broadleaf Demo - Heat Clinic')"></head>

<body>

    <div id="notification_bar"></div>

    <header th:substituteby="layout/partials/header" />

    

    <div id="content" class="width_setter group" role="main">

        <nav th:substituteby="layout/partials/nav" />

        <blc:content contentType="Homepage Banner Ad" />       

        <div id="banners" th:if="${contentItem !=null and contentItem['targetUrl'] != null and contentItem['imageUrl'] != null}">

            <a th:href="@{${contentItem['targetUrl']}}"><img blc:src="@{${contentItem['imageUrl']}}" /></a>       

        </div>

  ... 중략 ...

</body>



// HeadProcessor.java 일부 내역


import org.thymeleaf.processor.element.AbstractFragmentHandlingElementProcessor;

public class HeadProcessor extends AbstractFragmentHandlingElementProcessor {

    @Resource(name = "blHeadProcessorExtensionManager")

    protected HeadProcessorExtensionListener extensionManager;

    public static final String FRAGMENT_ATTR_NAME = StandardFragmentAttrProcessor.ATTR_NAME;

    protected String HEAD_PARTIAL_PATH = "layout/partials/head";


    public HeadProcessor() {

        super("head");

    }


    @Override

    public int getPrecedence() {

        return 10000;

    }


    @Override

    protected boolean getRemoveHostNode(final Arguments arguments, final Element element) {

        return true;

    }


    @Override

    @SuppressWarnings("unchecked")

    protected List<Node> computeFragment(final Arguments arguments, final Element element) {

        String pageTitle = element.getAttributeValue("pageTitle");

        try {

            Expression expression = (Expression) StandardExpressions.getExpressionParser(arguments.getConfiguration())

                    .parseExpression(arguments.getConfiguration(), arguments, pageTitle);

            pageTitle = (String) expression.execute(arguments.getConfiguration(), arguments);

        } catch (TemplateProcessingException e) {}


        ((Map<String, Object>) arguments.getExpressionEvaluationRoot()).put("pageTitle", pageTitle);

      ((Map<String, Object>) arguments.getExpressionEvaluationRoot()).put("additionalCss", element.getAttributeValue("additionalCss"));


        extensionManager.processAttributeValues(arguments, element);

        return new ArrayList<Node>();

    }

}


  - <blc:content> 는 특별히 admin/broadleaf-conentmanagement-modul 에서 org.broadleafcommerce.cms.web.processor.ContentProcessor 로 존재한다.

  - <blc.content> 태그는 내부적으로 서비스를 호출해서 contentItem을 값을 설정해 준다. 이때 contentType="Homepage Banner Ad"는 admin 페이지에서 설정해주는 것이다. 즉, 관리 도구를 통해 Content Mangement를 할 수 있고 이를 간단히 <blc:xxx> 태그를 사용한다.

1) admin 프로젝트에서 ant task중 jetty-demo 실행

2) http://localhost:8081/admin (id:admin, password: admin)

3) Content 메뉴의 content items 목록이 나온다. 여기서 "Content Type"으로 찾으면 됨 



  - Thymeleaf의 특정 인터페이스를 상속받으면 자신만의 태그를 쉽게 만들수 있고 Broadleaf에서는 XXXProcessor로 만들고 있는 것이다. 그리고 <blc:xxx> 태그를 만들어 xxxProcessor내부에서 Service를 통해 Hibernate를 호출해 업무적인 부분을 처리하고 있다.


  얼핏 보면 AngularJS의 Directive를 만들고 내부에서 AngularJS 서비스를 DI(Dependency Injection) 받아 호출하고 서비스내에서 서버로 AJAX 호출하는 계층을 두는 것과 유사하다. 따라서 Broadleaf에서 정의한 xxxProcessor들을 AngularJS Directive로 전환하면 AngularJS기반의 SPA(Single Page Application)으로의 전환이 가능하다. 




Twitter Bootstrap 적용하기 


  - 트위터 부트스트랩은 UI Framework으로 RWD (Responsible Web Design)을 적용할 수 있는 CSS를 제공하고 많이 사용하는 jQuery 플러그인도 함께 제공된다. 

  - 우선 트위터 부트스트랩 v3.3.1을 다운로드하고 압축을 풀면 css, fonts, js 폴더가 있다 

  - site/src/main/webapp 밑에 css, fonts, js를 복사한다. 

  - site/src/main/webapp/WEB-INF/templates/layout 폴더가 상/하/메인에 대한 레이아웃을 설정하는 템플릿 HTML이 존재한다

  - 상단 head.html에 bootstrap.css를 추가하고 하단 footer.html에 bootstrap.js를 하기와 같이 추가한다 

// layout/partials/head.html 


    <blc:bundle name="style.css" 

                mapping-prefix="/css/"

                files="bootstrap.css,

                       style.css,

                       jquery.rating.css" />



// layout/partials/footer.html


    <blc:bundle name="lib.js" 

                mapping-prefix="/js/"

                files="plugins.js,

                       libs/jquery.MetaData.js,

                       libs/jquery.rating.pack.js,

                       libs/jquery.dotdotdot-1.5.1.js,

                       bootstrap.js" />


  - 기존의 CSS는 전부 무시하고 트위터 부트스트랩을 재적용하는 단계이다. 따라서 DemoSite에 적용한 css/style.css 의 내용을 전부 제거 한다.

  - 제거한 내용에 몇가지 CSS를 하기와 같이 적용한다.

/************************************/

/* Header Customizing               */

/************************************/

.navbar .navbar-top {

height: 30px;

padding-top: 5px;

background-color: #F5F5F5;

transition: all 0.1s ease-out 0s;

-webkit-transition: all 0.1s ease-out 0s;

-moz-transition: all 0.1s ease-out 0s;

-ms-transition: all 0.1s ease-out 0s;

-o-transition: all 0.1s ease-out 0s;

font-size: 11px;

line-height: 11px;

text-transform: uppercase;

}


.navbar .container-fluid {

background-color:#525252;

}


.navbar-inverse .navbar-nav > li > a, .navbar-inverse .navbar-brand {

color: #FEFEFE;

}


html, body {

height: 100%;

}


  - site의 WEB-INF/templates/layout/home.html 의 내역에서 필요없는 부분을 주석처리 하고 partials/nav.html 를 사용한다. 

    + <blc:xxx> 로 사용하는 부분을 전부 제거했다. 나중에 필요한 부분에 넣을 것이다. 

    + partails/nav.html 에서 메뉴역할을 하도록 partails/header.html을 nav.html에 통합할 것이다. 

    + <blc:form> 을 통해 제품 검색을 수행한다. 

<head th:include="/layout/partials/head (pageTitle='Broadleaf And Twitter Bootstrap')"></head>


<body>

    <div id="notification_bar"></div>

    

    <nav th:substituteby="layout/partials/nav" />

    

    <footer th:substituteby="layout/partials/footer" />

    

</body>

</html>


  - 다음으로 메뉴를 nav.html에 적용한다. 

    + 트위터 부트스트랩에 맞게 RWD를 적용하고 카테고리(Categories)에 대해 Thymeleaf의 <li th:each> 구문을 적용한다.

    + Admin 화면 (http://localhost:8081/admin) 에서 Category에 해당하는 내용이 메뉴에 적용되는 것으로 broadleaf-framework-web 프로젝트의 org.broadleafcommerce.core.web.processor.CategoriesProcessor가 처리한다. 

<nav>   

    <div th:remove="all">

        <!--

             This template displays the navigation of the site by looking up the category named "Nav"

             and building a list of the categories sub-categories.                          

        -->

    </div>

    

    <blc:categories resultVar="navCategories" parentCategory="Primary Nav" maxResults="6" />

    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">

    

      <!-- TOP Header  -->

      <div class="navbar-top">

      <div class="container">

      <div class="pull-right">

   

    <div th:if="${customer.anonymous}"  th:remove="tag">

            <a th:href="@{/login}">

                <span th:text="#{home.login}">Login</span>

            </a>

            &nbsp;|&nbsp; 

            <a th:href="@{/register}">

                <span th:text="#{home.register}">Register</span>

            </a>

            &nbsp;|&nbsp; 

        </div>

        <div th:unless="${customer.anonymous}" th:remove="tag">

            <span><span th:text="#{home.welcome}">Welcome</span>, <a th:href="@{/account}" th:text="${customer.firstName}"></a></span>

            &nbsp;|&nbsp; 

            <a th:href="@{/logout}">

                <span th:text="#{home.logout}">Logout</span>

            </a>

            &nbsp;|&nbsp; 

        </div>

        <img blc:src="@{/img/icon-cart.jpg}" alt="Shopping Cart Icon" />&nbsp; 

        <a id="cartLink" th:href="@{/cart}">

            <span id="headerCartItemWordSingular_i18n" style="display: none;" th:text="#{home.item}" />

            <span id="headerCartItemWordPlural_i18n" style="display: none;" th:text="#{home.items}" />

            <span class="headerCartItemsCount" th:text="${cart.itemCount}" />

            <span class="headerCartItemsCountWord" th:text="${cart.itemCount == 1} ? #{home.item} :  #{home.items}" />

        </a>

   

    </div>   

    </div>

      </div>

      

      <!-- Responsible Menu -->

      <div class="container-fluid">

        <div class="navbar-header">

          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">

            <span class="sr-only">Toggle navigation</span>

            <span class="icon-bar"></span>

            <span class="icon-bar"></span>

            <span class="icon-bar"></span>

          </button>

          <a class="navbar-brand" href="/">eCommerce</a>

        </div>

        

        <div id="navbar" class="navbar-collapse collapse">

          <!-- Categories -->

          <ul class="nav navbar-nav navbar-right">

       

        <blc:form class="navbar-form navbar-right" style="margin-left:5px; margin-right:5px" th:action="@{/search}" method="GET">

            <input type="search" class="form-control" placeholder="SEARCH PROD" name="q" th:value="${originalQuery}" />

            <input type="submit" class="btn btn-default" value="go" />

    </blc:form>

   

            <li th:each="category : ${navCategories}" th:unless="${#strings.isEmpty(category.url)}">

            <a class="home" th:href="@{${category.url}}" th:class="${categoryStat.first}? 'home'" th:text="${category.name}"></a>

        </li>

          </ul>

        </div>

      </div>

      

    </nav>

    

</nav>


결과 화면은 다음과 같이 정상적인 화면과 RWD 메뉴가 적용된 화면이다. 



* 소스는 제공되지 않습니다. 관심있으신 분은 별도로 문의해 주세요. 



참조 


  - http://www.broadleafcommerce.org

  - Broadleaf core 깃헙 소스

  - http://www.thymeleaf.org

  - 트위터 부트스트랩

posted by 윤영식

BroadLeaf eCommerce 솔루션은 서버는 자바 Spring Framework와 Hibernate를 사용하고 상품에 대한 검색은 Solr를 사용한다. 그리고 클라이언트는 ThymeLeaf 템플릿 엔진을 사용해서 BroadLeaf과 강하게 결합해서 사용된다. BroadLeaf 데모 사이트를 실제 사용할 수 있는 eCommerce 사이트로 전환하는 과정을 살펴본다. 또한 Frontend 쪽에 Twitter BootstrapAngularJS를 적용해 Tymeleaf과 상호 운용될 수 있도록 설정해 본다.






Broadleaf DemoSite 설치 


  - BroadleafCommerce 는 코어 소스이고 Community 버전으로 오픈소스를 제공한다. 

  - BroadleafCommerce 코어 소스를 사용한 DemoSite를 제공한다. 

  - DemoSite를 깃헙에서 받아 사용해도 되고 eclipse workspace 파일을 별도로 이곳에서 받아도 된다. (다운로드 버전은 3.1.8 버전)

    + 만일 3.1.9 버전을 사용하고 싶다면 깃헙의 DemoSite 릴리즈에서 3.1.9-GA를 다운로드하면 된다.


  - DemoSite를 maven project로 eclipse에 import하는 과정은 BroadLeaf Starting 가이드를 참조한다.

    + eclipse는 luna를 사용하면 eclipse용 maven 플러그인이 기본 설치되어 있다. 

    + 단, Apache Maven 은 다운로드해서 설치하고 path를 잡아준다.

    + 최초 DemoSite/build.xml 을 Ant view에서 열고 "change-identifier"를 수행하고

       maven의 grouId를 com.xxxx 식으로 넣고  "y"를 선택하면 자바 패키지와 maven groupId등을 설정한 값으로 자동 변경한다. 


  - 최초에 메이븐 빌드를 한번 해준다. 먼저 DemoSite로 이동해서 하기 명령을 한번 수행한다

$ cd DemoSite

$ mvn clean install


  - Broadleaf은 Site와 Admin으로 구분되어 Site는 쇼핑사이트를 Admin은 카랄로그와 컨텐츠를 관리하고 각각 8080, 8081를 사용한다.

    + site/build.xml 과 admin/build.xml 을 ant view에서 열고 "jetty-demo"를 수행하면 된다. 

    + 데이터베이스는 기본으로 메모리DB인 HSQLDB를 사용한다.

Site

http://localhost:8080


Admin (id: admin, password: admin)

http://localhost:8081/admin/




MySQL전환하기 


  - MySQL 전환작업은 가이드를 참조한다. 

  - mysql로 전환하기 위해 데이터베이스와 계정을 broadleaf 로 지정하고 계정 암호는 abc1234 로 생성한다.

-- %> mysql --user=root -p mysql

-- Enter password: ******


-- mysql> grant all on broadleaf.* to broadleaf@localhost identified by 'abc1234' with grant option;

-- mysql> create database broadleaf;

-- mysql> flush privileges;


-- %> mysql -u broadleaf -p broadleaf

-- Enter password: ******


-- mysql> show grants for broadleaf@localhost;

-- mysql> select current_user;


  - admin과 site가 mysql을 사용하도록 DemoSite/pom.xml과 admin/pom.xml과 site/pom.xml에 다음을 설정한다 

// DemoSite/pom.xml  안에 설정 

<dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

    <version>5.1.34</version>

    <type>jar</type>

    <scope>compile</scope>

</dependency> 


// admin, site/pom.xml 안에 설정 

<dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

</dependency> 


  - jetty를 통해 기동될 때 데이터베이스 커넥션 유형을 3가지를 사용하는데 HSQLDB에서 MySQL로 환경값을 바꾸어 주어야 한다. 

    + /site/src/main/webapp/WEB-INF/jetty-env.xml

    + /admin/src/main/webapp/WEB-INF/jetty-env.xml

<New id="webDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/web</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>

<New id="webSecureDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/secure</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>

<New id="webStorageDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/storage</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>


  - 하이버네이트의 Dialect를 HSQLDB에서 MySQL로 바꾼다. 

     + /core/src/main/resources/runtime-properties/common-shared.properties  의 맨 하단에 다음을 넣는다. 

# overriding dialect 

blPU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blSecurePU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blCMSStorage.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


  - DemoSite의 build.properties의 기본 환경값을 HSQLDB에서 MySQL로 바꾼다. 

    + /DemoSite/build.properties 의 맨 하단에 다음을 넣는다. 

# new overriding 

ant.hibernate.sql.ddl.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


ant.blPU.url=jdbc:mysql://localhost:3306/broadleaf

ant.blPU.userName=broadleaf

ant.blPU.password=abc1234

ant.blPU.driverClassName=com.mysql.jdbc.Driver


ant.blSecurePU.url=jdbc:mysql://localhost:3306/broadleaf

ant.blSecurePU.userName=broadleaf

ant.blSecurePU.password=abc1234

ant.blSecurePU.driverClassName=com.mysql.jdbc.Driver


ant.blCMSStorage.url=jdbc:mysql://localhost:3306/broadleaf

ant.blCMSStorage.userName=broadleaf

ant.blCMSStorage.password=abc1234


  - 그리고 가장 중요한 설정은 site와 admin 의 common.properties에 맨 하단에 다음 사항을 필히 넣어 주어야 BLC_SKU 테이블이 생성됨

    + /site/src/main/resources/runtime-properties/common.properties

    + /admin/src/main/resources/runtime-properties/common.properties

# overriding dialect 

blPU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blSecurePU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blCMSStorage.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


  - 전환 후에는 ant task에서 "build-app"를 해주고 "jetty-run-no-db"를 선택하면 build한 target 디렉토리의 소스가 적용된다. 


     




참조 

  

  - Broadleaf v3.1 Guide

posted by 윤영식
prev 1 next