AngularJS 및 Play 프레임워크로 최신 웹 애플리케이션 구축

게시 됨: 2022-03-11

올바른 목적에 맞는 도구를 선택하면 특히 최신 웹 응용 프로그램을 구축할 때 많은 도움이 됩니다. 우리 중 많은 사람들이 AngularJS와 AngularJS가 강력한 웹 애플리케이션 프론트엔드 개발을 얼마나 쉽게 만드는지 잘 알고 있습니다. 많은 사람들이 이 인기 있는 웹 프레임워크의 사용에 대해 반대하지만 확실히 제공할 것이 많고 다양한 요구 사항에 적합한 선택이 될 수 있습니다. 반면에 백엔드에서 사용하는 구성 요소는 전반적인 사용자 경험에 영향을 미치므로 웹 응용 프로그램의 성능에 많은 영향을 미칩니다. Play는 Java 및 Scala용 고속 웹 프레임워크입니다. 가볍고 상태가 없는 웹 친화적 아키텍처를 기반으로 하며 Rails 및 Django와 유사한 MVC 패턴 및 원칙을 따릅니다.

angularjs와 플레이 프레임워크가 있는 웹 애플리케이션

이 기사에서는 AngularJS와 Play를 사용하여 기본 인증 메커니즘과 게시물 및 댓글 작성 기능을 갖춘 간단한 블로그 애플리케이션을 구축하는 방법을 살펴보겠습니다. 일부 Twitter Bootstrap의 장점이 있는 AngularJS 개발을 통해 Play 기반 REST API 백엔드 위에 단일 페이지 애플리케이션 경험을 제공할 수 있습니다.

웹 애플리케이션 - 시작하기

AngularJS 애플리케이션 스켈레톤

AngularJS 및 Play 앱은 그에 따라 클라이언트 및 서버 디렉토리에 상주합니다. 지금은 "클라이언트" 디렉토리를 생성하겠습니다.

 mkdir -p blogapp/client

AngularJS 애플리케이션 스켈레톤을 만들기 위해 놀라운 스캐폴딩 도구인 Yeoman을 사용할 것입니다. Yeoman을 설치하는 것은 쉽습니다. 간단한 골격 AngularJS 애플리케이션을 스캐폴딩하는 데 사용하는 것이 훨씬 더 쉬울 것입니다.

 cd blogapp/client yo angular

두 번째 명령을 실행하면 선택해야 하는 몇 가지 옵션이 표시됩니다. 이 프로젝트에는 "Sass(나침반 포함)"가 필요하지 않습니다. 다음 AngularJS 플러그인과 함께 Boostrap이 필요합니다.

  • angular-animate.js
  • angular-cookies.js
  • angular-resource.js
  • angular-route.js
  • angular-sanitize.js
  • angular-touch.js

이 시점에서 선택을 완료하면 터미널에 NPM 및 Bower 출력이 표시되기 시작합니다. 다운로드가 완료되고 패키지가 설치되면 AngularJS 애플리케이션 스켈레톤을 사용할 준비가 된 것입니다.

Play 프레임워크 애플리케이션 스켈레톤

새로운 Play 애플리케이션을 만드는 공식적인 방법은 Typesafe Activator 도구를 사용하는 것입니다. 사용하기 전에 컴퓨터에 다운로드하여 설치해야 합니다. Mac OS를 사용 중이고 Homebrew를 사용하는 경우 한 줄의 명령으로 이 도구를 설치할 수 있습니다.

 brew install typesafe-activator

명령줄에서 Play 애플리케이션을 만드는 것은 매우 쉽습니다.

 cd blogapp/ activator new server play-java cd server/

IDE로 가져오기

Eclipse 또는 IntelliJ와 같은 IDE에서 애플리케이션을 가져오려면 애플리케이션을 "eclipsify" 또는 "이상화"해야 합니다. 그렇게 하려면 다음 명령을 실행하십시오.

 activator

새 프롬프트가 표시되면 "eclipse" 또는 "idea"를 입력하고 Enter 키를 눌러 각각 Eclipse 또는 IntelliJ용 애플리케이션 코드를 준비합니다.

