김대철
Showing 173 changed files with 4797 additions and 0 deletions
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# compiled output
/dist
/node_modules
/output
# config env
.env
.envrc
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
builds/*
nohup.out
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
ormconfig.json
\ No newline at end of file
cache:
untracked: true
key: '$CI_BUILD_REF_NAME'
paths:
- node_modules/
build:
stage: build
script:
- npm install
- npm run build
only:
- master
tags:
- build
deploy:
stage: deploy
script:
- source ./env/production.sh
- pm2 start --exp-backoff-restart-delay=200
only:
- master
tags:
- deploy
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
## Description
드.론.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
File mode changed
module.exports = {
apps: [
{
name: 'backend',
script: 'npm',
args: 'start',
},
],
};
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}
This diff could not be displayed because it is too large.
{
"name": "web-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
},
"dependencies": {
"@nestjs/common": "^7.6.13",
"@nestjs/config": "^0.6.3",
"@nestjs/core": "^7.6.13",
"@nestjs/platform-express": "^7.6.13",
"@nestjs/platform-socket.io": "^7.6.15",
"@nestjs/platform-ws": "^7.6.15",
"@nestjs/typeorm": "^7.1.5",
"@nestjs/websockets": "^7.6.15",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.6.6",
"typeorm": "^0.2.32"
},
"devDependencies": {
"@nestjs/cli": "^7.5.6",
"@nestjs/schematics": "^7.2.7",
"@nestjs/testing": "^7.6.13",
"@types/express": "^4.17.11",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.31",
"@types/socket.io": "^2.1.13",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"eslint": "^7.20.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"jest": "^26.6.3",
"prettier": "^2.2.1",
"supertest": "^6.1.3",
"ts-jest": "^26.5.2",
"ts-loader": "^8.0.17",
"ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.1.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import configuration from './config/configuration';
import { DroneModule } from './drone/drone.module';
import { DroneEntity } from './entities/drone.entity';
import { DroneLogEntity } from './entities/drone.log.entity';
import { ScheduleEntity } from './entities/schedule.entity';
import { MemberEntity } from './entities/member.entity';
import { CodeEntity } from './entities/code.entity';
import { DroneScheduleMappingEntity } from './entities/drone.schedule.mapping.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('database.host'),
username: configService.get('database.user'),
password: configService.get('database.password'),
database: configService.get('database.name'),
port: configService.get('database.port'),
entities: [`${__dirname}/**/*.entity.{ts,js}`],
synchronize: false,
}),
}),
DroneModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
import 'dotenv/config';
export default () => ({
database: {
host: process.env.DATABASE_HOST || '',
user: process.env.DATABASE_USER || '',
name: process.env.DATABASE_NAME || '',
password: process.env.DATABASE_PASSWORD || '',
port: process.env.DATABASE_PORT || '',
},
});
\ No newline at end of file
This diff is collapsed. Click to expand it.
import {
Body,
Controller,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { DroneDataService } from 'src/drone/drone.data.service';
import { DroneApiDto } from 'src/drone/dto/drone.api.dto';
@Controller()
export class DroneDataController {
constructor(private droneDataService: DroneDataService) {
this.droneDataService = droneDataService;
}
@Post('/droneLog/list')
@UsePipes(new ValidationPipe({ transform: true }))
async saveDroneLogList(
@Body() saveDroneLogListDto: DroneApiDto.SaveDroneLogListDto,
) {
await this.droneDataService.saveDroneLogList(
saveDroneLogListDto.droneLogList,
);
return {
statusCode: 200,
statusMsg: '완료',
};
}
}
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DroneLogEntity } from 'src/entities/drone.log.entity';
import { DroneGateway } from 'src/drone/drone.gateway';
import { Repository } from 'typeorm/index';
import { DroneApiDto } from 'src/drone/dto/drone.api.dto';
@Injectable()
export class DroneDataService {
constructor(
@InjectRepository(DroneLogEntity)
private dronelogRepository: Repository<DroneLogEntity>,
private droneGateway: DroneGateway,
) {
this.dronelogRepository = dronelogRepository;
this.droneGateway = droneGateway;
}
async saveDroneLogList(droneLogList: DroneApiDto.SaveDroneLogDto[]) {
// 드론 데이터 전송
this.sendDroneLogList(droneLogList.map((log) => new DroneLogEntity(log)));
// 드론로그 생성
for (const droneLog of droneLogList) {
this.dronelogRepository.save({
droneId: droneLog.droneId,
scheduleId: droneLog.scheduleId,
latitude: droneLog.latitude,
longitude: droneLog.longitude,
verticalSpeed: droneLog.verticalSpeed,
horizontalSpeed: droneLog.horizontalSpeed,
aboveSeaLevel: droneLog.aboveSeaLevel,
aboveGroundLevel: droneLog.aboveGroundLevel,
});
}
}
sendDroneLogList(droneLogList) {
// 드론데이터 전송
this.droneGateway.sendToClientsDroneLogList(droneLogList);
for (const droneLog of droneLogList) {
let droneLogEntity = new DroneLogEntity(droneLog);
this.dronelogRepository.save(droneLogEntity);
}
}
async saveLog(droneLogEntity: DroneLogEntity): Promise<void> {
await this.dronelogRepository.save(droneLogEntity);
}
}
import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'ws';
import { DroneService } from './drone.service';
import { DroneLogEntity } from 'src/entities/drone.log.entity';
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
@WebSocketGateway(20206, { path: '/drone' })
export class DroneGateway {
constructor(private readonly droneService: DroneService) {}
@WebSocketServer()
server: Server;
private wsClients = [];
async afterInit() {
let globalCircleStep = 0;
while (1) {
if (globalCircleStep === 3600) {
globalCircleStep = 0;
} else {
globalCircleStep += 1;
}
this.wsClients.forEach((client) => {
const droneTestData = this.droneService.getDroneTestData({
globalCircleStep,
});
client.send(
JSON.stringify(
Object.assign({
data: { droneLog: droneTestData },
statusCode: 200,
}),
),
);
});
await sleep(1000);
}
}
async handleConnection(client: Socket) {
let clientId = this.wsClients.push(client);
}
handleDisconnect(client: Socket) {
let clientIndex = this.wsClients.find((wsClient) => wsClient === client);
if (clientIndex !== -1) {
this.wsClients.splice(clientIndex, 1);
}
}
@SubscribeMessage('')
handleMessages(client: Socket, payload: { name: string; text: string }) {
client.send(
JSON.stringify({
number: 10,
member: 'tony',
}),
);
}
sendToClientsDroneLogList(droneLogList: Array<DroneLogEntity>) {
this.wsClients.forEach((client) => {
client.send(
JSON.stringify(
Object.assign({
data: { droneLog: droneLogList },
statusCode: 200,
}),
),
);
});
}
}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DroneController } from './drone.controller';
import { DroneService } from './drone.service';
import { DroneDataController } from 'src/drone/drone.data.controller';
import { DroneDataService } from './drone.data.service';
import { DroneGateway } from './drone.gateway';
import { DroneEntity } from 'src/entities/drone.entity';
import { ScheduleEntity } from 'src/entities/schedule.entity';
import { DroneLogEntity } from 'src/entities/drone.log.entity';
import { MemberEntity } from 'src/entities/member.entity';
import { CodeEntity } from 'src/entities/code.entity';
import { DroneScheduleMappingEntity } from 'src/entities/drone.schedule.mapping.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
MemberEntity,
DroneEntity,
DroneLogEntity,
ScheduleEntity,
CodeEntity,
DroneScheduleMappingEntity,
]),
],
controllers: [DroneController, DroneDataController],
providers: [DroneGateway, DroneService, DroneDataService],
})
export class DroneModule {}
This diff is collapsed. Click to expand it.
import {
IsArray,
IsNotEmpty,
IsOptional,
IsNumber,
IsString,
ValidateNested,
IsDateString,
} from 'class-validator';
import { Type } from 'class-transformer';
import { DroneEntity } from 'src/entities/drone.entity';
export namespace DroneApiDto {
export class SaveDroneListDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => DroneEntity)
droneList: DroneEntity[];
}
export class UpdateDroneListDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdateDroneDto)
droneList: UpdateDroneDto[];
}
export class UpdateDroneDto {
@IsNotEmpty()
@IsNumber()
id: number;
@IsString()
@IsOptional()
maker: string;
@IsString()
@IsOptional()
usage: string;
@IsNumber()
@IsOptional()
specification: number;
@IsNumber()
@IsOptional()
weight: number;
}
export class SaveSchduleListDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => SaveSchduleDto)
schduleList: SaveSchduleDto[];
}
export class SaveSchduleDto {
@IsNotEmpty()
@IsNumber()
droneId: number;
@IsDateString()
@IsNotEmpty()
startTime: string;
@IsDateString()
@IsNotEmpty()
terminateTime: string;
@IsNumber()
@IsNotEmpty()
startLatitude: number;
@IsNumber()
@IsNotEmpty()
startLongitude: number;
@IsNumber()
@IsNotEmpty()
terminateLatitude: number;
@IsNumber()
@IsNotEmpty()
terminateLongitude: number;
}
export class UpdateSchduleListDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdateSchduleDto)
schduleList: UpdateSchduleDto[];
}
export class UpdateSchduleDto {
@IsNotEmpty()
@IsNumber()
id: number;
@IsDateString()
@IsNotEmpty()
startTime: string;
@IsDateString()
@IsNotEmpty()
terminateTime: string;
@IsNumber()
@IsNotEmpty()
startLatitude: number;
@IsNumber()
@IsNotEmpty()
startLongitude: number;
@IsNumber()
@IsNotEmpty()
terminateLatitude: number;
@IsNumber()
@IsNotEmpty()
terminateLongitude: number;
}
export class SaveDroneLogListDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => SaveDroneLogDto)
droneLogList: SaveDroneLogDto[];
}
export class SaveDroneLogDto {
@IsNotEmpty()
@IsNumber()
droneId: number;
@IsNotEmpty()
@IsNumber()
scheduleId: number;
@IsNotEmpty()
@IsNumber()
latitude: number;
@IsNotEmpty()
@IsNumber()
longitude: number;
@IsNotEmpty()
@IsNumber()
verticalSpeed: number;
@IsNotEmpty()
@IsNumber()
horizontalSpeed: number;
@IsNotEmpty()
@IsNumber()
aboveSeaLevel: number;
@IsNotEmpty()
@IsNumber()
aboveGroundLevel: number;
}
}
export class DroneDto {
private _id: number;
private _model_name: string;
private _maker: string;
private _usage: string;
private _picture: string;
private _specification: number;
private _weight: number;
constructor(
id: number,
model_name: string,
maker: string,
usage: string,
picture: string,
specification: number,
weight: number,
) {
this._id = id;
this._model_name = model_name;
this._maker = maker;
this._usage = usage;
this._picture = picture;
this._specification = specification;
this._weight = weight;
}
get id(): number {
return this._id;
}
set id(value: number) {
this._id = value;
}
get model_name(): string {
return this._model_name;
}
set model_name(value: string) {
this._model_name = value;
}
get maker(): string {
return this._maker;
}
set maker(value: string) {
this._maker = value;
}
get usage(): string {
return this._usage;
}
set usage(value: string) {
this._usage = value;
}
get picture(): string {
return this._picture;
}
set picture(value: string) {
this._picture = value;
}
get specification(): number {
return this._specification;
}
set specification(value: number) {
this._specification = value;
}
get weight(): number {
return this._weight;
}
set weight(value: number) {
this._weight = value;
}
}
export class DroneLogDto {
constructor(props) {
// props.id = props.id ? parseInt(props.id) : props.id;
props.droneId = props.droneId ? parseInt(props.droneId) : props.droneId;
props.scheduleId = props.scheduleId
? parseInt(props.scheduleId)
: props.scheduleId;
Object.assign(this, props);
}
id: number;
droneId: number;
scheduleId: number;
latitude: number;
longitude: number;
verticalSpeed: number;
horizontalSpeed: number;
aboveSeaLevel: number;
aboveGroundLevel: number;
}
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'code', schema: 'public' })
export class CodeEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: number;
@Column({ length: 20, name: 'code_group' })
code_Group: string;
@Column({ length: 20, name: 'code_group_name' })
codeGroupName: string;
@Column({ length: 20, name: 'code_text' })
codeText: string;
@Column({ length: 20, name: 'code_value' })
codeValue: string;
@Column({ length: 20, name: 'code_value_name' })
codeValueName: string;
}
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'drone', schema: 'public' })
export class DroneEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: number;
@Column({ length: 20, name: 'model_name' })
modelName: string;
@Column({ length: 20 })
maker: string;
@Column({ length: 20 })
usage: string;
@Column({ length: 20, name: 'usagename' })
usageName: string;
@Column({ length: 20 })
picture: string;
@Column()
specification: number;
@Column()
weight: number;
}
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'drone_log', schema: 'public' })
export class DroneLogEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: number;
@Column({ name: 'drone_id' })
droneId: number;
@Column({ name: 'schedule_id' })
scheduleId: number;
@Column()
latitude: number;
@Column()
longitude: number;
@Column({ name: 'vertical_speed' })
verticalSpeed: number;
@Column({ name: 'horizontal_speed' })
horizontalSpeed: number;
@Column({ name: 'above_sea_level' })
aboveSeaLevel: number;
@Column({ name: 'above_ground_level' })
aboveGroundLevel: number;
@Column({ type: 'timestamp', name: 'created_at' })
createdAt: Date;
constructor(props) {
Object.assign(this, props);
}
}
import { Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'drone_schedule_mapping', schema: 'public' })
export class DroneScheduleMappingEntity {
@PrimaryGeneratedColumn({ name: 'drone_id' })
droneId: number;
@PrimaryGeneratedColumn({ name: 'schedule_id' })
scheduleId: number;
}
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'member', schema: 'public' })
export class MemberEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: number;
@Column({ length: 15 })
name: string;
@Column({ length: 20, name:'tel_number' })
telNumber: string;
@Column({ length: 20 })
affiliation: string;
}
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm/index';
@Entity({ name: 'schedule', schema: 'public' })
export class ScheduleEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: number;
@Column({ type: 'timestamp', name: 'start_time' })
startTime: Date;
@Column({ type: 'timestamp', name: 'terminate_time' })
terminateTime: Date;
@Column({ name: 'start_latitude' })
startLatitude: number;
@Column({ name: 'start_longitude' })
startLongitude: number;
@Column({ name: 'terminate_latitude' })
terminateLatitude: number;
@Column({ name: 'terminate_longitude' })
terminateLongitude: number;
}
import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(20205);
}
bootstrap();
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true
}
}
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
node_modules
lib/api-client
**/*.ts
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
parserOptions: {
parser: 'babel-eslint',
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
],
plugins: [
],
// add your custom rules here
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'warn',
'comma-dangle': ['error', 'always-multiline'],
'linebreak-style': 0,
'import/no-extraneous-dependencies': 0,
'no-shadow': 0,
'import/prefer-default-export': 0,
'max-len': ['warn', { code: 200 }],
'import/extensions': ['error', 'always', {
js: 'never',
jsx: 'never',
vue: 'never',
}],
indent: [2, 2],
},
settings: {
'import/extensions': ['.js', '.jsx', '.vue'],
'import/resolver': {
alias: {
map: [
['@', './src'],
],
extensions: ['.js', '.vue', '.jsx'],
},
node: {
extensions: ['.js', '.vue', '.jsx'],
},
},
},
};
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
.DS_Store
stages:
- others
- build
- deploy
.build:
cache:
paths:
- node_modules/
.deploy:
variables:
GIT_STRATEGY: none
dev_build:
stage: build
extends: .build
script:
- yarn install
- yarn build
only:
- develop
tags:
- front
dev_deploy:
stage: deploy
extends: .deploy
script:
- pm2 start --exp-backoff-restart-delay=100
only:
- develop
tags:
- front
#(DEV) mockserver:
# stage: deploy
# extends: .deploy
# image: node:15
# script: yarn serve
# only:
# - develop
# artifacts:
# paths:
# - ./
# tags:
# - ws-server
#
#(DEV) websocket:
# stage: deploy
# extends: .deploy
# image: docker:latest
# script: yarn ws
# only:
# - develop
# artifacts:
# paths:
# - ./
# tags:
# - api-server
{
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false,
"arrowParens": "always"
}
# drone-web-nuxt
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
#!/bin/bash
echo "Running server in the background"
sudo systemctl restart hello-react
module.exports = {
apps: [
{
name: 'drone-front',
// package.json에 정의된 npm run start를 실행하게 해서 PM2로 관리하게 한다.
script: 'yarn',
args: 'run start',
instances: '1',
max_memory_restart: '1G',
error_file: 'err.log',
out_file: 'out.log',
log_file: 'combined.log',
},
],
};
[Unit]
Description=Drone front service
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=gitlab-runner
ExecStart=/home/gitlab-runner/builds/4hhEfxWU/0/khu-oz-wizard/drone-monitoring-web-ui yarn start
[Install]
WantedBy=multi-user.target
This diff could not be displayed because it is too large.
map $sent_http_content_type $expires {
"text/html" epoch;
"text/html; charset=utf-8" epoch;
default off;
}
server {
listen 20205;
server_name localhost;
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
gzip_min_length 1000;
location / {
expires $expires;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
proxy_pass http://127.0.0.1:3000;
}
}
/* eslint-disable no-unused-vars */
import api from './nuxtConfig/api';
import build from './nuxtConfig/build';
import theme from './nuxtConfig/theme';
import nuxtConfigModule from './nuxtConfig/module';
import io from './nuxtConfig/ioConfig';
import extendRouter from './nuxtConfig/extendRouter';
// 설정 내용이 짧은 것 및 구조화 하기 애매한 것은 별도 파일로 관리하지 않음.
export default {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'drone-web-nuxt',
htmlAttrs: {
lang: 'en',
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
],
},
// Auto import components: https://go.nuxtjs.dev/config-components
components: false,
// source Directory
srcDir: 'src/',
/* middleware */
serverMiddleware: [
'./serverMiddleWare/index',
],
router: {
// router middleware
middleware: 'router',
// router extend
// extendRoutes: extendRouter,
},
// module, plugin, alias, robots
...nuxtConfigModule,
// axios, proxy, auth
...api,
// env, runtimeConfig, build
...build,
// loading, transition, css
...theme,
// vue Global Config
vue: {
config: {
productionTip: true,
devtools: process.env.NODE_ENV === 'development',
// silent: process.env.NODE_ENV !== 'development',
// performance: process.env.NODE_ENV === 'development',
},
},
// robots Setting
robots: {
UserAgent: '*',
Disallow: '/',
},
// socket io Setting
io,
};
import { version } from '../package.json';
/**
* api 관련된 nuxt 옵션을 정리합니다.
* 해당 옵션은 아래와 같습니다.
* auth, axios, proxy,
*/
export default {
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
proxy: true,
retry: { retries: 3 },
// baseUrl: 'http://localhost:5555',
headers: {
common: {
Accept: 'application/json, text/plain, */*',
AppVersion: version,
},
delete: {},
get: {},
head: {},
post: {},
put: {},
patch: {},
},
},
proxy: {
'/api': {
target: process.env.BASE_API_URL || 'http://14.33.35.148:20205',
pathRewrite: {
'^/api': '',
},
changeOrigin: true,
},
},
auth: {
// Options
},
};
/**
* 빌드에 관련된 nuxt 옵션을 정리합니다.
* 해당 옵션은 아래와 같습니다.
* env, build,
*/
export default {
// modern property https://ko.nuxtjs.org/docs/2.x/configuration-glossary/configuration-modern
modern: false,
/* env Setting */
env: {
BASE_API_URL: process.env.BASE_API_URL,
BASE_APP_URL: process.env.BASE_APP_URL,
BASE_I18N_LOCALE: process.env.BASE_I18N_LOCALE,
BASE_I18N_FALLBACK_LOCALE: process.env.BASE_I18N_FALLBACK_LOCALE,
},
// public nuxt.context config variables
publicRuntimeConfig: {
BASE_API_URL: process.env.BASE_API_URL,
BASE_APP_URL: process.env.BASE_APP_URL,
BASE_I18N_LOCALE: process.env.BASE_I18N_LOCALE,
BASE_I18N_FALLBACK_LOCALE: process.env.BASE_I18N_FALLBACK_LOCALE,
},
// private nuxt.context config variables
privateRuntimeConfig: {
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
loaders: {
vue: {
transformAssetUrls: {
'vl-style-icon': 'src',
},
},
// for Antdv CustomTheme Setting
less: {
lessOptions: {
javascriptEnabled: true,
math: 'always',
},
},
},
devtool: true,
analyze: true,
},
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/eslint
'@nuxt/image',
],
image: {
staticFilename: '[publicPath]/images/[name]-[hash][ext]',
presets: {
avatar: {
modifiers: {
format: 'jpg',
width: 50,
height: 50,
},
},
},
},
};
export default function extendRoutes(routes, resolve) {
routes.push({
name: '404Page',
path: '*',
redirect: '/auth/404',
component: resolve(__dirname, '../src/pages/auth/404.vue'),
});
}
export default {
sockets: [
{
name: 'main',
url: 'http://localhost:8888',
default: true,
},
],
};
/**
* 모듈에 관련된 nuxt 옵션을 정리합니다.
* 해당 옵션은 아래와 같습니다.
* module, plugin, alias
*/
import { resolve } from 'path';
export default {
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
resolve(__dirname, '../src/plugins/ApiClient/index'),
resolve(__dirname, '../src/plugins/antDesign'),
resolve(__dirname, '../src/plugins/Dayjs/index'),
{ src: '@/plugins/client-only.client.js' },
{ src: '@/plugins/globalMixins' },
{ src: '@/plugins/vuelayers.js', ssr: false },
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
'@nuxtjs/style-resources',
'@nuxtjs/auth-next',
'@nuxtjs/sitemap',
'@nuxtjs/robots',
'nuxt-socket-io',
'nuxt-leaflet',
resolve(__dirname, '../src/modules/vuelayers.js'),
],
// alias
alias: {
'@': resolve(__dirname, '../src/'),
images: resolve(__dirname, '../src/assets/images'),
styles: resolve(__dirname, '../src/assets/styles'),
},
};
/**
* 테마에 관련된 nuxt 옵션을 정리합니다.
* 해당 옵션은 아래와 같습니다.
* trainsition, css, loading
*/
import { resolve } from 'path';
export default {
// Theme Animation
loading: {
color: '#1890ff',
height: '4px',
},
layoutTransition: {
name: 'default-layout',
mode: 'out-in',
},
pageTransition: {
name: 'default-page',
mode: 'out-in',
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
resolve(__dirname, '../src/assets/styles/less/index'),
resolve(__dirname, '../src/assets/styles/scss/index'),
],
};
{
"name": "drone-web-nuxt",
"version": "1.0.3",
"private": true,
"scripts": {
"dev": "nuxt",
"analyze-dev": "nuxt --analyze",
"build": "nuxt build",
"start": "nuxt start",
"build-mo": "nuxt build --modern=server",
"analyze-build": "nuxt build --analyze",
"start-mo": "nuxt start --modern=server",
"generate": "nuxt generate",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint": "yarn lint:js",
"serve": "json-server --watch lib/db.json --port 5555",
"socket": "node ./serverMiddleWare/socket.js",
"ws": "node ./serverMiddleWare/ws.js",
"wsm": "node ./serverMiddleWare/multWs.js",
"wsmp": "node ./serverMiddleWare/multPWs.js",
"k6": "k6 run ./serverMiddleWare/k6.js",
"k6m": "k6 run ./serverMiddleWare/k6mult.js"
},
"dependencies": {
"@nuxtjs/auth-next": "5.0.0-1613647907.37b1156",
"@nuxtjs/axios": "^5.13.1",
"@nuxtjs/robots": "^2.5.0",
"@nuxtjs/sitemap": "^2.4.0",
"ant-design-vue": "^1.7.2",
"core-js": "^3.8.3",
"cors": "^2.8.5",
"dayjs": "^1.10.4",
"express": "^4.17.1",
"highcharts": "^9.0.1",
"highcharts-vue": "^1.3.5",
"nuxt": "^2.14.12",
"nuxt-leaflet": "^0.0.25",
"nuxt-socket-io": "^1.1.14",
"socket.io": "^4.0.0",
"store2": "^2.12.0",
"vuelayers": "^0.11.35",
"vuex": "^3.6.2",
"ws": "^7.4.4"
},
"devDependencies": {
"@nuxt/image": "^0.4.13",
"@nuxtjs/eslint-config": "^5.0.0",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/style-resources": "^1.0.0",
"@vue/cli-service": "^4.5.11",
"@vue/eslint-config-airbnb": "^5.3.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.18.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-vue": "^7.5.0",
"fibers": "^5.0.0",
"json-server": "^0.16.3",
"less": "^4.1.1",
"less-loader": "7",
"sass": "^1.32.8",
"sass-loader": "10"
}
}
// <project root>/api/index.js
// const express = require('express');
const app = require('express')();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// 실제로는 /api 라우트를 처리하는 메소드가 된다.
app.get('/', (req, res) => {
console.log('hi');
io.emit('connection');
res.send('API root');
});
io.of('/analytics').on('connect', (socket) => {
console.log('클라이언트 접속');
socket.on('disconnect', () => {
console.log('클라이언트 접속 종료');
});
setInterval(() => {
socket.emit('message', '메세지');
}, 3000);
});
// 모듈로 사용할 수 있도록 export
// 앱의 /api/* 라우트로 접근하는 모든 요청은 모두 app 인스턴스에게 전달된다.
module.exports = {
path: '/socket',
handler: app,
};
import ws from 'k6/ws';
import { check } from 'k6';
export const options = {
// vus: 100,
// duration: '30s',
stages: [
{ duration: '60s', target: 10 },
],
};
export default function () {
const url = 'ws://localhost:8080';
const params = { tags: { my_tag: 'hello' } };
const res = ws.connect(url, params, (socket) => {
socket.on('open', () => console.log('connected'));
socket.on('message', (data) => console.log('Message received: '));
socket.on('close', () => console.log('disconnected'));
socket.setTimeout(() => {
console.log('60 seconds passed, closing the socket');
socket.close();
}, 30000);
});
check(res, { 'status is 101': (r) => r && r.status === 101 });
}
import ws from 'k6/ws';
import { check } from 'k6';
export const options = {
// vus: 100,
// duration: '30s',
stages: [
{ duration: '60s', target: 10 },
// { duration: '10s', target: 30 },
// { duration: '20s', target: 50 },
],
};
export default function () {
const url = 'ws://localhost:8080';
const params = { tags: { my_tag: 'hello' } };
const res = {};
for (let i = 0; i < 10; i += 1) {
res[i] = ws.connect(`${url}/${i + 1}`, params, (socket) => {
socket.on('open', () => console.log('connected', i));
socket.on('message', (data) => console.log('Message received: ', i));
socket.on('close', () => console.log('disconnected'));
socket.setTimeout(() => {
console.log('60 seconds passed, closing the socket');
socket.close();
}, 60000);
});
}
console.log(res);
check(res, {
'status1 is 101': (r) => r && r[0].status === 101,
'status2 is 101': (r) => r && r[1].status === 101,
'status3 is 101': (r) => r && r[2].status === 101,
'status4 is 101': (r) => r && r[3].status === 101,
});
}
//
// const res1 = ws.connect(`${url}/${1}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 1'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res2 = ws.connect(`${url}/${2}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 2'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res3 = ws.connect(`${url}/${3}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 3'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res4 = ws.connect(`${url}/${4}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 4'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res5 = ws.connect(`${url}/${5}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 5'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res6 = ws.connect(`${url}/${6}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 6'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res7 = ws.connect(`${url}/${7}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 7'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res8 = ws.connect(`${url}/${8}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 8'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res9 = ws.connect(`${url}/${9}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 9'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
//
// const res10 = ws.connect(`${url}/${10}`, params, (socket) => {
// socket.on('open', () => console.log('connected'));
// socket.on('message', (data) => console.log('Message received: 10'));
// socket.on('close', () => console.log('disconnected'));
// socket.setTimeout(() => {
// console.log('60 seconds passed, closing the socket');
// socket.close();
// }, 60000);
// });
// check([res1, res2, res3, res10], { 'status is 101': (r) => r && r.status === 101 });
/* eslint-disable prefer-arrow-callback,consistent-return,no-param-reassign,no-mixed-operators,no-use-before-define */
const http = require('http');
const WebSocket = require('ws');
const url = require('url');
const server = http.createServer();
const wss = {};
const dataNum = process.argv[2];
const wsServerCnt = process.argv[3];
const port = process.argv[4];
const dataPerWsServer = dataNum / wsServerCnt;
console.log(process.argv);
let pingInterval = null;
let sendInterval = null;
const logData = [];
for (let i = 0; i < dataPerWsServer; i += 1) {
logData.push({
latitude: getRandomArbitrary(37200000000000, 37300000000000) / 1000000000000,
longitude: getRandomArbitrary(126900000000000, 127100000000000) / 1000000000000,
id: i,
});
}
function getRandomArbitrary(min, max) {
return parseInt((Math.random() * (max - min) + min), 10);
}
function circleMove(x, y, radius, max, circleStep) {
return {
latitude: x + radius * Math.cos(2 * Math.PI * circleStep / max),
longitude: y + radius * Math.sin(2 * Math.PI * circleStep / max),
};
}
function makeCoordData(log, circleStep) {
if (circleStep == null) {
circleStep = 0;
}
if (circleStep === 3600) {
circleStep = 0;
} else circleStep += 1;
// console.log('step', circleStep);
return Array.from(
{ length: dataPerWsServer },
(v, i) => ({
id: i,
...circleMove(log[i].latitude, log[i].longitude, 0.05, 3600, circleStep),
time: new Date(),
}),
);
}
function heartbeat() {
this.isAlive = true;
console.log('client Heartbeat');
}
function noop() {}
for (let i = 0; i < wsServerCnt; i += 1) {
wss[i + 1] = new WebSocket.Server({ noServer: true });
}
Object.entries(wss).forEach(([key, wss]) => {
wss.on('connection', (ws) => {
console.log('connected', key);
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
let circleStep = 0;
sendInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
const coordData = makeCoordData(logData, circleStep);
ws.send(JSON.stringify(coordData));
}
circleStep += 1;
}, 1000);
ws.on('close', function close() {
console.log('websocket Closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
// sendInterval = null;
});
/* ping check */
pingInterval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping(noop);
});
}, 30000);
wss.on('close', function close() {
console.log('server closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
});
});
});
server.on('upgrade', (request, socket, head) => {
const { pathname } = url.parse(request.url);
Object.entries(wss).forEach(([key, wss]) => {
if (`/${key}` === pathname) {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
});
});
server.listen(port);
/* eslint-disable prefer-arrow-callback,consistent-return,no-param-reassign,no-mixed-operators,no-use-before-define */
const http = require('http');
const WebSocket = require('ws');
const url = require('url');
const server = http.createServer();
const wss = {};
const dataNum = 20000;
const wsServerCnt = process.argv[2];
console.log(wsServerCnt);
const dataPerWsServer = dataNum / wsServerCnt;
let pingInterval = null;
let sendInterval = null;
const logData = [];
for (let i = 0; i < dataPerWsServer; i += 1) {
logData.push({
latitude: getRandomArbitrary(37200000000000, 37300000000000) / 1000000000000,
longitude: getRandomArbitrary(126900000000000, 127100000000000) / 1000000000000,
id: i,
});
}
function getRandomArbitrary(min, max) {
return parseInt((Math.random() * (max - min) + min), 10);
}
function circleMove(x, y, radius, max, circleStep) {
return {
latitude: x + radius * Math.cos(2 * Math.PI * circleStep / max),
longitude: y + radius * Math.sin(2 * Math.PI * circleStep / max),
};
}
function makeCoordData(log, circleStep) {
if (circleStep == null) {
circleStep = 0;
}
if (circleStep === 3600) {
circleStep = 0;
} else circleStep += 1;
// console.log('step', circleStep);
return Array.from(
{ length: dataPerWsServer },
(v, i) => ({
id: i,
...circleMove(log[i].latitude, log[i].longitude, 0.05, 3600, circleStep),
time: new Date(),
}),
);
}
function heartbeat() {
this.isAlive = true;
console.log('client Heartbeat');
}
function noop() {}
for (let i = 0; i < wsServerCnt; i += 1) {
wss[i + 1] = new WebSocket.Server({ noServer: true });
}
Object.entries(wss).forEach(([key, wss]) => {
wss.on('connection', (ws) => {
console.log('connected', key);
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
let circleStep = 0;
sendInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
const coordData = makeCoordData(logData, circleStep);
ws.send(JSON.stringify(coordData));
}
circleStep += 1;
}, 1000);
ws.on('close', function close() {
console.log('websocket Closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
// sendInterval = null;
});
/* ping check */
pingInterval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping(noop);
});
}, 30000);
wss.on('close', function close() {
console.log('server closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
});
});
});
server.on('upgrade', (request, socket, head) => {
const { pathname } = url.parse(request.url);
Object.entries(wss).forEach(([key, wss]) => {
if (`/${key}` === pathname) {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
});
});
server.listen(20203);
/* eslint-disable prefer-arrow-callback */
// <project root>/api/index.js
// const express = require('express');
function getRandomArbitrary(min, max) {
return parseInt((Math.random() * (max - min) + min), 10);
}
const app = require('express')();
const http = require('http').createServer(app);
const io = require('socket.io')(http, {
cors: true,
origins: ['http://127.0.0.1:3000', 'http://127.0.0.1:8888', 'http://localhost:3000'],
});
const cors = require('cors');
app.use(cors());
// 실제로는 /api 라우트를 처리하는 메소드가 된다.
app.get('/', (req, res) => {
console.log('hi');
io.of('/testSoc').emit('connection', { data: '1234' });
res.send('API root');
});
io.of('/testSoc').on('connect', (socket) => {
console.log('클라이언트 접속');
socket.on('getMessage', (data) => {
console.log('fromClient', data);
});
socket.on('disconnect', () => {
console.log('클라이언트 접속 종료');
});
setInterval(() => {
socket.emit('receiveLog', { num: getRandomArbitrary(10, 100), time: new Date() });
}, 1000);
});
http.listen(8888, () => {
console.log('Socket IO server listening on port 8888');
});
import ws from 'k6/ws';
import { check } from 'k6';
export default function () {
const url = 'ws://echo.websocket.org';
const params = { tags: { my_tag: 'hello' } };
const res = ws.connect(url, params, (socket) => {
socket.on('open', () => {
console.log('connected');
socket.setInterval(() => {
socket.ping();
console.log('Pinging every 1sec (setInterval test)');
}, 1000);
});
socket.on('ping', () => {
console.log('PING!');
});
socket.on('pong', () => {
console.log('PONG!');
});
socket.on('close', () => {
console.log('disconnected');
});
socket.setTimeout(() => {
console.log('2 seconds passed, closing the socket');
socket.close();
}, 2000);
});
check(res, {
'status is 101': (r) => r && r.status === 101,
'Homepage body size is 11026 bytes': (r) => r.body && r.body.length === 11026,
test: (r) => r,
});
}
/* eslint-disable prefer-arrow-callback,consistent-return,no-param-reassign,no-mixed-operators,no-use-before-define */
let pingInterval = null;
let sendInterval = null;
const logData = [];
const dataNum = 20000;
for (let i = 0; i < dataNum; i += 1) {
logData.push({
latitude: getRandomArbitrary(37200000000000, 37300000000000) / 1000000000000,
longitude: getRandomArbitrary(126900000000000, 127100000000000) / 1000000000000,
id: i,
});
}
function getRandomArbitrary(min, max) {
return parseInt((Math.random() * (max - min) + min), 10);
}
function circleMove(x, y, radius, max, circleStep) {
return {
latitude: x + radius * Math.cos(2 * Math.PI * circleStep / max),
longitude: y + radius * Math.sin(2 * Math.PI * circleStep / max),
};
}
function makeCoordData(log, circleStep) {
if (circleStep == null) {
circleStep = 0;
}
if (circleStep === 3600) {
circleStep = 0;
} else circleStep += 1;
console.log('step', circleStep);
return Array.from(
{ length: dataNum },
(v, i) => ({
id: i,
...circleMove(log[i].latitude, log[i].longitude, 0.05, 3600, circleStep),
time: new Date(),
}),
);
}
function heartbeat() {
this.isAlive = true;
console.log('client Heartbeat');
}
function noop() {}
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 20202,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 * 1024,
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024, // Size (in bytes) below which messages
// should not be compressed.
},
});
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
let circleStep = 0;
sendInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
const coordData = makeCoordData(logData, circleStep);
ws.send(JSON.stringify(coordData));
}
circleStep += 1;
}, 1000);
ws.on('close', function close() {
console.log('websocket Closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
// sendInterval = null;
});
});
/* ping check */
pingInterval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping(noop);
});
}, 30000);
wss.on('close', function close() {
console.log('server closed');
clearInterval(pingInterval);
clearInterval(sendInterval);
});
# ASSETS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
@import 'node_modules/ant-design-vue/dist/antd.less';
@import '@/assets/styles/less/partials/antdesignCustom.less';
/* default Variable */
@primary-color: #1890ff; // primary color for all components
@link-color: #1890ff; // link color
@success-color: #52c41a; // success state color
@warning-color: #faad14; // warning state color
@error-color: #f5222d; // error state color
@font-size-base: 14px; // major text font size
@heading-color: rgba(0, 0, 0, 0.85); // heading text color
@text-color: rgba(0, 0, 0, 0.65); // major text color
@text-color-secondary: rgba(0, 0, 0, 0.45); // secondary text color
@disabled-color: rgba(0, 0, 0, 0.25); // disable state color
@border-radius-base: 4px; // major border radius
@border-color-base: #d9d9d9; // major border color
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // major shadow for layers
// Colors
$white: #fff;
$black: #001529;
$blue: var(--kit-color-primary);
$blue-light: #3d6ee7;
$blue-dark: #103daf;
$gray-1: #f2f4f8;
$gray-2: #e4e9f0;
$gray-3: #dde2ec;
$gray-4: #c3bedc;
$gray-5: #aca6cc;
$gray-6: #786fa4;
$yellow: #ff0;
$orange: #f2a654;
$red: #b52427;
$pink: #fd3995;
$purple: #652eff;
$green: #41b883;
$kdis-color: #0c7037;
$antblue: #1890ff;
$text: $gray-6;
$border: $gray-2;
// Accent colors
$default: $gray-4;
$primary: $kdis-color;
$secondary: $gray-5;
$success: $green;
$info: $blue-light;
$warning: $orange;
$danger: $red;
$light: $gray-1;
$dark: $black;
// dark theme
$dark-gray-1: #aeaee0;
$dark-gray-2: #7575a3;
$dark-gray-3: #4f4f7a;
$dark-gray-4: #2a274d;
$dark-gray-5: #161537;
$dark-gray-6: #100f28;
// Font Family
$base-font-family: 'Noto Sans KR', sans-serif;
// Font Size
$base-font-size: 15 !default;
@import '@/assets/styles/scss/partials/transition.scss';
@import '@/assets/styles/scss/partials/page.scss';
@import '@/assets/styles/scss/partials/layouts.scss';
@import '@/assets/styles/scss/partials/description.scss';
@import '@/assets/styles/scss/partials/box.scss';
@import '@/assets/styles/scss/partials/test.scss';
@import '@/assets/styles/scss/partials/pagination.scss';
@import '@/assets/styles/scss/partials/alert.scss';
.filter-alert{
padding-top: 5px;
padding-bottom: 5px;
margin-top: -10px;
.ant-alert-icon{
top: 9px;
left: 14px;
}
}
.mapBox {
::-webkit-scrollbar {
width: 7px;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: #47749E;
border: 2px solid transparent;
width: 5px;
border-radius: 2px;
}
::-webkit-scrollbar-track {
background-color: rgb(236, 236, 236);
border-radius: 0px 2px 2px 0px;
}
.search-box {
position: absolute;
right: 10px;
top: 10px;
.searchBtn{
width: 50px;
height: 50px;
padding: 7px;
background: white;
border: 2px solid rgba(0,0,0,0.2);
border-radius: 4px;
cursor: pointer;
}
.ant-modal-content {
position: absolute;
right: 60px;
top: 0px;
width: 400px;
max-height: calc(100vh - 120px);
.ant-modal-header {
background: #47749e;
.ant-modal-title {
color: white;
text-align: center;
}
}
.ant-modal-body {
padding: 20px;
}
.ant-input:hover {
border-color: #47749e;
border-right-width: 1px !important;
}
.ant-input:focus {
border-color: #47749e;
}
.ant-btn-primary {
background-color: #47749e;
border-color: #47749e;
}
.ant-list-header {
color: white;
background: #47749e;
}
.ant-input-search {
width: 100%;
margin-bottom: 10px;
}
.ant-list-items {
max-height: calc(100vh - 300px);
overflow-y: scroll;
}
.ant-list-item {
padding: 10px;
background: white;
width: 100%;
}
.ant-list-item:hover {
color: #47749e;
font-weight: 600;
}
}
}
}
.filter-feature-box {
.ant-modal-content {
position: absolute;
right: 70px;
top: 10px;
width: 300px;
.ant-modal-header {
background: #47749e;
.ant-modal-title {
color: white;
text-align: center;
}
}
}
.label {
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
margin-top: 20px;
span {
display: inline-block;
border: 2px solid #47749e;
border-radius: 3px;
padding: 2px 5px;
}
}
}
.boxBtn {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
padding: 7px;
background: white;
border: 2px solid rgba(0,0,0,0.2);
border-radius: 4px;
cursor: pointer;
font-size: 30px;
}
.bottom-tool-box {
display: flex;
gap: 10px;
position: absolute;
bottom: 10px;
right: calc(50% - 25px);
.filterBox {
.filterBtn {
width: 50px;
height: 50px;
padding: 7px;
background: white;
border: 2px solid rgba(0,0,0,0.2);
border-radius: 4px;
cursor: pointer;
}
}
}
\ No newline at end of file
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&family=Roboto:wght@500&display=swap');
.ant-descriptions-bordered .ant-descriptions-item-label {
background-color: #47749e;
color: white;
}
.ant-descriptions-item {
display: inline-flex;
margin-right: 10px;
}
.ant-descriptions-row {
font-family: 'Noto Sans KR', sans-serif;
.ant-descriptions-item-content {
min-width: 150px;
font-size: 13px;
background: white;
}
}
.ant-descriptions-item > span {
align-self: center;
}
.ant-descriptions-bordered .ant-descriptions-item-label,
.ant-descriptions-bordered .ant-descriptions-item-content {
padding: 10px 10px;
}
.description-box .ant-descriptions-item {
display: table-cell;
}
.drone-detail {
.ant-descriptions-view {
max-height: calc(100vh - 120px);
overflow: auto;
}
.ant-descriptions-item-label.ant-descriptions-item-colon {
font-size: 16px;
}
}
.ant-page-header-content {
.search-box .ant-descriptions-row .ant-descriptions-item-content {
min-width: 0;
}
.search-box .ant-descriptions-row {
font-family: none;
}
}
.mt-size-default{
margin-top: 8px;
}
.mt-size-sm {
margin-top: 4px;
}
.mt-size-md {
margin-top: 12px;
}
.mt-size-lg {
margin-top: 20px;
}
.ml-size-default{
margin-left: 8px;
}
.ml-size-sm {
margin-left: 4px;
}
.ml-size-md {
margin-left: 12px;
}
.ml-size-lg {
margin-left: 20px;
}
.mr-size-default{
margin-right: 8px;
}
.mr-size-sm {
margin-right: 4px;
}
.mr-size-md {
margin-right: 12px;
}
.mr-size-lg {
margin-right: 20px;
}
.mb-size-default{
margin-bottom: 8px;
}
.mb-size-sm {
margin-bottom: 4px;
}
.mb-size-md {
margin-bottom: 12px;
}
.mb-size-lg {
margin-bottom: 20px;
}
.margin-size-default{
margin: 8px;
}
.margin-size-sm {
margin: 4px;
}
.margin-size-md {
margin: 12px;
}
.margin-size-lg {
margin: 20px;
}
.pt-size-default{
padding-top: 8px;
}
.pt-size-sm {
padding-top: 4px;
}
.pt-size-md {
padding-top: 12px;
}
.pt-size-lg {
padding-top: 20px;
}
.pl-size-default{
padding-left: 8px;
}
.pl-size-sm {
padding-left: 4px;
}
.pl-size-md {
padding-left: 12px;
}
.pl-size-lg {
padding-left: 20px;
}
.pr-size-default{
padding-right: 8px;
}
.pr-size-sm {
padding-right: 4px;
}
.pr-size-md {
padding-right: 12px;
}
.pr-size-lg {
padding-right: 20px;
}
.pb-size-default{
padding-bottom: 8px;
}
.pb-size-sm {
padding-bottom: 4px;
}
.pb-size-md {
padding-bottom: 12px;
}
.pb-size-lg {
padding-bottom: 20px;
}
.padding-size-default{
padding: 8px;
}
.padding-size-sm {
padding: 4px;
}
.padding-size-md {
padding: 12px;
}
.padding-size-lg {
padding: 20px;
}
.r-flex{
display: flex;
&.center{
justify-content: center;
align-items: center;
}
&.space-between{
justify-content: space-between;
align-items: center;
}
&.space-around{
justify-content: space-around;
align-items: center;
}
&.space-evenly{
justify-content: space-evenly;
align-items: center;
}
&.start{
justify-content: start;
align-items: center;
}
&.end{
justify-content: flex-end;
align-items: center;
}
&.gap-1 {
gap: 4px
}
&.gap-2 {
gap: 8px
}
&.gap-3 {
gap: 12px
}
&.gap-4 {
gap: 16px
}
&.gap-5 {
gap: 20px
}
&.gap-6 {
gap: 24px
}
&.gap-default{
gap: 8px
}
&.gap-sm {
gap: 6px
}
&.gap-md {
gap: 12px
}
&.gap-lg {
gap: 20px
}
}
@import '@/assets/styles/mixins.scss';
.page-header {
background-color: white;
border: $gray-3 1px solid;
border-radius: 6px;
}
.search-input {
width: 50%;
min-width: 200px;
}
.page-main {
margin-top: 20px;
padding: 20px 20px 20px 20px;
background-color: white;
border-radius: 6px;
border: $gray-2 1px solid;
}
.page-main-without-header {
padding: 20px 20px 20px 20px;
background-color: white;
border-radius: 6px;
border: $gray-2 1px solid;
}
.ant-table-pagination.ant-pagination {
float: none;
text-align: center;
}
.ant-pagination-prev .ant-pagination-item-link,
.ant-pagination-next .ant-pagination-item-link,
.ant-pagination-item {
border: none;
outline: none;
}
.ant-pagination-item {
font-size: 1rem;
}
.ant-pagination-item-active a {
font-weight: bolder;
}
@import '@/assets/styles/mixins.scss';
.default-layout-enter-active,
.default-layout-leave-active {
transition: opacity 0.5s;
}
.default-layout-enter,
.default-layout-leave-active {
opacity: 0;
}
//
.default-page-enter-active,
.default-page-leave-active {
transition: opacity 0.3s;
}
.default-page-enter,
.default-page-leave-active {
opacity: 0;
}
<template>
<pie-chart :chart-data="chartData"
:chart-settings="chartSettings"
/>
</template>
<script>
import PieChart from '@/components/_Common/Chart/pieChart';
export default {
name: 'droneCategoryPieChart',
components: {
PieChart,
},
props: {
chartData: {
type: Array,
default: null,
},
},
data() {
return {
chartSettings: {
chart: {
height: 400,
},
title: {
text: '카테고리별 드론 기체 수',
style: {
fontSize: '20px',
},
},
width: '50%',
plotOptions: {
pie: {
shadow: true,
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
useHTML: true,
distance: -50,
formatter() {
if (this.percentage.toFixed(0) < 6) return '';
return `<div style="padding: 6px 4px 4px 6px;
background-color: rgba(0, 0, 0, 0.5);
border: 2px solid #f2f4f8;
border-radius: 6px;
">${this.percentage.toFixed(1)}%</div>`;
},
style: {
fontSize: '16px',
color: 'white',
},
},
showInLegend: true,
},
},
tooltip: {
formatter() {
return `
<span style="font-size:16px; color:${this.color}">${this.key}</span>
<table>
<tr>
<td style="padding:0">${this.series.name}</td>
<td style="padding:0">: <b>${this.y}</b></td>
</tr>
<tr>
<td style="padding:0">점유율</td>
<td style="color:{series.color};padding:0">: <b>${this.percentage.toFixed(1)}%</b></td>
</tr>
</table>
`;
},
},
},
};
},
};
</script>
<style scoped>
</style>
<template>
<live-line-chart :chart-data="chartData"
:chart-settings="chartSettings"
/>
</template>
<script>
import LiveLineChart from '@/components/_Common/Chart/liveLineChart';
import dayjs from 'dayjs';
export default {
components: {
LiveLineChart,
},
props: {
chartData: {
type: Array,
default: null,
},
},
data() {
return {
chartSettings: {
chart: {
height: 400,
},
title: {
text: '시간별 드론 기체 수',
style: {
fontSize: '20px',
},
},
width: '100%',
xAxis: {
type: 'datetime',
},
yAxis: {
title: {
text: null,
},
min: 0,
},
tooltip: {
formatter() {
const trList = this.points.map((elem) => `
<tr>
<td style="font-size: 16px; padding:0; color:${elem.color}">${elem.series.name}</td>
<td style="font-size: 16px; padding:0; color:${elem.color}">: <b>${elem.y}</b></td>
</tr>
`);
return `
<span style="font-size:12px; color:${this.color}">${dayjs(this.x).format('YYYY-MM-DD HH:mm:ss')}</span>
<table>
${trList}
</table>
`;
},
},
data: {
enablePolling: true,
dataRefreshRate: 2,
},
},
};
},
};
</script>
<style scoped lang="scss">
</style>
<template>
<pie-chart :chart-data="chartData"
:chart-settings="chartSettings"
/>
</template>
<script>
import PieChart from '@/components/_Common/Chart/pieChart';
export default {
name: 'makerPieChart',
components: {
PieChart,
},
props: {
chartData: {
type: Array,
default: null,
},
},
data() {
return {
chartSettings: {
chart: {
height: 400,
},
title: {
text: '제조사별 드론 기체 수',
style: {
fontSize: '20px',
},
},
width: '50%',
plotOptions: {
pie: {
shadow: true,
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
useHTML: true,
distance: -50,
formatter() {
if (this.percentage.toFixed(0) < 6) return '';
return `<div style="padding: 6px 4px 4px 6px;
background-color: rgba(0, 0, 0, 0.5);
border: 2px solid #f2f4f8;
border-radius: 6px;
">${this.percentage.toFixed(1)}%</div>`;
},
style: {
fontSize: '16px',
color: 'white',
},
},
showInLegend: true,
},
},
tooltip: {
formatter() {
return `
<span style="font-size:16px; color:${this.color}">${this.key}</span>
<table>
<tr>
<td style="padding:0">${this.series.name}</td>
<td style="padding:0">: <b>${this.y}</b></td>
</tr>
<tr>
<td style="padding:0">점유율</td>
<td style="color:{series.color};padding:0">: <b>${this.percentage.toFixed(1)}%</b></td>
</tr>
</table>
`;
},
},
},
};
},
};
</script>
<style scoped>
</style>
<template>
<column-chart :chart-data="chartData"
:chart-settings="chartSettings"/>
</template>
<script>
import ColumnChart from '@/components/_Common/Chart/columnChart';
export default {
name: 'timeCategoryColumnChart',
components: {
ColumnChart,
},
props: {
chartData: {
type: Array,
default: () => [],
},
},
data() {
return {
chartSettings: {
chart: {
height: 400,
},
title: {
text: '시간-드론 타입별 드론 기체 수',
style: {
fontSize: '20px',
},
},
width: '100%',
xAxis: {
categories: [
'00:00 ~ 04:00',
'04:00 ~ 08:00',
'08:00 ~ 12:00',
'12:00 ~ 16:00',
'16:00 ~ 20:00',
'20:00 ~ 24:00',
],
},
yAxis: {
title: {
text: null,
},
min: 0,
},
},
};
},
};
</script>
<style scoped lang="scss">
</style>
<template>
<a-page-header
class="page-header"
title="Analytics"
sub-title="Chart for Realtime Drone Data"
>
<template slot="extra">
<a-button key="1" type="primary">
action
</a-button>
</template>
</a-page-header>
</template>
<script>
export default {
name: 'AnalyticsHeader',
components: {},
data() {
return {
};
},
};
</script>
<style scoped lang="scss">
</style>
<template>
<a-page-header
class="page-header"
title="드론 정보"
sub-title="Drone-Schedule"
@back="$router.go(-1)"
>
<div :style="{display: 'flex'}">
<div :style="{width: '30%'}">
<img :src="droneInfo.picture" :style="{width: '90%'}" />
</div>
<div :style="{width: '70%', marginLeft: '20px'}">
<div class="label-modelName">{{ droneInfo.modelName }}</div>
<div :style="{display: 'flex', marginBottom: '15px'}">
<div class="label-info">info</div>
<a-descriptions
:column="{xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1}"
class="description-box"
>
<a-descriptions-item label="모델명">
{{ droneInfo.modelName }}
</a-descriptions-item>
<a-descriptions-item label="제조사">
{{ droneInfo.maker == null ? 'None' : droneInfo.maker }}
</a-descriptions-item>
<a-descriptions-item label="종류">
{{ droneInfo.usage == null ? 'None' : droneInfo.usage }}
</a-descriptions-item>
<a-descriptions-item label="제원">
{{
droneInfo.specification == null
? 'None'
: droneInfo.specification
}}
</a-descriptions-item>
<a-descriptions-item label="무게">
{{ droneInfo.weight == null ? '?' : droneInfo.weight }} kg
</a-descriptions-item>
<a-descriptions-item label="No">
{{ droneInfo.id }}
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</div>
<a-divider />
</a-page-header>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'DatabaseDetailHeader',
components: {},
data() {
return {
droneInfo: {},
};
},
computed: {
...mapGetters('Drone/detail', {
getDetailData: 'getDetailData',
}),
...mapGetters('Code', {
getCodes: 'getCodes',
}),
droneCategory() {
return (data) => {
switch (parseInt(data, 10)) {
case 1:
return '촬영용';
case 2:
return '레이싱용';
case 3:
return '완구용';
default:
return null;
}
};
},
},
created() {
console.log(this.getDetailData);
this.droneInfo = this.getDetailData;
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.label-modelName {
font-size: 30px;
color: $antblue;
}
.label-info {
padding: 0 10px;
border-right: 1px solid #777777;
min-width: 20%;
}
.description-box {
padding-left: 10px;
}
</style>
<template>
<a-page-header
class="page-header"
title="Database"
sub-title="Table - Drone list"
>
<template slot="extra">
<a-button key="1" type="primary" form="form" html-type="submit">
검색
</a-button>
</template>
<a-form id="form" @submit.prevent="searchData" class="form-wrapper">
<a-form-item label="모델명" class="form-item">
<a-input v-model="searchParams.modelName" class="search-input" />
</a-form-item>
<a-form-item label="제조사" class="form-item">
<a-input v-model="searchParams.maker" class="search-input" />
</a-form-item>
<a-form-item label="무게" class="form-item">
<a-button
v-if="searchOpenFlag"
class="search-input"
@click="searchOpenFlag = !searchOpenFlag"
>{{ sliderBegin }} - {{ sliderEnd == 50 ? '50+' : sliderEnd }} kg
<a-icon type="up" />
</a-button>
<a-button
v-else
class="search-input"
@click="searchOpenFlag = !searchOpenFlag"
>{{ sliderBegin }} - {{ sliderEnd == 50 ? '50+' : sliderEnd }} kg
<a-icon type="down" />
</a-button>
<div v-if="searchOpenFlag" class="slider-box">
<a-slider
range
:marks="marks"
:max="50"
:step="1"
:default-value="[sliderBegin, sliderEnd]"
class="search-input"
@change="onSliderChange"
@afterChange="onSliderAfterChange"
/>
</div>
</a-form-item>
<a-form-item label="종류" has-feedback class="form-item">
<a-select
class="search-input"
default-value=""
@change="handleSelectChange"
>
<a-select-option value="">
선택 안 함
</a-select-option>
<a-select-option value="촬영용">
촬영용
</a-select-option>
<a-select-option value="산업용">
산업용
</a-select-option>
<a-select-option value="군사용">
군사용
</a-select-option>
<a-select-option value="레이싱용">
레이싱용
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-page-header>
</template>
<script>
import {droneCategory} from '@/utils/CommonData/selectOptions';
import {mapActions, mapGetters} from 'vuex';
export default {
name: 'DatabaseSearchFilter',
components: {},
data() {
return {
searchOpenFlag: false,
categoryOptions: droneCategory,
searchParams: {},
marks: {
0: '0kg',
50: '50+kg',
},
sliderBegin: 0,
sliderEnd: 50,
};
},
computed: {
...mapGetters('Drone/page', {
getPageParams: 'getPageParams',
}),
},
created() {
this.searchParams = JSON.parse(JSON.stringify(this.getPageParams));
},
methods: {
...mapActions('Drone/page', {
setPageParams: 'setPageParams',
}),
searchData() {
console.log(this.searchParams);
this.setPageParams(this.searchParams);
this.$emit('loadData');
},
handleSelectChange(value) {
this.searchParams.usageName = value;
this.searchData();
},
onSliderChange(value) {
this.sliderBegin = value[0];
this.sliderEnd = value[1];
},
onSliderAfterChange(value) {
this.searchParams.minWeight = value[0];
this.searchParams.maxWeight = value[1];
if (value[1] == 50) {
this.searchParams.maxWeight = null;
}
this.searchOpenFlag = !this.searchOpenFlag;
},
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.form-wrapper {
height: 40px;
}
.form-wrapper,
.form-item {
display: flex;
margin-left: 10px;
margin-bottom: 0;
}
.slider-box {
background: white;
padding: 10px 20px;
border-radius: 20px;
border: solid 1px $antblue;
position: fixed;
z-index: 10;
}
</style>
<template>
<a-table
rowKey="id"
bordered
:loading="childLoading"
:columns="columns"
:data-source="getPageData"
:scroll="{x: 1000}"
:pagination="pagination"
@change="changePage"
>
<a slot="modelName" slot-scope="data, row" @click="goDetail(row)">
{{ data }}
</a>
<div slot="usageName" slot-scope="data">
{{ data == null ? '?' : data }}
</div>
<div slot="weight" slot-scope="data">
{{ data == null ? '?' : data + 'kg' }}
</div>
<div slot="specification" slot-scope="data">
{{ data == null ? '?' : data }}
</div>
<div slot="droneCategory" slot-scope="data">
{{ data }}
</div>
</a-table>
</template>
<script>
import databaseColumn from '@/utils/ColumnData/databaseColumn';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'DatabaseTable',
props: {
childLoading: {
type: Boolean,
default: false,
},
},
data() {
return {
columns: databaseColumn,
};
},
computed: {
...mapGetters('Drone/page', {
getPageData: 'getPageData',
getPagination: 'getPagination',
getPageParams: 'getPageParams',
}),
pagination: {
get() {
return this.getPagination;
},
set(e) {
this.setPagination({
size: e.size,
page: e.page,
});
},
},
},
methods: {
...mapActions('Drone/page', {
setPagination: 'setPagination',
fetchPageData: 'fetchPageData',
}),
changePage(e) {
this.pagination = {
size: e.pageSize,
page: e.current,
};
this.fetchPageData(this.getPageParams);
},
goDetail(row) {
this.$router.push({
path: `/database/drone/${row.id}`,
});
},
},
};
</script>
<style scoped lang="scss"></style>
<template>
<a-page-header
class="page-header"
title="Database"
sub-title="Table - Drone Log"
>
<template slot="extra">
<a-button
key="1"
type="primary"
form="form"
html-type="submit"
:style="{padding: '0 15px'}"
>
검색
</a-button>
</template>
<a-form id="form" @submit.prevent="searchData" class="form-wrapper">
<a-descriptions
:column="{xs: 1, sm: 3, md: 3, lg: 6, xl: 7, xxl: 7}"
class="search-box"
>
<a-descriptions-item label="Schedule ID" class="form-item">
<a-input v-model="searchParams.scheduleId" class="search-input-log" />
</a-descriptions-item>
<a-descriptions-item label="수평 속도" class="form-item">
<a-button v-if="hsOpenFlag" @click="hsOpenFlag = !hsOpenFlag"
>{{ minHorizontalSpeed }} -
{{ maxHorizontalSpeed == 100 ? '100+' : maxHorizontalSpeed }} km/s
<a-icon type="up" />
</a-button>
<a-button v-else @click="hsOpenFlag = !hsOpenFlag"
>{{ minHorizontalSpeed }} -
{{ maxHorizontalSpeed == 100 ? '100+' : maxHorizontalSpeed }} km/s
<a-icon type="down" />
</a-button>
<div v-if="hsOpenFlag" class="slider-box">
<a-slider
range
:marks="speedMarks"
:max="100"
:step="1"
:default-value="[minHorizontalSpeed, maxHorizontalSpeed]"
class="search-input"
@change="onHorizontalSpeedChange"
@afterChange="onHorizontalSpeedAfterChange"
/>
</div>
</a-descriptions-item>
<a-descriptions-item label="수직 속도" class="form-item">
<a-button v-if="vsOpenFlag" @click="vsOpenFlag = !vsOpenFlag"
>{{ minVerticalSpeed }} -
{{ maxVerticalSpeed == 100 ? '100+' : maxVerticalSpeed }} km/s
<a-icon type="up" />
</a-button>
<a-button v-else @click="vsOpenFlag = !vsOpenFlag"
>{{ minVerticalSpeed }} -
{{ maxVerticalSpeed == 100 ? '100+' : maxVerticalSpeed }} km/s
<a-icon type="down" />
</a-button>
<div v-if="vsOpenFlag" class="slider-box">
<a-slider
range
:marks="speedMarks"
:max="100"
:step="1"
:default-value="[minVerticalSpeed, maxVerticalSpeed]"
class="search-input"
@change="onVerticalSpeedChange"
@afterChange="onVerticalSpeedAfterChange"
/>
</div>
</a-descriptions-item>
<a-descriptions-item label="지면 고도" class="form-item">
<a-button v-if="aglOpenFlag" @click="aglOpenFlag = !aglOpenFlag"
>{{ minAboveGroundLevel }} -
{{ maxAboveGroundLevel == 200 ? '200+' : maxAboveGroundLevel }} m
<a-icon type="up" />
</a-button>
<a-button v-else @click="aglOpenFlag = !aglOpenFlag"
>{{ minAboveGroundLevel }} -
{{ maxAboveGroundLevel == 200 ? '200+' : maxAboveGroundLevel }} m
<a-icon type="down" />
</a-button>
<div v-if="aglOpenFlag" class="slider-box">
<a-slider
range
:marks="levelMarks"
:max="200"
:step="1"
:default-value="[minAboveGroundLevel, maxAboveGroundLevel]"
class="search-input"
@change="onAboveGroundLevelChange"
@afterChange="onAboveGroundLevelAfterChange"
/>
</div>
</a-descriptions-item>
<a-descriptions-item label="해발 고도" class="form-item">
<a-button v-if="aslOpenFlag" @click="aslOpenFlag = !aslOpenFlag"
>{{ minAboveSeaLevel }} -
{{ maxAboveSeaLevel == 200 ? '200+' : maxAboveSeaLevel }} m
<a-icon type="up" />
</a-button>
<a-button v-else @click="aslOpenFlag = !aslOpenFlag"
>{{ minAboveSeaLevel }} -
{{ maxAboveSeaLevel == 200 ? '200+' : maxAboveSeaLevel }} m
<a-icon type="down" />
</a-button>
<div v-if="aslOpenFlag" class="slider-box">
<a-slider
range
:marks="levelMarks"
:max="200"
:step="1"
:default-value="[minAboveSeaLevel, maxAboveSeaLevel]"
class="search-input"
@change="onAboveSeaLevelChange"
@afterChange="onAboveSeaLevelAfterChange"
/>
</div>
</a-descriptions-item>
<a-descriptions-item label="위도" class="form-item">
<a-input v-model="searchParams.latitude" class="search-input-log" />
</a-descriptions-item>
<a-descriptions-item label="경도" class="form-item">
<a-input v-model="searchParams.longitude" class="search-input-log" />
</a-descriptions-item>
</a-descriptions>
</a-form>
</a-page-header>
</template>
<script>
/* eslint-disable prefer-destructuring */
import {mapActions, mapGetters} from 'vuex';
export default {
name: 'LogSearchFilter',
components: {},
data() {
return {
hsOpenFlag: false,
vsOpenFlag: false,
aslOpenFlag: false,
aglOpenFlag: false,
searchParams: {},
speedMarks: {
0: '0km/h',
100: '100+km/h',
},
levelMarks: {
0: '0m',
200: '200+m',
},
minVerticalSpeed: 0,
maxVerticalSpeed: 100,
minHorizontalSpeed: 0,
maxHorizontalSpeed: 100,
minAboveSeaLevel: 0,
maxAboveSeaLevel: 200,
minAboveGroundLevel: 0,
maxAboveGroundLevel: 200,
};
},
computed: {
...mapGetters('Log/page', {
getPageParams: 'getPageParams',
}),
},
created() {
this.searchParams = JSON.parse(JSON.stringify(this.getPageParams));
},
methods: {
...mapActions('Log/page', {
setPageParams: 'setPageParams',
}),
searchData() {
console.log(this.searchParams);
this.setPageParams(this.searchParams);
this.$emit('loadData');
},
onHorizontalSpeedChange(value) {
this.minHorizontalSpeed = value[0];
this.maxHorizontalSpeed = value[1];
},
onHorizontalSpeedAfterChange(value) {
this.hsOpenFlag = !this.hsOpenFlag;
this.searchParams.minHorizontalSpeed = this.minHorizontalSpeed;
this.searchParams.maxHorizontalSpeed = this.maxHorizontalSpeed;
if (this.maxHorizontalSpeed == 100) {
this.searchParams.maxHorizontalSpeed = null;
}
},
onVerticalSpeedChange(value) {
this.minVerticalSpeed = value[0];
this.maxVerticalSpeed = value[1];
},
onVerticalSpeedAfterChange(value) {
this.vsOpenFlag = !this.vsOpenFlag;
this.searchParams.minVerticalSpeed = this.minVerticalSpeed;
this.searchParams.maxVerticalSpeed = this.maxVerticalSpeed;
if (this.maxVerticalSpeed == 100) {
this.searchParams.maxVerticalSpeed = null;
}
},
onAboveSeaLevelChange(value) {
this.minAboveSeaLevel = value[0];
this.maxAboveSeaLevel = value[1];
},
onAboveSeaLevelAfterChange(value) {
this.aslOpenFlag = !this.aslOpenFlag;
this.searchParams.minAboveSeaLevel = this.minAboveSeaLevel;
this.searchParams.maxAboveSeaLevel = this.maxAboveSeaLevel;
if (this.maxAboveSeaLevel == 200) {
this.searchParams.maxAboveSeaLevel = null;
}
},
onAboveGroundLevelChange(value) {
this.minAboveGroundLevel = value[0];
this.maxAboveGroundLevel = value[1];
},
onAboveGroundLevelAfterChange(value) {
this.aglOpenFlag = !this.aglOpenFlag;
this.searchParams.minAboveGroundLevel = this.minAboveGroundLevel;
this.searchParams.maxAboveGroundLevel = this.maxAboveGroundLevel;
if (this.maxAboveGroundLevel == 200) {
this.searchParams.maxAboveGroundLevel = null;
}
},
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.form-wrapper {
//height: 40px;
justify-content: space-between;
}
.form-wrapper,
.form-item {
display: flex;
margin-right: 10px;
margin-bottom: 0;
}
.slider-box {
background: white;
padding: 10px 23px;
border-radius: 20px;
border: solid 1px $antblue;
position: fixed;
z-index: 9;
}
.search-input-log {
width: 100px;
}
.ant-btn {
padding: 0 10px;
}
</style>
<template>
<a-table
rowKey="id"
bordered
:loading="childLoading"
:columns="columns"
:data-source="getPageData"
:scroll="{x: 1000}"
:pagination="pagination"
@change="changePage"
>
<a slot="modelName" slot-scope="data, row" @click="goDetail(row)">
{{ data }}
</a>
<div slot="horizontalSpeed" slot-scope="data">{{ data }} km/h</div>
<div slot="verticalSpeed" slot-scope="data">{{ data }} km/h</div>
<div slot="aboveGroundLevel" slot-scope="data">{{ data }} m</div>
<div slot="aboveSeaLevel" slot-scope="data">{{ data }} m</div>
</a-table>
</template>
<script>
import databaseLogColumn from '@/utils/ColumnData/databaseLogColumn';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'LogTable',
props: {
childLoading: {
type: Boolean,
default: false,
},
},
data() {
return {
columns: databaseLogColumn,
};
},
computed: {
...mapGetters('Log/page', {
getPageData: 'getPageData',
getPagination: 'getPagination',
getPageParams: 'getPageParams',
}),
pagination: {
get() {
return this.getPagination;
},
set(e) {
this.setPagination({
size: e.size,
page: e.page,
});
},
},
},
methods: {
...mapActions('Log/page', {
setPagination: 'setPagination',
fetchPageData: 'fetchPageData',
}),
...mapActions('Drone/detail', {
fetchDroneDetailData: 'fetchDetailData',
}),
...mapActions('Schedule/detail', {
fetchScheduleDetailData: 'fetchDetailData',
}),
changePage(e) {
this.pagination = {
size: e.pageSize,
page: e.current,
};
this.fetchPageData(this.getPageParams);
},
goDetail(row) {
this.fetchDroneDetailData(row.droneId);
this.fetchScheduleDetailData(row.scheduleId);
this.$router.push({
path: `/database/schedule/${row.scheduleId}`,
});
},
},
};
</script>
<style scoped lang="scss"></style>
<template>
<a-page-header
class="page-header"
title="드론 정보"
sub-title="Drone-Schedule-Log"
@back="$router.go(-1)"
>
<div :style="{display: 'flex'}">
<div :style="{width: '30%'}">
<img :src="droneInfo.picture" :style="{width: '90%'}" />
</div>
<div :style="{width: '70%', marginLeft: '20px'}">
<div class="label-modelName">{{ droneInfo.modelName }}</div>
<div :style="{display: 'flex', marginBottom: '15px'}">
<div class="label-info">info</div>
<a-descriptions
:column="{xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1}"
class="description-box"
>
<a-descriptions-item label="모델명">
{{ droneInfo.modelName }}
</a-descriptions-item>
<a-descriptions-item label="제조사">
{{ droneInfo.maker == null ? 'None' : droneInfo.maker }}
</a-descriptions-item>
<a-descriptions-item label="종류">
{{ droneInfo.usage == null ? 'None' : droneInfo.usageName }}
</a-descriptions-item>
<a-descriptions-item label="제원">
{{
droneInfo.specification == null
? 'None'
: droneInfo.specification
}}
</a-descriptions-item>
<a-descriptions-item label="무게">
{{ droneInfo.weight == null ? '?' : droneInfo.weight }} kg
</a-descriptions-item>
<a-descriptions-item label="No">
{{ droneInfo.id }}
</a-descriptions-item>
</a-descriptions>
</div>
<div :style="{display: 'flex'}">
<div class="label-info">Schedule</div>
<a-descriptions
:column="{xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1}"
class="description-box"
>
<a-descriptions-item label="시작 시간">
{{ mc_dateTime(ScheduleInfo.startTime) }}
</a-descriptions-item>
<a-descriptions-item label="시작 위도">
{{ ScheduleInfo.startLatitude }}
</a-descriptions-item>
<a-descriptions-item label="시작 경도">
{{ ScheduleInfo.startLongitude }}
</a-descriptions-item>
<a-descriptions-item label="종료 시간">
{{ mc_dateTime(ScheduleInfo.terminateTime) }}
</a-descriptions-item>
<a-descriptions-item label="종료 위도">
{{ ScheduleInfo.terminateLatitude }}
</a-descriptions-item>
<a-descriptions-item label="종료 경도">
{{ ScheduleInfo.terminateLongitude }}
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</div>
<a-divider />
</a-page-header>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'DatabaseDetailHeader',
components: {},
data() {
return {
droneInfo: {},
ScheduleInfo: {},
};
},
created() {
this.droneInfo = this.getDetailData;
this.ScheduleInfo = this.getScheduleDetailData;
},
computed: {
...mapGetters('Drone/detail', {
getDetailData: 'getDetailData',
}),
...mapGetters('Schedule/detail', {
getScheduleDetailData: 'getDetailData',
}),
...mapGetters('Code', {
getCodes: 'getCodes',
}),
droneCategory() {
return (data) => {
switch (parseInt(data, 10)) {
case 1:
return '촬영용';
case 2:
return '레이싱용';
case 3:
return '완구용';
default:
return null;
}
};
},
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.label-modelName {
font-size: 30px;
color: $antblue;
}
.label-info {
padding: 0 10px;
border-right: 1px solid #777777;
min-width: 20%;
}
.description-box {
padding-left: 10px;
}
</style>
<template>
<a-page-header
class="page-header"
title="Database"
sub-title="Table - DroneSchedule"
>
<template slot="extra">
<a-button key="1" type="primary" form="form" html-type="submit">
검색
</a-button>
</template>
<a-form id="form" @submit.prevent="searchData" class="form-wrapper">
<a-form-item label="검색 범위" class="form-item">
<div>
<a-date-picker
v-model="startValue"
:disabled-date="disabledStartDate"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="Start Date"
@openChange="handleStartOpenChange"
/>
<span :style="{padding: '0 10px'}">-</span>
<a-date-picker
v-model="endValue"
:disabled-date="disabledEndDate"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="End Date"
:open="endOpen"
@openChange="handleEndOpenChange"
/>
</div>
</a-form-item>
</a-form>
</a-page-header>
</template>
<script>
import {mapActions, mapGetters} from 'vuex';
export default {
name: 'DatabaseSearchFilter',
components: {},
data() {
return {
searchParams: {},
startValue: null,
endValue: null,
endOpen: false,
};
},
watch: {
startValue(val) {
if (val != null) {
this.searchParams.startTime = val.format('YYYY-MM-DD HH:mm:ss');
} else {
this.searchParams.startTime = null;
}
},
endValue(val) {
if (val !== null) {
this.searchParams.terminateTime = val.format('YYYY-MM-DD HH:mm:ss');
} else {
this.searchParams.terminateTime = null;
}
},
},
computed: {
...mapGetters('Schedule/page', {
getPageParams: 'getPageParams',
}),
},
created() {
this.searchParams = JSON.parse(JSON.stringify(this.getPageParams));
},
methods: {
...mapActions('Schedule/page', {
setPageParams: 'setPageParams',
}),
searchData() {
console.log(this.searchParams);
this.setPageParams(this.searchParams);
this.$emit('loadData');
},
disabledStartDate(startValue) {
const endValue = this.endValue;
if (!startValue || !endValue) {
return false;
}
return startValue.valueOf() > endValue.valueOf();
},
disabledEndDate(endValue) {
const startValue = this.startValue;
if (!endValue || !startValue) {
return false;
}
return startValue.valueOf() >= endValue.valueOf();
},
handleStartOpenChange(open) {
if (!open) {
this.endOpen = true;
}
},
handleEndOpenChange(open) {
this.endOpen = open;
},
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.form-wrapper {
height: 40px;
}
.form-wrapper,
.form-item {
display: flex;
margin-left: 10px;
margin-bottom: 0;
}
.slider-box {
background: white;
padding: 10px 15px;
border-radius: 20px;
border: solid 1px $antblue;
position: fixed;
z-index: 9;
}
</style>
<template>
<a-table
rowKey="id"
bordered
:loading="childLoading"
:columns="columns"
:data-source="getPageData"
:scroll="{x: 1000}"
:pagination="pagination"
@change="changePage"
>
<a slot="modelName" slot-scope="data, row" @click="goDetail(row)">
{{ data }}
</a>
<div slot="startTime" slot-scope="data">
{{ mc_dateTime(data) }}
</div>
<div slot="terminateTime" slot-scope="data">
{{ mc_dateTime(data) }}
</div>
</a-table>
</template>
<script>
import databaseScheduleColumn from '@/utils/ColumnData/databaseScheduleColumn';
import {mapActions, mapGetters} from 'vuex';
export default {
name: 'DatabaseTable',
props: {
childLoading: {
type: Boolean,
default: false,
},
},
data() {
return {
columns: databaseScheduleColumn,
};
},
computed: {
...mapGetters('Schedule/page', {
getPageData: 'getPageData',
getPagination: 'getPagination',
getPageParams: 'getPageParams',
}),
pagination: {
get() {
return this.getPagination;
},
set(e) {
this.setPagination({
size: e.size,
page: e.page,
});
},
},
},
methods: {
...mapActions('Schedule/page', {
setPagination: 'setPagination',
fetchPageData: 'fetchPageData',
}),
...mapActions('Drone/detail', {
fetchDetailData: 'fetchDetailData',
}),
changePage(e) {
this.pagination = {
size: e.pageSize,
page: e.current,
};
this.fetchPageData(this.getPageParams);
},
goDetail(row) {
this.fetchDetailData(row.droneId);
this.$router.push({
path: `/database/schedule/${row.id}`,
});
},
},
};
</script>
<style scoped lang="scss"></style>
<template>
<div>
<h4 class="footer-font">
Drone Simulation Map
</h4>
<h5 class="footer-font">
@2021 TwentyOz & KHU
</h5>
</div>
</template>
<script>
export default {
name: 'LayoutFooter',
data() {
return {
};
},
computed: {
},
watch: {
},
created() {
},
method: {
},
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/mixins.scss';
.footer-font {
color: $gray-3
}
</style>
<template>
<div>
<div class="bottom-tool-box">
<div class="boxBtn" @click="clickWeatherBtn">
<a-icon type="cloud"/>
</div>
<FilterBtnBox
class="filterBox"
@clickClose="clickFilterBoxClose"
@changeFilterMode="e => this.$emit('changeFilterMode', e)"
@changeSearchMode="e => this.$emit('changeSearchMode', e)"
/>
<div class="boxBtn" @click="clickBookMarkBtn">
<a-icon type="star"/>
</div>
</div>
</div>
</template>
<script>
import FilterBtnBox from '../FilterBox/filterBtnBox';
export default {
head() {
return {
title: 'Drone',
meta: [
{
hid: 'database',
name: 'Descriptions',
content: 'DroneWeb-Content',
},
],
};
},
components: {
FilterBtnBox
},
props: {
},
data() {
return {
searchMode: false,
filterMode: false,
};
},
computed: {
},
created() {
},
methods: {
clickWeatherBtn() {
console.log('click')
this.$notification['warning']({
message: '날씨 기능은 추후 추가될 예정입니다.',
duration: 3,
})
},
clickBookMarkBtn() {
this.$notification['warning']({
message: '즐겨찾기 기능은 추후 추가될 예정입니다.',
duration: 3,
})
},
toggleFilterMode() {
this.$emit('toggleFilterMode')
},
toggleSearchMode() {
this.$emit('toggleSearchMode')
},
clickSearchBoxClose() {
this.searchMode = false;
},
clickFilterBoxClose() {
this.filterMode = false;
},
clickSearchBtn() {
this.filterMode = false;
this.searchMode = true;
},
clickFilterBtn() {
this.searchMode = false;
this.filterMode = true;
},
},
};
</script>
<style lang="scss">
</style>
<template>
<div>
<a-descriptions layout="vertical" bordered>
<a-descriptions-item :span="4">
<template v-slot:label>
<div>{{ foundDrone.modelName }}</div>
</template>
<img
:src="foundDrone.picture || require('@/assets/images/drone-image.jpg')"
:style="{width: '300px'}"
/>
</a-descriptions-item>
<a-descriptions-item label="실시간 정보" :span="4">
<div class="des-sub-title">현재 위치</div>
<div class="des-sub-cont-grid-4" style="margin-bottom: 10px;">
<div>
<div>경도</div>
<div>{{ selectedLastLog.longitude ? selectedLastLog.longitude.toFixed(3) : '?' }}</div>
</div>
<div>
<div>위도</div>
<div>{{ selectedLastLog.latitude ? selectedLastLog.latitude.toFixed(3) : '?' }}</div>
</div>
<div>
<div>이동거리</div>
<div>{{ Math.floor(getDetailData.distance + selectedLastLog.distance) }}m</div>
</div>
<div>
<div>운용시간</div>
<div>{{ getTimeDiff(foundSchedule.startTime) }}</div>
</div>
</div>
<div class="des-sub-cont-grid-2" style="margin-bottom: 10px;">
<div>
<div class="des-sub-title">현재 속도</div>
<div class="des-sub-cont-grid-2">
<div>
<div>수평 속도</div>
<div>{{ selectedLastLog.horizontalSpeed }}km/h</div>
</div>
<div>
<div>수직 속도</div>
<div>{{ selectedLastLog.verticalSpeed }}km/h</div>
</div>
</div>
</div>
<div>
<div class="des-sub-title">현재 고도</div>
<div class="des-sub-cont-grid-2">
<div>
<div>지면고도</div>
<div>{{ selectedLastLog.aboveGroundLevel }}m</div>
</div>
<div>
<div>해발고도</div>
<div>{{ selectedLastLog.aboveSeaLevel }}m</div>
</div>
</div>
</div>
</div>
</a-descriptions-item>
<a-descriptions-item label="스케쥴" :span="4" :style="{padding: '0px'}">
<div class="des-sub-cont-grid-2" style="margin-bottom: 10px;">
<div>
<div class="des-sub-title">시작 스케쥴</div>
<div>
<div>날짜/시간</div>
<div style="margin-bottom: 5px;">{{ mc_dateTime(foundSchedule.startTime) || '?' }}</div>
<div>경도</div>
<div style="margin-bottom: 5px;">{{ foundSchedule.startLongitude || '?' }}</div>
<div>위도</div>
<div style="margin-bottom: 5px;">{{ foundSchedule.startLatitude || '?' }}</div>
</div>
</div>
<div>
<div class="des-sub-title">실제 시작</div>
<div>
<div>날짜/시간</div>
<div style="margin-bottom: 5px;">{{ droneLogs.length !== 0 ? mc_dateTime(droneLogs[0].createdAt) : "?" }}</div>
<div>경도</div>
<div style="margin-bottom: 5px;">{{ droneLogs.length !== 0 ? droneLogs[0].longitude : "?" }}</div>
<div>위도</div>
<div style="margin-bottom: 5px;">{{ droneLogs.length !== 0 ? droneLogs[0].latitude : "?" }}</div>
</div>
</div>
</div>
<div class="des-sub-title">종료 스케쥴</div>
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr">
<div>
<div>날짜/시간</div>
<div>{{ mc_dateTime(foundSchedule.terminateTime) || '?' }}</div>
</div>
<div>
<div>경도</div>
<div>{{ foundSchedule.terminateLongitude || '?' }}</div>
</div>
<div>
<div>위도</div>
<div>{{ foundSchedule.terminateLatitude || '?' }}</div>
</div>
</div>
</a-descriptions-item>
<a-descriptions-item label="드론 정보" :span="6">
<div class="des-sub-cont-grid-4">
<div>
<div class="des-sub-title">제조사</div>
<div>{{ foundDrone.maker || '?' }}</div>
</div>
<div>
<div class="des-sub-title">종류</div>
<div>{{ foundDrone.usageName || '?' }}</div>
</div>
<div>
<div class="des-sub-title">제원</div>
<div>{{ foundDrone.specification || '?' }}</div>
</div>
<div>
<div class="des-sub-title">무게</div>
<div>{{ foundDrone.weight }}g</div>
</div>
</div>
</a-descriptions-item>
</a-descriptions>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
head() {
return {
title: 'Drone',
meta: [
{
hid: 'database',
name: 'Descriptions',
content: 'DroneWeb-Content',
},
],
};
},
watch: {
getSelectedLastLog: {
deep: true,
handler(newVal) {
this.selectedLastLog = newVal;
},
},
},
data() {
return {
selectedLastLog: {},
};
},
computed: {
...mapGetters('Drone/drone', {
getDetailData: 'getDetailData',
getSelectedLastLog: 'getSelectedLastLog',
}),
foundSchedule() {
return this.getDetailData.foundSchedule || {};
},
foundDrone() {
return this.getDetailData.foundDrone || {};
},
droneLogs() {
return this.getDetailData.droneLogs || [];
},
selectedLastLog() {
return this.getSelectedLastLog || {};
},
},
methods: {
getTimeDiff(startTime) {
const totalSeconds = this.$dayjs().diff(this.$dayjs(startTime), 's');
const seconds = totalSeconds % 60;
const minutes = Math.floor(totalSeconds / 60) % 60;
const hours = Math.floor(totalSeconds / 3600);
return `${hours}:${minutes}:${seconds}`;
},
},
};
</script>
<style lang="scss">
.des-sub-title {
font-size: 16px;
font-weight: 900;
}
.des-sub-cont-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
}
.des-sub-cont-grid-4 {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
.des-sub-cont {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
</style>
<template>
<div>
<img
@click="clickFilterBtn"
class="filterBtn"
:src="require('@/static/img/filter.png')"/>
</div>
</template>
<script>
import CloseBox from '@/components/_Common/CloseBox/closeBox';
import { mapActions, mapGetters } from 'vuex';
export default {
head() {
return {
title: 'Drone',
meta: [
{
hid: 'database',
name: 'Descriptions',
content: 'DroneWeb-Content',
},
],
};
},
components: {
CloseBox,
},
props: {},
data() {
return {
filterMode: false,
filteredDroneList: [],
manufacturerValue: [],
weightValue: [0, 50],
weightMarks: {
0: '0kg',
50: '50kg+',
},
altitudeValue: [0, 200],
altitudeMarks: {
0: '0m',
200: '200m+',
},
speedValue: [0, 100],
speedMarks: {
0: '0km/h',
100: '100km/h+',
},
filmingOptions: ['True', 'False'],
filmingValue: 'True',
};
},
computed: {
...mapGetters('Etc', {
getMakers: 'getMakers',
}),
...mapGetters('Drone', {
getLogFilter: 'drone/getLogFilter',
getFixedDroneList: 'list/getFixedDroneList',
}),
},
created() {
},
methods: {
...mapActions('Drone/drone', {
setLogFilter: 'setLogFilter',
clearLogFilter: 'clearLogFilter',
}),
clickFilterBtn() {
this.$emit('changeSearchMode', false)
this.$emit('changeFilterMode', true)
},
clickClose() {
this.$emit('clickClose');
},
changeManufacturer(value) {
this.manufacturerValue = value;
this.filteredDroneList = this.getFixedDroneList.filter((v) => !!this.manufacturerValue.find((e) => v.maker === e));
},
changeWeight(weight) {
this.weightValue = weight;
},
changeAltitude(altitude) {
this.altitudeValue = altitude;
},
changeSpeed(speed) {
this.speedValue = speed;
},
applyFilter() {
this.setLogFilter({
checkFilter: true,
maker: this.manufacturerValue,
filteredDroneList: this.filteredDroneList,
weight: this.weightValue,
altitude: this.altitudeValue,
speed: this.speedValue,
filming: this.filmingValue,
});
},
clickReset() {
this.clearLogFilter();
this.filteredDroneList = [];
this.manufacturerValue = [];
this.weightValue = [0, 50];
this.altitudeValue = [0, 200];
this.speedValue = [0, 100];
this.filmingValue = [];
},
},
};
</script>
<style lang="scss">
</style>
<template>
<div>
<CloseBox
@clickClose="clickClose"
>
<template v-slot:header>
드론 필터
</template>
<template v-slot:body>
<a-alert
v-if="getLogFilter.checkFilter"
message="필터가 적용된 상태입니다."
type="info" show-icon
class="filter-alert"
/>
<div :style="{padding: '0 10px 0 5px', marginTop: getLogFilter.checkFilter ? '-5px' : '-20px'}">
<div class="label">
<span>제조사</span>
</div>
<a-select
:value="manufacturerValue"
mode="tags"
style="width: 100%;"
placeholder="제조사"
@change="changeManufacturer">
<a-select-option v-for="(maker,index) in getMakers"
:key="index"
:value="maker"
>
{{ maker }}
</a-select-option>
</a-select>
<div class="label"><span>무게</span></div>
<a-slider
:value="weightValue"
range
:marks="weightMarks"
:max="50"
:min="0"
@change="changeWeight"
/>
<div class="label"><span>고도</span></div>
<a-slider
:value="altitudeValue"
range
:marks="altitudeMarks"
:max="200"
:min="0"
@change="changeAltitude"/>
<div class="label"><span>속력</span></div>
<a-slider
:value="speedValue"
range
:marks="speedMarks"
:max="100"
:min="0"
@change="changeSpeed"/>
<div style="display: flex; justify-content: space-between; text-align: right; margin-top: 40px">
<a-button @click="applyFilter">
<a-icon type="check"/>
필터 적용
</a-button>
<a-button @click="clickReset">
<a-icon type="reload"/>
필터 리셋
</a-button>
</div>
</div>
</template>
</CloseBox>
</div>
</template>
<script>
import CloseBox from '@/components/_Common/CloseBox/closeBox';
import { mapActions, mapGetters } from 'vuex';
export default {
head() {
return {
title: 'Drone',
meta: [
{
hid: 'database',
name: 'Descriptions',
content: 'DroneWeb-Content',
},
],
};
},
components: {
CloseBox,
},
props: {},
data() {
return {
filteredDroneList: [],
manufacturerValue: [],
weightValue: [0, 50],
weightMarks: {
0: '0kg',
50: '50kg+',
},
altitudeValue: [0, 200],
altitudeMarks: {
0: '0m',
200: '200m+',
},
speedValue: [0, 100],
speedMarks: {
0: '0km/h',
100: '100km/h+',
},
filmingOptions: ['True', 'False'],
filmingValue: 'True',
};
},
computed: {
...mapGetters('Etc', {
getMakers: 'getMakers',
}),
...mapGetters('Drone', {
getLogFilter: 'drone/getLogFilter',
getFixedDroneList: 'list/getFixedDroneList',
}),
},
created() {
},
methods: {
...mapActions('Drone/drone', {
setLogFilter: 'setLogFilter',
clearLogFilter: 'clearLogFilter',
}),
clickClose() {
this.$emit('clickClose');
},
changeManufacturer(value) {
this.manufacturerValue = value;
this.filteredDroneList = this.getFixedDroneList.filter((v) => !!this.manufacturerValue.find((e) => v.maker === e));
},
changeWeight(weight) {
this.weightValue = weight;
},
changeAltitude(altitude) {
this.altitudeValue = altitude;
},
changeSpeed(speed) {
this.speedValue = speed;
},
applyFilter() {
this.setLogFilter({
checkFilter: true,
maker: this.manufacturerValue,
filteredDroneList: this.filteredDroneList,
weight: this.weightValue,
altitude: this.altitudeValue,
speed: this.speedValue,
filming: this.filmingValue,
});
},
clickReset() {
this.clearLogFilter();
this.filteredDroneList = [];
this.manufacturerValue = [];
this.weightValue = [0, 50];
this.altitudeValue = [0, 200];
this.speedValue = [0, 100];
this.filmingValue = [];
},
},
};
</script>
<style lang="scss">
</style>
<template>
<div>
<img
class="searchBtn"
@click="clickSearchBtn"
:src="require('@/static/img/search.png')"/>
</div>
</template>
<script>
import CloseBox from '@/components/_Common/CloseBox/closeBox';
import { mapGetters } from 'vuex';
export default {
head() {
return {
title: 'Drone',
meta: [
{
hid: 'database',
name: 'Descriptions',
content: 'DroneWeb-Content',
},
],
};
},
components: {
CloseBox,
},
props: {},
data() {
return {
searchValue: null,
searchType: 'modelName',
searchData: [],
dataSource: [],
searchMode: false,
};
},
computed: {
...mapGetters('Etc', {
getMakerDroneList: 'getMakerDroneList',
}),
...mapGetters('Drone', {
getLogFilter: 'drone/getLogFilter',
getDroneLogs: 'drone/getDroneLogs',
}),
},
created() {
},
methods: {
async onSearch(value) {
const params = {};
params[this.searchType] = value;
const result = await this.$store.dispatch('Drone/list/fetchListContents', params);
const objectDrone = {};
this.getDroneLogs.forEach((e) => {
objectDrone[Number(e.droneId)] = true;
});
this.searchData = [...result.drones.filter((v) => objectDrone[Number(v.id)])];
},
clickSearchBtn() {
this.$emit('changeFilterMode', false)
this.$emit('changeSearchMode', true)
},
clickClose() {
this.$emit('clickClose');
this.searchMode = false;
},
clickListItem(item) {
console.log('click', item);
},
},
};
</script>
<style lang="scss">
</style>
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
No preview for this file type
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.