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