간결함을 위해 이 기사에서는 프로젝트를 IntelliJ로 가져오는 프로세스만 다룰 것입니다. Eclipse로 가져오는 프로세스도 똑같이 간단해야 합니다. 프로젝트를 IntelliJ로 가져오려면 "파일 -> 새로 만들기" 아래에 있는 "기존 소스의 프로젝트..." 옵션을 활성화하여 시작합니다. 다음으로 build.sbt 파일을 선택하고 "확인"을 클릭합니다. 다음 대화 상자에서 "확인"을 다시 클릭하면 IntelliJ가 Play 응용 프로그램을 SBT 프로젝트로 가져오기 시작해야 합니다.

Typesafe Activator는 또한 이 골격 응용 프로그램 코드를 만드는 데 사용할 수 있는 그래픽 사용자 인터페이스와 함께 제공됩니다.

이제 Play 애플리케이션을 IntelliJ로 가져왔으므로 AngularJS 애플리케이션도 작업 공간으로 가져와야 합니다. 별도의 프로젝트 또는 Play 애플리케이션이 있는 기존 프로젝트의 모듈로 가져올 수 있습니다.

여기에서는 Angular 애플리케이션을 모듈로 가져올 것입니다. "파일" 메뉴에서 "새로 만들기 -> 기존 소스의 모듈..." 옵션을 선택합니다. 대화 상자에서 "클라이언트" 디렉토리를 선택하고 "확인"을 클릭합니다. 다음 두 화면에서 각각 "다음" 및 "마침"을 클릭합니다.

로컬 서버 생성

이 시점에서 AngularJS 애플리케이션을 IDE에서 Grunt 작업으로 시작할 수 있어야 합니다. 클라이언트 폴더를 확장하고 Gruntfile.js를 마우스 오른쪽 버튼으로 클릭합니다. 팝업 메뉴에서 "Grunt 작업 표시"를 선택합니다. "Grunt"라고 표시된 패널이 작업 목록과 함께 나타납니다.

로컬 서버 생성

응용 프로그램 제공을 시작하려면 "serve"를 두 번 클릭하십시오. 그러면 기본 웹 브라우저가 즉시 열리고 로컬 호스트 주소를 가리켜야 합니다. Yeoman의 로고가 있는 스텁 AngularJS 페이지가 표시되어야 합니다.

다음으로 백엔드 애플리케이션 서버를 시작해야 합니다. 계속 진행하기 전에 몇 가지 문제를 해결해야 합니다.

  1. 기본적으로 AngularJS 애플리케이션(Yoman이 부트스트랩)과 Play 애플리케이션은 모두 포트 9000에서 실행을 시도합니다.
  2. 프로덕션에서는 두 애플리케이션이 하나의 도메인에서 실행될 가능성이 높으며 Nginx를 사용하여 그에 따라 요청을 라우팅할 것입니다. 그러나 개발 모드에서 이러한 응용 프로그램 중 하나의 포트 번호를 변경하면 웹 브라우저는 해당 응용 프로그램이 다른 도메인에서 실행되는 것처럼 취급합니다.

이 두 가지 문제를 모두 해결하려면 Play 애플리케이션에 대한 모든 AJAX 요청이 프록시되도록 Grunt 프록시를 사용하기만 하면 됩니다. 이를 통해 본질적으로 두 애플리케이션 서버 모두 동일한 명백한 포트 번호에서 사용할 수 있습니다.

먼저 Play 애플리케이션 서버의 포트 번호를 9090으로 변경하겠습니다. 이렇게 하려면 "실행 -> 구성 편집"을 클릭하여 "실행/디버그 구성" 창을 엽니다. 그런 다음 "열려는 URL" 필드에서 포트 번호를 변경합니다. "확인"을 클릭하여 이 변경 사항을 승인하고 창을 닫습니다. "실행" 버튼을 클릭하면 종속성 해결 프로세스가 시작되어야 합니다. 이 프로세스의 로그가 표시되기 시작합니다.

로컬 서버 생성

완료되면 웹 브라우저에서 http://localhost:9090으로 이동할 수 있으며 몇 초 안에 Play 애플리케이션을 볼 수 있을 것입니다. Grunt 프록시를 구성하려면 먼저 NPM을 사용하여 작은 Node.js 패키지를 설치해야 합니다.

 cd blogapp/client npm install grunt-connect-proxy --save-dev

