Compare commits

...

96 Commits

Author SHA1 Message Date
지대한 06ab6ee75f feat: websocket client 호출시 parameter 추가 범위 6 months ago
지대한 7f0a159b9e feat: websocket client drone data 전송 방식 변경(scheduled -> call) 6 months ago
지대한 3074ca6530 feat: websocket 데이터 전송 방식 변경 6 months ago
지대한 f39c5b1403 feat: socket - storage 통합 6 months ago
지대한 a553d64b7f fix: log 정리 6 months ago
지대한 87cbef018e feat: docker test 환경 구축 6 months ago
지대한 32b003ec24 fix: cache로 사용하는 DroneStorage의 잘못된 참조 수정 7 months ago
지대한 8b576a31d1 fix: db connection pool 에러로 인한 수정 7 months ago
지대한 4c6cf6f540 fix: 드론 이력 저장 중 연관관계에 의하여 발생되는 부분 수정 7 months ago
지대한 d6a8d6effe fix: rsponse가 필요 없는 경우 async사용하여 병렬 처리 7 months ago
지대한 09547a55e7 fix: database connection pool check 7 months ago
지대한 07804766ce feat: docker 실행 7 months ago
지대한 264d26d133 feat: kac-app <-> socket <-> websocket 7 months ago
지대한 71aee790ca fix: timeout 발생 7 months ago
지대한 658388d917 refactor: repository key type integer to long 7 months ago
지대한 2d9588eacf feat: kac-app <-> socket 통신 7 months ago
지대한 8fd0194ec3 refactor: dron 오타 수정 > drone 7 months ago
지대한 6670dfd803 feat: kac-app control 정보 가져오기 7 months ago
지대한 e488d3be33 feat: socket - ws - front 테스트 확인 7 months ago
지대한 04001446ed refactor: http-client global 설정 변경 7 months ago
지대한 9641c5ccf3 fix: decode 활성화 7 months ago
지대한 cd450abd99 feat: socket test용 js추가 7 months ago
지대한 a7b7ae30a5 feat: utm 연동 7 months ago
지대한 c8e36fdd42 feat: 각 서버 통신 기본설정 7 months ago
지대한 9981f5d801 feat: websocket http test 7 months ago
지대한 c36129450b refactor: model 공통으로 가져오기 7 months ago
지대한 80836e34ea feat: websocket 기본 구성 7 months ago
지대한 6d5d09842e feat: dron 구성 7 months ago
지대한 6733144ba1 feat: dron 구성 7 months ago
지대한 7fbed6288a feat: socket 기본 틀 구성 7 months ago
지대한 946dc35c24 fix: ddl-auto none 7 months ago
지대한 c22d5a1424 fix: auto_increment 버그로 인하여 임시로 table 전략으로 변경 7 months ago
지대한 05e2c63fec fix: auto_increment 버그로 인하여 임시로 table 전략으로 변경 7 months ago
지대한 4a787f054c refactor: db h2 test를 위한 수정 7 months ago
지대한 359c96ee8b fix: api-common scurity util 사용하도록 수정 7 months ago
지대한 04b1913753 fix: open해둔 uri중 GET method만 적용이 되는 현상 7 months ago
지대한 322103ad9b feat: session helper 구현 7 months ago
지대한 3d17bec1bb refactor: app에서 querydsl 설정 제거 7 months ago
지대한 e28b1b01ea fix: api-common에서 jpa annotation 못읽어오는 현상 7 months ago
지대한 56b0c72f63 refactor: 모듈 수정 7 months ago
지대한 061663c8b5 refactor: gradle jpa 설정 및 code package 수정 7 months ago
지대한 ac6865a15e refactor: gradle api 명령어 사용하기 위한 수정 7 months ago
지대한 395972cd3f refactor: app gradle print 제거 7 months ago
지대한 6c32044d93 refactor: module 위치 변경 7 months ago
지대한 67ab2bba16 refactor: rest doc gradle test option 적용 7 months ago
lkd9125(이경도) 1463649563 fix : Test 스킵 스크립트 추가 [gradle build -x test] 7 months ago
lkd9125(이경도) d69d14e15a Merge remote-tracking branch 'origin/develop' into develop 8 months ago
lkd9125(이경도) b679f86a08 refactor: 코드정리 8 months ago
lkd9125(이경도) 1ccf1140a3 refactor: [임시] 테스트 데이터 삽입 후 삭제 조치 8 months ago
지대한 5a974631e9 fix: web ignore 사용시 warning 8 months ago
지대한 90802890de feature/data (#6) 8 months ago
lkd9125(이경도) e63d7a64db refactor: Test order 삭제 8 months ago
qkr7828 6b714c0f46 fix : Qualifier 적용되도록 설정 변경 8 months ago
lkd9125(이경도) 0fcad045a6 fix: PermitURL 변경 8 months ago
lkd9125(이경도) 17e1affce5 Merge commit '536dff756b314723675d61f1be5d22c160e3af25' into develop 8 months ago
lkd9125(이경도) 487af94496 Refactor: 트랜잭션 처리 추가, DB에 데이터 쌓이지 않게 변경 8 months ago
lkd9125(이경도) 562df6b0ce Add: RestDocs 스니펫 커스텀으로인한 항목 추가, 테스트 Transaction 롤백기능추가 8 months ago
qkr7828 536dff756b feat : swagger uri 추가 8 months ago
지대한 a085711156 refactor: data module 분기 8 months ago
지대한 36d78d82c5 feat: security 구성 8 months ago
lkd9125(이경도) d584d8c0b7 fix: SpringRestDocs build시 -x test Option으로 테스트 안하고 빌드하도록 수정 8 months ago
지대한 c06fe45fe1 feat: util - masking 추가 8 months ago
지대한 c9422c1a66 feat: util 모듈 app에 추가 8 months ago
지대한 1c27a48858 feat: 암복구화 및 단방향 암호화 유틸 추가 8 months ago
lkd9125(이경도) 1d5bed9ce7 feat: SpringRestDocs 라이브러리 및 예제 추가 8 months ago
지대한 6fa8d61898 Merge pull request 'feat: 공통 exception 설정' (#3) from feature/error into develop 8 months ago
지대한 1fc4f1c93d feat: 공통 exception 설정 8 months ago
지대한 1274960091 feat: message source 구성 (error message 용도) 8 months ago
lkd9125 27d26daa46 Merge pull request 'feature/common/exception' (#2) from feature/common/exception into develop 8 months ago
lkd9125(이경도) e5f77b3de6 feat: Error RS 포맷팅 변경, 서버용 로그 추가 8 months ago
lkd9125(이경도) 95562bf996 move: exception commone 코어 모듈로 이동 8 months ago
지대한 c736566683 feat: querydsl 기능 적용 8 months ago
지대한 679e50bed0 feat: header Accept-Language 언어 강제하기 8 months ago
지대한 9806887ee8 move: exception관련 코드 패키지 이동 8 months ago
지대한 0970cba328 feat: jpa embedded to idclass 8 months ago
박재우 59b6625881 feat: swagger example, config 작업 8 months ago
지대한 41aacc4c14 feat: common root module 생성 8 months ago
지대한 0276f9b767 del: .DS_Store 제거 8 months ago
지대한 7cc65313c4 del: .DS_Store 제거 8 months ago
지대한 5b9398b44f del: .DS_Store 제거 8 months ago
지대한 56bd7d5142 del: .DS_Store 제거 8 months ago
지대한 069406969c del: .DS_Store 제거 8 months ago
지대한 40c4e8dcb5 del: .DS_Store 제거 8 months ago
박재우 708534570a feat : swagger 설정 추가 8 months ago
박재우 cf07273d30 springdoc implement 8 months ago
지대한 e986ad4848 feat: module 분리 8 months ago
지대한 c8c16724bc feat: code api 구성 8 months ago
lkd9125 7777f85203 Merge pull request 'ExceptionHandler추가' (#1) from feature/exception into develop 8 months ago
lkd9125(이경도) 44214a819c ExceptionHandler추가 8 months ago
지대한 087d11efa1 feat: data - code 구성 8 months ago
지대한 18d1e99b23 feat: code 연관관계 구성 8 months ago
지대한 4fd51aa4da docs: 프로젝트 최초 실행 가이드 8 months ago
지대한 f3edb2286e add: repository 객체 추가 8 months ago
지대한 c36929b090 chore: jpa 설정 8 months ago
지대한 0c0eb8ba30 add: entity 객체 추가 8 months ago
지대한 d713a2cb8e chore: docker 관련 내용 추가 9 months ago
  1. BIN
      .DS_Store
  2. 10
      .gitignore
  3. 61
      README.md
  4. BIN
      app/.DS_Store
  5. BIN
      app/kac-app/.DS_Store
  6. 12
      app/kac-app/Dockerfile
  7. 98
      app/kac-app/build.gradle
  8. BIN
      app/kac-app/src/.DS_Store
  9. 175
      app/kac-app/src/docs/asciidoc/index.adoc
  10. BIN
      app/kac-app/src/main/.DS_Store
  11. BIN
      app/kac-app/src/main/java/.DS_Store
  12. 5
      app/kac-app/src/main/java/kr/co/palnet/kac/app/KacAppApplication.java
  13. 43
      app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/controller/InnerSocketController.java
  14. 23
      app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/scheduled/InnerSocketScheduled.java
  15. 301
      app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/service/InnerSocketService.java
  16. 48
      app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/storage/ControlGpsStorage.java
  17. 31
      app/kac-app/src/main/java/kr/co/palnet/kac/app/config/AppReadyEvent.java
  18. 22
      app/kac-app/src/main/java/kr/co/palnet/kac/app/config/JsonConfig.java
  19. 26
      app/kac-app/src/main/java/kr/co/palnet/kac/app/config/MessageSourceConfig.java
  20. 112
      app/kac-app/src/main/java/kr/co/palnet/kac/app/config/SwaggerConfig.java
  21. 19
      app/kac-app/src/main/java/kr/co/palnet/kac/app/config/WebConfig.java
  22. 131
      app/kac-app/src/main/java/kr/co/palnet/kac/app/core/exception/ServerExceptionHandler.java
  23. 62
      app/kac-app/src/main/java/kr/co/palnet/kac/app/core/security/AppSecurityConfig.java
  24. 67
      app/kac-app/src/main/java/kr/co/palnet/kac/app/core/web/BaseAcceptHeaderLocaleResolver.java
  25. 3
      app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/PingController.java
  26. 131
      app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/TestErrorContoller.java
  27. 31
      app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/TestSessionContrller.java
  28. 3
      app/kac-app/src/main/java/lombok.config
  29. 41
      app/kac-app/src/main/resources/application-swagger.yml
  30. 101
      app/kac-app/src/main/resources/application.yml
  31. 2
      app/kac-app/src/main/resources/log4jdbc.log4j2.properties
  32. 22
      app/kac-app/src/main/resources/messages/errors/error.properties
  33. 22
      app/kac-app/src/main/resources/messages/errors/error_en.properties
  34. 45
      app/kac-app/src/test/java/kr/co/palnet/kac/BaseTest.java
  35. 600
      app/kac-app/src/test/java/kr/co/palnet/kac/com/code/ComnCodeControllerTest.java
  36. 11
      app/kac-app/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet
  37. 10
      app/kac-app/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet
  38. 12
      app/kac-app/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet
  39. 10
      app/kac-app/src/test/resources/org/springframework/restdocs/templates/request-headers.snippet
  40. 12
      app/kac-app/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet
  41. 12
      app/kac-socket-app/Dockerfile
  42. 10
      app/kac-socket-app/build.gradle
  43. 15
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java
  44. 31
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DroneDecoder.java
  45. 27
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DroneEncoder.java
  46. 8
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/DroneCommand.java
  47. 119
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDroneCommandImpl.java
  48. 117
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDroneCommandImpl.java
  49. 119
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDroneCommandImpl.java
  50. 35
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java
  51. 44
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AsyncConfig.java
  52. 30
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/JsonConfig.java
  53. 76
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java
  54. 105
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DroneHandler.java
  55. 25
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/DroneRq.java
  56. 15
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/DroneRs.java
  57. 37
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/UtmDto.java
  58. 37
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/ChannelInitializer.java
  59. 43
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/SocketServer.java
  60. 58
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/AuthKeyStorage.java
  61. 143
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/DroneStorage.java
  62. 52
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/ExternalService.java
  63. 81
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/KacAppService.java
  64. 113
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/ScheduledService.java
  65. 67
      app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/WebSocketService.java
  66. 68
      app/kac-socket-app/src/main/resources/application.yml
  67. 13
      app/kac-websocket-app/Dockerfile
  68. 8
      app/kac-websocket-app/build.gradle
  69. 14
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java
  70. 53
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java
  71. 49
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/ControlDecoder.java
  72. 35
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java
  73. 30
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/JsonConfig.java
  74. 76
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java
  75. 70
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java
  76. 15
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/BoundaryCoordinates.java
  77. 19
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlRq.java
  78. 15
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ErrorRs.java
  79. 36
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/ChannelInitializer.java
  80. 42
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java
  81. 41
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java
  82. 82
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java
  83. 96
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java
  84. 54
      app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java
  85. 52
      app/kac-websocket-app/src/main/resources/application.yml
  86. 44
      build.gradle
  87. 12
      common/config-db/build.gradle
  88. 69
      common/config-db/src/main/java/kr/co/palnet/kac/config/db/KacJpaConfig.java
  89. 20
      common/config-db/src/main/java/kr/co/palnet/kac/config/db/QueryDslConfig.java
  90. 36
      common/config-db/src/main/resources/application-db.yml
  91. 6
      common/core/build.gradle
  92. 4
      common/core/src/main/java/kr/co/palnet/kac/core/Sample.java
  93. 54
      common/core/src/main/java/kr/co/palnet/kac/core/exception/BaseErrorCode.java
  94. 132
      common/core/src/main/java/kr/co/palnet/kac/core/exception/BaseException.java
  95. 9
      common/core/src/main/java/kr/co/palnet/kac/core/exception/Level.java
  96. 28
      common/core/src/main/java/kr/co/palnet/kac/core/exception/model/BaseErrorModel.java
  97. 4
      common/model/build.gradle
  98. 86
      common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneControlDto.java
  99. 105
      common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneDto.java
  100. 20
      common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneHistoryDto.java
  101. Some files were not shown because too many files have changed in this diff Show More

BIN
.DS_Store vendored

Binary file not shown.

10
.gitignore vendored

@ -35,3 +35,13 @@ out/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
/db/
.DS_Store
**/.DS_Store
### logs ###
logs/
logs/**
### local ###
/.java-version

61
README.md

@ -0,0 +1,61 @@
# Spec
## 환경 구성
### 기본 정보
- java 21
- spring boot 3.2.1
- jpa
- mysql
## 작업전 준비
### 1. mysql 설치
```
docker-compose up -d database
```
### 2. application.yml 설정 변경
최초 실행시 table 생성하기 위한 작업
`ddl-auto: create`로 변경 후 실행한 다음 Table이 생성되었는지 확인하고 원래 설정값으로 변경한다.
```yaml
spring:
jpa:
hibernate:
ddl-auto: create
```
## 설정 정보
### log4jdbc.log4j2.properties 속성
| 속성 | 설명 |
|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log4jdbc.spylogdelegator.name | 로그4jdbc에서 사용할 로그 델리게이트(Delegate)의 이름을 지정합니다. 이 구성을 통해 로그 델리게이트를 변경할 수 있습니다. 이 코드에서는 SLF4J를 사용하는 net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator가 지정되어 있습니다. |
| log4jdbc.dump.sql.maxlinelength | SQL문의 최대 길이를 지정합니다. 이 속성은 SQL문을 자동 줄 바꿈하는 데 사용됩니다. 이 코드에서는 0으로 설정되어 있으므로 SQL문의 길이에 제한이 없습니다. |
| log4jdbc.spylogdelegator.name | 로그4jdbc에서 사용할 로그 델리게이트(Delegate)의 이름을 지정합니다. 이 구성을 통해 로그 델리게이트를 변경할 수 있습니다. 이 코드에서는 SLF4J를 사용하는 net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator가 지정되어 있습니다. |
| log4jdbc.dump.sql.maxlinelength | SQL문의 최대 길이를 지정합니다. 이 속성은 SQL문을 자동 줄 바꿈하는 데 사용됩니다. 이 코드에서는 0으로 설정되어 있으므로 SQL문의 길이에 제한이 없습니다. |
| log4jdbc.auto.load.popular.drivers | 자주 사용되는 드라이버를 자동으로 로드할지 여부를 지정합니다. 이 코드에서는 true로 설정되어 있습니다. |
| log4jdbc.trim.sql.enabled | SQL 문의 앞뒤 공백을 자동으로 제거할지 여부를 지정합니다. 이 코드에서는 true로 설정되어 있습니다. |
| log4jdbc.trim.sql.extrablanklines | SQL 문의 공백 라인을 제거할지 여부를 지정합니다. 이 코드에서는 false로 설정되어 있습니다. |
| log4jdbc.suppress.generated.keys.exception | SQL 문에서 생성된 키 예외를 억제할지 여부를 지정합니다. 이 코드에서는 false로 설정되어 있습니다.
## spring rest doc
### URL
- uri : `/docs/index.html`
- local : http://localhost:8080/docs/index.html
## spring swagger
### URL
- uri : `/swagger-ui/index.html`
- local : http://localhost:8080/swagger-ui/index.html

BIN
app/.DS_Store vendored

Binary file not shown.

BIN
app/kac-app/.DS_Store vendored

Binary file not shown.

12
app/kac-app/Dockerfile

@ -0,0 +1,12 @@
FROM openjdk:21
ENV TZ=Asia/Seoul
#ENV JAVA_OPTS="-Xms512M -Xmx512M"
ENV JAVA_OPTS=""
EXPOSE 8000
WORKDIR /app
#ENTRYPOINT ["java","-jar","/app/kac-app-1.0.0.jar"]
CMD java $JAVA_OPTS -jar /app/kac-app-1.0.0.jar

98
app/kac-app/build.gradle

@ -1,9 +1,103 @@
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
asciidoctorExt
}
dependencies { dependencies {
implementation "$boot:spring-boot-starter-web" implementation "$boot:spring-boot-starter-web"
runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
// security
implementation "$boot:spring-boot-starter-security"
// implementation "com.auth0:java-jwt:4.4.0"
// db
// runtimeOnly "com.mysql:mysql-connector-j"
// implementation "org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16"
implementation("org.springframework:spring-tx")
// jpa
implementation "$boot:spring-boot-starter-data-jpa"
// querydsl
// implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
// rest doc
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// test
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation project(":common:core")
// implementation project(":common:config-db")
implementation project(":common:util")
implementation project(":common:model")
implementation project(":web:security")
implementation project(":web:api-common")
implementation project(":data:ctr")
implementation project(":data:com")
implementation project(":data:flt")
// implementation project(":data:cns")
// implementation project(":data:com")
// implementation project(":data:other")
// implementation project(":data:pty")
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
compileOnly project(":data-user") ext {
snippetsDir = file('build/generated-snippets')
} }
tasks {
def isTest = gradle.startParameter.excludedTaskNames.contains('test')
if (!isTest) {
asciidoctor {
dependsOn test
inputs.dir snippetsDir
doFirst {
delete file('src/main/resources/static/docs')
}
}
}
test {
outputs.dir snippetsDir
}
bootJar {
if (!isTest) {
dependsOn asciidoctor
from("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
}
task copyDocument(type: Copy) {
if (!isTest) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
}
build {
if (!isTest) {
dependsOn copyDocument
}
}
}

BIN
app/kac-app/src/.DS_Store vendored

Binary file not shown.

175
app/kac-app/src/docs/asciidoc/index.adoc

@ -0,0 +1,175 @@
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]
= KAC App Docs
API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[Common-Code-API]]
== Common-Code API
[[Code-All]]
=== [Code 전체코드 조회]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/all/http-request.adoc[]
include::{snippets}/com/code/all/query-parameters.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/all/http-response.adoc[]
include::{snippets}/com/code/all/response-fields.adoc[]
***
=== [Code 코드목록 조회]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/code/http-request.adoc[]
include::{snippets}/com/code/code/query-parameters.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/code/http-response.adoc[]
include::{snippets}/com/code/code/response-fields.adoc[]
***
=== [Code 그룹목록 조회]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/group/http-request.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/group/http-response.adoc[]
include::{snippets}/com/code/group/response-fields.adoc[]
***
=== [Code 그룹 등록]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/group/create/http-request.adoc[]
include::{snippets}/com/code/group/create/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/group/create/http-response.adoc[]
include::{snippets}/com/code/group/create/response-fields.adoc[]
***
=== [Code 코드 등록]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/code/create/http-request.adoc[]
include::{snippets}/com/code/code/create/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/code/create/http-response.adoc[]
include::{snippets}/com/code/code/create/response-fields.adoc[]
***
=== [Code 코드언어 등록]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/lang/create/http-request.adoc[]
include::{snippets}/com/code/lang/create/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/lang/create/http-response.adoc[]
include::{snippets}/com/code/lang/create/response-fields.adoc[]
***
=== [Code 그룹 수정]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/group/update/http-request.adoc[]
include::{snippets}/com/code/group/update/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/group/update/http-response.adoc[]
include::{snippets}/com/code/group/update/response-fields.adoc[]
***
=== [Code 코드 수정]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/code/update/http-request.adoc[]
include::{snippets}/com/code/code/update/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/code/update/http-response.adoc[]
include::{snippets}/com/code/code/update/response-fields.adoc[]
***
=== [Code 코드언어 수정]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/lang/update/http-request.adoc[]
include::{snippets}/com/code/lang/update/request-fields.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/lang/update/http-response.adoc[]
include::{snippets}/com/code/lang/update/response-fields.adoc[]
***
=== [Code 그룹 삭제]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/group/delete/http-request.adoc[]
include::{snippets}/com/code/group/delete/query-parameters.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/group/delete/http-response.adoc[]
***
=== [Code 코드 삭제]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/code/delete/http-request.adoc[]
include::{snippets}/com/code/code/delete/query-parameters.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/code/delete/http-response.adoc[]
***
=== [Code 언어 삭제]
==== [HTTP REQUEST INFO]
include::{snippets}/com/code/lang/delete/http-request.adoc[]
include::{snippets}/com/code/lang/delete/query-parameters.adoc[]
==== [HTTP RESPONSE INFO]
include::{snippets}/com/code/lang/delete/http-response.adoc[]
***

BIN
app/kac-app/src/main/.DS_Store vendored

Binary file not shown.

BIN
app/kac-app/src/main/java/.DS_Store vendored

Binary file not shown.

5
app/kac-app/src/main/java/kr/co/palnet/kac/app/KacAppApplication.java

@ -3,11 +3,14 @@ package kr.co.palnet.kac.app;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication(scanBasePackages = { @SpringBootApplication(scanBasePackages = {
"kr.co.palnet.kac.app", "kr.co.palnet.kac.app",
"kr.co.palnet.kac.data", "kr.co.palnet.kac.data",
"kr.co.palnet.kac.api" "kr.co.palnet.kac.api",
"kr.co.palnet.kac.config"
}) })
public class KacAppApplication { public class KacAppApplication {
public static void main(String[] args) { public static void main(String[] args) {

43
app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/controller/InnerSocketController.java

@ -0,0 +1,43 @@
package kr.co.palnet.kac.app.api.v1.inner.socket.controller;
import kr.co.palnet.kac.app.api.v1.inner.socket.service.InnerSocketService;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/inner/socket")
public class InnerSocketController {
private final InnerSocketService innerSocketService;
// dorn의 control 조회
@GetMapping("/control/{objectId}")
public ResponseEntity<SimpleControlDto> getControlId(@PathVariable String objectId) {
SimpleControlDto simpleControlDto = innerSocketService.getControlId(objectId);
return ResponseEntity.ok(simpleControlDto);
}
// dron 정보 저장
@PostMapping("/receiver/drone")
public ResponseEntity<Void> receiverDronData(@RequestBody DroneDto droneDto) {
innerSocketService.saveDronData(droneDto);
return ResponseEntity.ok().build();
}
// dron 정보 저장(전체)
@PostMapping("/receiver/drone/all")
public ResponseEntity<Void> receiverDronDataAll(@RequestBody Map<String, List<DroneDto>> droneDtoAll) {
innerSocketService.saveDronDataAll(droneDtoAll);
return ResponseEntity.ok().build();
}
}

23
app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/scheduled/InnerSocketScheduled.java

@ -0,0 +1,23 @@
package kr.co.palnet.kac.app.api.v1.inner.socket.scheduled;
import kr.co.palnet.kac.app.api.v1.inner.socket.service.InnerSocketService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class InnerSocketScheduled {
private final InnerSocketService innerSocketService;
/**
* 최신 수신일로 부터 특정 시간동안 데이터 수신되지 않는 경우 종료 처리
*/
@Scheduled(fixedDelay = 10 * 1000)
public void stopDronFlight() {
innerSocketService.stopDronFlight();
}
}

301
app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/service/InnerSocketService.java

@ -0,0 +1,301 @@
package kr.co.palnet.kac.app.api.v1.inner.socket.service;
import kr.co.palnet.kac.app.api.v1.inner.socket.storage.ControlGpsStorage;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import kr.co.palnet.kac.core.exception.BaseErrorCode;
import kr.co.palnet.kac.data.com.domain.ComArcrftBas;
import kr.co.palnet.kac.data.com.domain.ComIdntfBas;
import kr.co.palnet.kac.data.com.repository.ComArcrftBasRepository;
import kr.co.palnet.kac.data.com.repository.ComIdntBasRepository;
import kr.co.palnet.kac.data.ctr.model.CtrCntrlBas;
import kr.co.palnet.kac.data.ctr.model.CtrCntrlHstry;
import kr.co.palnet.kac.data.ctr.model.CtrCntrlHstryArea;
import kr.co.palnet.kac.data.ctr.repository.CtrCntrlBasRepository;
import kr.co.palnet.kac.data.ctr.repository.CtrCntrlHstryAreaRepository;
import kr.co.palnet.kac.data.ctr.repository.CtrCntrlHstryRepository;
import kr.co.palnet.kac.data.flt.model.FltPlanBas;
import kr.co.palnet.kac.data.flt.model.FltPlanCtrCntrlRel;
import kr.co.palnet.kac.data.flt.repository.FltPlanBasRepository;
import kr.co.palnet.kac.data.flt.repository.FltPlanCtrCntrlRelRepository;
import kr.co.palnet.kac.util.CoordUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@Slf4j
@RequiredArgsConstructor
@Service
@Transactional
public class InnerSocketService {
private final FltPlanBasRepository fltPlanBasRepository;
private final CtrCntrlBasRepository ctrCntrlBasRepository;
private final CtrCntrlHstryRepository ctrCntrlHstryRepository;
private final CtrCntrlHstryAreaRepository ctrCntrlHstryAreaRepository;
private final ComArcrftBasRepository comArcrftBasRepository;
private final ComIdntBasRepository comIdntBasRepository;
private final FltPlanCtrCntrlRelRepository fltPlanCtrCntrlRelRepository;
private long STOP_UNKON = 5;
// socket에 보내줄 control 조회 (controlId 발급용도)
@Transactional(readOnly = true)
public SimpleControlDto getControlId(String objectId) {
CtrCntrlBas latestControl = ctrCntrlBasRepository.findFirstByIdntfNumOrderByCreateDtDesc(objectId).orElse(null);
boolean isControl = false;
if (latestControl != null) {
CtrCntrlHstry latestHistory = ctrCntrlHstryRepository.findFirstByCntrlIdOrderBySrvrRcvDtDesc(latestControl.getCntrlId()).orElse(null);
if (latestHistory != null) {
long diffMinute = ChronoUnit.MINUTES.between(latestHistory.getSrvrRcvDt(), Instant.now());
// log.debug("DIFF MINUTE : {}", diffMinute);
if ("01".equals(latestControl.getEndTypeCd()) || diffMinute > 5) {
isControl = false;
}
if (!"01".equals(latestControl.getEndTypeCd()) && diffMinute < 5) {
isControl = true;
}
} else {
isControl = false;
}
} else {
isControl = false;
}
SimpleControlDto simpleControlDto = new SimpleControlDto();
if (isControl) {
simpleControlDto.setControlId(latestControl.getCntrlId());
simpleControlDto.setTypeCd("02");
simpleControlDto.setAreaTrnsYn("E");
simpleControlDto.setControlStartDt(latestControl.getCntrlStDt());
} else {
String controlId = UUID.randomUUID().toString();
simpleControlDto.setControlId(controlId);
simpleControlDto.setTypeCd("01");
simpleControlDto.setAreaTrnsYn("N");
simpleControlDto.setControlStartDt(Instant.now());
// 기체 식별번호의 관제 ID 저장 (single ton)
ControlGpsStorage simpleControlStorage = ControlGpsStorage.getInstance();
simpleControlStorage.put(objectId, controlId);
}
return simpleControlDto;
}
// socket으로부터 받은 dron 정보 저장
public void saveDronData(DroneDto droneDto) {
// 비행 이력 기본 생성
CtrCntrlBas ctrCntrlBas = ctrCntrlBasRepository.findById(droneDto.getControlId()).orElse(null);
if (ctrCntrlBas == null) {
CtrCntrlBas insertCtrCntrlBas = CtrCntrlBas.builder()
.cntrlId(droneDto.getControlId())
.idntfNum(droneDto.getObjectId())
.objectTypeCd(droneDto.getObjectType())
.statusCd(droneDto.getTypeCd())
.procStatusYn("N")
.cntrlStDt(droneDto.getControlStartDt())
.createDt(Instant.now())
.updateDt(Instant.now())
.build();
ctrCntrlBas = ctrCntrlBasRepository.save(insertCtrCntrlBas);
} else {
log.debug(BaseErrorCode.DATA_ALREADY_EXISTS.message());
}
if ("01".equals(droneDto.getTypeCd())) {
// 비행 이력 생성
// 비행 이력, 비행 계획서 mapping
if (!"PA".equals(droneDto.getObjectId().substring(0, 2))) return;
FltPlanCtrCntrlRel fltPlanCtrCntrlRel = new FltPlanCtrCntrlRel();
// 1. 식별 번호의 소유자 정보 저장.
ComIdntfBas comIdntfBas = comIdntBasRepository.findFirstByIdntfNumOrderByCreateDtAsc(droneDto.getObjectId()).orElse(null);
// 1-1. 식별 번호의 모델 정보 조회
// PAV-KAC에서는 기초데이터가 필수가 아니여서 idntBas가 null일 수 있음.
ComArcrftBas comArcrftBas = null;
if (comIdntfBas != null && comIdntfBas.getArcrftSno() != null) {
comArcrftBas = comArcrftBasRepository.findById(comIdntfBas.getArcrftSno()).orElse(null);
}
if (ctrCntrlBas.getCntrlId() != null && !ctrCntrlBas.getCntrlId().isEmpty()) {
fltPlanCtrCntrlRel.setCntrlId(ctrCntrlBas.getCntrlId());
}
if (ctrCntrlBas.getIdntfNum() != null && !ctrCntrlBas.getIdntfNum().isEmpty()) {
fltPlanCtrCntrlRel.setIdntfNum(ctrCntrlBas.getIdntfNum());
}
if (comIdntfBas != null && comIdntfBas.getCstmrSno() != null) {
fltPlanCtrCntrlRel.setIdntfCstmrSno(comIdntfBas.getCstmrSno());
}
if (comArcrftBas != null && comArcrftBas.getGroupId() != null && !comArcrftBas.getGroupId().isEmpty()) {
fltPlanCtrCntrlRel.setGroupId(comArcrftBas.getGroupId());
}
// 2. 해당 되는 비행계획서 정보 저장.
List<FltPlanBas> fltPlanBas = null;
if (ctrCntrlBas.getCntrlId() != null && !ctrCntrlBas.getCntrlId().isEmpty()) {
fltPlanBas = fltPlanBasRepository.findAllByServerRctDtAndIdntfNum(ctrCntrlBas.getIdntfNum(), droneDto.getServerRcvDt());
for (FltPlanBas plan : fltPlanBas) {
fltPlanCtrCntrlRel.setPlanSno(plan.getPlanSno());
}
}
// 3. mapping 정보 주입.
if (fltPlanCtrCntrlRel.getCntrlId() != null && !fltPlanCtrCntrlRel.getCntrlId().isEmpty() && fltPlanCtrCntrlRel.getPlanSno() != null) {
fltPlanCtrCntrlRelRepository.save(fltPlanCtrCntrlRel);
}
}
// history 생성
CtrCntrlHstry ctrCntrlHstry = CtrCntrlHstry.builder()
.cntrlId(droneDto.getControlId())
.trmnlId(droneDto.getTerminalId())
.mvDstnc(droneDto.getMoveDistance())
.mvDstncType(droneDto.getMoveDistanceType())
.mssgTypeCd(droneDto.getMessageType())
.statusCd(droneDto.getDroneStatus())
.lat(droneDto.getLat())
.lon(droneDto.getLon())
.speed(droneDto.getSpeed())
.speedType(droneDto.getSpeedType())
.heading(droneDto.getHeading())
.elev(droneDto.getElev())
.elevType(droneDto.getElevType())
.bttrLvl(droneDto.getBetteryLevel())
.bttrVltg(droneDto.getBetteryVoltage())
.trmnlRcvDt(droneDto.getTerminalRcvDt())
.srvrRcvDt(droneDto.getServerRcvDt())
.sensorCo(droneDto.getSensorCo())
.sensorSo2(droneDto.getSensorSo2())
.sensorNo2(droneDto.getSensorNo2())
.sensorO3(droneDto.getSensorO3())
.sensorDust(droneDto.getSensorDust())
.build();
ctrCntrlHstry = ctrCntrlHstryRepository.save(ctrCntrlHstry);
if ("01".equals(droneDto.getTypeCd()) || "99".equals(droneDto.getTypeCd())) {
CtrCntrlHstryArea ctrCntrlHstryArea = CtrCntrlHstryArea.builder()
.cntrlId(droneDto.getControlId())
.actnType(droneDto.getTypeCd())
.prcsYn("N")
.lat(droneDto.getLat())
.lon(droneDto.getLon())
.hstrySno(ctrCntrlHstry.getHstrySno())
.createDt(Instant.now())
.updateDt(Instant.now())
.build();
ctrCntrlHstryAreaRepository.save(ctrCntrlHstryArea);
}
}
/**
* 특정 시간동안 비행하지않는 데이터 종료처리
*/
public void stopDronFlight() {
ControlGpsStorage simpleControlStorage = ControlGpsStorage.getInstance();
Map<String, String> keys = simpleControlStorage.getAll();
keys.forEach((objectId, controlId) -> {
CtrCntrlHstry history = ctrCntrlHstryRepository.findFirstByCntrlIdOrderBySrvrRcvDtDesc(controlId).orElse(null);
if (Objects.nonNull(history)) {
long diffMinute = ChronoUnit.MINUTES.between(history.getSrvrRcvDt(), Instant.now());
if (diffMinute > 5) {
CtrCntrlBas ctrCntrlBas = ctrCntrlBasRepository.findById(controlId).orElse(null);
if (ctrCntrlBas != null) {
// 기존
ctrCntrlBas.setEndTypeCd("01");
ctrCntrlBas.setCntrlEndDt(Instant.now());
ctrCntrlBas.setStatusCd("99");
// 총 비행거리 Update
Double distance = getDistancSum(history.getCntrlId());
if (distance == null) log.warn("No Search - CntrlId => {}", history.getCntrlId());
ctrCntrlBas.setTtlDstnc(distance);
ctrCntrlBas.setTtlDstncType("M");
ctrCntrlBas.setUpdateDt(Instant.now());
ctrCntrlBasRepository.save(ctrCntrlBas);
}
/* 종료지점 주소 저장 */
CtrCntrlHstryArea area = CtrCntrlHstryArea.builder()
.cntrlId(history.getCntrlId())
.actnType("99")
.prcsYn("N")
.lat(history.getLat())
.lon(history.getLon())
.hstrySno(history.getHstrySno())
.createDt(Instant.now())
.updateDt(Instant.now())
.build();
ctrCntrlHstryAreaRepository.save(area);
simpleControlStorage.remove(objectId);
}
}
});
}
private Double getDistancSum(String cntrlId) {
List<CtrCntrlHstry> result = ctrCntrlHstryRepository.findAllByCntrlIdOrderBySrvrRcvDt(cntrlId);
if (result.size() == 0) return null;
List<Double> distanceList = new ArrayList<>();
for (int i = 0; i < result.size(); i++) {
if (i == 0) continue;
CtrCntrlHstry prevNode = result.get(i - 1);
CtrCntrlHstry node = result.get(i);
CoordUtil.Coordinates prevCoord = CoordUtil.Coordinates.builder()
.x(prevNode.getLat())
.y(prevNode.getLon())
.build();
CoordUtil.Coordinates coord = CoordUtil.Coordinates.builder()
.x(node.getLat())
.y(node.getLon())
.build();
Double distance = CoordUtil.calculateDistance(prevCoord, coord);
distanceList.add(distance);
}
return distanceList.stream().mapToDouble(Double::doubleValue).sum();
}
public void saveDronDataAll(Map<String, List<DroneDto>> droneDtoAll) {
droneDtoAll.values().forEach(droneDtoList -> {
droneDtoList.forEach(this::saveDronData);
});
}
}

48
app/kac-app/src/main/java/kr/co/palnet/kac/app/api/v1/inner/socket/storage/ControlGpsStorage.java

@ -0,0 +1,48 @@
package kr.co.palnet.kac.app.api.v1.inner.socket.storage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ControlGpsStorage {
private Map<String, String> mappingMap;
private ControlGpsStorage() {
mappingMap = new ConcurrentHashMap<>();
}
public static ControlGpsStorage getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final ControlGpsStorage INSTANCE = new ControlGpsStorage();
}
public void put(String objectId, String controlId) {
mappingMap.put(objectId, controlId);
}
public String get(String objectId) {
return mappingMap.get(objectId);
}
public String remove(String objectId) {
return mappingMap.remove(objectId);
}
public boolean containsKey(String objectId) {
return mappingMap.containsKey(objectId);
}
public List<String> getAllList() {
return (List<String>) mappingMap.values();
}
public Map<String, String> getAll() {
return mappingMap;
}
}

31
app/kac-app/src/main/java/kr/co/palnet/kac/app/config/AppReadyEvent.java

@ -0,0 +1,31 @@
package kr.co.palnet.kac.app.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
@Slf4j
@RequiredArgsConstructor
@Component
public class AppReadyEvent implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
float mb = 1024f * 1024f;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
log.info("current heap memory init(xms): {}mb, max(xmx): {}mb", memoryBean.getHeapMemoryUsage().getInit() / mb, memoryBean.getHeapMemoryUsage().getMax() / mb);
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
log.info("Pool: {} (type {}) = {}", mp.getName(), mp.getType(), mp.getUsage().getMax() / mb);
}
} catch (Exception e) {
log.warn("when start app, not read jvm heap memory information.");
}
}
}

