Translate

2017년 11월 19일 일요일

[TypeScript] TS90010 Type A is not assignable to Type A 이슈





Framework: Angular 4


HTTP 요청에 파라미터를 추가하여 날리기 위해 URLSearchParams 에서 AppendAll 메소드를 사용하는데 아래와 같이 오류가 발생을 했다.
1004lucifer

1004lucifer

TS90010:Type 'URLSearchParams' is not assignable to type 'URLSearchParams'. Two different types with this name exist, but they are unrelated. Property 'rawParams' is missing in type 'URLSearchParams'.

(두개의 URLSearchParams 가 실은 같은 타입이 아니라는 이야기..)





지난번 기술한 이슈와 동일한 원인이며 해결방법 또한 동일하다.
(아래의 링크를 참조)

- [TypeScript] TS2345 Argument of type A is not assignable to parameter of type A 이슈


2017년 11월 16일 목요일

[Angular 2-4] Locale 사용 시 브라우저마다 DatePipe 결과가 다른 증상





환경: Angular 4



최근 다국어를 고려한 프로젝트를 제작중인데,
DatePipe 사용 시 의도하지 않은 결과가 나와서 혼란스럽다.



증상
1004lucifer
아래의 소스를 사용 시 브라우저마다 결과값이 다르게 나올 수가 있다.
(Module 에서 LOCALE_ID 지정시에만 증상 발생)

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { LOCALE_ID, NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    { provide: LOCALE_ID, useValue: 'ko' }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }


app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  nowDate = new Date();
}

app.component.html
<h1>
  {{ nowDate | date:'yyyy-MM-dd HH:mm:ss'}}
</h1>






처음에 'yyyy-MM-dd HH:mm:ss' 패턴대로 나올꺼라고 생각했지만 결과는 아래와 같다.

Chrome v62.0.3202.89




Microsoft Edge 38.14393.0.0



1004lucifer

JSFiddle 에서는 아래와 같이 보여진다.
(이것역시 보는 브라우저에 따라 결과가 다르다.)







원인

왜그런지 한참을 찾았는데 원인은
Angular 4 버전까지 DatePipe에서 Browser의 i18n API 를 사용하기 때문이다.


현재 Angular 5 가 출시 되었으며 i18n 에서 다중 브라우저 불일치 버그로 인해 브라우저 API 를 사용하지 않고 Unicode Common Locale Data Repository (CLDR) 기반의 데이터를 자체적으로 가지고 있다고 한다.
https://github.com/angular/angular/blob/master/CHANGELOG.md#i18n-pipes

1004lucifer
Angular 5 가 나온 사실을 모르고 github의 한글 형식의 CLDR 소스(링크)를 보고 어디서 사용해서 자동으로 한글이 붙어서 나오는걸까 알아봤었고, Angular 사이트에서 본 DatePipe API 또한 Angular 5 버전 4버전이라 착각하며 보면서 한참을 삽질을 하고 있었다.

http://han41858.tistory.com/43
삽질을 하다가 위의 블로그의 Angular5 소식을 보고 바로 의문이 풀렸다.





해결방법

현재 상황으로는 아래의 우회방법 말고는 딱히 해결방법이 없어보인다.

아직 테스트 해보지는 못했으나 Angular 5 에서는 이슈가 해결되거나 Locale을 바꾸지 않는 정상적인 방법으로 해결이 가능하지 않을까 싶다.
(지금 쓰고있는 JHipster 가 아직 Angular 5지원을 안하는데 어쩌지?? ㅠㅠ)
1004lucifer
app.component.ts
import { Component } from '@angular/core';
import {DatePipe} from '@angular/common';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  nowDate = new Date();
  datePipe: DatePipe;
  constructor() {
    this.datePipe = new DatePipe('en-US');
  }
}


app.component.html
<h1>
  {{ datePipe.transform(nowDate, 'yyyy-MM-dd HH:mm:ss') }}
</h1>



JSFiddle



2017년 11월 14일 화요일

[CSS] 상속받은 상위 속성(style) 무효화(초기화) 하기





PS.
모든 상위속성을 무시하는 걸 원한다면 아래 링크의 라이브러리를 사용하기를 권장한다.
- 10+ Best CSS Reset Stylesheets





많은 상황에서 상속받은 CSS에 대하여
depth를 이용하거나 CSS우선순위를 이용하여 덮어쓰기(!?) 하여 해결이 가능한것 같다.


하지만 다른 style을 적용하지 않고 부모 엘리먼트의 style 상속을 받지 않아야 하는 경우가 발생하여 알아보니 :not(selector) 라는 기능이 있었다.
1004lucifer
엄밀히 말하면 상속을 초기화(reset) 한다기 보다..
부모 Element 에서 style 적용 시 특정 Element에 style이 적용되지 않도록 하는 기능이다.





예제

<div class="parent">
  <span>aaaaaa</span>< br/>
  <p>aaaaaa</p>< br/>
  <span>aaaaaa</span>< br/>
  <span>aaaaaa</span>
</div>
.parent :not(p) {
  color:red;
}
1004lucifer









아래와 같은 형식으로도 사용이 가능하니 의외로 활용성이 좀 있어 보인다.

- div :not(.home) {…}







- div *:not(p) em {…}







- input:not([type="text"]) {…}







- h2:not(.foo):not(.bar) {…}







- li:not(.pingback) .comment-content p:first-child:first-line {…}







- div:not(.home) h2 + p:first-letter {…}







- .post:not(.first-post) a:hover {…}





PS.
정보를 찾다보니 jQuery 도 :not() Selector 가 있다는걸 알았다.
https://api.jquery.com/not-selector/

무효, 리셋,


[혼잣말]
지금 알고있는걸 그때도 알았더라면..
상속받은 상위 속성을 무효화(무시) 할줄 몰라서 다시 style을 덮어쓰기 하며 힘들게 작업하던 스스로에게 안쓰러워진다..;;


[Angular 2+] Java의 ZonedDateTime 형식 Formatting





환경 - Angular 4



다국어 홈페이지를 제작하려다보니 DateTime 형식을
Java 8의 ZonedDateTime을 사용하게 되었다.


1004lucifer
아래와 같은 포멧을 사용한다. (ISO 8601)

- 2017-11-14T12:52:23+00:00
- 2017-11-14T12:52:23Z
- 20171114T125223Z



앵귤러에서 지원하는게 있을까 한참 찾다가 포멧을 직접 만들까 했는데
ES6 에서 기본 지원이 되고 있었다.






ES6에서 아래와 같이 사용 가능하다 (TypeScript 사용가능)
var today = new Date('05 October 2011 14:48 UTC');