다음으로 Gruntfile.js를 수정해야 합니다. 해당 파일에서 "연결" 작업을 찾고 그 뒤에 "프록시" 키/값을 삽입합니다.

 proxies: [ { context: '/app', // the context of the data service host: 'localhost', // wherever the data service is running port: 9090, // the port that the data service is running on changeOrigin: true } ],

이제 Grunt는 "/app/*"에 대한 모든 요청을 백엔드 Play 애플리케이션에 프록시합니다. 이렇게 하면 백엔드에 대한 모든 호출을 화이트리스트에 추가하지 않아도 됩니다. 또한 livereload 동작을 조정해야 합니다.

 livereload: { options: { open: true, middleware: function (connect) { var middlewares = []; // Setup the proxy middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest); // Serve static files middlewares.push(connect.static('.tmp')); middlewares.push(connect().use( '/bower_components', connect.static('./bower_components') )); middlewares.push(connect().use( '/app/styles', connect.static('./app/styles') )); middlewares.push(connect.static(appConfig.app)); return middlewares; } } },

마지막으로 "serve" 작업에 새로운 종속성 "'configureProxies:server"를 추가해야 합니다.

 grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'configureProxies:server', 'connect:livereload', 'watch' ]); });

Grunt를 다시 시작하면 프록시가 실행 중임을 나타내는 다음 줄이 로그에 표시되어야 합니다.

 Running "autoprefixer:server" (autoprefixer) task File .tmp/styles/main.css created. Running "configureProxies:server" (configureProxies) task Running "connect:livereload" (connect) task Started connect web server on http://localhost:9000

가입 양식 만들기

먼저 블로그 애플리케이션에 대한 가입 양식을 작성합니다. 이를 통해 모든 것이 제대로 작동하는지 확인할 수도 있습니다. Yeoman을 사용하여 가입 컨트롤러를 만들고 AngularJS에서 볼 수 있습니다.

 yo angular:controller signup yo angular:view signup

다음으로 새로 생성된 뷰를 참조하도록 애플리케이션의 라우팅을 업데이트하고 자동 생성된 중복 "about" 컨트롤러 및 뷰를 제거해야 합니다. "app/scripts/app.js" 파일 내에서 "app/scripts/controllers/about.js" 및 "app/views/about.html"에 대한 참조를 제거하고 다음을 남겨둡니다.

 .config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });

마찬가지로 "app/index.html" 파일을 업데이트하여 중복 링크를 제거하고 가입 페이지에 대한 링크를 추가합니다.

 <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#/">Home</a></li> <li><a ng-href="#/signup">Signup</a></li> </ul> </div> </div>

또한 "about.js"에 대한 스크립트 태그를 제거합니다.

 <!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/controllers/main.js"></script> <script src="scripts/controllers/signup.js"></script> <!-- endbuild --> </body> </html>

다음으로 "signup.html" 파일에 양식을 추가합니다.

 <form name="signupForm" ng-submit="signup()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Sign up!</button> </form>

Angular 컨트롤러에서 양식을 처리하도록 해야 합니다. 뷰가 로드되기 전에 "app.js"의 라우팅 로직이 컨트롤러를 자동으로 실행하기 때문에 뷰에 "ng-controller" 속성을 특별히 추가할 필요가 없다는 점은 주목할 가치가 있습니다. 이 양식을 연결하기 위해 우리가 해야 할 일은 $scope에 정의된 적절한 "가입" 기능을 갖는 것입니다. "signup.js" 파일에서 이 작업을 수행해야 합니다.

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .success(function(data) { $log.debug(data); }); }; });

이제 Chrome 개발자 콘솔을 열고 "네트워크" 탭으로 전환한 다음 가입 양식을 제출해 보겠습니다.

Angular Play 가입 양식 예시

Play 백엔드가 자연스럽게 "Action not found" 오류 페이지로 응답하는 것을 볼 수 있습니다. 이것은 아직 구현되지 않았기 때문에 예상됩니다. 그러나 이것이 의미하는 바는 Grunt 프록시 설정이 올바르게 작동한다는 것입니다!

다음으로 Play 애플리케이션 컨트롤러에서 본질적으로 메소드인 "Action"을 추가할 것입니다. "app/controllers" 패키지의 "Application" 클래스에서 "signup"이라는 새 메서드를 추가합니다.

 public static Result signup() { return ok("Success!"); }

이제 "conf/routes" 파일을 열고 다음 줄을 추가합니다.

 POST /app/signup controllers.Application.signup