22
app/kac-app/src/main/java/kr/co/palnet/kac/app/config/JsonConfig.java

@ -0,0 +1,22 @@
package kr.co.palnet.kac.app.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JsonConfig {
@Bean
public ObjectMapper objectMapper() {
return ObjectMapperUtil.getObjectMapper();
}
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return ObjectMapperUtil.getObjectMapperBuilder();
}
}

26
app/kac-app/src/main/java/kr/co/palnet/kac/app/config/MessageSourceConfig.java

@ -0,0 +1,26 @@
package kr.co.palnet.kac.app.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
@Configuration
public class MessageSourceConfig {
@Bean(name = "errorMessageSource")
public MessageSource getErrorMessageSource() {
ReloadableResourceBundleMessageSource errorMessageSource = new ReloadableResourceBundleMessageSource();
errorMessageSource.setBasenames("classpath:messages/errors/error");
errorMessageSource.setDefaultEncoding("UTF-8");
errorMessageSource.setCacheSeconds(300);
return errorMessageSource;
}
@Bean(name = "errorMessageSourceAccessor")
public MessageSourceAccessor errorMessageSourceAccessor(@Qualifier("errorMessageSource") MessageSource errorMessageSource) {
return new MessageSourceAccessor(errorMessageSource);
}
}

112
app/kac-app/src/main/java/kr/co/palnet/kac/app/config/SwaggerConfig.java