console.log(today.toISOString()); // Returns 2011-10-05T14:48:00.000Z

1004lucifer

ES5의 JavaScript 에서는 아래와 같이 prototype 추가해서 사용 가능하다.
if (!Date.prototype.toISOString) {
  (function() {

    function pad(number) {
      if (number < 10) {
        return '0' + number;
      }
      return number;
    }

    Date.prototype.toISOString = function() {
      return this.getUTCFullYear() +
        '-' + pad(this.getUTCMonth() + 1) +
        '-' + pad(this.getUTCDate()) +
        'T' + pad(this.getUTCHours()) +
        ':' + pad(this.getUTCMinutes()) +
        ':' + pad(this.getUTCSeconds()) +
        '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
        'Z';
    };

  }());
}




참고
- http://doohyun.tistory.com/55
- https://stackoverflow.com/questions/29297257/what-time-zone-does-the-javascript-new-date-use
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString

2017년 11월 13일 월요일

[MySQL][해결방법] Deadlock found when trying to get lock; try restarting transaction





환경
- MySQL v5.7
- Java (Spring-Boot)




증상

Java Spring 애플리케이션에서 아래와 같이 오류가 발생했다.
1004lucifer



2017-11-13 00:00:23.547 ERROR 19976 --- [TaskScheduler-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task.

org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may involve [_PACKAGE_NAME_].[_CLASS_NAME_].[_METHOD_NAME_]-Inline
### The error occurred while setting parameters
### SQL: insert into ~~~~
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; SQL []; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
        at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:263) ~[spring-jdbc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73) ~[spring-jdbc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) ~[mybatis-spring-1.3.1.jar:1.3.1]
        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.1.jar:1.3.1]
        at com.sun.proxy.$Proxy254.insert(Unknown Source) ~[na:na]
        at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:278) ~[mybatis-spring-1.3.1.jar:1.3.1]
        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:57) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.4.jar:3.4.4]
        at com.sun.proxy.$Proxy259.[_METHOD_NAME_](Unknown Source) ~[na:na]
        at [_PACKAGE_NAME_].[_CLASS_NAME_].[_METHOD_NAME_]([_CLASS_NAME_].java:55) ~[classes/:na]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_151]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_151]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151]
        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
        at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
        at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_151]
        at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [na:1.8.0_151]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_151]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [na:1.8.0_151]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151]
        at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_151]
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_151]
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_151]
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_151]
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.Util.getInstance(Util.java:408) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:951) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3973) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3909) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2527) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2680) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2490) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1858) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1197) ~[mysql-connector-java-5.1.42.jar:5.1.42]
        at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:46) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:74) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198) ~[mybatis-3.4.4.jar:3.4.4]
        at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185) ~[mybatis-3.4.4.jar:3.4.4]
        at sun.reflect.GeneratedMethodAccessor306.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151]
        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433) ~[mybatis-spring-1.3.1.jar:1.3.1]
        ... 19 common frames omitted







원인


 insert into TableA 로직이 수행되는 시점의 직전에
다른 모듈에서 TableA 를 select 하여 다른 테이블에 insert 하는 로직이 수행되었다.

1004lucifer
트랜잭션 관련 이슈로 인해 문제가 발생을 했으며 아래 링크의 글을 읽어보면 해당 이슈에 대해서 이해할 수 있다.
MySQL 트랜잭션 Isolation Level로 인한 장애 사전 예방 법
MySQL 5.7 Reference Manual - SET TRANSACTION Syntax
MySQL 5.7 Reference Manual - Transaction Isolation Levels


해결방안 적용시 아래의 문제가 생길 가능성 있는것 같음.
(binlog 관련 - 추후 문제 발생 시 이 글을 업데이트 예정)
[MySQL] innoDB transaction isolation level과 binlog enable 시 문제점 정리




해결방안
1004lucifer
my.cnf 파일에 아래의 옵션을 추가 후 MySQL, Java 프로그램 재기동
(4가지 옵션이 있으며 '원인' 부분에 제공된 링크를 보고 본인에 맞게 적용하면 된다.)
- READ-UNCOMMITTED
- READ-COMMITTED
- REPEATABLE-READ
- SERIALIZABLE


[mysqld]
transaction-isolation    = READ-COMMITTED




PS.
애플리케이션에서 트랜잭션이 일어나지 않게 설계를 하면 좀 더 좋겠지만 그렇지 않을 수 있다. Oracle 사용할 때는 위와같은 이슈로 고민해본적은 없었는데..
아마 Oracle 에서는 'READ-UNCOMMITTED | READ-COMMITTED' 둘중에 하나의 개념을 기본으로 사용하고 있지 않을까 싶다.



2017년 11월 11일 토요일

[Angular 2+] multipart 보낼 시 헤더에 Content-type 추가하기





작업환경: Angular 4



기존 글에 이어 수정해야 할 부분이 있어 추가로 글을 쓰게 되었다.

이전글
 - [Angular 2+] Form Submit 할 시 파일 포함하기 (multipart) - File Upload






Form Submit 할 시 문제가 되었던 부분은 아래와 같다.

Request 내용을 확인해보니 각각의 파라메터에 Content-Type이 생성되지 않고 Content-Disposition이 붙어서 Request가 발생을 했다.
그러다보니 서버사이드에서 사용하는 Spring의 객체에 데이터가 들어가지 않게 되었다.



스펙을 보니 FormData Object 에 파라메터를 추가 시 Content-Disposition 헤더가 붙는다고 기술되어 있다.
https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects




서버사이드 Spring 에서 파일과 같이 객체에 데이터를 받기위해 아래와 같이 작업을 했다.

1004lucifer

multipart-form.service.ts
import {Injectable} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Headers, Http, RequestMethod, Response} from '@angular/http';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class MultipartFormService {

    constructor(
        private http: Http
    ) {
    }

    /**
     * Object / File 객체를 Multipart 로 묶어서 서버에 전송한다.
     * @param {requestMethod} RequestMethod
     * @param {string} url
     * @param {Object} jsonObject
     * @param {Array<{name: string; file: File}>} files
     * @returns {Observable<Response>}
     */
    sendMultipartJsonObject(requestMethod: RequestMethod, url: string, jsonObject: Object, files?: Array<{name: string, file: File}>): Observable<Response> {

        const formData = new FormData();

        let blob;
        for (const key in jsonObject) {
            if (jsonObject.hasOwnProperty(key)) {
                blob = new Blob([jsonObject[key]], {type: 'application/json; charset=UTF-8'});
                formData.append(key, blob);
            }
        }

        if (files) {
            files
                .filter((file) => file.name != null && file.file != null)  // 정상적이지 않은 객체 Filter
                .forEach((file) => {
                    formData.append(file.name, file.file);
                });
        }

        const headers = new Headers({'enctype': 'multipart/form-data'});
        return requestMethod === RequestMethod.Post ? this.http.post(url, formData, { headers }) :
            requestMethod === RequestMethod.Put ? this.http.put(url, formData, { headers }) : null;
    }

    extractFile(event) {
        const files = event.target.files || event.srcElement.files;
        return files.length === 0 ? null : files[0];
    }
    extractFileName(event) {
        const file = this.extractFile(event);
        return file ? file.name : '';
    }
}