마지막으로 웹 브라우저 http://localhost:9000/#/signup으로 돌아갑니다. 이번에 "제출" 버튼을 클릭하면 다른 결과가 나타납니다.

브라우저의 Angular Play 가입 양식 예

하드코딩된 값이 반환된 것을 볼 수 있어야 합니다. 이 값은 우리가 signup 메소드에서 작성한 것입니다. 이 경우 개발 환경이 준비되고 Angular 및 Play 애플리케이션 모두에 대해 작업 중이므로 계속 진행할 준비가 되었습니다.

Play에서 Ebean 모델 정의하기

모델을 정의하기 전에 먼저 데이터 저장소를 선택하겠습니다. 이 기사에서는 H2 인메모리 데이터베이스를 사용합니다. 이를 활성화하려면 "application.conf" 파일에서 다음 줄을 찾아 주석 처리를 제거합니다.

 db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password="" ... ebean.default="models.*"

그리고 다음 줄을 추가합니다.

 applyEvolutions.default=true

블로그 도메인 모델은 다소 간단합니다. 우선, 게시물을 작성할 수 있는 사용자가 있으며 로그인한 모든 사용자가 각 게시물에 댓글을 달 수 있습니다. Ebean 모델을 만들어 봅시다.

사용자

 // User.java @Entity public class User extends Model { @Id public Long id; @Column(length = 255, unique = true, nullable = false) @Constraints.MaxLength(255) @Constraints.Required @Constraints.Email public String email; @Column(length = 64, nullable = false) private byte[] shaPassword; @OneToMany(cascade = CascadeType.ALL) @JsonIgnore public List<BlogPost> posts; public void setPassword(String password) { this.shaPassword = getSha512(password); } public void setEmail(String email) { this.email = email.toLowerCase(); } public static final Finder<Long, User> find = new Finder<Long, User>( Long.class, User.class); public static User findByEmailAndPassword(String email, String password) { return find .where() .eq("email", email.toLowerCase()) .eq("shaPassword", getSha512(password)) .findUnique(); } public static User findByEmail(String email) { return find .where() .eq("email", email.toLowerCase()) .findUnique(); } public static byte[] getSha512(String value) { try { return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }

블로그 게시물

 // BlogPost.java @Entity public class BlogPost extends Model { @Id public Long id; @Column(length = 255, nullable = false) @Constraints.MaxLength(255) @Constraints.Required public String subject; @Column(columnDefinition = "TEXT") @Constraints.Required public String content; @ManyToOne public User user; public Long commentCount; @OneToMany(cascade = CascadeType.ALL) public List<PostComment> comments; public static final Finder<Long, BlogPost> find = new Finder<Long, BlogPost>( Long.class, BlogPost.class); public static List<BlogPost> findBlogPostsByUser(final User user) { return find .where() .eq("user", user) .findList(); } public static BlogPost findBlogPostById(final Long id) { return find .where() .eq("id", id) .findUnique(); } }

댓글 달기

 // PostComment.java @Entity public class PostComment extends Model { @Id public Long id; @ManyToOne @JsonIgnore public BlogPost blogPost; @ManyToOne public User user; @Column(columnDefinition = "TEXT") public String content; public static final Finder<Long, PostComment> find = new Finder<Long, PostComment>( Long.class, PostComment.class); public static List<PostComment> findAllCommentsByPost(final BlogPost blogPost) { return find .where() .eq("post", blogPost) .findList(); } public static List<PostComment> findAllCommentsByUser(final User user) { return find .where() .eq("user", user) .findList(); } }

실제 가입 액션

이제 사용자가 가입할 수 있는 첫 번째 실제 작업을 만들어 보겠습니다.

 // Application.java public static Result signup() { Form<SignUp> signUpForm = Form.form(SignUp.class).bindFromRequest(); if ( signUpForm.hasErrors()) { return badRequest(signUpForm.errorsAsJson()); } SignUp newUser = signUpForm.get(); User existingUser = User.findByEmail(newUser.email); if(existingUser != null) { return badRequest(buildJsonResponse("error", "User exists")); } else { User user = new User(); user.setEmail(newUser.email); user.setPassword(newUser.password); user.save(); session().clear(); session("username", newUser.email); return ok(buildJsonResponse("success", "User created successfully")); } } public static class UserForm { @Constraints.Required @Constraints.Email public String email; } public static class SignUp extends UserForm { @Constraints.Required @Constraints.MinLength(6) public String password; } private static ObjectNode buildJsonResponse(String type, String message) { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", message); wrapper.put(type, msg); return wrapper; }