@ -0,0 +1,112 @@
package kr.co.palnet.kac.app.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Slf4j
@Configuration
public class SwaggerConfig {
// @Bean
// public Docket api() {
// return new Docket(DocumentationType.OAS_30) // open api spec 3.0
// .apiInfo(new ApiInfoBuilder().version("1.0").title("PAV").build())
// .forCodeGeneration(true).securitySchemes(Arrays.asList(apiKey()))
// .select()
// .apis(RequestHandlerSelectors.any())
// .paths(PathSelectors.any())
// .build()
// .apiInfo(apiInfo())
// .directModelSubstitute(Date.class, String.class)
// .directModelSubstitute(LocalDate.class, String.class)
// .directModelSubstitute(LocalDateTime.class, String.class)
// .directModelSubstitute(Pageable.class, SwaggerPageable.class)
// .securityContexts(Arrays.asList(securityContext()))
// .securitySchemes(Arrays.asList(apiKey()));
// }
//
//
// private ApiInfo apiInfo() {
// return new ApiInfoBuilder()
// .title("PAV API")
// .description("----")
// .version("1.0")
// .build();
// }
//
// //ApiKey 정의
// private ApiKey apiKey() {
// return new ApiKey("Authorization", "Authorization", "header");
// }
//
// //JWT SecurityContext 구성
// private SecurityContext securityContext() {
// return SecurityContext.builder().securityReferences(defaultAuth()).build();
// }
//
// private List<SecurityReference> defaultAuth() {
// AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEveryThing");
// AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
// authorizationScopes[0] = authorizationScope;
// return Arrays.asList(new SecurityReference("Authorization", authorizationScopes));
// }
private static final String BEARER_TOKEN_PREFIX = "palnet";
@Bean
public OpenAPI openAPI() {
final String securitySchemeName = "BearerAuth";
SecurityRequirement securityRequirement = new SecurityRequirement().addList(securitySchemeName, List.of("read", "write"));
Components components = new Components()
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("Authorization")
.description("prefix add 'palnet ' + token")
);
// Swagger UI 접속 후, 딱 한 번만 accessToken을 입력해주면 모든 API에 토큰 인증 작업이 적용됩니다.
return new OpenAPI()
.addSecurityItem(securityRequirement)
.components(components)
.info(new Info()
.title("PAV API")
.version("1.0")
.description("PAV API")
);
}
@Bean
public GroupedOpenApi commonCode() {
return GroupedOpenApi.builder()
.group("공통-코드")
.pathsToMatch("/v1/com/code/**")
.build();
}
@Bean
public GroupedOpenApi selectAll() {
return GroupedOpenApi.builder()
.group("All")
.pathsToMatch("/**")
.build();
}
}

19
app/kac-app/src/main/java/kr/co/palnet/kac/app/config/WebConfig.java

@ -0,0 +1,19 @@
package kr.co.palnet.kac.app.config;
import kr.co.palnet.kac.app.core.web.BaseAcceptHeaderLocaleResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.Locale;
@Configuration
public class WebConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver localeResolver = new BaseAcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(Locale.KOREA);
return localeResolver;
}
}

131
app/kac-app/src/main/java/kr/co/palnet/kac/app/core/exception/ServerExceptionHandler.java