1004lucifer

devices.component.ts
import {Component, Input, OnInit} from '@angular/core';
import {RequestMethod} from '@angular/http';
import {MultipartFormService} from '../../shared/multipart-form/multipart-form.service';

@Component({
    selector: 'jhi-devices',
    templateUrl: './devices.component.html',
    styles: []
})
export class DevicesComponent implements OnInit {

    private readonly apiUrl = '/api/urlname';

    isNew: boolean;

    deviceInfo: Device;

    file1: File;
    file2: File;

    constructor(
        private multipartFormService: MultipartFormService
    ) {
    }

    onSubmit() {
        this.multipartFormService.sendMultipartJsonObject(
            this.isNew ? RequestMethod.Post : RequestMethod.Put,
            this.apiUrl,
            {'device': JSON.stringify(this.deviceInfo)},
            [{'name': 'file1', 'file': this.file1}, {'name': 'file2', 'file': this.file2}]
        ).subscribe(
            (response) => console.log(response.json()),
            (response) => console.log(response.json())
        );
    }

}



DeviceResource.java
/**
 * REST controller for managing Device.
 */
@RestController
@RequestMapping("/api")
public class DeviceResource {

    /**
     * POST  /devices : Create a new device.
     *
     * @param device the device to create
     * @return the ResponseEntity with status 201 (Created) and with body the new device, or with status 400 (Bad Request) if the device has already an ID
     * @throws URISyntaxException if the Location URI syntax is incorrect
     */
    @PostMapping("/devices")
    @Timed
    public ResponseEntity<Device> createDevice(
  @RequestPart Device device, 
  @RequestParam (value = "file1", required = false) MultipartFile file1, 
  @RequestParam (value = "file2", required = false) MultipartFile file2
 ) throws URISyntaxException {
  
        log.debug("REST request to save Device : {}", device);
        if (device.getId() != null) {
            return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", "A new device cannot already have an ID")).body(null);
        }
        if (file1 != null) {
   // File 저장 로직
        }

        if (file2 != null) {
   // File 저장 로직
        }
  
  // 이후 로직 ..
  
    }
}



PS.
정석대로 한다면 Header에 multipart-mixed로 지정을 하고 일반 파라메터에는 filename 같은게 붙으면 안되지만..
위의 방법으로는 blob를 사용함으로써 쓸데없는 헤더가 붙어 제대로된 방법은 아니나, 작동하는데 크게 문제가 없으며, 이해하기 쉽고 소스가 많이 줄어 그냥 위와같이 하게 되었다.
1004lucifer
아래의 방법을 한번 볼 필요가 있다. (소스가 깔끔하지는 않다..ㅠ)
https://stackoverflow.com/questions/28133289/angularjs-how-to-send-multipart-mixed




참고
- https://ko.wikipedia.org/wiki/MIME
- https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Submitting_forms_and_uploading_files
- http://restlet.com/company/blog/2016/08/29/whats-new-in-the-http-module-of-angular-2/   (나와 같은 방법으로 작업이 되어있다. 깔끔한 설명!!!)



[Spring Boot] 기동 시 발생하는 SpringBootServletInitializer 에러





IDE(IntelliJ)에서 Spring Boot - MainApplication.java 를 기동하려 했는데
아래와 같은 에러가 발생을 하면서 기동되지 않았다.




  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2017-11-11 11:10:16.266  INFO 54148 --- [           main] k.c._.test.MainApplication         : Starting MainApplication on 1004lucifer-han with PID 54148 (C:\Users\1004lucifer\IdeaProjects\test\test-core\target\classes started by 1004lucifer in C:\Users\1004lucifer\IdeaProjects\test)
2017-11-11 11:10:16.275  INFO 54148 --- [           main] k.c._.test.MainApplication         : No active profile set, falling back to default profiles: default
2017-11-11 11:10:17.753  WARN 54148 --- [           main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [kr.co._1004lucifer.test.MainApplication]; nested exception is java.lang.IllegalStateException: Failed to introspect annotated methods on class org.springframework.boot.web.support.SpringBootServletInitializer
2017-11-11 11:10:17.777 ERROR 54148 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Destroy method on bean with name 'org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory' threw an exception

java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: org.springframework.context.annotation.AnnotationConfigApplicationContext@6fec725d: startup date [Sat Nov 11 11:10:16 KST 2017]; root of context hierarchy
at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:404) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.ApplicationListenerDetector.postProcessBeforeDestruction(ApplicationListenerDetector.java:97) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:253) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:578) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:554) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:961) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:523) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingletons(DefaultListableBeanFactory.java:968) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1033) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at kr.co._1004lucifer.test.MainApplication.main(MainApplication.java:74) [classes/:na]

2017-11-11 11:10:17.846 ERROR 54148 --- [           main] o.s.boot.SpringApplication               : Application startup failed

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [kr.co._1004lucifer.test.MainApplication]; nested exception is java.lang.IllegalStateException: Failed to introspect annotated methods on class org.springframework.boot.web.support.SpringBootServletInitializer
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:180) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:308) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:228) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:270) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:93) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:686) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:524) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE]
at kr.co._1004lucifer.test.MainApplication.main(MainApplication.java:74) [classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect annotated methods on class org.springframework.boot.web.support.SpringBootServletInitializer
at org.springframework.core.type.StandardAnnotationMetadata.getAnnotatedMethods(StandardAnnotationMetadata.java:163) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.retrieveBeanMethodMetadata(ConfigurationClassParser.java:374) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:308) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:244) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:197) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:166) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE]
... 12 common frames omitted
Caused by: java.lang.NoClassDefFoundError: javax/servlet/ServletContext
at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.7.0_80]
at java.lang.Class.privateGetDeclaredMethods(Class.java:2625) ~[na:1.7.0_80]
at java.lang.Class.getDeclaredMethods(Class.java:1868) ~[na:1.7.0_80]
at org.springframework.core.type.StandardAnnotationMetadata.getAnnotatedMethods(StandardAnnotationMetadata.java:152) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]
... 17 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.servlet.ServletContext
at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_80]
at java.net.URLClassLoader$1.run(URLClassLoader.java:355) ~[na:1.7.0_80]
at java.security.AccessController.doPrivileged(Native Method) ~[na:1.7.0_80]
at java.net.URLClassLoader.findClass(URLClassLoader.java:354) ~[na:1.7.0_80]
at java.lang.ClassLoader.loadClass(ClassLoader.java:425) ~[na:1.7.0_80]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) ~[na:1.7.0_80]
at java.lang.ClassLoader.loadClass(ClassLoader.java:358) ~[na:1.7.0_80]
... 21 common frames omitted