이 앱에서 사용하는 인증은 매우 기본적인 것이므로 프로덕션 용도로 사용하지 않는 것이 좋습니다.

흥미로운 부분은 Play 양식을 사용하여 가입 양식을 처리한다는 것입니다. SignUp 양식 클래스에 몇 가지 제약 조건을 설정했습니다. 유효성 검사는 명시적인 유효성 검사 논리 없이 자동으로 수행됩니다.

웹 브라우저에서 AngularJS 애플리케이션으로 돌아가서 "제출"을 다시 클릭하면 서버가 이제 적절한 오류로 응답하는 것을 볼 수 있습니다. 이 필드는 필수 항목입니다.

플레이 양식 제출

AngularJS에서 서버 오류 처리

따라서 서버에서 오류가 발생하지만 응용 프로그램 사용자는 무슨 일이 일어나고 있는지 전혀 모릅니다. 우리가 할 수 있는 최소한의 일은 사용자에게 오류를 표시하는 것입니다. 이상적으로는 어떤 종류의 오류가 발생하는지 이해하고 사용자에게 친숙한 메시지를 표시해야 합니다. 오류를 표시하는 데 도움이 되는 간단한 경고 서비스를 만들어 보겠습니다.

먼저 Yeoman으로 서비스 템플릿을 생성해야 합니다.

 yo angular:service alerts

다음으로 이 코드를 "alerts.js"에 추가합니다.

 angular.module('clientApp') .factory('alertService', function($timeout) { var ALERT_TIMEOUT = 5000; function add(type, msg, timeout) { if (timeout) { $timeout(function(){ closeAlert(this); }, timeout); } else { $timeout(function(){ closeAlert(this); }, ALERT_TIMEOUT); } return alerts.push({ type: type, msg: msg, close: function() { return closeAlert(this); } }); } function closeAlert(alert) { return closeAlertIdx(alerts.indexOf(alert)); } function closeAlertIdx(index) { return alerts.splice(index, 1); } function clear(){ alerts = []; } function get() { return alerts; } var service = { add: add, closeAlert: closeAlert, closeAlertIdx: closeAlertIdx, clear: clear, get: get }, alerts = []; return service; } );

이제 경고를 담당하는 별도의 컨트롤러를 만들어 보겠습니다.

 yo angular:controller alerts
 angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });

이제 실제로 멋진 부트스트랩 오류 메시지를 표시해야 합니다. 가장 쉬운 방법은 Angular UI를 사용하는 것입니다. Bower를 사용하여 설치할 수 있습니다.

 bower install angular-bootstrap --save

"app.js"에서 Angular UI 모듈을 추가합니다.

 angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])

"index.html" 파일에 경고 지시문을 추가해 보겠습니다.

 <div class="container"> <div ng-controller="AlertsCtrl"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="alert.close()">{{ alert.msg }}</alert> </div> <div ng-view=""></div> </div>

마지막으로 SignUp 컨트롤러를 업데이트해야 합니다.

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location, userService) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } if(status === 500) { alertService.add('danger', 'Internal server error!'); } }) }; });

이제 빈 양식을 다시 보내면 양식 위에 오류가 표시됩니다.

앵귤러 플레이 오류

이제 오류가 처리되었으므로 사용자 가입이 성공하면 조치를 취해야 합니다. 게시물을 추가할 수 있는 대시보드 페이지로 사용자를 리디렉션할 수 있습니다. 하지만 먼저 다음을 생성해야 합니다.

 yo angular:view dashboard yo angular:controller dashboard

성공 시 사용자를 리디렉션하도록 "signup.js" 컨트롤러 가입 방법을 수정합니다.

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });

"apps.js"에 새 경로 추가:

 .when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })

또한 사용자가 로그인했는지 추적해야 합니다. 이를 위해 별도의 서비스를 생성해 보겠습니다.

 yo angular:service user
 // user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });

또한 사용자를 방금 등록한 사용자로 설정하도록 등록 컨트롤러를 수정합니다.

 .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });

