스프링 시큐리티와 연계하여 로그인에 실패했을 경우 로그인화면으로 리다이렉트 시키면서 이메일을 다시 입력폼에 유지하려고 여러가지 시도를 해봤다.

스프링 시큐리티에서 로그인 실패를 담당하는 인터페이스는 AuthenticationFailureHandler 이다.

public interface AuthenticationFailureHandler {
 
/**
 * Called when an authentication attempt fails.
 * @param request the request during which the authentication attempt occurred.
 * @param response the response.
 * @param exception the exception which was thrown to reject the authentication
 * request.
 */
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOExceptionServletException;
}

위의 형태를 가지는 간단한 인터페이스로, 스프링 시큐리티 설정에서 formLogin() 에 설정하면 로그인이 실패했을 떄 호출되며 이에 대한 처리를 수행한다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .anyRequest().authenticated()
        // form 로그인 
        .and()
        .formLogin()
        .loginPage("/login").permitAll()
        .usernameParameter(USERNAME_PARAMETER)
        .passwordParameter(PASSWORD_PARAMETER)
        .successHandler(authenticationSuccessHandler()) (1)
        .failureHandler(authenticationFailureHandler()) (2)
}
 
// 생략 
@Bean
AuthenticationSuccessHandler authenticationSuccessHandler() {
    return new HoneymonUserAuthenticationSuccessHandler();
}
 
@Bean
AuthenticationFailureHandler authenticationFailureHandler() {
    return new HoneymonUserAuthenticationFailureHandler();
}
로그인 성공시 후속처리
로그인 실패시 후속처리

그리고 HoneymonUserAuthenticationFailureHandler가 호출되면 /login?error={message}&email={email}의 형식으로 리다이렉트되도록 해뒀다. 그런데 암만 로그인 실패가 나도 /login?error= 로 리다이렉트 되지를 않는 것이다. 이 문제 떄문에 한참 씨름을 했다.

public class HoneymonUserAuthenticationFailureHandler implements AuthenticationFailureHandler {
 
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOExceptionServletException {
        if(exception instanceof BadCredentialsException) {
            exception = new BadCredentialsException("security.exception.bad_credentials");
        }
        response.sendRedirect("/login?error=" + exception.getMessage() + "&email=" + request.getParameter(USERNAME_PARAMETER));
    }
}

옆에 누군가가 봐줬으면 참 좋았을텐데…​

그러다가 정말 우연찮게

'아, 내가 LoginWeb' 클래스를 지우고 ViewController 로 처리하도록 했지?

하는 생각이 미쳤다. 리다이렉트가 되지 않고 있는 url에 대해서는 다음과 같이 설정되어 있었다.

@Configuration
public class WebConfig  extends WebMvcConfigurerAdapter {
  @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("front/login");  // 로그인 
        registry.addViewController("/signup").setViewName("front/signup");  // 회원가입 
    }
}

이 간단한 설정이 나를 괴롭혔던 주 원인이었다.

ViewController 에서 /login 요청에 대한 처리를 수행하기는 하지만 뒤에 전달되는 파라미터들을 모두 소거시킨다는 것을 이해하지 못한 것이다.

어떤 장애든지 기본적인 접근은 그 장애에 영향을 주는 설정부터 살펴보도록 하자. ㅡ_-);; 내가 허비한 시간 우짤꺼야!!

This is a shortcut for defining a ParameterizableViewController that immediately forwards to a view when invoked. Use it in static cases when there is no Java controller logic to execute before the view generates the response.



HttpServletResponse 의 sendRedirect 메서드를 사용하다가 다음과 같은 메시지가 나왔다.

java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendRedirect(ResponseFacade.java:494)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.firewall.FirewalledResponse.sendRedirect(FirewalledResponse.java:41)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
  // 중략

이런 코드가 발생하는 경우를 찾아보면, 다음처럼 되어 있었다.

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
}
 
response.sendRedirect(redirectUrl);

예외 메시지를 살펴보니 sendRedirect 메서드가 호출된 이후에 재호출하려고 하면 java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed 가 출력되는 것이다. 호출된 이후에는 다시 호출할 수 없다는 뜻으로 변경할 수 없다는 뜻이다.