Process finished with exit code 1






servlet 관련 오류가 보이길래 혹시 이전에 War 파일로 패키징 하기위한 부분에 문제가 있나 싶어 확인해보니 tomcat 관련 부분이 추가되어 있었다.

아래의 tomcat 을 제거하니 MainApplication.java 가 정상적으로 기동이 되었다.


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
    <groupId>kr.co._1004lucifer.realestate_core</groupId>
 <artifactId>test-core</artifactId>
 <packaging>war</packaging>
 <name>test-core</name>
    <version>0.0.1_SNAPSHOT</version>

    <parent>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot</artifactId>
        <version>1.3.0</version>
    </parent>

 <properties>
  <java.version>1.7</java.version>
  <tomcat.version>7.0.78</tomcat.version>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <start-class>kr.co._1004lucifer.test.MainApplication</start-class>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
   <version>1.5.2.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-tomcat</artifactId>
   <scope>provided</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>

  <!-- Spring Boot Mail -->
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-mail</artifactId>
   <version>1.4.3.RELEASE</version>
  </dependency>

  <!-- MyBatis Spring Boot -->
  <dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
  </dependency>
  <dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter-test</artifactId>
   <version>1.3.0</version>
  </dependency>

  <!-- MySQL DataBase -->
  <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.42</version>
  </dependency>

  <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.0</version>
        </dependency>

  <!-- HTML Parser -->
  <dependency>
   <groupId>org.jsoup</groupId>
   <artifactId>jsoup</artifactId>
   <version>1.10.2</version>
  </dependency>

 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>1.5.3.RELEASE</version>
   </plugin>
  </plugins>
 </build>
</project>



2017년 11월 9일 목요일

[IntelliJ] SVN commit 시 발생하는 E720005 오류 증상






IntelliJ ver: 2017.2.5



증상

SVN commit 작업 시 아래와 같이 warning 이 발생했다.



 Warning: not all local changes may be shown due to an error: svn: E720005: Can't check path '~~~'

1004lucifer




해결방법

별다른 방법이 있는게 아니고 IntelliJ를 재구동하면 SVN 커밋 시 warning 이 없어지며 정상적으로 이용이 가능하다.


참고
https://stackoverflow.com/questions/25272326/jetbrains-warning-not-all-local-changes-may-be-shown-due-to-an-error-svn-e2


2017년 10월 27일 금요일

[IntelliJ] CSV 파일을 테이블 형식으로 보면서 편집하자.






CSV 파일 사용할일이 종종 있는데,,

IntelliJ 에서 편하게 볼 수 있는 방법이 없을까 하고 알아보니 기본 기능에 포함이 되어 있었다.
아래와 같이 사용이 가능하다.



1. CSV 파일을 열면 기본적으로 아래와 같이 보여지게 된다.
1004lucifer



2. Edit > Edit as Table 선택




3. 아래와 같이 설정화면이 나오는데 그냥 OK를 누르던가 설정을 변경하면 된다.
  (나의 경우엔 세미콜론으로 구분하기위해 포멧을 하나 더 만들었다.)
1004lucifer



4. 아래와 같이 화면이 변경된다.
  (하단에 Text / Data 구분하여 볼 수 있는 탭이 생긴다.)




참고
https://www.jetbrains.com/help/idea/edit-as-table-file-name-format-dialog.html

2017년 10월 12일 목요일

[Angular 2+] Form Submit 할 시 파일 포함하기 (multipart) - File Upload





Angular 로 파일 업로드 시 아래의 모듈을 많이 사용하는 것 같다.

1. ng2-file-upload
  - https://github.com/valor-software/ng2-file-upload

2. ng2-fileupload
  - https://github.com/thinkholic/ng2-fileupload


Angular 4

모듈이라는 개념에 맞게 순수하게 파일 업로드 기능만 들어있다.
저 모듈을 이용하게되면 우선 파일을 업로드 후 form submit 을 시켜야 할 것으로 보여진다.

서버 API 작업 시 multipart 로 text/file 파라메터를 동시에 받아 작업하는게 편한 경우가 있어 한번에 submit 시킬 수 있도록 작업을 했다.


작업환경: Angular 4

1004lucifer

multipart-form.service.ts
import {Injectable} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Headers, Http, Response} from '@angular/http';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class MultipartFormService {

    constructor(
        private http: Http
    ) {
    }

    /**
     * Form의 string / File 파라메터를 Multipart 로 묶어서 서버에 전송한다.
     * @param {string} url
     * @param {FormGroup} formGroup
     * @param {Array<{name: string; file: File}>} files - [{'name': 'fileKey1', 'file': this.file1}, ...]
     * @returns {Observable<Response>}
     */
    sendMultipartForm(url: string, formGroup: FormGroup, files?: Array<{name: string, file: File}>): Observable<Response> {

        const formData = new FormData();

        for (const key in formGroup.value) {
            if (formGroup.value.hasOwnProperty(key)) {
                formData.append(key, formGroup.value[key]);
            }
        }

        if (files) {
            files
                .filter((file) => file.name != null && file.file != null)  // 정상적이지 않은 객체 Filter
                .forEach((file) => {
                    formData.append(file.name, file.file);
                });
        }

        const headers = new Headers({'enctype': 'multipart/form-data'});
        return this.http.post(url, formData, { headers });
    }
}






사용 (실제 사용된 소스를 약간 수정했다.)