게시물을 추가하는 주요 기능을 추가하기 전에 로그인 및 로그아웃 기능, 대시보드에 사용자 정보 표시, 백엔드에서 인증 지원 추가와 같은 다른 중요한 기능을 처리해 보겠습니다.

기본 인증

Play 애플리케이션으로 이동하여 로그인 및 로그아웃 작업을 구현해 보겠습니다. "Application.java"에 다음 줄을 추가합니다.

 public static Result login() { Form<Login> loginForm = Form.form(Login.class).bindFromRequest(); if (loginForm.hasErrors()) { return badRequest(loginForm.errorsAsJson()); } Login loggingInUser = loginForm.get(); User user = User.findByEmailAndPassword(loggingInUser.email, loggingInUser.password); if(user == null) { return badRequest(buildJsonResponse("error", "Incorrect email or password")); } else { session().clear(); session("username", loggingInUser.email); ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "Logged in successfully"); msg.put("user", loggingInUser.email); wrapper.put("success", msg); return ok(wrapper); } } public static Result logout() { session().clear(); return ok(buildJsonResponse("success", "Logged out successfully")); } public static Result isAuthenticated() { if(session().get("username") == null) { return unauthorized(); } else { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "User is logged in already"); msg.put("user", session().get("username")); wrapper.put("success", msg); return ok(wrapper); } } public static class Login extends UserForm { @Constraints.Required public String password; }

다음으로 인증된 사용자에게만 특정 백엔드 호출을 허용하는 기능을 추가해 보겠습니다. 다음 코드로 "Secured.java"를 생성합니다.

 public class Secured extends Security.Authenticator { @Override public String getUsername(Context ctx) { return ctx.session().get("username"); } @Override public Result onUnauthorized(Context ctx) { return unauthorized(); } }

우리는 나중에 새로운 액션을 보호하기 위해 이 클래스를 사용할 것입니다. 다음으로 사용자 이름과 로그아웃 링크를 표시하도록 AngularJS 애플리케이션 기본 메뉴를 조정해야 합니다. 이를 위해 컨트롤러를 생성해야 합니다.

 yo angular:controller menu
 // menu.js angular.module('clientApp') .controller('MenuCtrl', function ($scope, $http, userService, $location) { $scope.user = userService; $scope.logout = function() { $http.get('/app/logout') .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = ''; $location.path('/login'); } }); }; $scope.$watch('user.username', function (newVal) { if(newVal === '') { $scope.isLoggedIn = false; } else { $scope.username = newVal; $scope.isLoggedIn = true; } }); });

로그인 페이지에 대한 보기와 컨트롤러도 필요합니다.

 yo angular:controller login yo angular:view login
 <!-- login.html --> <form name="loginForm" ng-submit="login()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Log in</button> </form>
 // login.js angular.module('clientApp') .controller('LoginCtrl', function ($scope, userService, $location, $log, $http, alertService) { $scope.isAuthenticated = function() { if(userService.username) { $log.debug(userService.username); $location.path('/dashboard'); } else { $http.get('/app/isauthenticated') .error(function() { $location.path('/login'); }) .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); } }; $scope.isAuthenticated(); $scope.login = function() { var payload = { email : this.email, password : this.password }; $http.post('/app/login', payload) .error(function(data, status){ if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { alertService.add('danger', 'Invalid login or password!'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data){ $log.debug(data); if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); }; });

다음으로 사용자 데이터를 표시할 수 있도록 메뉴를 조정합니다.

 <!-- index.html --> <div class="collapse navbar-collapse" ng-controller="MenuCtrl"> <ul class="nav navbar-nav pull-right" ng-hide="isLoggedIn"> <li><a ng-href="/#/signup">Sign up!</a></li> <li><a ng-href="/#/login">Login</a></li> </ul> <div class="btn-group pull-right acc-button" ng-show="isLoggedIn"> <button type="button" class="btn btn-default">{{ username }}</button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button> <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul> </div> </div>

이제 애플리케이션에 로그인하면 다음 화면을 볼 수 있습니다.

스크린샷 재생

게시물 추가