@ -0,0 +1,131 @@
package kr.co.palnet.kac.app.core.exception;
import kr.co.palnet.kac.core.exception.BaseErrorCode;
import kr.co.palnet.kac.core.exception.BaseException;
import kr.co.palnet.kac.core.exception.Level;
import kr.co.palnet.kac.core.exception.model.BaseErrorModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.time.Instant;
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
public class ServerExceptionHandler {
@Qualifier("errorMessageSourceAccessor")
private final MessageSourceAccessor errorMessageSourceAccessor;
@ExceptionHandler(BaseException.class)
public ResponseEntity<BaseErrorModel> appExceptionHandle(BaseException e) {
BaseErrorCode errorType = e.getErrorCode();
BaseErrorModel baseBody = new BaseErrorModel();
baseBody.setTimestamp(Instant.now());
baseBody.setStatus(errorType.status().value());
baseBody.setError(errorType.status().getReasonPhrase());
baseBody.setCode(errorType.code());
baseBody.setMessage(this.getMessage(errorType));
baseBody.setPath(this.getUrl());
this.printLog(e);
return ResponseEntity.status(errorType.status()).body(baseBody);
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<BaseErrorModel> noResourceFoundException(NoResourceFoundException e) {
BaseErrorModel baseBody = new BaseErrorModel();
baseBody.setTimestamp(Instant.now());
baseBody.setStatus(e.getBody().getStatus());
baseBody.setError(e.getBody().getDetail());
baseBody.setCode(BaseErrorCode.WEB_NOT_FOUND.code());
baseBody.setMessage(this.getMessage(BaseErrorCode.WEB_NOT_FOUND));
baseBody.setPath(e.getResourcePath());
log.error("ERROR: ", e);
return ResponseEntity.status(e.getBody().getStatus()).body(baseBody);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<BaseErrorModel> exceptionHandle(Exception e) {
BaseErrorCode errorType = BaseErrorCode.UNKNOWN;
BaseErrorModel baseBody = new BaseErrorModel();
baseBody.setTimestamp(Instant.now());
baseBody.setStatus(errorType.status().value());
baseBody.setError(errorType.status().getReasonPhrase());
baseBody.setCode(errorType.code());
baseBody.setMessage(this.getMessage(errorType));
baseBody.setPath(this.getUrl());
log.error("ERROR: ", e);
return ResponseEntity.status(errorType.status()).body(baseBody);
}
private String getUrl() {
ServletRequestAttributes servletRequestAttr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (servletRequestAttr != null) return servletRequestAttr.getRequest().getRequestURI();
else return null;
}
private String getMessage(BaseErrorCode e) {
String message = null;
try {
// message = errorMessageSourceAccessor.getMessage(e.code(), LocaleContextHolder.getLocale());
message = errorMessageSourceAccessor.getMessage(e.code());
} catch (NoSuchMessageException noSuchMessageException) {
message = "관리자에게 문의해주세요.";
}
return message;
}
private void printLog(BaseException e) {
Level level = e.getLevel();
String message = e.getLogMessage() == null ? e.getMessage() : e.getLogMessage();
Object[] paramArray = e.getParamArray();
switch (level) {
case TRACE:
log.trace("message: {}, params: {}, trace: ", message, paramArray, e);
log.trace("", e);
break;
case DEBUG:
log.debug("message: {}, params: {}, trace: ", message, paramArray, e);
log.debug("", e);
break;
case INFO:
log.info("message: {}, params: {}, trace: ", message, paramArray, e);
log.info("", e);
break;
case WARN:
log.warn("message: {}, params: {}, trace: ", message, paramArray, e);
log.warn("", e);
break;
case ERROR:
log.error("message: {}, params: {}, trace: ", message, paramArray, e);
log.error("ERROR: ", e);
break;
default:
break;
}
}
}

62
app/kac-app/src/main/java/kr/co/palnet/kac/app/core/security/AppSecurityConfig.java

@ -0,0 +1,62 @@
package kr.co.palnet.kac.app.core.security;
import kr.co.palnet.kac.config.security.SecurityConfig;
import kr.co.palnet.kac.config.security.exception.BaseAccessDeniedHandler;
import kr.co.palnet.kac.config.security.exception.BaseAuthenticationEntryPoint;
import kr.co.palnet.kac.config.security.service.BaseUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@EnableWebSecurity
@Configuration
public class AppSecurityConfig extends SecurityConfig {
// 시큐리티 적용 안하는 URL 목록
private final String[] IGNORE_URL = {
"/v1/inner/socket/**",
"/v1/com/code/**",
"/actuator/**",
"/test/**",
};
// 권한(ROLE)별 URL
private final String[] USER_URL = {
};
public AppSecurityConfig(BaseUserDetailsService baseUserDetailsService, BaseAuthenticationEntryPoint baseAuthenticationEntryPoint, BaseAccessDeniedHandler baseAccessDeniedHandler) {
super(baseUserDetailsService, baseAuthenticationEntryPoint, baseAccessDeniedHandler);
}
@Override
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 기본 security 설정을 불러온다.
this.setDefaultHttpSecurity(http);
// 여기서는 role별 허용 url 설정을 한다.
http
.securityMatchers(matchers -> matchers.requestMatchers("/**"))
.authorizeHttpRequests(authz ->
authz
.requestMatchers(USER_URL).hasRole("USER")
.anyRequest().authenticated()
)
;
return http.build();
}
// security filter 제외 - permission all
@Override
protected List<String> getExcludeURI() {
return new ArrayList<>(Arrays.asList(IGNORE_URL));
}
}

67
app/kac-app/src/main/java/kr/co/palnet/kac/app/core/web/BaseAcceptHeaderLocaleResolver.java

@ -0,0 +1,67 @@
package kr.co.palnet.kac.app.core.web;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Slf4j
public class BaseAcceptHeaderLocaleResolver extends AcceptHeaderLocaleResolver {
private final List<Locale> LOCALES = Arrays.asList(
new Locale("ko"),
new Locale("ko", "KR"),
new Locale("en"),
new Locale("en", "US")
);
private final String LOCALE_HEADER_KEY = "Accept-Language";
/**
* resolve locale 구성
*
* @param request the request to resolve the locale for
* @return
*/
@Override
public Locale resolveLocale(HttpServletRequest request) {
if (!StringUtils.hasText(request.getHeader(LOCALE_HEADER_KEY))) {
return this.getDefaultLocale();
}
try {
Locale locale = LOCALES.stream().filter(_locale -> {
String defaultLocale = _locale.toString()
.replaceAll("-", "")
.replaceAll("_", "")
.toLowerCase();
String headerLocale = request.getHeader(LOCALE_HEADER_KEY).toString()
.replaceAll("-", "")
.replaceAll("_", "")
.toLowerCase();
return defaultLocale.equals(headerLocale);
}).findFirst().orElse(null);
if (locale == null) {
locale = LOCALES.stream()
.filter(_locale -> {
String language = _locale.getLanguage();
return request.getHeader(LOCALE_HEADER_KEY).contains(language);
})
.map(_locale -> {
String language = _locale.getLanguage();
return new Locale(language);
})
.distinct()
.findFirst().orElse(Locale.KOREAN);
}
// log.debug("{}, {}", request.getHeader(LOCALE_HEADER_KEY), locale);
return locale;
} catch (IllegalArgumentException e) {
log.warn("{}", e.getMessage());
return this.getDefaultLocale();
}
}
}

3
app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/PingController.java

@ -1,8 +1,10 @@
package kr.co.palnet.kac.app.ping.controller; package kr.co.palnet.kac.app.ping.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController @RestController
public class PingController { public class PingController {
@ -10,4 +12,5 @@ public class PingController {
public String ping() { public String ping() {
return "pong"; return "pong";
} }
} }

131
app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/TestErrorContoller.java

@ -0,0 +1,131 @@
package kr.co.palnet.kac.app.ping.controller;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.core.exception.BaseErrorCode;
import kr.co.palnet.kac.core.exception.BaseException;
import kr.co.palnet.kac.util.EncryptUtil;
import kr.co.palnet.kac.util.KisaEncryptUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/test")
@RestController
public class TestErrorContoller {
@Qualifier("errorMessageSource")
private final MessageSource errorMessageSource;
@Qualifier("errorMessageSourceAccessor")
private final MessageSourceAccessor errorMessageSourceAccessor;
@PostMapping("/json/parse")
public DroneDto jsonTest(@RequestBody DroneDto dto){
return dto;
}
@GetMapping("/encrypt/{plainText}")
public String ecrypt(@PathVariable String plainText) {
String encrypt = KisaEncryptUtil.CbcEncrypt.encrypt(plainText);
log.debug("e : {}", encrypt);
String decrypt = KisaEncryptUtil.CbcEncrypt.decrypt(encrypt);
log.debug("d : {}", decrypt);
String encrypt1 = EncryptUtil.encrypt(plainText);
log.debug("e : {}", encrypt1);
String decrypt1 = EncryptUtil.decrypt(encrypt1);
log.debug("d : {}", decrypt1);
return decrypt;
}
@GetMapping("/error/{caseNumber}")
public String errorTest(@PathVariable("caseNumber") Integer caseNumber) throws Exception {
log.debug("{}", caseNumber);
return switch (caseNumber) {
case 1 -> throw new BaseException(BaseErrorCode.IO_ERROR);
case 2 -> throw new Exception("test");
default -> "success";
};
}
@GetMapping("/message/locale")
public Map<String, String> locale() {
Map<String, String> map = new HashMap<>();
map.put("message", errorMessageSourceAccessor.getMessage("TEST001"));
map.put("locale", LocaleContextHolder.getLocale().toString());
return map;
}
@GetMapping("/message/error")
public void messageSource() {
Map<String, String> map = new HashMap<>();
try {
String ko_KR = errorMessageSource.getMessage("TEST001", null, Locale.KOREA);
map.put("ko_KR", ko_KR);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String ko = errorMessageSource.getMessage("TEST001", null, Locale.KOREAN);
map.put("ko", ko);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String en = errorMessageSource.getMessage("TEST001", null, Locale.ENGLISH);
map.put("en", en);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String en_US = errorMessageSource.getMessage("TEST001", null, Locale.US);
map.put("en_US", en_US);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
log.debug("==========================================================================================");
try {
String df = errorMessageSourceAccessor.getMessage("TEST001");
map.put("df", df);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String str = errorMessageSourceAccessor.getMessage("TEST001", Locale.KOREAN);
map.put("s_ko", str);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String str = errorMessageSourceAccessor.getMessage("TEST001", Locale.KOREA);
map.put("s_ko_kr", str);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String str = errorMessageSourceAccessor.getMessage("TEST001", Locale.ENGLISH);
map.put("s_en", str);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
try {
String str = errorMessageSourceAccessor.getMessage("TEST001", Locale.US);
map.put("s_en_us", str);
} catch (Exception e) {
log.error("{}", e.getMessage());
}
log.debug("{}", map);
}
}

31
app/kac-app/src/main/java/kr/co/palnet/kac/app/ping/controller/TestSessionContrller.java

@ -0,0 +1,31 @@
package kr.co.palnet.kac.app.ping.controller;
import kr.co.palnet.kac.config.security.model.BaseUserDetails;
import kr.co.palnet.kac.config.security.util.SessionHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test/session")
public class TestSessionContrller {
@GetMapping("/auth")
public void test() {
BaseUserDetails userInfo = SessionHelper.getUserInfo();
log.debug("userInfo ::: {}", userInfo);
Long userNo = SessionHelper.getCstmrSno();
log.debug("userNo ::: {}", userNo);
String userId = SessionHelper.getUserId();
log.debug("userId ::: {}", userId);
boolean isRole1 = SessionHelper.hasRole("USER");
log.debug("isRole1 ::: {}", isRole1);
boolean isRole2 = SessionHelper.hasRole("ADMIN");
log.debug("isRole2 ::: {}", isRole2);
boolean isRole3 = SessionHelper.hasRole("ADMINasdfasdf");
log.debug("isRole3 ::: {}", isRole3);
}
}

3
app/kac-app/src/main/java/lombok.config

@ -0,0 +1,3 @@
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
lombok.anyConstructor.addConstructorProperties=true

41
app/kac-app/src/main/resources/application-swagger.yml

@ -0,0 +1,41 @@
springdoc:
api-docs:
path: /api-docs
groups:
enabled: true
# paths-to-exclude:
# - /api/v1/utm
# - /api/bas/flight/**
# - /api/ctr/cntrl/contains
# - /api/file/**
swagger-ui:
# 각 API의 그룹 표시 순서
# path, query, body, response 순으로 출력
path: /index.html
display-request-duration: true
groups-order: ASC
# 태그 정렬 순서.
# alpha: 알파벳 순 정렬
# method: OpenAPI specification file에 원하는 태그 정렬 방식 직접 기재
tags-sorter: alpha
# 컨트롤러 정렬 순서.
# method는 delete - get - patch - post - put 순으로 정렬된다.
# alpha를 사용해 알파벳 순으로 정렬할 수 있다.
operations-sorter: alpha
# Spring Actuator의 endpoint까지 보여줄 것인지?
show-actuator: false
# request media type 의 기본 값
default-consumes-media-type: application/json
# response media type 의 기본 값
default-produces-media-type: application/json
# Swagger UI에서 기본적으로 펼쳐져 보이는 경로의 수
doc-expansion : none

101
app/kac-app/src/main/resources/application.yml

@ -0,0 +1,101 @@
spring:
profiles:
include:
- swagger
- db
threads:
virtual:
enabled: true
server.port: 8000
logging:
level:
kr.co.palnet: DEBUG
# jdbc:
# audit: OFF
# resultset: OFF
# resultsettable: INFO #SQL 결과 데이터 Table을 로그로 남긴다.
# sqlonly: OFF #SQL만 로그로 남긴다.
# sqltiming: info #SQL과 소요시간을 표기한다.
# connection : trace # 커넥션 확인가능
# org.hibernate:
# SQL: off
# type.descriptor.sql.BasicBinder: TRACE
# com.zaxxer.hikari.HikariConfig: DEBUG
# com.zaxxer.hikari: TRACE
# file:
# name: ./logs/kac-app/data.log
# logback:
# rollingpolicy:
# clean-history-on-start: false
# # file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
# file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log
# max-file-size: 10MB
# max-history: 60
# total-size-cap: 100MB
management:
endpoints:
web:
exposure:
include: '*'
---
spring:
config:
activate:
on-profile: local
logging:
level:
kr.co.palnet: info
jdbc: off
# audit: OFF
# resultset: OFF
# resultsettable: INFO #SQL 결과 데이터 Table을 로그로 남긴다.
# sqlonly: OFF #SQL만 로그로 남긴다.
# sqltiming: info #SQL과 소요시간을 표기한다.
# connection : trace # 커넥션 확인가능
org.hibernate: off
# SQL: off
# type.descriptor.sql.BasicBinder: TRACE
# com.zaxxer.hikari.HikariConfig: off
com.zaxxer.hikari: off
file:
name: ./logs/kac-app/data.log
logback:
rollingpolicy:
clean-history-on-start: false
# file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log
max-file-size: 10MB
max-history: 60
total-size-cap: 100MB
---
spring:
config:
activate:
on-profile: docker
logging:
level:
kr.co.palnet: info
jdbc: off
org.hibernate: off
# jdbc:
# audit: OFF
# resultset: OFF
# resultsettable: INFO #SQL 결과 데이터 Table을 로그로 남긴다.
# sqlonly: OFF #SQL만 로그로 남긴다.
# sqltiming: info #SQL과 소요시간을 표기한다.
# connection : trace # 커넥션 확인가능
# org.hibernate:
# SQL: off
# type.descriptor.sql.BasicBinder: TRACE
# com.zaxxer.hikari.HikariConfig: DEBUG
# com.zaxxer.hikari: TRACE

2
app/kac-app/src/main/resources/log4jdbc.log4j2.properties

@ -0,0 +1,2 @@
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

22
app/kac-app/src/main/resources/messages/errors/error.properties

@ -0,0 +1,22 @@
CM001=\uC131\uACF5\uC785\uB2C8\uB2E4.
CM900=\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uC785\uB2C8\uB2E4.
CM999=\uC2E4\uD328\uC785\uB2C8\uB2E4.
CM002=\uC694\uCCAD\uD558\uC2E0 \uB0B4\uC6A9\uC774 \uCC98\uB9AC \uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
CM003=\uC785\uCD9C\uB825 \uC624\uB958\uC785\uB2C8\uB2E4.
CM100=\uB85C\uADF8\uC778\uC744 \uB2E4\uC2DC \uD574\uC8FC\uC138\uC694.
CM101=\uC774\uBBF8 \uB85C\uADF8\uC778\uD55C \uC0AC\uC6A9\uC790\uC785\uB2C8\uB2E4.
CM102=\uB85C\uADF8\uC778 \uC2DC\uB3C4\uAC00 \uC815\uD574\uC9C4 \uD69F\uC218\uB97C \uCD08\uACFC\uD558\uC600\uC2B5\uB2C8\uB2E4.
CM110=SNS \uB85C\uADF8\uC778\uC744 \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
CM111=\uC774\uBBF8 \uAC00\uC785\uB41C SNS \uACC4\uC815\uC785\uB2C8\uB2E4.
CM120=\uD1A0\uD070\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
CM121=\uC798\uBABB\uB41C \uD1A0\uD070\uC785\uB2C8\uB2E4.
CM130=\uC0AC\uC6A9\uC790\uC758 \uACC4\uC815\uC774 \uC815\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
CM131=\uC0AC\uC6A9\uC790\uC758 \uACC4\uC815\uC774 \uD734\uBA74\uC0C1\uD0DC\uC785\uB2C8\uB2E4.
CM200=\uCE90\uC2DC \uB85C\uB529\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.
CM201=\uCE90\uC2DC \uB9AC\uB85C\uB529\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.
CM401=\uC0AC\uC6A9\uC790\uC758 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.
WB400=\uD30C\uB77C\uBBF8\uD130\uB97C \uB2E4\uC2DC \uD655\uC778 \uD6C4 \uC694\uCCAD\uD574\uC8FC\uC138\uC694.
WB500=\uC678\uBD80 \uC5F0\uB3D9 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.
WB404=\uC694\uCCAD\uD558\uC2E0 URL\uC774 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
DT001=\uC774\uBBF8 \uB4F1\uB85D\uB41C \uB370\uC774\uD130 \uC785\uB2C8\uB2E4.
DT002=\uC694\uCCAD\uD55C \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.

22
app/kac-app/src/main/resources/messages/errors/error_en.properties

@ -0,0 +1,22 @@
CM001=It's a success.
CM900=Unknown error.
CM999=It's a failure.
CM002=Your request has not been processed.
CM003=Input/output error.
CM100=Please log in again.
CM101=This is a user who has already logged in.
CM102=The login attempt exceeded the specified number of times.
CM110=I can't log in to SNS.
CM111=This is an SNS account that has already been subscribed.
CM120=The token has expired.
CM121=Invalid token.
CM130=Your account has been suspended.
CM131=Your account is dormant.
CM200=Cache loading failed.
CM201=Cache reloading failed.
CM401=You do not have permissions.
WB400=Please check the parameters again and request them.
WB500=External interlocking error occurred.
WB404=The URL you requested does not exist.
DT001=Data already registered.
DT002=No data requested.

45
app/kac-app/src/test/java/kr/co/palnet/kac/BaseTest.java

@ -0,0 +1,45 @@
package kr.co.palnet.kac;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.palnet.kac.app.KacAppApplication;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
@SpringBootTest(classes = KacAppApplication.class)
@ExtendWith({RestDocumentationExtension.class})
public class BaseTest {
@Autowired
protected WebApplicationContext webApplicationContext;
public MockMvc mockMvc;
@BeforeEach
public void setUpAll(RestDocumentationContextProvider restDocumentationContextProvider){
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) // @Autowired로 빈주입 받은 context
.addFilter(new CharacterEncodingFilter("UTF-8", true)) // UTF-8 인코딩 필터
.apply(
MockMvcRestDocumentation.documentationConfiguration(restDocumentationContextProvider)
.operationPreprocessors()
.withRequestDefaults(
modifyUris().scheme("http").host("localhost").port(8080), prettyPrint() // URL 정보를 넣어주시면 됩니다.
)
.withResponseDefaults(
prettyPrint()
)
)
.build();
}
}

600
app/kac-app/src/test/java/kr/co/palnet/kac/com/code/ComnCodeControllerTest.java

@ -0,0 +1,600 @@
package kr.co.palnet.kac.com.code;
import kr.co.palnet.kac.BaseTest;
import kr.co.palnet.kac.api.v1.common.code.model.FormCodeGroupRQ;
import kr.co.palnet.kac.api.v1.common.code.model.FormCodeLangRQ;
import kr.co.palnet.kac.api.v1.common.code.model.FormCodeRQ;
import kr.co.palnet.kac.api.v1.common.code.service.ComCodeService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class ComnCodeControllerTest extends BaseTest {
@Autowired
private ComCodeService comCodeService;
private final String BASE_URL = "/v1/com/code";
@DisplayName("언어코드 삭제")
@Test
public void deleteLang() throws Exception {
String groupCd = "NEW_GROUP001";
String cdId = "NEW_CODE001";
String langDivCd = "ko_KR";
this.mockMvc.perform(
delete(this.BASE_URL + "/lang?groupCd=" + groupCd + "&cdId=" + cdId + "&langDivCd=" + langDivCd)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/lang/delete", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("groupCd").description("그룹 아이디"),
parameterWithName("cdId").description("코드 아이디"),
parameterWithName("langDivCd").description("삭제할 언어코드")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드 삭제")
@Test
public void deleteCode() throws Exception {
String groupCd = "NEW_GROUP001";
String cdId = "NEW_CODE001";
this.mockMvc.perform(
delete(this.BASE_URL + "/code?groupCd=" + groupCd + "&cdId=" + cdId)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/code/delete", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("groupCd").description("그룹 아이디"),
parameterWithName("cdId").description("코드 아이디")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("그룹 삭제")
@Test
public void deleteCodeGroup() throws Exception {
String deleteGroupCd = "NEW_GROUP001";
this.mockMvc.perform(
delete(this.BASE_URL + "/group?groupCd=" + deleteGroupCd)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/group/delete", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("groupCd").description("그룹 아이디")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드의 언어코드 수정")
@Test
@Transactional
public void updateCodeLang() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
FormCodeLangRQ langRq = FormCodeLangRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").langDivCd("ko_KR").cdNm("신규코드001").rm("비고....").build();
comCodeService.createCodeLang(langRq);
String jsonRQ = """
{
"groupCd": "%s",
"cdId": "NEW_CODE001",
"langDivCd": "ko_KR",
"cdNm": "신규코드001",
"rm": "수정..."
}
""".formatted(randomNm);
this.mockMvc.perform(
put(this.BASE_URL + "/lang")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/lang/update", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("langDivCd").type(JsonFieldType.STRING).description("언어코드"),
fieldWithPath("cdNm").type(JsonFieldType.STRING).description("코드이름[언어별]"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("langDivCd").type(JsonFieldType.STRING).description("언어코드"),
fieldWithPath("cdNm").type(JsonFieldType.STRING).description("코드이름[언어별]"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드 수정")
@Test
@Transactional
public void updateCode() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
FormCodeLangRQ langRq = FormCodeLangRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").langDivCd("ko_KR").cdNm("신규코드001").rm("비고....").build();
comCodeService.createCodeLang(langRq);
String jsonRQ = """
{
"groupCd": "%s",
"cdId": "NEW_CODE001",
"sortOrdr": 0,
"addInfoValue": "수정!!",
"useYn": "Y"
}
""".formatted(randomNm);
this.mockMvc.perform(
put(this.BASE_URL + "/code")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/code/update", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("sortOrdr").type(JsonFieldType.NUMBER).description("정렬순서"),
fieldWithPath("addInfoValue").type(JsonFieldType.STRING).description("추가사항"),
fieldWithPath("useYn").type(JsonFieldType.STRING).description("사용여부")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("upperCd").type(JsonFieldType.STRING).description("상위 아이디").optional(),
fieldWithPath("sortOrdr").type(JsonFieldType.NUMBER).description("정렬순서"),
fieldWithPath("addInfoValue").type(JsonFieldType.STRING).description("추가사항"),
fieldWithPath("useYn").type(JsonFieldType.STRING).description("사용여부"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("그룹 수정")
@Test
@Transactional
public void updateCodeGroup() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
String jsonRQ = """
{
"groupCd": "%s",
"siteCd": "KAC",
"groupCdNm": "신규그룹001",
"rm": "비고...."
}
""".formatted(randomNm);
this.mockMvc.perform(
put(this.BASE_URL + "/group")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/group/update", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("siteCd").type(JsonFieldType.STRING).description("사이트코드"),
fieldWithPath("groupCdNm").type(JsonFieldType.STRING).description("그룹이름"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("siteCd").type(JsonFieldType.STRING).description("사이트코드"),
fieldWithPath("groupCdNm").type(JsonFieldType.STRING).description("그룹이름"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드의 언어코드 추가")
@Test
@Transactional
public void createCodeLang() throws Exception {
String langDivCd = "ko_KR";
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
String jsonRQ = """
{
"groupCd": "%s",
"cdId": "NEW_CODE001",
"langDivCd": "%s",
"cdNm": "신규코드001",
"rm": "비고...."
}
""".formatted(randomNm, langDivCd);
this.mockMvc.perform(
post(this.BASE_URL + "/lang")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/lang/create", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("langDivCd").type(JsonFieldType.STRING).description("언어코드"),
fieldWithPath("cdNm").type(JsonFieldType.STRING).description("코드이름[언어별]"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("langDivCd").type(JsonFieldType.STRING).description("언어코드"),
fieldWithPath("cdNm").type(JsonFieldType.STRING).description("코드이름[언어별]"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드 추가")
@Test
@Transactional
public void createCode() throws Exception {
String codeNm = "NEW_CODE001";
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
String jsonRQ = """
{
"groupCd": "%s",
"cdId": "%s",
"sortOrdr": 0,
"addInfoValue": "추가!!",
"useYn": "Y"
}
""".formatted(randomNm, codeNm);
this.mockMvc.perform(
post(this.BASE_URL + "/code")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/code/create", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("sortOrdr").type(JsonFieldType.NUMBER).description("정렬순서"),
fieldWithPath("addInfoValue").type(JsonFieldType.STRING).description("추가사항"),
fieldWithPath("useYn").type(JsonFieldType.STRING).description("사용여부")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("cdId").type(JsonFieldType.STRING).description("코드 아이디"),
fieldWithPath("upperCd").type(JsonFieldType.STRING).description("상위 아이디").optional(),
fieldWithPath("sortOrdr").type(JsonFieldType.NUMBER).description("정렬순서"),
fieldWithPath("addInfoValue").type(JsonFieldType.STRING).description("추가사항"),
fieldWithPath("useYn").type(JsonFieldType.STRING).description("사용여부"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("코드그룹 추가")
@Test
@Transactional
public void createCodeGroup() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
String jsonRQ = """
{
"groupCd": "%s",
"siteCd": "KAC",
"groupCdNm": "신규그룹001",
"rm": "비고...."
}
""".formatted(randomNm);
this.mockMvc.perform(
post(this.BASE_URL + "/group")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(jsonRQ)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/group/create", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("siteCd").type(JsonFieldType.STRING).description("사이트코드"),
fieldWithPath("groupCdNm").type(JsonFieldType.STRING).description("그룹이름"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고")
),
responseFields(
fieldWithPath("groupCd").type(JsonFieldType.STRING).description("그룹코드"),
fieldWithPath("siteCd").type(JsonFieldType.STRING).description("사이트코드"),
fieldWithPath("groupCdNm").type(JsonFieldType.STRING).description("그룹이름"),
fieldWithPath("rm").type(JsonFieldType.STRING).description("비고"),
fieldWithPath("createUserId").type(JsonFieldType.STRING).description("등록한 유저 아이디"),
fieldWithPath("createDt").type(JsonFieldType.STRING).description("등록일시"),
fieldWithPath("updateUserId").type(JsonFieldType.STRING).description("수정한 유저 아이디"),
fieldWithPath("updateDt").type(JsonFieldType.STRING).description("수정일시")
)
)
)
.andExpect(status().isOk());
}
@DisplayName("모든 코드 조회")
@Test
@Transactional(propagation = Propagation.SUPPORTS)
public void getAllCodeByGroup() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
FormCodeLangRQ langRq = FormCodeLangRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").langDivCd("ko_KR").cdNm("신규코드001").rm("비고....").build();
comCodeService.createCodeLang(langRq);
String langDivCd = "ko_KR";
this.mockMvc.perform(
get(this.BASE_URL + "/code")
.param("groupCd", randomNm)
.param("langDivCd", langDivCd)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/code", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("groupCd").description("그룹코드"),
parameterWithName("langDivCd").description("언어코드")
),
responseFields(
fieldWithPath("[].groupCd").type(JsonFieldType.STRING).description("코드의 그룹").optional(),
fieldWithPath("[].cdId").type(JsonFieldType.STRING).description("코드의 ID").optional(),
fieldWithPath("[].cdNm").type(JsonFieldType.STRING).description("코드의 이름").optional(),
fieldWithPath("[].upperCd").type(JsonFieldType.STRING).description("상위 코드의 이름").optional(),
fieldWithPath("[].sortOrdr").type(JsonFieldType.NUMBER).description("코드의 정렬순서").optional(),
fieldWithPath("[].addInfoValue").type(JsonFieldType.STRING).description("코드의 추가 밸류").optional(),
fieldWithPath("[].children").type(JsonFieldType.STRING).description("코드의 하위요소").optional()
)
)
)
.andExpect(status().isOk());
// comCodeService.deleteLang(randomNm, "NEW_CODE001", "ko_KR");
// comCodeService.deleteCode(randomNm, "NEW_CODE001");
comCodeService.deleteCodeGroup(randomNm);
}
@DisplayName("모든 그룹과 코드 조회")
@Test
@Transactional
public void getAllGroupCode() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
FormCodeLangRQ langRq = FormCodeLangRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").langDivCd("ko_KR").cdNm("신규코드001").rm("비고....").build();
comCodeService.createCodeLang(langRq);
this.mockMvc.perform(
get(this.BASE_URL + "/group")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/group", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
responseFields(
fieldWithPath("[].groupCd").type(JsonFieldType.STRING).description("그룹 코드").optional(),
fieldWithPath("[].siteCd").type(JsonFieldType.STRING).description("사이트 코드").optional(),
fieldWithPath("[].groupCdNm").type(JsonFieldType.STRING).description("그룹 명").optional(),
fieldWithPath("[].rm").type(JsonFieldType.STRING).description("비고").optional(),
fieldWithPath("[].codeList").type(JsonFieldType.ARRAY).description("그룹에 포함된 코드 리스트").optional()
)
)
)
.andExpect(status().isOk());
}
@DisplayName("모든 그룹 조회")
@Test
@Transactional
public void getAllGroup() throws Exception {
String randomNm = "NEW_GROUP_" + System.currentTimeMillis();
randomNm = randomNm.substring(0, 20);
FormCodeGroupRQ groupRq = FormCodeGroupRQ.builder().groupCd(randomNm).siteCd("KAC").groupCdNm("신규그룹001").rm("비고....").build();
comCodeService.createCodeGroup(groupRq);
FormCodeRQ codeRq = FormCodeRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").upperCd(null).sortOrdr(0).addInfoValue("추가!!").useYn("Y").build();
comCodeService.createCode(codeRq);
FormCodeLangRQ langRq = FormCodeLangRQ.builder().groupCd(randomNm).cdId("NEW_CODE001").langDivCd("ko_KR").cdNm("신규코드001").rm("비고....").build();
comCodeService.createCodeLang(langRq);
String siteCd = "KAC";
String langDivCd = "ko_KR";
this.mockMvc.perform(
get(this.BASE_URL + "/all")
.param("siteCd", siteCd)
.param("langDivCd", langDivCd)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
)
.andDo(print())
.andDo(
document(
"com/code/all", // gradle build를 하게 되면 generated-snippets에 만들어질 폴더이름
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("siteCd").description("사이트코드"),
parameterWithName("langDivCd").description("언어코드")
),
responseFields(
fieldWithPath("[].groupCd").type(JsonFieldType.STRING).description("그룹 코드").optional(),
fieldWithPath("[].siteCd").type(JsonFieldType.STRING).description("사이트 코드").optional(),
fieldWithPath("[].groupCdNm").type(JsonFieldType.STRING).description("그룹 명").optional(),
fieldWithPath("[].rm").type(JsonFieldType.STRING).description("비고").optional(),
fieldWithPath("[].codeList").type(JsonFieldType.ARRAY).description("그룹에 포함된 코드 리스트").optional(),
fieldWithPath("[].codeList[].groupCd").type(JsonFieldType.STRING).description("코드의 그룹").optional(),
fieldWithPath("[].codeList[].cdId").type(JsonFieldType.STRING).description("코드의 ID").optional(),
fieldWithPath("[].codeList[].cdNm").type(JsonFieldType.STRING).description("코드의 이름").optional(),
fieldWithPath("[].codeList[].upperCd").type(JsonFieldType.STRING).description("상위 코드의 이름").optional(),
fieldWithPath("[].codeList[].sortOrdr").type(JsonFieldType.NUMBER).description("코드의 정렬순서").optional(),
fieldWithPath("[].codeList[].addInfoValue").type(JsonFieldType.STRING).description("코드의 추가 밸류").optional(),
fieldWithPath("[].codeList[].children").type(JsonFieldType.STRING).description("코드의 하위요소").optional()
)
)
)
.andExpect(status().isOk());
}
}

11
app/kac-app/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet vendored

@ -0,0 +1,11 @@
.+{{path}}+
|===
|파라미터|설명|필수
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}false{{/optional}}{{^optional}}true{{/optional}}{{/tableCellContent}}
{{/parameters}}
|===

10
app/kac-app/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet vendored

@ -0,0 +1,10 @@
|===
|파라미터|설명|필수
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}false{{/optional}}{{^optional}}true{{/optional}}{{/tableCellContent}}
{{/parameters}}
|===

12
app/kac-app/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet vendored

@ -0,0 +1,12 @@
|===
|파라미터|타입|설명|필수
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}false{{/optional}}{{^optional}}true{{/optional}}{{/tableCellContent}}
{{/fields}}
|===

10
app/kac-app/src/test/resources/org/springframework/restdocs/templates/request-headers.snippet vendored

@ -0,0 +1,10 @@
|===
|파라미터|설명|필수
{{#headers}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}false{{/optional}}{{^optional}}true{{/optional}}{{/tableCellContent}}
{{/headers}}
|===

12
app/kac-app/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet vendored

@ -0,0 +1,12 @@
|===
|파라미터|타입|설명|필수
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}false{{/optional}}{{^optional}}true{{/optional}}{{/tableCellContent}}
{{/fields}}
|===

12
app/kac-socket-app/Dockerfile

@ -0,0 +1,12 @@
FROM openjdk:21
ENV TZ=Asia/Seoul
#ENV JAVA_OPTS="-Xms512M -Xmx512M"
ENV JAVA_OPTS=""
EXPOSE 8003
WORKDIR /app
#ENTRYPOINT ["java",${JAVA_OPTS},"-jar","/app/kac-socket-app-1.0.0.jar"]
CMD java $JAVA_OPTS -jar /app/kac-socket-app-1.0.0.jar

10
app/kac-socket-app/build.gradle

@ -0,0 +1,10 @@
dependencies {
implementation "$boot:spring-boot-starter"
implementation "$spring:spring-web"
implementation "io.netty:netty-all:4.1.68.Final"
implementation project(":common:util")
implementation project(":common:model")
}

15
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/KacSocketApplication.java

@ -0,0 +1,15 @@
package kr.co.palnet.kac.socket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class KacSocketApplication {
public static void main(String[] args) {
SpringApplication.run(KacSocketApplication.class, args);
}
}

31
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DroneDecoder.java

@ -0,0 +1,31 @@
package kr.co.palnet.kac.socket.core.codec;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import kr.co.palnet.kac.socket.core.model.DroneRq;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class DroneDecoder extends MessageToMessageDecoder<String> {
private final ObjectMapper objectMapper = ObjectMapperUtil.getObjectMapper();
@Override
protected void decode(ChannelHandlerContext ctx, String in, List<Object> out) throws Exception {
try {
DroneRq droneRq = objectMapper.readValue(in, DroneRq.class);
if (droneRq != null) {
out.add(droneRq);
}
} catch (Exception e) {
log.warn("decode parsing error : {}\ndata in : {}", e.getMessage(), in);
}
}
}

27
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/codec/DroneEncoder.java

@ -0,0 +1,27 @@
package kr.co.palnet.kac.socket.core.codec;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class DroneEncoder extends MessageToMessageEncoder<Object> {
private final ObjectMapper objectMapper = ObjectMapperUtil.getObjectMapper();
@Override
protected void encode(ChannelHandlerContext ctx, Object in, List<Object> out) throws Exception {
try {
String json = objectMapper.writeValueAsString(in);
out.add(json);
} catch (Exception e) {
log.warn("json parsing error : {}\ndata in : {}", e.getMessage(), in);
}
}
}

8
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/DroneCommand.java

@ -0,0 +1,8 @@
package kr.co.palnet.kac.socket.core.command;
import kr.co.palnet.kac.socket.core.model.DroneRq;
public interface DroneCommand {
void command(final DroneRq rq);
String getCommandName();
}

119
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AdsbDroneCommandImpl.java

@ -0,0 +1,119 @@
package kr.co.palnet.kac.socket.core.command.impl;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.DroneHistoryDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import kr.co.palnet.kac.socket.core.command.DroneCommand;
import kr.co.palnet.kac.socket.core.model.DroneRq;
import kr.co.palnet.kac.socket.core.storage.DroneStorage;
import kr.co.palnet.kac.socket.service.KacAppService;
import kr.co.palnet.kac.socket.service.WebSocketService;
import kr.co.palnet.kac.util.DroneUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Component
public class AdsbDroneCommandImpl implements DroneCommand {
private final WebSocketService webSocketService;
private final KacAppService kacAppService;
@Override
public String getCommandName() {
return "ADS-B";
}
@Override
public void command(DroneRq rq) {
List<DroneDto> resultList = rq.getBody();
/** 전문 설정 ***/
final String messageType = "LTEM";
final String objectType = "DRON";
/** 데이터 모델링 **/
for (DroneDto drone : resultList) {
// 위,경도 좌표가 0으로 들어오는 것은 무시 처리
if (DroneUtil.checkCoordinates(drone.getLat(), drone.getLon())) {
drone.setObjectType(objectType);
drone.setMessageType(messageType);
drone.setTerminalId(rq.getTerminalId());
drone.setRegDt(Instant.now());
// 서버 수신 시간 정보
drone.setServerRcvDt(Instant.now());
// 관제 이력 정보
List<DroneHistoryDto> hisList;
DroneHistoryDto history = DroneHistoryDto.builder()
.objectId(drone.getObjectId())
.lat(drone.getLat())
.lon(drone.getLon())
.build();
if (drone.getPostionHistory() != null) {
hisList = drone.getPostionHistory();
} else {
hisList = new ArrayList<>();
}
hisList.add(history);
drone.setPostionHistory(hisList);
// STEP 1. 전에 내부 메모리에서 controlId 조회
DroneStorage droneStorage = DroneStorage.getInstance();
DroneDto lastDroneDto = droneStorage.getLast(drone.getObjectId());
if (lastDroneDto == null) {
// STEP 1. Control ID 발급 -> Application Server Http 통신
try {
SimpleControlDto simpleControlDto = kacAppService.getControlId(drone.getObjectId());
drone.setControlId(simpleControlDto.getControlId());
drone.setTypeCd(simpleControlDto.getTypeCd());
drone.setAreaTrnsYn(simpleControlDto.getAreaTrnsYn());
drone.setControlStartDt(Instant.now());
drone.setRegDt(Instant.now());
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
} else {
drone.setControlId(lastDroneDto.getControlId());
drone.setTypeCd("02");
drone.setAreaTrnsYn(lastDroneDto.getAreaTrnsYn());
drone.setControlWarnCd(lastDroneDto.isControlWarnCd());
drone.setControlStartDt(lastDroneDto.getControlStartDt());
drone.setRegDt(Instant.now());
}
// STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리
// STEP 3. 화면에 표출할 정보 WebSocket 전달
try {
// 저장 해 놓았다가 한거번에 전송 - 필요한 곳에 전송(HISTORY, UTM, WEB-SOCKET)
droneStorage.add(drone);
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
} else {
log.error("좌표 정보가 존재하지 않습니다.");
throw new IllegalArgumentException("좌표 정보가 존재하지 않습니다.");
}
}
}
}

117
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/AntosDroneCommandImpl.java

@ -0,0 +1,117 @@
package kr.co.palnet.kac.socket.core.command.impl;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.DroneHistoryDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import kr.co.palnet.kac.socket.core.command.DroneCommand;
import kr.co.palnet.kac.socket.core.model.DroneRq;
import kr.co.palnet.kac.socket.core.storage.DroneStorage;
import kr.co.palnet.kac.socket.service.KacAppService;
import kr.co.palnet.kac.socket.service.WebSocketService;
import kr.co.palnet.kac.util.DroneUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Component
public class AntosDroneCommandImpl implements DroneCommand {
private final WebSocketService webSocketService;
private final KacAppService kacAppService;
@Override
public String getCommandName() {
return "ANTOS";
}
@Override
public void command(DroneRq rq) {
List<DroneDto> resultList = rq.getBody();
/** 전문 설정 ***/
final String messageType = "LTEM";
final String objectType = "DRON";
/** 데이터 모델링 **/
for (DroneDto drone : resultList) {
// 위,경도 좌표가 0으로 들어오는 것은 무시 처리
if (DroneUtil.checkCoordinates(drone.getLat(), drone.getLon())) {
drone.setObjectType(objectType);
drone.setMessageType(messageType);
drone.setTerminalId(rq.getTerminalId());
drone.setRegDt(Instant.now());
// 서버 수신 시간 정보
drone.setServerRcvDt(Instant.now());
// 관제 이력 정보
List<DroneHistoryDto> hisList;
DroneHistoryDto history = DroneHistoryDto.builder()
.objectId(drone.getObjectId())
.lat(drone.getLat())
.lon(drone.getLon())
.build();
if (drone.getPostionHistory() != null) {
hisList = drone.getPostionHistory();
} else {
hisList = new ArrayList<>();
}
hisList.add(history);
drone.setPostionHistory(hisList);
// STEP 1. 전에 내부 메모리에서 controlId 조회
DroneStorage droneStorage = DroneStorage.getInstance();
DroneDto lastDroneDto = droneStorage.getLast(drone.getObjectId());
if (lastDroneDto == null) {
// STEP 1. Control ID 발급 -> Application Server Http 통신
try {
SimpleControlDto simpleControlDto = kacAppService.getControlId(drone.getObjectId());
drone.setControlId(simpleControlDto.getControlId());
drone.setTypeCd(simpleControlDto.getTypeCd());
drone.setAreaTrnsYn(simpleControlDto.getAreaTrnsYn());
drone.setControlStartDt(simpleControlDto.getControlStartDt() != null ? simpleControlDto.getControlStartDt() : Instant.now());
drone.setRegDt(Instant.now());
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
} else {
drone.setControlId(lastDroneDto.getControlId());
drone.setTypeCd("02");
drone.setAreaTrnsYn(lastDroneDto.getAreaTrnsYn());
drone.setControlWarnCd(lastDroneDto.isControlWarnCd());
drone.setControlStartDt(lastDroneDto.getControlStartDt());
drone.setRegDt(Instant.now());
}
// STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리
// STEP 3. 화면에 표출할 정보 WebSocket 전달
try {
// 저장 해 놓았다가 한거번에 전송 - 필요한 곳에 전송(HISTORY, UTM, WEB-SOCKET)
droneStorage.add(drone);
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
} else {
log.error("좌표 정보가 존재하지 않습니다.");
throw new IllegalArgumentException("좌표 정보가 존재하지 않습니다.");
}
}
}
}

119
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/command/impl/SandboxDroneCommandImpl.java

@ -0,0 +1,119 @@
package kr.co.palnet.kac.socket.core.command.impl;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.DroneHistoryDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import kr.co.palnet.kac.socket.core.command.DroneCommand;
import kr.co.palnet.kac.socket.core.model.DroneRq;
import kr.co.palnet.kac.socket.core.storage.DroneStorage;
import kr.co.palnet.kac.socket.service.KacAppService;
import kr.co.palnet.kac.socket.service.WebSocketService;
import kr.co.palnet.kac.util.DroneUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Component
public class SandboxDroneCommandImpl implements DroneCommand {
private final WebSocketService webSocketService;
private final KacAppService kacAppService;
@Override
public String getCommandName() {
return "SANDBOX";
}
@Override
public void command(DroneRq rq) {
List<DroneDto> resultList = rq.getBody();
/** 전문 설정 ***/
final String messageType = "LTEM";
final String objectType = "DRON";
/** 데이터 모델링 **/
for (DroneDto drone : resultList) {
// 위,경도 좌표가 0으로 들어오는 것은 무시 처리
if (DroneUtil.checkCoordinates(drone.getLat(), drone.getLon())) {
drone.setObjectType(objectType);
drone.setMessageType(messageType);
drone.setTerminalId(rq.getTerminalId());
drone.setRegDt(Instant.now());
// 서버 수신 시간 정보
drone.setServerRcvDt(Instant.now());
// 관제 이력 정보
List<DroneHistoryDto> hisList;
DroneHistoryDto history = DroneHistoryDto.builder()
.objectId(drone.getObjectId())
.lat(drone.getLat())
.lon(drone.getLon())
.build();
if (drone.getPostionHistory() != null) {
hisList = drone.getPostionHistory();
} else {
hisList = new ArrayList<>();
}
hisList.add(history);
drone.setPostionHistory(hisList);
Long start = System.currentTimeMillis();
// STEP 1. 전에 내부 메모리에서 controlId 조회
DroneStorage droneStorage = DroneStorage.getInstance();
DroneDto lastDroneDto = droneStorage.getLast(drone.getObjectId());
if (lastDroneDto == null) {
// STEP 1. Control ID 발급 -> Application Server Http 통신
try {
SimpleControlDto simpleControlDto = kacAppService.getControlId(drone.getObjectId());
drone.setControlId(simpleControlDto.getControlId());
drone.setTypeCd(simpleControlDto.getTypeCd());
drone.setAreaTrnsYn(simpleControlDto.getAreaTrnsYn());
drone.setControlStartDt(Instant.now());
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
} else {
drone.setControlId(lastDroneDto.getControlId());
drone.setTypeCd("02");
drone.setAreaTrnsYn(lastDroneDto.getAreaTrnsYn());
drone.setControlWarnCd(lastDroneDto.isControlWarnCd());
drone.setControlStartDt(lastDroneDto.getControlStartDt());
drone.setRegDt(Instant.now());
}
// STEP 2. 이력 생성할 전문 전달 -> DRON의 대한 식별정보만 이력 관리
// STEP 3. 화면에 표출할 정보 WebSocket 전달
try {
// 저장 해 놓았다가 한거번에 전송 - 필요한 곳에 전송(HISTORY, UTM, WEB-SOCKET)
droneStorage.add(drone);
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
}
Long end = System.currentTimeMillis();
// log.debug(">>> during time : {} - {}", drone.getObjectId(), end - start);
} else {
log.error("좌표 정보가 존재하지 않습니다.");
throw new IllegalArgumentException("좌표 정보가 존재하지 않습니다.");
}
}
}
}

35
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AppReadyEvent.java

@ -0,0 +1,35 @@
package kr.co.palnet.kac.socket.core.config;
import kr.co.palnet.kac.socket.core.socket.SocketServer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
@Slf4j
@RequiredArgsConstructor
@Component
public class AppReadyEvent implements ApplicationListener<ApplicationReadyEvent> {
private final SocketServer socketServer;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
float mb = 1024f * 1024f;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
log.info("current heap memory init(xms): {}mb, max(xmx): {}mb", memoryBean.getHeapMemoryUsage().getInit() / mb, memoryBean.getHeapMemoryUsage().getMax() / mb);
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
log.info("Pool: {} (type {}) = {}", mp.getName(), mp.getType(), mp.getUsage().getMax() / mb);
}
} catch (Exception e) {
log.warn("when start app, not read jvm heap memory information.");
}
socketServer.start();
}
}

44
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/AsyncConfig.java

@ -0,0 +1,44 @@
package kr.co.palnet.kac.socket.core.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Slf4j
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("th-async-default-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> log.error("ERROR : {}", ex.getMessage(), ex);
}
@Bean
public Executor restClientThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(1000);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("th-async-rc-");
executor.initialize();
return executor;
}
}

30
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/JsonConfig.java

@ -0,0 +1,30 @@
package kr.co.palnet.kac.socket.core.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
public class JsonConfig {
@Bean
public ObjectMapper objectMapper() {
return ObjectMapperUtil.getObjectMapper();
}
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return ObjectMapperUtil.getObjectMapperBuilder();
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(objectMapper());
}
}

76
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/config/NettyConfig.java

@ -0,0 +1,76 @@
package kr.co.palnet.kac.socket.core.config;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import kr.co.palnet.kac.socket.core.socket.ChannelInitializer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.InetSocketAddress;
@Slf4j
@RequiredArgsConstructor
@Configuration
public class NettyConfig {
@Value("${netty.socket.tcp-port}")
private int port;
@Value("${netty.socket.boss-count}")
private int bossCount;
@Value("${netty.socket.keep-alive}")
private boolean keepAlive;
@Value("${netty.socket.tcp-nodelay}")
private boolean tcpNodelay;
@Value("${netty.socket.backlog}")
private int backlog;
@Bean
public ServerBootstrap serverBootstrap(ChannelInitializer channelInitializer) {
// ServerBootstrap: 서버 설정을 도와주는 class
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup(), workerGroup())
// NioServerSocketChannel: incoming connections를 수락하기 위해 새로운 Channel을 객체화할 때 사용
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
// ChannelInitializer: 새로운 Channel을 구성할 때 사용되는 특별한 handler. 주로 ChannelPipeline으로 구성
.childHandler(channelInitializer);
// ServerBootstarp에 다양한 Option 추가 가능
// SO_BACKLOG: 동시에 수용 가능한 최대 incoming connections 개수
// 이 외에도 SO_KEEPALIVE, TCP_NODELAY 등 옵션 제공
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
b.option(ChannelOption.SO_BACKLOG, backlog); // 동시에 수용할 수 있는 소켓 연결 요청 수입니다.
b.childOption(ChannelOption.TCP_NODELAY, tcpNodelay); // 반응속도를 높이기 위해 Nagle 알고리즘을 비활성화 합니다
b.childOption(ChannelOption.SO_LINGER, 0); // 소켓이 close될 때 신뢰성있는 종료를 위해 4way-handshake가 발생하고 이때 TIME_WAIT로 리소스가 낭비됩니다. 이를 방지하기 위해 0으로 설정합니다.
b.childOption(ChannelOption.SO_KEEPALIVE, keepAlive); // Keep-alive를 켭니다.
b.childOption(ChannelOption.SO_REUSEADDR, false); // SO_LINGER설정이 있으면 안해도 되나 혹시나병(!)으로 TIME_WAIT걸린 포트를 재사용할 수 있도록 설정합니다.
return b;
}
// boss: incoming connection을 수락하고, 수락한 connection을 worker에게 등록(register)
@Bean(destroyMethod = "shutdownGracefully")
public NioEventLoopGroup bossGroup() {
return new NioEventLoopGroup(bossCount);
}
// worker: boss가 수락한 연결의 트래픽 관리
@Bean(destroyMethod = "shutdownGracefully")
public NioEventLoopGroup workerGroup() {
return new NioEventLoopGroup();
}
// IP 소켓 주소(IP 주소, Port 번호)를 구현
// 도메인 이름으로 객체 생성 가능
@Bean
public InetSocketAddress inetSocketAddress() {
return new InetSocketAddress(port);
}
}

105
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/handler/DroneHandler.java

@ -0,0 +1,105 @@
package kr.co.palnet.kac.socket.core.handler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import kr.co.palnet.kac.socket.core.command.DroneCommand;
import kr.co.palnet.kac.socket.core.model.DroneRq;
import kr.co.palnet.kac.socket.core.model.DroneRs;
import kr.co.palnet.kac.socket.core.storage.AuthKeyStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@ChannelHandler.Sharable
@Component
public class DroneHandler extends SimpleChannelInboundHandler<DroneRq> {
private final Map<String, DroneCommand> commandMap;
public DroneHandler(List<DroneCommand> commandList) {
commandMap = new HashMap<>();
for (DroneCommand command : commandList) {
commandMap.put(command.getCommandName(), command);
}
}
//
@Override
protected void channelRead0(ChannelHandlerContext ctx, DroneRq rq) throws Exception {
// key 검사
if (rq == null) {
ctx.writeAndFlush(DroneRs.builder()
.code("-1000")
.message("data is empty.")
.build());
return;
}
if (rq.getCommand() == null || rq.getCommand().isEmpty()) {
ctx.writeAndFlush(DroneRs.builder()
.code("-1001")
.message("command is empty.")
.build());
return;
}
if (rq.getAuthKey() == null || rq.getAuthKey().isEmpty()) {
ctx.writeAndFlush(DroneRs.builder()
.code("-2000")
.message("auth key is empty.")
.build());
return;
}
if (AuthKeyStorage.getInstance().checkAuthKey(rq.getCommand())) {
ctx.writeAndFlush(DroneRs.builder()
.code("-2000")
.message("Invalid auth key.")
.build());
return;
}
DroneCommand commandService = commandMap.get(rq.getCommand().trim());
if (commandService == null) {
ctx.writeAndFlush(DroneRs.builder()
.code("-2100")
.message("this command is not exist.")
.build());
return;
}
try {
commandService.command(rq);
} catch (Exception e) {
ctx.writeAndFlush(DroneRs.builder()
.code("-500")
.message("server error - execute command")
.build());
return;
}
ctx.writeAndFlush(DroneRs.builder()
.code("0")
.message("success")
.build());
}
// 에러 처리
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("ERROR: {}", cause.getMessage(), cause);
DroneRs rs = DroneRs.builder()
.code("-9999")
.message("error - etc")
.build();
ctx.writeAndFlush(rs);
ctx.close();
}
}

25
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/DroneRq.java

@ -0,0 +1,25 @@
package kr.co.palnet.kac.socket.core.model;
import kr.co.palnet.kac.common.model.common.DroneDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DroneRq {
private String authKey;
private String terminalId;
private String command;
private List<DroneDto> body;
}

15
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/DroneRs.java

@ -0,0 +1,15 @@
package kr.co.palnet.kac.socket.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DroneRs {
private String code;
private String message;
}

37
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/model/UtmDto.java

@ -0,0 +1,37 @@
package kr.co.palnet.kac.socket.core.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UtmDto {
@JsonProperty("GPSime")
private String GPSime; // 시간 yyyyMMddHHmmss
@JsonProperty("droneInfo")
private List<DroneInfo> droneInfo;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DroneInfo {
@JsonProperty("Id")
private String id;
@JsonProperty("Latitude")
private String latitude;
@JsonProperty("Longitude")
private String longitude;
@JsonProperty("Height")
private String height;
}
}

37
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/ChannelInitializer.java

@ -0,0 +1,37 @@
package kr.co.palnet.kac.socket.core.socket;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import kr.co.palnet.kac.socket.core.codec.DroneDecoder;
import kr.co.palnet.kac.socket.core.codec.DroneEncoder;
import kr.co.palnet.kac.socket.core.handler.DroneHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class ChannelInitializer extends io.netty.channel.ChannelInitializer<SocketChannel> {
private final DroneHandler dronSocketHandler;
// 클라이언트 소켓 채널이 생성될 때 호출
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 뒤이어 처리할 디코더 및 핸들러 추가
pipeline.addLast("json-decoder", new JsonObjectDecoder());
pipeline.addLast("string-decoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("string-encoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("drone-decoder", new DroneDecoder());
pipeline.addLast("drone-encoder", new DroneEncoder());
pipeline.addLast("drone-handler", dronSocketHandler);
}
}

43
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/socket/SocketServer.java

@ -0,0 +1,43 @@
package kr.co.palnet.kac.socket.core.socket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Slf4j
@RequiredArgsConstructor
@Component
public class SocketServer {
private final ServerBootstrap serverBootstrap;
private final InetSocketAddress tcpPort;
private Channel serverChannel;
public void start() {
try {
// ChannelFuture: I/O operation의 결과나 상태를 제공하는 객체
// 지정한 host, port로 소켓을 바인딩하고 incoming connections을 받도록 준비함
ChannelFuture serverChannelFuture = serverBootstrap.bind(tcpPort).sync();
// 서버 소켓이 닫힐 때까지 기다림
serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
// Bean을 제거하기 전에 해야할 작업이 있을 때 설정
@PreDestroy
public void stop() {
if (serverChannel != null) {
serverChannel.close();
serverChannel.parent().closeFuture();
}
}
}

58
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/AuthKeyStorage.java

@ -0,0 +1,58 @@
package kr.co.palnet.kac.socket.core.storage;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class AuthKeyStorage {
private Set<String> keys = new HashSet<>();
private AuthKeyStorage() {
log.debug("AuthKeyUtil : init keys");
initKey();
}
public static AuthKeyStorage getInstance() {
return LazyHolder.INSTANCE;
}
public static class LazyHolder {
private static final AuthKeyStorage INSTANCE = new AuthKeyStorage();
}
private void initKey() {
keys.add("1cc2e08e-0c5c-43b2-8d4d-cddd3de558e3");// 지텔인증키
keys.add("35ea4080-a3f2-4e34-8361-78db06bac6fc");// PAL 인증키
keys.add("4d244c0a-6cf5-4d57-ae48-331a71010c3d");// ADS-B
}
public boolean checkAuthKey(String authKey) {
// keys에서 authKey와 동일한 값이 잇으면 true반환
return keys.contains(authKey);
}
public boolean addKey(String authKey) {
try {
return keys.add(authKey);
} catch (Exception e) {
log.debug("fail add key.");
return false;
}
}
public boolean removeKey(String authKey) {
try {
if (keys.contains(authKey)) {
return keys.remove(authKey);
}
} catch (Exception e) {
log.debug("fail remove key.");
return false;
}
return false;
}
}

143
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/core/storage/DroneStorage.java

@ -0,0 +1,143 @@
package kr.co.palnet.kac.socket.core.storage;
import kr.co.palnet.kac.common.model.common.DroneDto;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class DroneStorage {
private final Map<String, List<DroneDto>> droneMap;
private final int REMOVE_TIME = 1000 * 10;
private DroneStorage() {
droneMap = new ConcurrentHashMap<>();
}
public static DroneStorage getInstance() {
return LazyHolder.INSTANCE;
}
public static class LazyHolder {
private static final DroneStorage INSTANCE = new DroneStorage();
}
public Map<String, List<DroneDto>> getAll() {
if (droneMap.keySet().isEmpty()) {
return null;
}
return droneMap;
}
public List<DroneDto> get(String objectId) {
if (objectId == null || objectId.isEmpty()) {
return null;
}
if (droneMap.get(objectId) == null) {
return null;
}
return droneMap.get(objectId);
}
public DroneDto getLast(String objectId) {
List<DroneDto> droneDtoList = droneMap.get(objectId);
if (droneDtoList == null || droneDtoList.isEmpty()) return null;
return droneDtoList.getLast();
// return droneDtoList.get(droneDtoList.size() - 1);
}
public void add(DroneDto drone) {
if (drone == null || drone.getObjectId() == null || drone.getObjectId().isEmpty()) {
return;
}
List<DroneDto> list = this.droneMap.get(drone.getObjectId());
if (list == null) {
list = new ArrayList<>();
}
list.add(drone);
this.droneMap.put(drone.getObjectId(), list);
// list.offer(drone);
if ("PA-DRON-004-000".equals(drone.getObjectId()))
log.info("add :: {}::{}::{}", drone.getObjectId(), droneMap.keySet().size(), list.size());
}
// 1분 이상된 데이터 삭제 또는 처리가 끝난 데이터 삭제
public void removeByCondition() {
for (String objectId : droneMap.keySet()) {
List<DroneDto> list = droneMap.get(objectId);
if (list == null) {
continue;
} else if (list.isEmpty()) {
droneMap.remove(objectId);
continue;
}
// 1분 전 시간
Instant compareTime = Instant.now().minusSeconds(60);
// 마지막 데이터가 1분 이상된 데이터라면 삭제
// DroneDto lastDroneDto = list.stream().toList().get(list.size() - 1);
DroneDto lastDroneDto = list.stream().toList().getLast();
if ((lastDroneDto.isSendAll()) || compareTime.isAfter(lastDroneDto.getRegDt())) {
droneMap.remove(objectId);
continue;
}
// 그외 데이터에서 조건에 맞는 데이터들만 삭제
list.removeIf(drone -> (drone.isSendAll()) || compareTime.isAfter(drone.getRegDt()));
}
}
public List<DroneDto> getAllByUtm() {
// 보내지 않은 모든 데이터 추출
List<DroneDto> resultList = new ArrayList<>();
droneMap.values().forEach(droneDtoList -> {
List<DroneDto> list = droneDtoList.stream().filter(droneDto -> !droneDto.isSendUtm()).toList();
resultList.addAll(list);
});
return resultList;
}
public Map<String, List<DroneDto>> getAllByHistory() {
// 보내지 않은 모든 데이터 추출
Map<String, List<DroneDto>> sendList = new HashMap<>();
for (String objectId : droneMap.keySet()) {
List<DroneDto> droneDtoList = droneMap.get(objectId);
if (droneDtoList != null) {
List<DroneDto> list = droneDtoList.stream().filter(droneDto -> !droneDto.isSendHistory()).toList();
if (!list.isEmpty()) {
sendList.put(objectId, list);
}
}
}
return sendList;
}
public Map<String, List<DroneDto>> getAllByWebSocket() {
// 보내지 않은 모든 데이터 추출
Map<String, List<DroneDto>> sendList = new HashMap<>();
for (String objectId : droneMap.keySet()) {
List<DroneDto> droneDtoList = droneMap.get(objectId);
if (droneDtoList != null) {
List<DroneDto> list = droneDtoList.stream().filter(droneDto -> !droneDto.isSendWebSocket()).toList();
if (!list.isEmpty()) {
sendList.put(objectId, list);
}
}
}
return sendList;
}
}

52
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/ExternalService.java

@ -0,0 +1,52 @@
package kr.co.palnet.kac.socket.service;
import kr.co.palnet.kac.socket.core.model.UtmDto;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Slf4j
@RequiredArgsConstructor
@Service
public class ExternalService {
public boolean sendDataToUtm(UtmDto utmDto) {
// 임시 하드코딩
final String UTM_HOST = "http://192.168.0.133:9000";
final String UTM_REALTIME_URI = "/api/v1/utm";
RestClient client = RestClient.builder()
.baseUrl(UTM_HOST)
.messageConverters(converts -> {
converts.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converts.add(new MappingJackson2HttpMessageConverter(ObjectMapperUtil.getObjectMapper()));
})
.build();
try {
ResponseEntity<Void> res = client.post()
.uri(UTM_REALTIME_URI)
.contentType(MediaType.APPLICATION_JSON)
.body(utmDto)
.retrieve()
.toEntity(Void.class);
if (res.getStatusCode() == HttpStatus.OK) {
return true;
}
} catch (Exception e) {
log.warn("fail send to utm. {}", e.getMessage());
}
return false;
}
}

81
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/KacAppService.java

@ -0,0 +1,81 @@
package kr.co.palnet.kac.socket.service;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.common.model.common.SimpleControlDto;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@Service
public class KacAppService {
@Value("${app.kac-app.host}")
private String kacAppHost;
private final String GET_CONTROL_ID = "/v1/inner/socket/control";
private final String SEND_HISTORY = "/v1/inner/socket/receiver/drone";
private final String SEND_HISTORY_ALL = "/v1/inner/socket/receiver/drone/all";
// control id 가져오기
public SimpleControlDto getControlId(String objectId) {
RestClient client = getRestClient();
ResponseEntity<SimpleControlDto> resp = client.get()
.uri(GET_CONTROL_ID + "/" + objectId)
.retrieve()
.toEntity(SimpleControlDto.class);
return resp.getBody();
}
// drone 데이터 전송
public void sendData(DroneDto dto) {
RestClient client = getRestClient();
client.post()
.uri(SEND_HISTORY)
.contentType(MediaType.APPLICATION_JSON)
.body(dto)
.retrieve()
.toBodilessEntity();
}
// 비동기
@Async("restClientThreadPoolTaskExecutor")
public void asyncSendData(DroneDto dto) {
sendData(dto);
}
public boolean sendDataAll(Map<String, List<DroneDto>> history) {
RestClient client = getRestClient();
ResponseEntity<Void> resp = client.post()
.uri(SEND_HISTORY_ALL)
.contentType(MediaType.APPLICATION_JSON)
.body(history)
.retrieve()
.toBodilessEntity();
return resp.getStatusCode() == HttpStatus.OK;
}
private RestClient getRestClient() {
return RestClient.builder()
.baseUrl(kacAppHost)
.messageConverters(converts -> {
converts.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converts.add(new MappingJackson2HttpMessageConverter(ObjectMapperUtil.getObjectMapper()));
})
.build();
}
}

113
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/ScheduledService.java

@ -0,0 +1,113 @@
package kr.co.palnet.kac.socket.service;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.socket.core.model.UtmDto;
import kr.co.palnet.kac.socket.core.storage.DroneStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Service
public class ScheduledService {
private final ExternalService externalService;
private final KacAppService kacAppService;
private final WebSocketService webSocketService;
@Scheduled(fixedDelay = 2 * 1000)
public void sendDataByWebSocket() {
DroneStorage droneStorage = DroneStorage.getInstance();
Map<String, List<DroneDto>> droneMap = droneStorage.getAllByWebSocket();
if (droneMap == null || droneMap.isEmpty()) {
log.debug("drone data for websocket is empty.");
return;
}
if (webSocketService.sendDataAll(droneMap)) {
droneMap.values().forEach(droneDtoList -> droneDtoList.forEach(droneDto -> droneDto.setSendWebSocket(true)));
}
}
@Scheduled(fixedDelay = 2 * 1000)
public void sendDataByHistory() {
DroneStorage droneStorage = DroneStorage.getInstance();
Map<String, List<DroneDto>> history = droneStorage.getAllByHistory();
// list 합
if (history == null || history.isEmpty()) {
log.debug("drone data for history is empty.");
return;
}
if (kacAppService.sendDataAll(history)) {
history.values().forEach(droneDtoList -> droneDtoList.forEach(droneDto -> droneDto.setSendHistory(true)));
}
}
// utm에 전송
@Scheduled(fixedDelay = 2 * 1000)
public void sendDataToUtm() {
DroneStorage droneStorage = DroneStorage.getInstance();
List<DroneDto> list = droneStorage.getAllByUtm();
try {
List<UtmDto.DroneInfo> droneInfoList = list.stream().map(
model -> UtmDto.DroneInfo.builder()
.id(model.getObjectId())
.latitude(model.getLat().toString())
.longitude(model.getLon().toString())
.height(model.getElev().toString())
.build()
).collect(Collectors.toList());
// 가공
if (droneInfoList.isEmpty()) return;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
.withZone(ZoneId.of("Asia/Seoul"));
String dateStr = formatter.format(Instant.now());
UtmDto utmDto = UtmDto.builder()
.GPSime(dateStr)
.droneInfo(droneInfoList)
.build();
// if (externalService.sendDataToUtm(utmDto)) {
// list.forEach(drone -> drone.setSendUtm(true));
// }
// TODO 현재 통신이 안되므로 모두 보낸다는 가정으로 진행
list.forEach(drone -> drone.setSendUtm(true));
} catch (Exception e) {
log.error("ERROR : {}", e.getMessage(), e);
if (list != null) {
log.warn("list : {}", list.size());
} else {
log.warn("list is null");
}
}
}
// 사용을 다한 데이터 제거
@Scheduled(fixedDelay = 30 * 1000)
public void removeDrone() {
DroneStorage droneStorage = DroneStorage.getInstance();
droneStorage.removeByCondition();
}
}

67
app/kac-socket-app/src/main/java/kr/co/palnet/kac/socket/service/WebSocketService.java

@ -0,0 +1,67 @@
package kr.co.palnet.kac.socket.service;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@Service
public class WebSocketService {
@Value("${app.web-socket-web.host}")
private String webSocketWebHost;
private final String SEND_DRONE = "/v1/api/ws/drone";
private final String SEND_DRONE_ALL = "/v1/api/ws/drone/all";
public void sendData(DroneDto dto) {
RestClient client = getRestClient();
client.post()
.uri(SEND_DRONE)
.contentType(MediaType.APPLICATION_JSON)
.body(dto)
.retrieve()
.toBodilessEntity();
}
@Async("restClientThreadPoolTaskExecutor")
public void asyncSendData(DroneDto dto) {
sendData(dto);
}
public boolean sendDataAll(Map<String, List<DroneDto>> droneDtoList) {
RestClient client = getRestClient();
ResponseEntity<Void> resp = client.post()
.uri(SEND_DRONE_ALL)
.contentType(MediaType.APPLICATION_JSON)
.body(droneDtoList)
.retrieve()
.toBodilessEntity();
return resp.getStatusCode() == HttpStatus.OK;
}
private RestClient getRestClient() {
return RestClient.builder()
.baseUrl(webSocketWebHost)
.messageConverters(converts -> {
converts.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converts.add(new MappingJackson2HttpMessageConverter(ObjectMapperUtil.getObjectMapper()));
})
.build();
}
}

68
app/kac-socket-app/src/main/resources/application.yml

@ -0,0 +1,68 @@
spring:
threads:
virtual:
enabled: true
netty:
socket:
tcp-port: 8003
boss-count: 1
keep-alive: false
tcp-nodelay: false
backlog: 3000
server:
port: 8004
app:
kac-app:
host: http://127.0.0.1:8000
web-socket:
host: http://127.0.0.1:8001
web-socket-web:
host: http://127.0.0.1:8002
logging:
level:
kr.co.palnet: debug
---
spring:
config:
activate:
on-profile: local
app:
kac-app:
host: http://127.0.0.1:8000
web-socket:
host: http://127.0.0.1:8001
web-socket-web:
host: http://127.0.0.1:8002
logging:
level:
kr.co.palnet: debug
---
spring:
config:
activate:
on-profile: docker
app:
kac-app:
host: http://kac-app:8000
web-socket:
host: http://kac-websocket-app:8001
web-socket-web:
host: http://kac-websocket-app:8002
logging:
level:
kr.co.palnet: info

13
app/kac-websocket-app/Dockerfile

@ -0,0 +1,13 @@
FROM openjdk:21
ENV TZ=Asia/Seoul
#ENV JAVA_OPTS="-Xms512M -Xmx512M"
ENV JAVA_OPTS=""
EXPOSE 8001
EXPOSE 8002
WORKDIR /app
#ENTRYPOINT ["java","-jar","/app/kac-websocket-app-1.0.0.jar"]
CMD java $JAVA_OPTS -jar /app/kac-websocket-app-1.0.0.jar

8
app/kac-websocket-app/build.gradle

@ -0,0 +1,8 @@
dependencies {
implementation "$boot:spring-boot-starter-web"
implementation "io.netty:netty-all:4.1.68.Final"
implementation project(":common:util")
implementation project(":common:model")
}

14
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/KacWebSocketApplication.java

@ -0,0 +1,14 @@
package kr.co.palnet.kac.websocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class KacWebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(KacWebSocketApplication.class, args);
}
}

53
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/controller/SocketReceiverController.java

@ -0,0 +1,53 @@
package kr.co.palnet.kac.websocket.controller;
import kr.co.palnet.kac.common.model.common.DroneControlDto;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.websocket.core.storage.ControlStorage;
import kr.co.palnet.kac.websocket.service.ControlService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/api/ws")
public class SocketReceiverController {
private final ControlService controlService;
@PostMapping("/drone")
public ResponseEntity<Void> receiver(@RequestBody DroneDto droneDto) {
DroneControlDto history = controlService.dronDtoToControlDtoConvert(droneDto);
// DRON의 대한 식별정보만 이력 관리
ControlStorage controlCache = ControlStorage.getInstance();
controlCache.put(history);
return ResponseEntity.ok().build();
}
@PostMapping("/drone/all")
public ResponseEntity<Void> receiver(@RequestBody Map<String, List<DroneDto>> droneDtoAll) {
// DRON의 대한 식별정보만 이력 관리
ControlStorage controlCache = ControlStorage.getInstance();
droneDtoAll.values().forEach(droneDtoList -> {
droneDtoList.forEach(droneDto -> {
DroneControlDto history = controlService.dronDtoToControlDtoConvert(droneDto);
controlCache.put(history);
});
});
return ResponseEntity.ok().build();
}
}

49
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/codec/ControlDecoder.java

@ -0,0 +1,49 @@
package kr.co.palnet.kac.websocket.core.codec;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import kr.co.palnet.kac.websocket.core.model.BoundaryCoordinates;
import kr.co.palnet.kac.websocket.core.model.ControlRq;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class ControlDecoder extends WebSocketExtensionDecoder {
private final ObjectMapper objectMapper = ObjectMapperUtil.getObjectMapper();
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
try {
if (msg instanceof TextWebSocketFrame textWebSocketFrame) {
String json = textWebSocketFrame.text();
JsonNode jsonNode = objectMapper.readTree(json);
String type = jsonNode.get("type").asText();
switch (type) {
case "CURRENT_DRONE":
ControlRq<List<BoundaryCoordinates>> controlRq = objectMapper.readValue(json, new TypeReference<>() {
});
out.add(controlRq);
break;
default:
log.warn("decode type is empty.");
break;
}
}
} catch (Exception e) {
log.warn("decode parsing error : {}\ndata in : {}", e.getMessage(), msg);
}
}
}

35
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/AppReadyEvent.java

@ -0,0 +1,35 @@
package kr.co.palnet.kac.websocket.core.config;
import kr.co.palnet.kac.websocket.core.socket.WebSocketServer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
@Slf4j
@RequiredArgsConstructor
@Component
public class AppReadyEvent implements ApplicationListener<ApplicationReadyEvent> {
private final WebSocketServer socketServer;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
float mb = 1024f * 1024f;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
log.info("current heap memory init(xms): {}mb, max(xmx): {}mb", memoryBean.getHeapMemoryUsage().getInit() / mb, memoryBean.getHeapMemoryUsage().getMax() / mb);
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
log.info("Pool: {} (type {}) = {}", mp.getName(), mp.getType(), mp.getUsage().getMax() / mb);
}
} catch (Exception e) {
log.warn("when start app, not read jvm heap memory information.");
}
socketServer.start();
}
}

30
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/JsonConfig.java

@ -0,0 +1,30 @@
package kr.co.palnet.kac.websocket.core.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
public class JsonConfig {
@Bean
public ObjectMapper objectMapper() {
return ObjectMapperUtil.getObjectMapper();
}
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return ObjectMapperUtil.getObjectMapperBuilder();
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(objectMapper());
}
}

76
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/config/NettyConfig.java

@ -0,0 +1,76 @@
package kr.co.palnet.kac.websocket.core.config;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import kr.co.palnet.kac.websocket.core.socket.ChannelInitializer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.InetSocketAddress;
@Slf4j
@RequiredArgsConstructor
@Configuration
public class NettyConfig {
@Value("${netty.socket.tcp-port}")
private int port;
@Value("${netty.socket.boss-count}")
private int bossCount;
@Value("${netty.socket.keep-alive}")
private boolean keepAlive;
@Value("${netty.socket.tcp-nodelay}")
private boolean tcpNodelay;
@Value("${netty.socket.backlog}")
private int backlog;
@Bean
public ServerBootstrap serverBootstrap(ChannelInitializer channelInitializer) {
// ServerBootstrap: 서버 설정을 도와주는 class
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup(), workerGroup());
// NioServerSocketChannel: incoming connections를 수락하기 위해 새로운 Channel을 객체화할 때 사용
b.channel(NioServerSocketChannel.class);
b.handler(new LoggingHandler(LogLevel.DEBUG));
// ChannelInitializer: 새로운 Channel을 구성할 때 사용되는 특별한 handler. 주로 ChannelPipeline으로 구성
b.childHandler(channelInitializer);
// ServerBootstarp에 다양한 Option 추가 가능
// SO_BACKLOG: 동시에 수용 가능한 최대 incoming connections 개수
// 이 외에도 SO_KEEPALIVE, TCP_NODELAY 등 옵션 제공
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
b.option(ChannelOption.SO_BACKLOG, backlog);
b.childOption(ChannelOption.TCP_NODELAY, tcpNodelay); // 반응속도를 높이기 위해 Nagle 알고리즘을 비활성화 합니다
b.childOption(ChannelOption.SO_LINGER, 0); // 소켓이 close될 때 신뢰성있는 종료를 위해 4way-handshake가 발생하고 이때 TIME_WAIT로 리소스가 낭비됩니다. 이를 방지하기 위해 0으로 설정합니다.
b.childOption(ChannelOption.SO_KEEPALIVE, keepAlive); // Keep-alive를 켭니다.
b.childOption(ChannelOption.SO_REUSEADDR, true); // SO_LINGER설정이 있으면 안해도 되나 혹시나병(!)으로 TIME_WAIT걸린 포트를 재사용할 수 있도록 설정합니다.
return b;
}
// boss: incoming connection을 수락하고, 수락한 connection을 worker에게 등록(register)
@Bean(destroyMethod = "shutdownGracefully")
public NioEventLoopGroup bossGroup() {
return new NioEventLoopGroup(bossCount);
}
// worker: boss가 수락한 연결의 트래픽 관리
@Bean(destroyMethod = "shutdownGracefully")
public NioEventLoopGroup workerGroup() {
return new NioEventLoopGroup();
}
// IP 소켓 주소(IP 주소, Port 번호)를 구현
// 도메인 이름으로 객체 생성 가능
@Bean
public InetSocketAddress inetSocketAddress() {
return new InetSocketAddress(port);
}
}

70
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/handler/WebSocketHandler.java

@ -0,0 +1,70 @@
package kr.co.palnet.kac.websocket.core.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import kr.co.palnet.kac.websocket.core.model.BoundaryCoordinates;
import kr.co.palnet.kac.websocket.core.model.ControlRq;
import kr.co.palnet.kac.websocket.core.model.ErrorRs;
import kr.co.palnet.kac.websocket.core.storage.ChannelStorage;
import kr.co.palnet.kac.websocket.service.ControlService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@ChannelHandler.Sharable
@Component
public class WebSocketHandler extends SimpleChannelInboundHandler<ControlRq<Object>> {
private final ControlService controlService;
private final ObjectMapper objectMapper = ObjectMapperUtil.getObjectMapper();
@Override
protected void channelRead0(ChannelHandlerContext ctx, ControlRq rq) throws Exception {
log.info("read :: {}", rq);
Object result = null;
switch (rq.getType()) {
case "CURRENT_DRONE":
if (rq.getBody() == null) {
// BoundaryCoordinates
log.warn("CURRENT_DRONE::boundary coordinates is null");
return;
}
log.info(">>class {}", rq.getBody().getClass().getName());
List<BoundaryCoordinates> list = (List<BoundaryCoordinates>) rq.getBody();
result = controlService.getList(list);
break;
default:
log.warn("type is not supported.");
result = ErrorRs.builder()
.code("-400")
.message("bad request : type error")
.build();
break;
}
String json = objectMapper.writeValueAsString(result);
ctx.channel().writeAndFlush(new TextWebSocketFrame(json));
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ChannelStorage channelStorage = ChannelStorage.getInstance();
channelStorage.add(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
}
}

15
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/BoundaryCoordinates.java

@ -0,0 +1,15 @@
package kr.co.palnet.kac.websocket.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoundaryCoordinates {
private double lat;
private double lon;
}

19
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ControlRq.java

@ -0,0 +1,19 @@
package kr.co.palnet.kac.websocket.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ControlRq<T> {
private String type;
private T body;
}

15
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/model/ErrorRs.java

@ -0,0 +1,15 @@
package kr.co.palnet.kac.websocket.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ErrorRs {
private String code;
private String message;
}

36
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/ChannelInitializer.java

@ -0,0 +1,36 @@
package kr.co.palnet.kac.websocket.core.socket;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.timeout.IdleStateHandler;
import kr.co.palnet.kac.websocket.core.codec.ControlDecoder;
import kr.co.palnet.kac.websocket.core.handler.WebSocketHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class ChannelInitializer extends io.netty.channel.ChannelInitializer<SocketChannel> {
private final WebSocketHandler webSocketHandler;
// 클라이언트 소켓 채널이 생성될 때 호출
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true));
pipeline.addLast(new IdleStateHandler(0, 0, 180));
pipeline.addLast(new ControlDecoder());
pipeline.addLast(webSocketHandler);
}
}

42
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/socket/WebSocketServer.java

@ -0,0 +1,42 @@
package kr.co.palnet.kac.websocket.core.socket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Slf4j
@RequiredArgsConstructor
@Component
public class WebSocketServer {
private final ServerBootstrap serverBootstrap;
private final InetSocketAddress tcpPort;
private Channel serverChannel;
public void start() {
try {
// ChannelFuture: I/O operation의 결과나 상태를 제공하는 객체
// 지정한 host, port로 소켓을 바인딩하고 incoming connections을 받도록 준비함
ChannelFuture serverChannelFuture = serverBootstrap.bind(tcpPort).sync();
// 서버 소켓이 닫힐 때까지 기다림
serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
} catch (InterruptedException e) {
log.error("ERROR: {}", e.getMessage(), e);
}
}
// Bean을 제거하기 전에 해야할 작업이 있을 때 설정
@PreDestroy
public void stop() {
if (serverChannel != null) {
serverChannel.close();
serverChannel.parent().closeFuture();
}
}
}

41
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ChannelStorage.java

@ -0,0 +1,41 @@
package kr.co.palnet.kac.websocket.core.storage;
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.stereotype.Component;
@Component
public class ChannelStorage {
private final ChannelGroup channelGroup;
private ChannelStorage() {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
public static ChannelStorage getInstance() {
return ChannelStorage.LazyHolder.INSTANCE;
}
public static class LazyHolder {
private static final ChannelStorage INSTANCE = new ChannelStorage();
}
public void add(Channel channel) {
channelGroup.add(channel);
}
public Channel get(ChannelId channelId) {
return channelGroup.find(channelId);
}
public ChannelGroup getGroup() {
return channelGroup;
}
}

82
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/core/storage/ControlStorage.java

@ -0,0 +1,82 @@
package kr.co.palnet.kac.websocket.core.storage;
import kr.co.palnet.kac.common.model.common.DroneControlDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class ControlStorage {
private final int REMOVE_TIME_SECOND = 60; // 화면 노출 시간
private final Map<String, DroneControlDto> controlMap;
private ControlStorage() {
controlMap = new ConcurrentHashMap<>();
}
public static ControlStorage getInstance() {
return LazyHolder.INSTANCE;
}
public static class LazyHolder {
private static final ControlStorage INSTANCE = new ControlStorage();
}
public Map<String, DroneControlDto> getAll() {
if (controlMap.keySet().isEmpty()) {
return null;
}
return controlMap;
}
public DroneControlDto get(String objectId) {
if (objectId == null || objectId.isEmpty()) {
return null;
}
if (controlMap.get(objectId) == null) {
return null;
}
return controlMap.get(objectId);
}
public DroneControlDto put(DroneControlDto control) {
if (control == null || control.getObjectId() == null || control.getObjectId().isEmpty()) {
return null;
}
return controlMap.put(control.getObjectId(), control);
}
public List<DroneControlDto> getList() {
return new ArrayList<>(controlMap.values());
}
public DroneControlDto remove(String objectId) {
return controlMap.remove(objectId);
}
// 10초 마다 오랜된 데이터(1분 이상 새로운 데이터가 없는 경우) 제거
@Scheduled(fixedDelay = 1000 * 10)
private void remove() {
// Key 의 존재하는 데이터는 마지막 서버수신 History Data
if (Objects.nonNull(controlMap)) {
controlMap.forEach((objectId, controlDTO) -> {
Instant serverRcvDt = controlDTO.getServerRcvDt();
// Instant serverRcvDt와 now의 차이
long diffSecond = Instant.now().getEpochSecond() - serverRcvDt.getEpochSecond();
if (diffSecond > REMOVE_TIME_SECOND) {
this.remove(objectId);
}
});
}
}
}

96
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ControlService.java

@ -0,0 +1,96 @@
package kr.co.palnet.kac.websocket.service;
import kr.co.palnet.kac.common.model.common.DroneControlDto;
import kr.co.palnet.kac.common.model.common.DroneDto;
import kr.co.palnet.kac.util.CoordUtil;
import kr.co.palnet.kac.websocket.core.model.BoundaryCoordinates;
import kr.co.palnet.kac.websocket.core.storage.ControlStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Service
public class ControlService {
public List<DroneControlDto> getList(List<BoundaryCoordinates> coordinatesList) {
List<CoordUtil.Coordinates> polygon = coordinatesList.stream().map(bc -> CoordUtil.Coordinates.builder().x(bc.getLon()).y(bc.getLat()).build()).toList();
ControlStorage controlStorage = ControlStorage.getInstance();
List<DroneControlDto> controlDtoList = controlStorage.getList();
if (controlDtoList == null || controlDtoList.isEmpty()) return new ArrayList<>();
List<DroneControlDto> result = controlDtoList.stream().filter(controlDto ->
// CoordUtil.isInsidePolygon(polygon, CoordUtil.Coordinates.builder().x(controlDto.getLon()).y(controlDto.getLat()).build())
CoordUtil.isInsideRectangle(polygon, CoordUtil.Coordinates.builder().x(controlDto.getLon()).y(controlDto.getLat()).build())
).sorted(Comparator.reverseOrder()).toList();
return result;
}
public DroneControlDto dronDtoToControlDtoConvert(DroneDto dronDto) {
ControlStorage controlCache = ControlStorage.getInstance();
DroneControlDto prevControlDto = controlCache.get(dronDto.getObjectId());
DroneControlDto controlDto = new DroneControlDto();
controlDto.setObjectId(dronDto.getObjectId());
controlDto.setControlId(dronDto.getControlId());
controlDto.setControlStartDt(dronDto.getControlStartDt());
controlDto.setObjectTypeCd(dronDto.getObjectType());
controlDto.setLat(dronDto.getLat());
controlDto.setLon(dronDto.getLon());
controlDto.setElevType(dronDto.getElevType());
controlDto.setElev(dronDto.getElev());
controlDto.setSpeedType(dronDto.getSpeedType());
controlDto.setSpeed(dronDto.getSpeed());
controlDto.setBetteryLevel(dronDto.getBetteryLevel());
controlDto.setBetteryVoltage(dronDto.getBetteryVoltage());
controlDto.setDroneStatus(dronDto.getDroneStatus());
controlDto.setHeading(dronDto.getHeading());
controlDto.setMoveDistance(dronDto.getMoveDistance());
controlDto.setMoveDistanceType(dronDto.getMoveDistanceType());
controlDto.setServerRcvDt(dronDto.getServerRcvDt());
// 환경 데이터 필드 추가
controlDto.setSensorCo(dronDto.getSensorCo());
controlDto.setSensorSo2(dronDto.getSensorSo2());
controlDto.setSensorNo2(dronDto.getSensorNo2());
controlDto.setSensorO3(dronDto.getSensorO3());
controlDto.setSensorDust(dronDto.getSensorDust());
// 비정상 상황 식별코드 추가
controlDto.setControlWarnCd(dronDto.isControlWarnCd());
if (prevControlDto == null) {
if (controlDto.isControlWarnCd()) {
controlDto.setControlWarnNotyCd(true); // 최초 비정상 발생
}
} else {
if (prevControlDto.isControlWarnCd() && controlDto.isControlWarnCd()) {
controlDto.setControlWarnNotyCd(false); // 비정상 -> 비정상
}
if (prevControlDto.isControlWarnCd() && !controlDto.isControlWarnCd()) {
controlDto.setControlWarnNotyCd(false); // 비정상 -> 정상
}
if (!prevControlDto.isControlWarnCd() && controlDto.isControlWarnCd()) {
controlDto.setControlWarnNotyCd(true); // 정상 -> 비정상상
}
}
controlDto.setControlCacheCount(1);
controlDto.setRegDt(dronDto.getRegDt());
return controlDto;
}
}

54
app/kac-websocket-app/src/main/java/kr/co/palnet/kac/websocket/service/ScheduledService.java

@ -0,0 +1,54 @@
package kr.co.palnet.kac.websocket.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import kr.co.palnet.kac.common.model.common.DroneControlDto;
import kr.co.palnet.kac.util.ObjectMapperUtil;
import kr.co.palnet.kac.websocket.core.storage.ChannelStorage;
import kr.co.palnet.kac.websocket.core.storage.ControlStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Service
public class ScheduledService {
private final ObjectMapper objectMapper = ObjectMapperUtil.getObjectMapper();
// 접속한 모든 채널에 데이터 전송
// @Scheduled(fixedDelay = 10 * 1000) // 10초
public void sendAllChannel() {
// 채널 가져오기
ChannelStorage channelStorage = ChannelStorage.getInstance();
ChannelGroup channelGroup = channelStorage.getGroup();
// 데이터 가져오기
ControlStorage controlStorage = ControlStorage.getInstance();
List<DroneControlDto> controlDtoList = controlStorage.getList();
if (controlDtoList == null || controlDtoList.isEmpty()) return;
controlDtoList.sort(Comparator.reverseOrder());
try {
String json = objectMapper.writeValueAsString(controlDtoList);
channelGroup.forEach(channel -> {
channel.writeAndFlush(new TextWebSocketFrame(json));
});
} catch (JsonProcessingException e) {
log.warn("send fail to all channel. : json parsing error : {}", e.getMessage(), e);
} catch (Exception e) {
log.warn("send fail to all channel. : {}", e.getMessage(), e);
}
log.debug("done send to all channel");
}
}

52
app/kac-websocket-app/src/main/resources/application.yml

@ -0,0 +1,52 @@
spring:
threads:
virtual:
enabled: true
netty:
socket:
tcp-port: 8001
boss-count: 1
keep-alive: true
tcp-nodelay: true
backlog: 3000
---
spring:
config:
activate:
on-profile: local
app:
kac-app:
host: http://kac-app:8000
server:
port: 8002
logging:
level:
kr.co.palnet: debug
---
spring:
config:
activate:
on-profile: docker
app:
kac-app:
host: http://kac-app:8000
server:
port: 8002
logging:
level:
kr.co.palnet: info

44
build.gradle

@ -1,29 +1,36 @@
buildscript { buildscript {
ext { ext {
spring = '3.2.1' spring = 'org.springframework'
boot = 'org.springframework.boot' boot = 'org.springframework.boot'
bootVersion = '3.2.2'
lombok = 'org.projectlombok:lombok' lombok = 'org.projectlombok:lombok'
} }
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("$boot:spring-boot-gradle-plugin:$spring") classpath("$boot:spring-boot-gradle-plugin:$bootVersion")
} }
} }
allprojects { allprojects {
group = "kr.co.palnet" group = "kr.co.palnet"
version = "1.0.0" version = "1.0.0"
} }
subprojects {
apply plugin: "java" ["app", "data", "web", "common"].each {
apply plugin: boot def rootDirName = it;
apply plugin: "io.spring.dependency-management" def subProjectDir = new File(projectDir, it)
apply plugin: "idea" subProjectDir.eachDir { dir ->
def projectName = ":${it}:${dir.name}"
project(projectName) {
plugins.apply("java")
plugins.apply("java-library")
plugins.apply(boot)
plugins.apply("io.spring.dependency-management")
plugins.apply("idea")
repositories { repositories {
mavenCentral() mavenCentral()
@ -34,6 +41,9 @@ subprojects {
runtimeClasspath { runtimeClasspath {
extendsFrom developmentOnly extendsFrom developmentOnly
} }
compileOnly {
extendsFrom annotationProcessor
}
} }
dependencies { dependencies {
@ -51,25 +61,15 @@ subprojects {
test { test {
useJUnitPlatform() useJUnitPlatform()
} }
}
["data", "web"].each { if (rootDirName == 'app') {
def subProjectDir = new File(projectDir, it) bootJar.enabled(true)
subProjectDir.eachDir {dir-> jar.enabled(false)
def projectName = ":${it}-${dir.name}" } else {
project(projectName){
bootJar.enabled(false) bootJar.enabled(false)
jar.enabled(true) jar.enabled(true)
} }
} }
}
["app"].each {
def subProjectDir = new File(projectDir, it)
subProjectDir.eachDir {dir->
def projectName = ":${it}-${dir.name}"
project(projectName){
}
} }
} }

12
common/config-db/build.gradle

@ -0,0 +1,12 @@
dependencies {
// jpa
implementation "$boot:spring-boot-starter-data-jpa"
// querydsl
implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
runtimeOnly "com.mysql:mysql-connector-j"
implementation "org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16"
}

69
common/config-db/src/main/java/kr/co/palnet/kac/config/db/KacJpaConfig.java

@ -0,0 +1,69 @@
package kr.co.palnet.kac.config.db;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.Map;
import java.util.Objects;
@Configuration
@EnableJpaRepositories(
basePackages = "kr.co.palnet.kac.data.*.repository",
entityManagerFactoryRef = "kacEntityManagerFactory",
transactionManagerRef = "kacTransactionManager"
)
public class KacJpaConfig {
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
public KacJpaConfig(JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
this.jpaProperties = jpaProperties;
this.hibernateProperties = hibernateProperties;
}
@Bean(name = "kacDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.pav-kac")
public DataSource kacDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "kacEntityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean kacEntityManagerFactory(EntityManagerFactoryBuilder builder) {
Map<String, Object> prop = hibernateProperties.determineHibernateProperties(
jpaProperties.getProperties(),
new HibernateSettings()
);
return builder.dataSource(kacDataSource())
.properties(prop)
.packages(
"kr.co.palnet.kac.data.**.model",
"kr.co.palnet.kac.data.**.domain"
)
.persistenceUnit("kac")
.build();
}
@Bean(name = "kacTransactionManager")
@Primary
public PlatformTransactionManager kacTransactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(Objects.requireNonNull(kacEntityManagerFactory(builder).getObject()));
}
}

20
common/config-db/src/main/java/kr/co/palnet/kac/config/db/QueryDslConfig.java

@ -0,0 +1,20 @@
package kr.co.palnet.kac.config.db;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}

36
common/config-db/src/main/resources/application-db.yml

@ -0,0 +1,36 @@
spring:
jpa:
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
datasource:
pav-kac:
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
jdbc-url: jdbc:log4jdbc:mysql://localhost:13306/PAV_KAC?characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
username: kac
password: palnet!234
minimumidle: 5
maximumpoolsize: 5
idletimeout: 60000
maxlifetime: 300000
connectiontimeout: 30000
validationtimeout: 5000
---
spring:
config:
activate:
on-profile: docker
jpa:
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
datasource:
pav-kac:
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
jdbc-url: jdbc:log4jdbc:mysql://database:3306/PAV_KAC?characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
username: kac
password: palnet!234

6
common/core/build.gradle

@ -0,0 +1,6 @@
dependencies {
implementation "$boot:spring-boot-starter-web"
}

4
common/core/src/main/java/kr/co/palnet/kac/core/Sample.java

@ -0,0 +1,4 @@
package kr.co.palnet.kac.core;
public class Sample {
}

54
common/core/src/main/java/kr/co/palnet/kac/core/exception/BaseErrorCode.java

@ -0,0 +1,54 @@
package kr.co.palnet.kac.core.exception;
import org.springframework.http.HttpStatus;
public enum BaseErrorCode {
SUCCESS("CM001", HttpStatus.OK, "성공"),
FAILED("CM900", HttpStatus.INTERNAL_SERVER_ERROR, "실패"),
UNKNOWN("CM999", HttpStatus.INTERNAL_SERVER_ERROR, "실패(사용자 정의 없는 에러)"),
NOT_PROCESS("CM002", HttpStatus.INTERNAL_SERVER_ERROR, "처리되지 않음"),
IO_ERROR("CM003", HttpStatus.INTERNAL_SERVER_ERROR, "IO 에러"),
LOGIN_FAILED("CM100", HttpStatus.INTERNAL_SERVER_ERROR, "로그인 실패"),
LOGIN_DUPLICATED("CM101", HttpStatus.INTERNAL_SERVER_ERROR, "중복 로그인"),
LOGIN_ATTEMPT_COUNT("CM102", HttpStatus.INTERNAL_SERVER_ERROR, "로그인 실패 횟수 초과"),
LOGIN_SNS_FAILED("CM110", HttpStatus.INTERNAL_SERVER_ERROR, "SNS 로그인 실패"),
LOGIN_SNS_DUPLICATED("CM111", HttpStatus.INTERNAL_SERVER_ERROR, "SNS 가입 중복"),
TOKEN_EXPIRED("CM120", HttpStatus.INTERNAL_SERVER_ERROR, "토큰 만료"),
TOKEN_INVALID("CM121", HttpStatus.INTERNAL_SERVER_ERROR, "잘못된 토큰"),
ACCOUNT_LOCK("CM130", HttpStatus.INTERNAL_SERVER_ERROR, "계정 잠김"),
ACCOUNT_DORMANT("CM131", HttpStatus.INTERNAL_SERVER_ERROR, "휴면 계정"),
CACHE_LOAD_FAILED("CM200", HttpStatus.INTERNAL_SERVER_ERROR, "캐시 로딩 실패"),
CACHE_RELOAD_FAILED("CM201", HttpStatus.INTERNAL_SERVER_ERROR, "캐시 리로딩 실패"),
AUTH_EMPTY("CM401", HttpStatus.UNAUTHORIZED, "권한 없음"),
WEB_PARAM_INVALID("WB400", HttpStatus.INTERNAL_SERVER_ERROR, "잘못된 파라미터"),
WEB_NOT_FOUND("WB404", HttpStatus.INTERNAL_SERVER_ERROR, "찾을수 없는 페이지"),
WEB_API_ERROR("WB500", HttpStatus.INTERNAL_SERVER_ERROR, "외부 연동 에러"),
DATA_ALREADY_EXISTS("DT001", HttpStatus.INTERNAL_SERVER_ERROR, "이미 등록된 데이터"),
DATA_EMPTY("DT002", HttpStatus.INTERNAL_SERVER_ERROR, "데이터 없음");
private final String code;
private final HttpStatus status;
private final String message;
private BaseErrorCode(String code, HttpStatus status, String message) {
this.code = code;
this.status = status;
this.message = message;
}
public String code() {
return this.code;
}
public String message() {
return this.message;
}
public HttpStatus status() {
return this.status;
}
}

132
common/core/src/main/java/kr/co/palnet/kac/core/exception/BaseException.java

@ -0,0 +1,132 @@
package kr.co.palnet.kac.core.exception;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import java.util.Locale;
@Getter
public class BaseException extends RuntimeException {
private final BaseErrorCode errorCode;
private final String logMessage;
private final Level level;
private final Object[] paramArray;
public BaseException() {
this(BaseErrorCode.FAILED, null, Level.WARN, null);
}
public BaseException(Object[] paramArray) {
this(BaseErrorCode.FAILED, null, Level.WARN, paramArray);
}
public BaseException(Level level) {
this(BaseErrorCode.FAILED, null, level, null);
}
public BaseException(Level level, Object[] paramArray) {
this(BaseErrorCode.FAILED, null, level, paramArray);
}
public BaseException(String logMessage) {
this(BaseErrorCode.FAILED, logMessage, Level.WARN, null);
}
public BaseException(String logMessage, Object[] paramArray) {
this(BaseErrorCode.FAILED, logMessage, Level.WARN, paramArray);
}
public BaseException(String logMessage, Level level) {
this(BaseErrorCode.FAILED, logMessage, level, null);
}
public BaseException(String logMessage, Level level, Object[] paramArray) {
this(BaseErrorCode.FAILED, logMessage, level, paramArray);
}
public BaseException(BaseErrorCode errorCode) {
this(errorCode, null, Level.WARN, null);
}
public BaseException(BaseErrorCode errorCode, Object[] paramArray) {
this(errorCode, null, Level.WARN, paramArray);
}
public BaseException(BaseErrorCode errorCode, Level level) {
this(errorCode, null, level, null);
}
public BaseException(BaseErrorCode errorCode, Level level, Object[] paramArray) {
this(errorCode, null, level, paramArray);
}
public BaseException(BaseErrorCode errorCode, String logMessage) {
this(errorCode, logMessage, Level.WARN, null);
}
public BaseException(BaseErrorCode errorCode, String logMessage, Object[] paramArray) {
this(errorCode, logMessage, Level.WARN, paramArray);
}
public BaseException(BaseErrorCode errorCode, String logMessage, Level level) {
this(errorCode, logMessage, level, null);
}
public BaseException(BaseErrorCode errorCode, String logMessage, Level level, Object[] paramArray) {
this.errorCode = errorCode;
this.logMessage = logMessage;
this.level = level;
this.paramArray = paramArray;
}
public BaseException(String message, BaseErrorCode errorCode, String logMessage, Level level, Object[] paramArray) {
super(message);
this.errorCode = errorCode;
this.logMessage = logMessage;
this.level = level;
this.paramArray = paramArray;
}
public BaseException(String message, Throwable cause, BaseErrorCode errorCode, String logMessage, Level level, Object[] paramArray) {
super(message, cause);
this.errorCode = errorCode;
this.logMessage = logMessage;
this.level = level;
this.paramArray = paramArray;
}
public BaseException(Throwable cause, BaseErrorCode errorCode, String logMessage, Level level, Object[] paramArray) {
super(cause);
this.errorCode = errorCode;
this.logMessage = logMessage;
this.level = level;
this.paramArray = paramArray;
}
public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, BaseErrorCode errorCode, String logMessage, Level level, Object[] paramArray) {
super(message, cause, enableSuppression, writableStackTrace);
this.errorCode = errorCode;
this.logMessage = logMessage;
this.level = level;
this.paramArray = paramArray;
}
public String getCode() {
return errorCode.code();
}
public String getErrorMessage(MessageSource ms) {
if (ms == null) {
return BaseErrorCode.UNKNOWN.message();
}
return ms.getMessage(getCode(), getParamArray(), BaseErrorCode.UNKNOWN.message(), Locale.getDefault());
}
}

9
common/core/src/main/java/kr/co/palnet/kac/core/exception/Level.java

@ -0,0 +1,9 @@
package kr.co.palnet.kac.core.exception;
public enum Level {
TRACE,
DEBUG,
INFO,
WARN,
ERROR
}

28
common/core/src/main/java/kr/co/palnet/kac/core/exception/model/BaseErrorModel.java

@ -0,0 +1,28 @@
package kr.co.palnet.kac.core.exception.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BaseErrorModel {
private Instant timestamp;
private int status;
private String error;
private String path;
private String message;
private String code;
}

4
common/model/build.gradle

@ -0,0 +1,4 @@
dependencies {
implementation project(":common:util")
}

86
common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneControlDto.java

@ -0,0 +1,86 @@
package kr.co.palnet.kac.common.model.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DroneControlDto implements Comparable<DroneControlDto> {
private String messageTypeCd;
private String controlId;
private String trmnlId;
private String objectTypeCd;
private String objectId;
private Double lat;
private Double lon;
private String elevType;
private Double elev;
private String speedType;
private Double speed;
private Double betteryLevel;
private Double betteryVoltage;
private String takeOffPositon;
private String droneStatus;
private Double heading;
private String terminalRcvDt;
private Instant serverRcvDt;
private Instant controlStartDt;
private Double moveDistance;
private String moveDistanceType;
// 환경센서 필드
private Double sensorCo;
private Double sensorSo2;
private Double sensorNo2;
private Double sensorO3;
private Double sensorDust;
private List<Map<String, Double>> lastHistory;
// 비정상 상황 식별 코드 (비정상: true)
private boolean controlWarnCd;
// 비정상 상황 알림 표출 코드 (알림: true, 미알림: false)
private boolean controlWarnNotyCd;
// 비정상 상황 알림 중복 체크
private Integer controlCacheCount;
// 큐가 Socket서버에 도착한 시간 TODO : 타입 문제로 인한 임시주석
private Instant regDt;
@Override
public int compareTo(DroneControlDto o) {
if (o.getControlStartDt() != null && controlStartDt != null) {
return o.getControlStartDt().compareTo(controlStartDt);
}
return 0;
}
}

105
common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneDto.java

@ -0,0 +1,105 @@
package kr.co.palnet.kac.common.model.common;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import kr.co.palnet.kac.common.model.core.convert.InstantConvert;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DroneDto {
private String typeCd; // 01 : 최초 들어온 데이터 , 99 : 종료 시킬 데이터
private String messageType;
private String terminalId;
@Builder.Default
private Double moveDistance = 0.0;
private String moveDistanceType;
private String controlId; // 처음 위치 데이터가 들어 왔을때 생성 함
private String objectType;
private String objectId;
@Builder.Default
private Double lat = 0.0;
@Builder.Default
private Double lon = 0.0;
private String elevType;
@Builder.Default
private Double elev = 0.0;
private String speedType;
@Builder.Default
private Double speed = 0.0;
@Builder.Default
private Double betteryLevel = 0.0;
@Builder.Default
private Double betteryVoltage = 0.0;
private String droneStatus;
@Builder.Default
private Double heading = 0.0;
@JsonDeserialize(using = InstantConvert.Deserializer.class)
private Instant terminalRcvDt;
private Instant serverRcvDt;
private Instant controlStartDt;
private Instant controlEndDt;
private String areaTrnsYn;
// 환경센서 필드
@Builder.Default
private Double sensorCo = 0.0;
@Builder.Default
private Double sensorSo2 = 0.0;
@Builder.Default
private Double sensorNo2 = 0.0;
@Builder.Default
private Double sensorO3 = 0.0;
@Builder.Default
private Double sensorDust = 0.0;
//최근 5건만 저장
private List<DroneHistoryDto> recentPositionHistory;
// 전체 히스토리 저장
private List<DroneHistoryDto> postionHistory;
// 비정상 상황 식별 코드
private boolean controlWarnCd;
// 큐가 Socket서버에 도착한 시간
private Instant regDt;
// 큐가 Socket서버에 도착한 시간
private boolean isSendUtm; // 불법드론 전송 여부
private boolean isSendHistory; // 서버 전송 여부
private boolean isSendWebSocket; // WebSocket 전송 여부
public boolean isSendAll() {
return isSendHistory && isSendUtm && isSendWebSocket;
}
}

20
common/model/src/main/java/kr/co/palnet/kac/common/model/common/DroneHistoryDto.java

@ -0,0 +1,20 @@
package kr.co.palnet.kac.common.model.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DroneHistoryDto {
private String objectId;
@Builder.Default
private Double lat = 0.0;
@Builder.Default
private Double lon = 0.0;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save