use.component.html
<div>
    <form [formGroup]="myForm" (ngSubmit)="onSubmit()">
        <table class="tbl_w1 mrg_T20">
            <colgroup>
                <col width="150">
                <col width="">
            </colgroup>
            <tr>
                <td>param1</td>
                <td><input formControlName="param1" type="text" class="white_grayline input_wmiddle back_gray"></td>
            </tr>
            <tr>
                <td>param2</td>
                <td><input formControlName="param2" type="text" class="white_grayline input_wmiddle back_gray"></td>
            </tr>
            <tr>
                <td>file1</td>
                <td><input #file1Name class="file_input_textbox2" readonly/>
                    <div class="file_input_div">
                        <input type="button" value="찾아보기" class="file_input_button"/>
                        <input (change)="file1 = extractFile($event);
                                        file1Name.value = extractFileName($event)"
                               type="file" class="file_input_hidden"/>
                    </div>
                </td>
            </tr>
            <tr>
                <td>file2</td>
                <td><input #file2Name class="file_input_textbox2" readonly/>
                    <div class="file_input_div">
                        <input type="button" value="찾아보기" class="file_input_button"/>
                        <input (change)="file2 = extractFile($event);
                                        file2Name.value = extractFileName($event)"
                               type="file" class="file_input_hidden"/>
                    </div>
                </td>
            </tr>
        </table>

        <p class="aln_c mrg_T20">
            <button type="submit">확인</button>
        </p>
    </form>
</div>

1004lucifer

use.component.ts
import {Component, Input, OnInit} from '@angular/core';
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {FormControl, FormGroup} from '@angular/forms';
import {MultipartFormService} from './multipart-form.service';

@Component({
    selector: 'use-selector',
    templateUrl: './use.component.html',
    styles: []
})
export class UseComponent implements OnInit {

    myForm: FormGroup;
    file1: File;
    file2: File;

    constructor(
        public activeModal: NgbActiveModal,
        private multipartFormService: MultipartFormService
    ) {
        this.myForm = new FormGroup({
            'param1': new FormControl(),
            'param2': new FormControl()
        });
    }

    ngOnInit() {
    }

    extractFile(event) {
        const files = event.target.files || event.srcElement.files;
        return files.length === 0 ? null : files[0];
    }
    extractFileName(event) {
        const file = this.extractFile(event);
        return file ? file.name : '';
    }

    onSubmit() {
        this.multipartFormService.sendMultipartForm(
            '/api/resourceName',
            this.myForm,

            // files 파라메터는 옵션으로 추가하지 않아도 된다.
            [{'name': 'fileKey1', 'file': this.file1}, {'name': 'fileKey2', 'file': this.file2}],
        ).subscribe(
            (response) => {
                console.log(response.json());
            },
            (response) => {
                console.log(response.json());
            });
    }
    /**
    onSubmit() {
        this.multipartFormService.sendMultipartForm(
            '/api/resourceName',
            this.myForm,
        ).subscribe(
            (response) => {
                console.log(response.json());
            },
            (response) => {
                console.log(response.json());
            });
    }
    */
}



use.module.ts
@NgModule({
    imports: [
        ReactiveFormsModule,
  ...
    ],
    declarations: [UseComponent],
    providers: [
        MultipartFormService
    ],
})
export class UseModule {
}



참고
- http://nberserk.github.io/default/2017/02/12/angular-form-file.html
  (사실상 위의 링크를 보고 만들었다.)
- https://www.angularjs4u.com/angularjs2/10-angular-2-file-upload-demos/





PS.
서버사이드를 Java Spring 을 사용하는데 위와같이 작업 시 이름이 같은 중복 파라메터는 보낼 수 없어 JSON 형식으로 객체를 보내는 방법으로 수정을 했는데 Content-Type 이슈로 인해 Spring 에서 JSON<=>객체 매핑이 제대로 되지 않았다.
아래와 같은 방법으로 Content-Type 헤더를 추가하여 이슈를 해결하였다.

링크: [Angular 2+] multipart 보낼 시 헤더에 Content-type 추가하기


[Angular 2+] Button 태그 없이 Submit 수행하기




Angular 4 에서 <a> (Anchor Tag) 로 Submit 을 해야 하는 상황이 생겼다.
(퍼블리싱된 페이지를 보니 <button> 이 아니라 <a> 로 되어있었다.)



아래와 같이 작업을 했다. (O)
1004lucifer
<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
 <p class="aln_c mrg_T20">
  <button type="submit" #submitBtn style="display:none;"></button>
  <a (click)="submitBtn.click()"><span class="btn_blue btn_popcon">확 인</span></a>
  <a (click)="activeModal.close()"><span class="btn_gray btn_popcon">취 소</span></a>
 </p>
</form>





이것저것 해보며 아래와 같은 방법도 시도를 했는데,
아래와 같이 하게되면 페이지가 새로고침 되어버리므로 사용할 수가 없다. (X)

<form [formGroup]="myForm" (ngSubmit)="onSubmit()" #formElem>
 <p class="aln_c mrg_T20">
  <a (click)="formElem.click()"><span class="btn_blue btn_popcon">확 인</span></a>
  <a (click)="activeModal.close()"><span class="btn_gray btn_popcon">취 소</span></a>
 </p>
</form>


1004lucifer

PS.
(click) 속성으로 이렇게 저렇게 해보면서 한참을 시도 해봤는데 결국 button 태그가 꼭 있어야 ajax로 submit 이 가능했었다.ㅠ
다음엔 퍼블리싱 할 때 꼭 버튼은 button 태그를 이용해 달라고 이야기 해야 할듯..
 submit without but

2017년 10월 8일 일요일

[Angular 2+] 브라우저 현재 URL 가져오기





Angular 4 사용하면서 상단의 네비게이션이 현재 주소에 맞게 보여지도록(선택된) 보여야 하는데 현재 보여지는 URL을 가지고 구성하면 되겠다고 생각해서 아래와 같이 URL을 가져와 네비게이션 작업을 했다.

1004lucifer
get current url


menu.component.html
<div class="naviWrap">
 <ul class="navi">
  <li id="home" routerLink="/home/menu1" class="{{selectedMenu == 'home' ? 'on' : ''}}">홈</li>
  <li id="system" routerLink="/system/menu1" class="{{selectedMenu== 'system' ? 'on' : ''}}">시스템 설정</li>
 </ul>
</div>
<div id="homeSub" class="subnaviWrap" *ngIf="selectedMenu == 'home'">
 <ul class="subnavi">
  <li routerLink="/home/menu1" routerLinkActive="on">메뉴1</li>
  <li class="line">|</li>
  <li routerLink="/home/menu2" routerLinkActive="on">메뉴2</li>
  <li class="line">|</li>
  <li routerLink="/home/menu3" routerLinkActive="on">메뉴3</li>
 </ul>
</div>
<div id="homeSub" class="subnaviWrap" *ngIf="selectedMenu == 'system'">
 <ul class="subnavi">
  <li routerLink="/system/menu1" routerLinkActive="on">메뉴1</li>
  <li class="line">|</li>
  <li routerLink="/system/menu2" routerLinkActive="on">메뉴2</li>
  <li class="line">|</li>
  <li routerLink="/system/menu3" routerLinkActive="on">메뉴3</li>
 </ul>