이제 기본적인 등록 및 인증 메커니즘이 준비되었으므로 게시 기능을 구현할 수 있습니다. 게시물을 추가하기 위한 새로운 뷰와 컨트롤러를 추가해 보겠습니다.

 yo angular:view addpost
 <!-- addpost.html --> <form name="postForm" ng-submit="post()" novalidate> <div> <label for="subject">Subject</label> <input name="subject" class="form-control" type="subject" placeholder="Subject" ng-model="subject"> </div> <div> <label for="content">Post</label> <textarea name="content" class="form-control" placeholder="Content" ng-model="content"></textarea> </div> <button type="submit" class="btn btn-primary">Submit post</button> </form>
 yo angular:controller addpost
 // addpost.js angular.module('clientApp') .controller('AddpostCtrl', function ($scope, $http, alertService, $location) { $scope.post = function() { var payload = { subject : $scope.subject, content: $scope.content }; $http.post('/app/post', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'subject' || key === 'content') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { $scope.subject = ''; $scope.content = ''; alertService.add('success', data.success.message); }); }; });

그런 다음 다음을 포함하도록 "app.js"를 업데이트합니다.

 .when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })

다음으로 대시보드 메뉴에 "addpost" 보기에 대한 링크를 추가하기 위해 "index.html"을 수정합니다.

 <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li><a ng-href="/#/addpost">Add post</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul>

이제 Play 애플리케이션 측에서 addPost 메소드를 사용하여 새 컨트롤러 Post를 생성해 보겠습니다.

 // Post.java public class Post extends Controller { public static Result addPost() { Form<PostForm> postForm = Form.form(PostForm.class).bindFromRequest(); if (postForm.hasErrors()) { return badRequest(postForm.errorsAsJson()); } else { BlogPost newBlogPost = new BlogPost(); newBlogPost.commentCount = 0L; newBlogPost.subject = postForm.get().subject; newBlogPost.content = postForm.get().content; newBlogPost.user = getUser(); newBlogPost.save(); } return ok(Application.buildJsonResponse("success", "Post added successfully")); } private static User getUser() { return User.findByEmail(session().get("username")); } public static class PostForm { @Constraints.Required @Constraints.MaxLength(255) public String subject; @Constraints.Required public String content; } }

라우팅에 새로 추가된 메서드를 처리할 수 있도록 route 파일에 새 항목을 추가합니다.

 POST /app/post controllers.Post.addPost

이 시점에서 새 게시물을 추가할 수 있어야 합니다.

Play에 새 게시물 추가

게시물 표시

게시물을 추가하는 것은 표시할 수 없다면 거의 가치가 없습니다. 우리가 하려는 것은 메인 페이지에 모든 게시물을 나열하는 것입니다. 애플리케이션 컨트롤러에 새 메서드를 추가하는 것으로 시작합니다.

 // Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }

그리고 경로 파일에 등록합니다.

 GET /app/posts controllers.Application.getPosts

다음으로 AngularJS 애플리케이션에서 메인 컨트롤러를 수정합니다.

 // main.js angular.module('clientApp') .controller('MainCtrl', function ($scope, $http) { $scope.getPosts = function() { $http.get('app/posts') .success(function(data) { $scope.posts = data; }); }; $scope.getPosts(); });

마지막으로 "main.html"에서 모든 것을 제거하고 다음을 추가합니다.

 <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | <a ng-href="/#/viewpost/{{ post.id }}">Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>

이제 애플리케이션 홈 페이지를 로드하면 다음과 유사한 내용이 표시되어야 합니다.

Angularjs Play 예제 로드됨

또한 개별 게시물에 대해 별도의 보기가 있어야 합니다.

 yo angular:controller viewpost yo angular:view viewpost
 // viewpost.js angular.module('clientApp') .controller('ViewpostCtrl', function ($scope, $http, alertService, userService, $location) { $scope.user = userService; $scope.params = $routeParams; $scope.postId = $scope.params.postId; $scope.viewPost = function() { $http.get('/app/post/' + $scope.postId) .error(function(data) { alertService.add('danger', data.error.message); }) .success(function(data) { $scope.post = data; }); }; $scope.viewPost(); });
 <!-- viewpost.html --> <div class="panel panel-default" ng-show="post"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>

그리고 AngularJS 경로:

 app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })

이전과 마찬가지로 애플리케이션 컨트롤러에 새 메서드를 추가합니다.

 // Application.java public static Result getPost(Long id) { BlogPost blogPost = BlogPost.findBlogPostById(id); if(blogPost == null) { return notFound(buildJsonResponse("error", "Post not found")); } return ok(Json.toJson(blogPost)); }