이에 대한 해결책을 좀 찾아봤다.

이걸 해결항 수 있는 방법으로는,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
} else if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
} else {
    response.sendRedirect(redirectUrl);
}

와 같은 방식으로 완전히 조건문 내에서 한번만 호출되도록 처리하거나,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    redirectUrl = URL_SIGNUP_PROGRESS;
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    redirectUrl = URL_ROOT;
}
 
response.sendRedirect(redirectUrl);

위의 코드처럼 조건을 모두 마친 후에 마지막에 sendRedirect 를 호출하는 것이다. 마지막 방법이 좀 더 괜찮은 처리방법이라는 생각이…​


http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

거의... 이 녀석을 기반으로 작성할 건데....

... 다뤄야할 내용이 많다... ㅡ_-);;

... 하기 싫어진다(두렵다).


내 책 이름은... '부트 스프링 부트(boot spring boot)'

Gulp

설정 및 설치

  • package.json 생성
{
  "name": "test-front-web",
  "version": "0.0.1.SNAPSHOT",
  "devDependencies": {

  }
}
  • gulp 설치
$ npm install --save-dev gulp 

위의 명령을 실행하고 나면 package.json 파일에 다음과 같이 "gulp": "^3.9.1" 가 추가된 것을 확인할 수 있다.

{
  "name": "test-front-web",
  "version": "0.0.1.SNAPSHOT",
  "devDependencies": {
    "gulp": "^3.9.1"
  }
}

거기에 node_modules 디렉터리가 추가되고 다양한 의존성 라이브러리가 추가되었다.

프로젝트 폴더에 gulpfile.js 를 생성한다. 그리고 그 안에,

// Include gulp
var gulp = require('gulp');

코드를 추가한다.

추가할 수 있는 태스크의 종류는 많지만 그 중에서 몇가지만 소개한다:

  • gulp.task(name, function) - 이름으로 함수를 등록
  • gulp.watch(global, function) - 전역변수의 변경사항이 생겼을 때 호출할 함수 정의
  • gulp.src(global) - 읽어올 위치
  • gulp.dest(folder) - 작성된 파일을 놓을 위치

gulp와 파일 연결하기(Concatenating)

gulp만 설치한다고 끝나는 것은 아니다. gulp 가 동작할 수 있도록 플러그인을 설치하고 gulpfile 에 태스크를 추가해야 한다. gulp-concat 플러그인이 필요하다.

$ npm install --save-dev gulp-concat 

를 실행하고 나면 package.json 에 "gulp-concat": "^2.6.0" 가 추가된다.

이제 gulpfile 에 필요한 테스크를 추가한다.

// Include gulp
var gulp = require('gulp');

// Include plugins
var concat = require('gulp-concat');

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function() {
   return gulp.src('src/js/*.js')
       .pipe(concat('honeymon.js'))
       .pipe(gulp.dest('build/js'));
});

// Default Task
gulp.task('default', ['scripts']);

gulp-concat 플러그인 설정을 포함하고 gulp.task 를 사용하여 기본 테스크에 script 를 추가했다. 이 태스크는 3단계 과정으로 처리가 된다:

  1. gulp.src 에서 정의한 src/js 디렉토리에 있는 확장자 js 를 연결한다.
  2. main.js 로 연결한다.
  3. build/js 디렉토리에 main.js 파일을 밀어 넣는다.

gulp 를 이용한 JS 최소화하기

보통 자바스크립트를 배포할 때 최소화(Minify)하는 과정을 거치는데 gulp 에서는 이를 처리하는 플러그인이 있다. 그리고 나서 파일명 접미사로 .min 를 붙이는 rename 플러그인도 함께 설치한다.

$ npm install --save-dev gulp-unglify gulp-renmae 

이제 gulpfile.js 에 scripts 태스크를 수정한다.