</div>

1004lucifer


MenuComponent.ts
export class MenuComponent implements OnInit {

    selectedMenu: string;

    constructor(
        private router: Router,
    ) {
        this.selectedMenu = 'home'; // /home/menu1
    }

    ngOnInit() {
  // F5를 눌러 새로고침 시 MenuComponent가 초기화 되니 후 한번 실행
        const currentUrl = this.router.url.split('/');
        if (currentUrl[1]) {
            this.selectedMenu = currentUrl[1];
        }

  // menu.component.html 에서 routerLink 선택하여 메뉴(URL)이동 할 때 마다 실행
        this.router.events.subscribe((event) => {
            if (event instanceof NavigationEnd) {
                this.selectedMenu = event.url.split('/')[1];
            }
        });
    }
}


PS.
- Router Event Subscribe 사용 시 상황에 맞게 사용해야 한다.

(main)AppModule 의 bootstrap에 등록된 Component 에서 Router Event Subscribe 사용할 시 모든 페이지(URL) 이동 시 이벤트가 발생을 하고,
MenuComponent 에서 등록한 Router Event Subscribe 는 해당 컴포넌트에서 발생한 이벤트만 수행이 된다.


2017년 10월 6일 금요일

[SSMS] '데이터베이스 복원' 작업 시 멈춤 현상





OS: Windows Server 2012 R2
DB: SQL Server 2016 Developer
Tool: SSMS(SQL Server Management Studio) 17






다른 PC(Server)의 MS SQL Server 백업한 파일을 복원하기 위해 아래와 같이 데이터베이스 복원 기능을 이요하여 복원하려했다.
1004lucifer






백업된 파일을 선택하니 아래와 같은 화면에서 멈추고 더 이상 진행이 되지 않았다.








 인터넷으로 찾아봤는데 딱히 해결할 수 있는 명확한 방법이 나와있지 않고 SSMS를 repaire 하라는 이야기가 있었는데..
마침 사용하는 서버가 가상 서버이고 프로그램 설치 후 스냅샷을 생성한게 있어서 초기에 셋팅한 상태의 스냅샷 복구를 했다.

그동안 별다른 작업을 한것 같지는 않지만 별 문제 없이 다음으로 넘어가는 것을 확인 할 수 있었다.

추후 비슷한 문제 발생 시 SSMS 재설치를 한번 해봐야겠다.

1004lucifer




PS.
아래의 로그가 추후 문제를 찾는데 도움이 될지는 모르겠지만 로그를 추가로 첨부를 한다.
Log Event ID 검색 시 해결방법에 대한 다른 글을 찾지 못했다.


날짜 2017-10-04 오전 10:19:56
로그 Windows NT (Application)

원본 Windows Error Reporting
범주 (0)
이벤트 1001
컴퓨터 WIN-AAAAA

메시지
오류 버킷 , 유형 0
이벤트 이름: AppHangB1
응답: 사용할 수 없음
Cab ID: 0

문제 서명:
P1: Ssms.exe
P2: 2017.140.17177.0
P3: 5982ac65
P4: 84f5
P5: 67246336
P6: 
P7: 
P8: 
P9: 
P10: 

첨부 파일:

이 파일은 다음에서 사용할 수 있습니다.
C:\Users\Administrator\AppData\Local\Microsoft\Windows\WER\ReportQueue\Critical_Ssms.exe_5a36cc348872ae6636739daa81565639404590_d6281e50_13e4dda3

분석 기호: 
해결 방법 재확인: 0
보고서 ID: 1eed0c54-a8a2-11e7-80c4-08002755ea12
보고서 상태: 4100
해시된 패킷:



날짜 2017-10-04 오전 10:19:56
로그 Windows NT (Application)

원본 Application Hang
범주 (101)
이벤트 1002
컴퓨터 WIN-AAAAAAA

메시지
프로그램 Ssms.exe 버전 2017.140.17177.0에서 Windows와의 상호 작용을 중지했으며 해당 프로그램이 종료되었습니다. 문제에 대한 자세한 정보가 있는지 알아보려면 관리 센터 제어판에서 문제 기록을 확인하십시오.
 프로세스 ID: d10
 시작 시간: 01d33cada1efdc61
 종료 시간: 4294967295
 응용 프로그램 경로: C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\Ssms.exe
 보고서 ID: 1eed0c54-a8a2-11e7-80c4-08002755ea12
 오류 있는 패키지 전체 이름: 
 오류 있는 패키지에 상대적인 응용 프로그램 ID:




참고
https://connect.microsoft.com/SQLServer/feedback/details/3103750/ssms-2016-hangs-frequently
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/520b5cfe-f746-4d62-954b-df195cca97e4/ssms-2012-locks-up-in-restore-database-dialog?forum=sqlkjmanageability

2017년 9월 28일 목요일

[Angular 2+][DateTimePicker] ng-pick-datetime 컴포넌트 적용





Angular 초보가 Angular 4 에 DateTimePicker 컴포넌트를 붙이면서 공식문서만 보고 하려니 삽질이 하늘을 찔렀다.

모듈명: ng-pick-datetime
링크: https://danielykpan.github.io/date-time-picker/


JHipster(Angular+SpringBoot) v4.8.0 에 해당 컴포넌트를 아래와 같이 연동했다.
(같은 디자인의 DateTimePicker 가 여러곳에서 사용이 되기에 데이터 양방향 바인딩 가능한 WrapperComponent를 만들었다.)
1004lucifer

[package.json]
"dependencies": {
 "@angular/animations": "4.3.2",
 "@angular/common": "4.3.2",
 "@angular/compiler": "4.3.2",
 "@angular/core": "4.3.2",
 "@angular/forms": "4.3.2",
 "@angular/http": "4.3.2",
 "@angular/platform-browser": "4.3.2",
 "@angular/platform-browser-dynamic": "4.3.2",
 "@angular/router": "4.3.2",
 "@ng-bootstrap/ng-bootstrap": "1.0.0-beta.1",
 "bootstrap": "4.0.0-beta",
 "core-js": "2.4.1",
 "font-awesome": "4.7.0",
 "jquery": "3.2.1",
 "ng-jhipster": "0.2.12",
 "ng-pick-datetime": "5.0.0-beta.6",
 "ng2-webstorage": "1.8.0",
 "ngx-cookie": "1.0.0",
 "ngx-infinite-scroll": "0.5.1",
 "reflect-metadata": "0.1.10",
 "rxjs": "5.4.2",
 "swagger-ui": "2.2.10",
 "tether": "1.4.0",
 "zone.js": "0.8.16"
},