… 그리고 새로운 경로:

 GET /app/post/:id controllers.Application.getPost(id: Long)

이제 http://localhost:9000/#/viewpost/1로 이동하면 특정 게시물에 대한 보기를 로드할 수 있습니다. 다음으로 대시보드에서 사용자의 게시물을 볼 수 있는 기능을 추가해 보겠습니다.

 // dashboard.js angular.module('clientApp') .controller('DashboardCtrl', function ($scope, $log, $http, alertService, $location) { $scope.loadPosts = function() { $http.get('/app/userposts') .error(function(data, status) { if(status === 401) { $location.path('/login'); } else { alertService.add('danger', data.error.message); } }) .success(function(data) { $scope.posts = data; }); }; $scope.loadPosts(); });
 <!-- dashboard.html --> <h4>My Posts</h4> <div ng-hide="posts.length">No posts yet. <a ng-href="/#/addpost">Add a post</a></div> <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <a ng-href="/#/viewpost/{{ post.id }}">{{ post.subject }}</a> | Comments <span class="badge">{{ post.commentCount }}</span> </div> </div>

또한 Post 컨트롤러에 새 메서드를 추가하고 이 메서드에 해당하는 경로를 추가합니다.

 // Post.java public static Result getUserPosts() { User user = getUser(); if(user == null) { return badRequest(Application.buildJsonResponse("error", "No such user")); } return ok(Json.toJson(BlogPost.findBlogPostsByUser(user))); }
 GET /app/userposts controllers.Post.getUserPosts

이제 게시물을 만들면 대시보드에 나열됩니다.

대시보드에 나열된 새 게시물

주석 기능

주석 기능을 구현하려면 먼저 Post 컨트롤러에 새 메서드를 추가합니다.

 // Post.java public static Result addComment() { Form<CommentForm> commentForm = Form.form(CommentForm.class).bindFromRequest(); if (commentForm.hasErrors()) { return badRequest(commentForm.errorsAsJson()); } else { PostComment newComment = new PostComment(); BlogPost blogPost = BlogPost.findBlogPostById(commentForm.get().postId); blogPost.commentCount++; blogPost.save(); newComment.blogPost = blogPost; newComment.user = getUser(); newComment.content = commentForm.get().comment; newComment.save(); return ok(Application.buildJsonResponse("success", "Comment added successfully")); } } public static class CommentForm { @Constraints.Required public Long postId; @Constraints.Required public String comment; }

And as always, we need to register a new route for this method:

 POST /app/comment controllers.Post.addComment

In our AngularJS application, we add the following to “viewpost.js”:

 $scope.addComment = function() { var payload = { postId: $scope.postId, comment: $scope.comment }; $http.post('/app/comment', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'comment') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { alertService.add('success', data.success.message); $scope.comment = ''; $scope.viewPost(); }); };

And finally add the following lines to “viewpost.html”:

 <div class="well" ng-repeat="comment in post.comments"> <span class="label label-default">By: {{ comment.user.email }}</span> <br/> {{ comment.content }} </div> <div ng-hide="user.username || !post"><h4><a ng-href="/#/login">Login</a> to comment</h4></div> <form name="addCommentForm" ng-submit="addComment()" novalidate ng-show="user.username"> <div><h4>Add comment</h4></div> <div> <label for="comment">Comment</label> <textarea name="comment" class="form-control" placeholder="Comment" ng-model="comment"></textarea> </div> <button type="submit" class="btn btn-primary">Add comment</button> </form>

Now if you open any post, you will be able to add and view comments.

View comments screenshot

무엇 향후 계획?

In this tutorial, we have built an AngularJS blog with a Play application serving as a REST API back-end. Although the application lacks robust data validation (especially on the client side) and security, these topics were out of the scope of this tutorial. It was aiming to demonstrate one of the many possible ways of building an application of this kind. For convenience, the source code of this application has been uploaded to a GitHub repository.

If you find this combination of AngularJS and Play in web application development interesting, I highly recommend you review the following topics further:

  • AngularJS documentation
  • Play documentation
  • Recommended security approach in one page JS app with Play as back-end (contains example)
  • Secure REST API without OAuth
  • Ready Play authentication plug-in (might be not fully usable for single-page JavaScript applications, but can be used as a good example)