// Include gulp
var gulp = require('gulp');
// Include plugins
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function () {
    return gulp.src('src/js/*.js')
        .pipe(concat('honeymon.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(uglify())
        .pipe(gulp.dest('js'));
});

gulp 를 이용한 css 처리

gulp 를 이용해서 sass 파일을 컴파일하여 css 를 생성하는 태스크를 추가한다.

루비의 공식적인 sass 컴파일러를 설치한다.

$ npm install --save-dev gulp-ruby-sass 

이제 gulpfile.js 에 sass 태스크를 수정한다.

// Include gulp
var gulp = require('gulp');
// Include plugins
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var sass = require('gulp-ruby-sass');

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function () {
    return gulp.src('src/js/*.js')
        .pipe(concat('honeymon.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(uglify())
        .pipe(gulp.dest('build/js'));
});

//Compile SASS
gulp.task('sass', function () {
    sass('src/scss/style.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest('build/css'));
    sass('src/scss/honeymon/*.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest('build/css/honeymon'));
});

// Default Task
gulp.task('default', ['scripts', 'sass']);

프로젝트에 사용하는 이미지 이동

이제 gulp 에서 많은 시간이 걸리는 작업이 남았다. 스크립트나 스타일시트 작업보다 더 오래 걸릴 것이다. gulp-imagemin 와 gulp-cache 를 이용해서 이미지를 최적화하는 작업을 진행한다.

$ npm install --save-dev gulp-imagemin gulp-cache 

gulp-imagemin 와 gulp-cache 에 대한 의존성을 추가하고 image 태스크를 수정하자.

// Include gulp
var gulp = require('gulp');
// Include plugins
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var sass = require('gulp-ruby-sass');
var imagemin = require('gulp-imagemin');
var cache = require('gulp-cache');

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function () {
    return gulp.src('src/js/*.js')
        .pipe(concat('honeymon.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(uglify())
        .pipe(gulp.dest('build/js'));
});

//Move image files
gulp.task('images', function() {
    return gulp.src(srcPath + '/img/**/*')
        .pipe(cache(imagemin({ optimizationLevel: 5, progressive: true, interlaced: true })))
        .pipe(gulp.dest(buildPath + 'img'));
});

//Compile SASS
gulp.task('sass', function () {
    sass('src/scss/style.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest('build/css'));
    sass('src/scss/honeymon/*.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest('build/css/honeymon'));
});

//Build JavaScript, Images, Sass
gulp.task('build', ['scripts', 'images', 'sass']);

// Default Task
gulp.task('default', ['build']);

실제로 많이 걸린다..

➜  test-front-web git:(master) ✗ gulp
[14:27:52] Using gulpfile ~/workspace/honeymon/test-front-web/gulpfile.js
[14:27:52] Starting 'scripts'...
[14:27:52] Starting 'images'...
[14:27:52] Starting 'sass'...
[14:27:52] Finished 'sass' after 19 ms
[14:27:52] Finished 'scripts' after 147 ms
[14:27:54] Finished 'images' after 2.18 s
[14:27:54] Starting 'build'...
[14:27:54] Finished 'build' after 1.61 μs
[14:27:54] Starting 'default'...
[14:27:54] Finished 'default' after 1.5 μs

프로젝트에서 작성한 html 을 이동시키자.

html 을 옮기는 과정에서는 2가지 작업을 해야한다.

  • src html 에서 보고 있는 *.css 와 *.js 의 경로를 변경해야 한다.
  • build 로 이동시키면서 빌드 컴파일러된 *.css 와 *.js 를 보도록 변경해야 한다.

우선은 src 에서 build 로 이동처리를 먼저 하자.

// Include gulp
var gulp = require('gulp');
// Include plugins
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var sass = require('gulp-ruby-sass');

var buildPath = 'build/'
var srcPath = 'src/'

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function () {
    return gulp.src(srcPath + 'js/*.js')
        .pipe(concat('honeymon.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(uglify())
        .pipe(gulp.dest(buildPath + 'js'));
});

//Move image files
gulp.task('images', function() {
    gulp.src(srcPath + 'img')
        .pipe(gulp.dest(buildPath + 'img'));
});

//Compile SASS
gulp.task('sass', function () {
    sass(srcPath + 'scss/style.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest(buildPath + 'css'));
    sass(srcPath + 'scss/honeymon/*.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest(buildPath + 'css/honeymon'));
});

gulp.task('html-copy', function() {
    gulp.src([srcPath + 'index.html', srcPath + 'sample.html'])
        .pipe(gulp.dest(buildPath))

    gulp.src(srcPath + 'tx/*.html')
        .pipe(gulp.dest(buildPath + '/tx'))
});

//Build JavaScript, Images, Sass
gulp.task('build', ['scripts', 'images', 'sass', 'html-copy']);

// Default Task
gulp.task('default', ['build']);

이제 gulp-html-replace 플러그인을 설치하고 html 을 옮기면서 stylesheet 와 script 를 처리해보자.

$ npm install --save-dev gulp-html-replace

이제 html-copy 테스크에 추가한다.

gulp.task('html-copy', function () {
    //SEE: https://www.npmjs.com/package/gulp-html-replace
    gulp.src([srcPath + 'sample.html'])
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/style.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));

    gulp.src([srcPath + 'index.html'])
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/style.min.css',
                'css/honeymon/index.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));

    gulp.src(srcPath + 'tx-*.html')
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/style.min.css',
                'css/honeymon/tx.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));
});

src 디렉토리 감시하기

이제 src 디렉토리 내에 있는 파일들의 변경사항이 생겼을 때 이를 감지해서 build 태스크를 실행하도록 만들자.

watch 태스크를 생성한다.

gulp.task('watch', function() {
    // Watch .js files
    gulp.watch(srcPath + 'js/*.js', ['scripts']);
    // Watch .scss files
    gulp.watch(srcPath + 'scss/*.scss', ['sass']);
    // Watch image files
    gulp.watch(srcPath + 'images/**/*', ['images']);
});

그 후 기본 태스크에 watch 태스크를 추가한다.

// Include gulp
var gulp = require('gulp');
// Include plugins
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var sass = require('gulp-ruby-sass');
var imagemin = require('gulp-imagemin');
var cache = require('gulp-cache');
var htmlReplace = require('gulp-html-replace');
var cleanCSS = require('gulp-clean-css');

var buildPath = 'build/'
var srcPath = 'src/'

//Concatenate JS File: src/js 에 있는 파일들을 main.js 로 연결
gulp.task('scripts', function () {
    return gulp.src(srcPath + 'js/*.js')
        .pipe(concat('honeymon.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(uglify())
        .pipe(gulp.dest(buildPath + 'js'));
});

//MOVE libraries
gulp.task('libraries', function () {
    //Copy Javascript
    gulp.src([
        'node_modules/jquery/dist/jquery.min.js',
        'node_modules/bootstrap/dist/js/bootstrap.min.js',
        'node_modules/bootstrap-select/dist/js/bootstrap-select.min.js'])
        .pipe(gulp.dest(buildPath + 'js/libs'));

    //Copy StyleSheet
    gulp.src([
        'node_modules/bootstrap/dist/css/bootstrap.min.css',
        'node_modules/bootstrap-select/dist/css/bootstrap-select.min.css'])
        .pipe(gulp.dest(buildPath + 'css/libs'));

    //Copy font-awesome
    gulp.src(['./node_modules/font-awesome/css/font-awesome.min.css'])
        .pipe(gulp.dest(buildPath + 'css/libs/font-awesome/css'));
    gulp.src(['./node_modules/font-awesome/fonts/*'])
        .pipe(gulp.dest(buildPath + 'css/libs/font-awesome/fonts'));

    //copy bootstrap
});


//Move image files
gulp.task('images', function () {
    return gulp.src(srcPath + '/img/**/*')
        .pipe(cache(imagemin({optimizationLevel: 5, progressive: true, interlaced: true})))
        .pipe(gulp.dest(buildPath + 'img'));
});

//Compile SASS
gulp.task('sass', function () {
    sass(srcPath + 'scss/style.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest(buildPath + 'css'));
    sass(srcPath + 'scss/honeymon/*.scss', {style: 'compressed'})
        .pipe(rename({suffix: '.min'}))
        .pipe(gulp.dest(buildPath + 'css/honeymon'));
});

//Replace html js, css
gulp.task('html-copy', function () {
    //SEE: https://www.npmjs.com/package/gulp-html-replace
    gulp.src([srcPath + 'sample.html'])
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/style.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));

    gulp.src([srcPath + 'index.html'])
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/style.min.css',
                'css/honeymon/index.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));

    gulp.src(srcPath + 'tx-*.html')
        .pipe(htmlReplace({
            css: [
                'css/libs/bootstrap.min.css',
                'css/libs/font-awesome/css/font-awesome.min.css',
                'css/libs/bootstrap-select.min.css',
                'css/style.min.css',
                'css/honeymon/tx.min.css'
            ],
            js: [
                'js/libs/jquery.min.js',
                'js/libs/bootstrap.min.js',
                'js/libs/bootstrap-select.min.js',
                'js/honeymon.min.js'
            ]
        }))
        .pipe(gulp.dest(buildPath));
});

//Watch project
gulp.task('watch', function () {
    // Watch .js files
    gulp.watch(srcPath + 'js/*.js', ['scripts']);

    // Watch .scss files
    gulp.watch([srcPath + '**/*.scss'], ['sass']);
    // gulp.watch(srcPath + 'scss/honeymon/*.scss', ['sass']);

    // Watch image files
    gulp.watch(srcPath + 'img/**/*', ['images']);

    // Watch .html files
    gulp.watch(srcPath + '*.html', ['html-copy']);
});

//Build JavaScript, Images, Sass
gulp.task('build', ['scripts', 'libraries', 'images', 'sass', 'html-copy']);

// Default Task
gulp.task('default', ['build', 'watch']);

이렇게 테스트가 추가되고 나면 src 내에 작은 변화가 생기면 자동적으로 변환이 진행되며 그 결과를 확인할 수 있게 된다.

gulp 동작 실행

gulp 명령어만 실행하면 다음과 같이 build 태스크에 있는 것들이 실행되고 watch 태스크가 실행되면서 디렉토리의 변경사항을 감시하여 변동사항이 생기면 자동으로 빌드가 진행된다.

/usr/local/bin/node /honeymon/node_modules/gulp/bin/gulp.js --color --gulpfile /honeymon//gulpfile.js default
[10:58:15] Using gulpfile ~/workspace/honeymon/test-front-web/gulpfile.js
[10:58:15] Starting 'scripts'...
[10:58:15] Starting 'libraries'...
[10:58:15] Finished 'libraries' after 7.74 ms
[10:58:15] Starting 'images'...
[10:58:15] Starting 'sass'...
[10:58:15] Finished 'sass' after 33 ms
[10:58:15] Starting 'html-copy'...
[10:58:15] Finished 'html-copy' after 14 ms
[10:58:15] Starting 'watch'...
[10:58:15] Finished 'watch' after 56 ms
[10:58:16] Finished 'scripts' after 272 ms
[10:59:31] Starting 'sass'...
[10:59:31] Finished 'sass' after 6.61 ms
[10:59:32] write ./style.css
[11:00:21] Starting 'sass'...
[11:00:21] Finished 'sass' after 5.03 ms
[11:00:22] write ./style.css

참고문헌


'Javascript > Framework & Libs' 카테고리의 다른 글

Error: Cannot find module 'npmlog' 해결  (2) 2016.09.23


"Spring MVC 4 익히기" 라는 이름으로 번역한 책이 2016/08/25 에 출간되었습니다. ^^

종이책은 9월 중순 이후에 나올 예정입니다.


쉽게쉽게할 수 있을거라고 생각하고 시작했는데 쉽지 않았습니다. ㅎ.

처음 번역할 때보다는 절반이상 시간을 줄였습니다.


@_@); 이제 스프링부트 책을 한번 써보지 않겠냐는 제의가 주변에서 쏟아지고 있는데... 부담이 되네요.

써야지 하고 뼈대는 잡아놨는데...


다음 책의 이름은 "Boot Spring Boot" 입니다. 뒤집어 읽어도 "Boot Spring Boot!"


과연 완료할 수 있을지!!


+ Recent posts