PS. 작성 후 'npm install' 이나 'yarn install' 명령어를 통해 해당 패키지를 설치한다.





[vendor.css] node_module CSS가 들어가는 파일
/* after changing this file run 'yarn run webpack:build' */
/*@import '~bootstrap/dist/css/bootstrap.min.css';*/
@import '~ng-pick-datetime/assets/style/picker.min.css';
@import '~font-awesome/css/font-awesome.css';



1004lucifer

[datetimepicker.module.ts] (이름을 custom-datetimepicker.~ 형식으로 바꿀껄 그랬나..)
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
import {DateTimePickerModule} from 'ng-pick-datetime';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {CustomDateTimePickerComponent} from './datetimepicker.component';
import {FormsModule} from '@angular/forms';

@NgModule({
    imports: [
        FormsModule,
        DateTimePickerModule,
        BrowserAnimationsModule
    ],
    declarations: [CustomDateTimePickerComponent],
    exports: [CustomDateTimePickerComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class CustomDatetimepickerModule {
}





[datetimepicker.component.ts]
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';

@Component({
    selector: 'jhi-datetimepicker',
    templateUrl: './datetimepicker.component.html',
    styles: []
})
export class CustomDateTimePickerComponent implements OnInit {

    componentDatetime: Date;
    dateFormat = 'YYYY-MM-DD HH:mm';

    ko = {
        firstDayOfWeek: 0,
        dayNames: [ '일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일' ],
        dayNamesShort: [ ' 일', ' 월', ' 화', ' 수', ' 목', ' 금', ' 토' ],
        monthNames: [ '1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월' ],
        monthNamesShort: [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
    };

    @Input()
    get datetime() {
        return this.componentDatetime;
    }
    set datetime(value) {
        this.componentDatetime = value;
        this.datetimeChange.emit(this.componentDatetime);
    }
    @Output() datetimeChange = new EventEmitter();

    constructor() {
    }

    ngOnInit() {
    }
}
1004lucifer
설명:
짧은 이름의 요일명(일/월/화/수/목/금/토)이 화면상에 너무 왼쪽에 붙고 &nbsp 가 적용되지 않아 앞에 눈에 보이지 않는 특수문자를 넣어주었다. (ㄱ => 한자 => 1번)





[datetimepicker.component.html]
<owl-date-time
    [(ngModel)]="datetime"
    [locale]="ko"
    [dateFormat]="dateFormat"
    [placeHolder]="''"
></owl-date-time>





[적용.module.ts]
@NgModule({
    imports: [
        CustomDatetimepickerModule,
  ...
    ],
    declarations: [...],
 ...
})
export class UseModule {
}





[적용.component.ts]
@Component({
    ...
})
export class UseComponent implements OnInit {

    public startDateTime: Date;

    constructor() {
    }

    ngOnInit() {
    }

}





[적용.component.html]
<div>
<jhi-datetimepicker [(datetime)]="startDateTime"></jhi-datetimepicker>
</div>





PS.
적용한 컴포넌트가 기본적으로 상위의 CSS를 상속받아 Demo 와는 스타일이 약간 변경되었는데 시/분 입력란이 왼쪽으로 치우져처 있어서 아래와 같이 CSS 속성을 추가했다.
1004lucifer
[global.css] 전체 페이지에 적용되는 CSS
.owl-timer-wrapper .owl-timer-input {
    text-align: center;
}







적용된 결과는 아래 모습과 같다.



[Angular 2+] Angular 2+ 사용가능한 DateTimePicker 컴포넌트





Angular 4 에서 사용할만한 DateTimePicker 를 찾아봤는데 몇개 없다.
(DatePicker / TimePicker 는 정말 많이 있지만 괜찮은 DateTimePicker는 정말 찾기 힘들었다.)


그나마 좀 찾아본걸 리뷰 해본다.


1. ng-pick-datetime
  - 링크: https://danielykpan.github.io/date-time-picker/ (Demo 링크 동일)
1004lucifer
  - 찾아본 DateTimePicker 중에 가장 마음에 들며 디자인이나 기능도 무난하다.



PS.
위의 모듈을 아래의 글과 같이 적용했다.
http://1004lucifer.blogspot.kr/2017/09/angulardatetimepicker-angular-4-ng-pick.html





2. ng2-eonasdan-datetimepicker
  - 링크: https://github.com/atais/ng2-eonasdan-datetimepicker (Demo 링크)
1004lucifer
  - 처음엔 사용방법이 마음에 안들었었는데 지금에서야 다시보니 나쁘지 않은것 같기도..






3. ng2-datetime-picker
  - 링크: https://github.com/ng2-ui/datetime-picker  (Demo 링크)
1004lucifer
  - 사용성에 심각한 문제가 있다.
    날짜/시간/분 을 클릭하면 바로 팝업이 닫히며 시각이 입력된다.
    보통 날짜=>시간=>분 입력을 할텐데.. 암튼 사용해보면 안다.






PS.
https://ng-bootstrap.github.io 의 Component 중 pagination 을 사용하는데..
여기에 DateTimePicker 컴포넌트도 같이 있으면 좋았으려만..
DatePicker / TimePicker 이렇게 컴포넌트가 나뉘어져 있어 결국 다른 컴포넌트를 사용할 수 밖에 없었다.
다른 괜찮다 싶은 DateTimePicker 컴포넌트를 발견하니 AngularJS(1.x) 사용가능한 모듈이었다.


2017년 9월 26일 화요일

[JHipster] Angular 4 사용 시 Audits 메뉴 표시되지 않는 문제




version: JHipster 4.8.0



증상
1004lucifer
JHipster 프로젝트 생성 하고 admin 로그인 후 '관리자=>Audit' 메뉴 들어가게되면 화면이 오류가 나며 정상적으로 보여지지 않게된다.







{
  "type" : "http://www.jhipster.tech/problem/problem-with-message",
  "title" : "Bad Request",
  "status" : 400,
  "detail" : "Failed to convert value of type 'java.lang.String[]' to required type 'java.time.LocalDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.time.LocalDate] for value '2017년-08월-26일'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2017년-08월-26일]",
  "message" : "error.http.400"
}




디버깅을 해보니 날짜를 넘겨줄 때 '2017-08-26' 이렇게 넘겨줘야 하는걸 년/월/일 을 붙여서 넘겨주고 있었다.

Angular 에서 기본적으로 사용하는 DatePipe 모듈에서 timezone 개념이 있는데 EndUser의 시스템의 로케일을 기본으로 가져온다고 한다.




해결방법

아래와 같이 Locale 을 en-US 형식으로 DatePipe를 생성하여 정상적으로 나오는 것을 확인했다.
1004lucifer

src/main/webapp/app/admin/audits/audits.component.ts
export class AuditsComponent implements OnInit {
    audits: Audit[];
    fromDate: string;
    itemsPerPage: any;
    links: any;
    page: number;
    orderProp: string;
    reverse: boolean;
    toDate: string;
    totalItems: number;
    datePipe: DatePipe;

    constructor(
        private auditsService: AuditsService,
        private parseLinks: JhiParseLinks,
        private paginationConfig: PaginationConfig,
        // private datePipe: DatePipe
    ) {
        this.itemsPerPage = ITEMS_PER_PAGE;
        this.page = 1;
        this.reverse = false;
        this.orderProp = 'timestamp';
        this.datePipe = new DatePipe('en-US');
    }
 
 ...
}






[TypeScript] TS2345 Argument of type A is not assignable to parameter of type A 이슈




Framework: Angular 4


HTTP 요청에 파라미터를 추가하여 날리기 위해 URLSearchParams 에서 AppendAll 메소드를 사용하는데 아래와 같이 오류가 발생을 했다.



1004lucifer

TS2345:Argument of type 'URLSearchParams' is not assignable to parameter of type 'URLSearchParams'. Property 'rawParams' is missing in type 'URLSearchParams'.

(URLSearchParams 를 URLSearchParams 파라미터로 할당할 수 없다고!???)





알고보니 좀 허무했는데..

Angular 에 URLSearchParams 라는 클래스가 있고,
TypeScript 자체에 URLSearchParams 라는 인터페이스가 있다.
1004lucifer
Angular의 URLSearchParams를 사용해야 했는데 import를 하지 않으니 TypeScript의 URLSearchParams를 기본으로 사용을 해서 문제가 되었다.





다음과 같이 Angular 의 해당 클래스를 import 해주니 문제가 없어졌다.





참고:
https://github.com/Microsoft/TypeScript/issues/9358


2017년 9월 25일 월요일

[Angular 2+][ng-bootstrap] Modal Component 데이터 전달





version: Angular 4

ng-bootstrap: Modal
(https://ng-bootstrap.github.io/#/components/modal/examples)

1004lucifer
Component 간에 데이터 전달 시 태그 속성으로 emit 이벤트를 전달받을 함수를 넣으면 되는데.. Modal 사용 시 HTML 태그를 명시하지 않아 스크립트에서 어떻게 처리를 하는지 알아보았다.




modal.component.ts
data: string;
@Output() outputData = new EventEmitter<SystemDeviceFilter>();

constructor(
 // ng-bootstrap Modal
 public activeModal: NgbActiveModal
) {
}

doOutput() {
 this.outputData.emit(this.data);
 this.activeModal.close(); // Modal Close
}



1004lucifer

parent.component.ts
constructor(
 private modalService: NgbModal,
 ...
) {
}

const modalComponent = this.modalService.open(ModalComponent).componentInstance;
modalComponent.outputData.subscribe((data) => {
 // data을 활용한 로직
});



참조:
https://github.com/ng-bootstrap/ng-bootstrap/issues/861#issuecomment-253500089

2017년 9월 20일 수요일

[Angular 2+][ng-bootstrap] Customize the CSS for "ngb-pagination" Component





ngb-pagination Component:
(https://ng-bootstrap.github.io/#/components/pagination/examples)









jHipster(Angular 4) 를 이용해 프로젝트 제작 중 Paging 디자인을 아래와 같이 변경하였다.
1004lucifer
<ngb-pagination> Component 가 렌더링 된 후의 HTML 과 적용된 CSS를 수정하여 디자인을 대충 맞춰놨는데 bootstrap css 를 사용하지 않도록 vender.css(jHipster)에서 bootstrap을 주석처리하니 많이 틀어져버려 bootstrap css의 일부를 가져와야 했다.

 
<div class="row justify-content-center">
 <ngb-pagination [collectionSize]="totalItems" [(page)]="page" (pageChange)="loadPage(page)"></ngb-pagination>
</div>
 

 
.row.justify-content-center {
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    margin-right: -15px;
    margin-left: -15px;
    justify-content: center!important;
    margin-top: 20px;
}
ngb-pagination > .pagination {
    display: -ms-flexbox;
    display: flex;
    padding-left: 0;
    list-style: none;
    border-radius: .25rem;
}
ngb-pagination > .pagination > ol,ul,li {
    list-style: none;
}
ngb-pagination > ul > .page-item .page-link {
    box-sizing: border-box;
    border:0;
    width: 35px;
    height: 35px;
    font-size: 14px;
    text-align: center;
    padding: 8px;
    margin: 0 2px 0 2px;
    text-decoration: none;
    color: #555;
}
ngb-pagination > ul > .page-item .page-link > span {
    width: 0;
    height: 0;
    font-size: 0;
}
ngb-pagination > ul > .page-item > a {
    display: block;
}
ngb-pagination > ul > .page-item.active > a {
    color: #0e77d9;
    font-weight: bold;
}
ngb-pagination > ul > .page-item > a:hover {
    color: #0e77d9;
    font-weight: bold;
}
ngb-pagination > ul > .page-item > a[aria-label="First"] {
    background-image: url('../images/common/count_first.gif');
}
ngb-pagination > ul > .page-item > a[aria-label="Previous"] {
    background-image: url('../images/common/count_prev.gif');
}
ngb-pagination > ul > .page-item > a[aria-label="Next"] {
    background-image: url('../images/common/count_next.gif');
}
ngb-pagination > ul > .page-item > a[aria-label="Last"] {
    background-image: url('../images/common/count_end.gif');
}
 




PS.

How to customize the CSS for ng-bootstrap controls using Angular 2 / Bootstrap 4
1004lucifer
stackoverflow 사이트에서 위의 글에서는 /deep/ 또는 > 를 사용하라고 되어있어 처음에 /deep/ combinator를 사용했었다.
하지만 Chrome 에서는 정상적으로 나오지만 IE11 에서 문제가 있다는 것을 발견했다.
> selector 사용시 이슈가 해결되었다.


Browser: Internet Explorer 11

위는 퍼블리싱 소스
아래는 <ngb-pagination>

/deep/ Selector

> selector 



알아보니 Chrome 과 Opera 를 제외하고 Shadow DOM 을 지원하지 않아 발생한 이슈였다.
1004lucifer
[Shadow DOM 지원 브라우저 확인]
http://caniuse.com/#feat=shadowdom

[Shadow DOM 및 /deep/ combinator 설명]
https://www.html5rocks.com/ko/tutorials/webcomponents/shadowdom-201/