parent
a33d102393
commit
b294df9cf5
@ -0,0 +1,17 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
import { IsNumber, IsOptional, IsString } from 'class-validator'; |
||||||
|
|
||||||
|
export class CreateSensorDataDto { |
||||||
|
@ApiProperty({ example: 23.56, description: '센서 측정값(숫자)' }) |
||||||
|
@IsNumber() |
||||||
|
value!: number; |
||||||
|
|
||||||
|
@ApiProperty({ |
||||||
|
example: '2025-08-12T10:30:00Z', |
||||||
|
required: false, |
||||||
|
description: '기록 시각(ISO). 없으면 서버가 now() 사용', |
||||||
|
}) |
||||||
|
@IsOptional() |
||||||
|
@IsString() |
||||||
|
recordedAt?: string; |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; |
||||||
|
|
||||||
|
export class CreateSensorGroupDto { |
||||||
|
@ApiProperty({ description: '센서 그룹 이름', example: 'Sensor group 1' }) |
||||||
|
@IsNotEmpty() |
||||||
|
@IsString() |
||||||
|
name: string; |
||||||
|
|
||||||
|
@ApiProperty({ description: '설명', example: 'desc...', required: false }) |
||||||
|
@IsString() |
||||||
|
@IsOptional() |
||||||
|
description?: string; |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; |
||||||
|
|
||||||
|
export class CreateSensorDto { |
||||||
|
@ApiProperty({ description: '소속 센서 그룹 ID', example: 1 }) |
||||||
|
@IsInt() |
||||||
|
@Min(1) |
||||||
|
groupId!: number; |
||||||
|
|
||||||
|
@ApiProperty({ description: '센서 이름', example: '센서 1' }) |
||||||
|
@IsString() |
||||||
|
@IsNotEmpty() |
||||||
|
name!: string; |
||||||
|
|
||||||
|
@ApiProperty({ description: '센서 단위', example: '℃', required: false }) |
||||||
|
@IsString() |
||||||
|
@IsOptional() |
||||||
|
unit?: string; |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
|
||||||
|
export class SensorDataResponseDto { |
||||||
|
@ApiProperty({ example: 999 }) |
||||||
|
id!: number; |
||||||
|
|
||||||
|
@ApiProperty({ example: 23.56 }) |
||||||
|
value!: number; |
||||||
|
|
||||||
|
@ApiProperty({ example: '2025-08-12T10:30:00.000Z' }) |
||||||
|
recordedAt!: Date; |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
|
||||||
|
export class SensorGroupResponseDto { |
||||||
|
@ApiProperty({ example: 10 }) |
||||||
|
id!: number; |
||||||
|
|
||||||
|
@ApiProperty({ example: 'Factory A Line 1' }) |
||||||
|
name!: string; |
||||||
|
|
||||||
|
@ApiProperty({ example: 'Main production line group', required: false }) |
||||||
|
description?: string; |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import { ApiProperty } from '@nestjs/swagger'; |
||||||
|
|
||||||
|
export class SensorResponseDto { |
||||||
|
@ApiProperty({ example: 101 }) |
||||||
|
id!: number; |
||||||
|
|
||||||
|
@ApiProperty({ example: 'Temp Sensor 01' }) |
||||||
|
name!: string; |
||||||
|
|
||||||
|
@ApiProperty({ example: '°C', required: false }) |
||||||
|
unit?: string; |
||||||
|
|
||||||
|
@ApiProperty({ example: 10, description: '소속 그룹 ID' }) |
||||||
|
groupId!: number; |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { SensorsController } from './sensors.controller'; |
||||||
|
|
||||||
|
describe('SensorsController', () => { |
||||||
|
let controller: SensorsController; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
controllers: [SensorsController], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
controller = module.get<SensorsController>(SensorsController); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(controller).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,24 @@ |
|||||||
|
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; |
||||||
|
import { SensorsService } from './sensors.service'; |
||||||
|
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; |
||||||
|
import { Sensor } from './entities/sensor.entity'; |
||||||
|
|
||||||
|
@ApiTags('센서') |
||||||
|
@Controller('sensors') |
||||||
|
export class SensorsController { |
||||||
|
constructor(private readonly sensorService: SensorsService) {} |
||||||
|
|
||||||
|
@Get() |
||||||
|
@ApiOperation({ summary: '센서 목록 조회' }) |
||||||
|
@ApiOkResponse({ description: '센서 목록', type: [Sensor] }) |
||||||
|
async findAll(): Promise<Sensor[]> { |
||||||
|
return this.findAll(); |
||||||
|
} |
||||||
|
|
||||||
|
@Get(':id') |
||||||
|
@ApiOperation({ summary: '특정 센서 조회' }) |
||||||
|
@ApiOkResponse({ description: '센서', type: [Sensor] }) |
||||||
|
async findOne(@Param('id', ParseIntPipe) id: number): Promise<Sensor | null> { |
||||||
|
return this.findOne(id); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { SensorsService } from './sensors.service'; |
||||||
|
|
||||||
|
describe('SensorsService', () => { |
||||||
|
let service: SensorsService; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
providers: [SensorsService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
service = module.get<SensorsService>(SensorsService); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(service).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,148 @@ |
|||||||
|
import { |
||||||
|
BadRequestException, |
||||||
|
ForbiddenException, |
||||||
|
Injectable, |
||||||
|
NotFoundException, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||||
|
import { Sensor } from './entities/sensor.entity'; |
||||||
|
import { Repository } from 'typeorm'; |
||||||
|
import { SensorGroup } from './entities/sensor-group.entity'; |
||||||
|
import { SensorData } from './entities/sensor-data.entity'; |
||||||
|
import { User } from 'src/users/entities/user.entity'; |
||||||
|
import { CreateSensorGroupDto } from './dto/create-sensor-group.dto'; |
||||||
|
import { SensorGroupResponseDto } from './dto/sensor-group-response.dto'; |
||||||
|
import { CreateSensorDto } from './dto/create-sensor.dto'; |
||||||
|
import { SensorResponseDto } from './dto/sensor-response.dto'; |
||||||
|
import { SensorDataResponseDto } from './dto/sensor-data-response.dto'; |
||||||
|
import { CreateSensorDataDto } from './dto/create-sensor-data.dto'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class SensorsService { |
||||||
|
constructor( |
||||||
|
@InjectRepository(SensorGroup) private readonly groupRepo: Repository<SensorGroup>, |
||||||
|
@InjectRepository(Sensor) private readonly sensorRepo: Repository<Sensor>, |
||||||
|
@InjectRepository(SensorData) private readonly dataRepo: Repository<SensorData>, |
||||||
|
@InjectRepository(User) private readonly userRepo: Repository<User>, |
||||||
|
) {} |
||||||
|
|
||||||
|
//#region SensorGroup
|
||||||
|
async createGroup( |
||||||
|
currentUserId: number, |
||||||
|
dto: CreateSensorGroupDto, |
||||||
|
): Promise<SensorGroupResponseDto> { |
||||||
|
const me = await this.userRepo.findOne({ where: { id: currentUserId } }); |
||||||
|
if (!me) throw new NotFoundException('User not found'); |
||||||
|
|
||||||
|
const group = this.groupRepo.create({ |
||||||
|
name: dto.name, |
||||||
|
description: dto.description, |
||||||
|
users: [me], |
||||||
|
}); |
||||||
|
|
||||||
|
const saved = await this.groupRepo.save(group); |
||||||
|
return { |
||||||
|
id: saved.id, |
||||||
|
name: saved.name, |
||||||
|
description: saved.description ?? undefined, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async findMyGroups(currentUserId: number): Promise<SensorGroupResponseDto[]> { |
||||||
|
const groups = await this.groupRepo |
||||||
|
.createQueryBuilder('g') |
||||||
|
.innerJoin('g.users', 'u', 'u.id = :uid', { uid: currentUserId }) |
||||||
|
.select(['g.id AS id', 'g.name AS name', 'g.description AS description']) |
||||||
|
.distinct(true) |
||||||
|
.orderBy('g.id', 'DESC') |
||||||
|
.getRawMany<{ id: number; name: string; description: string | null }>(); |
||||||
|
|
||||||
|
return groups.map((g) => ({ id: g.id, name: g.name, description: g.description ?? undefined })); |
||||||
|
} |
||||||
|
|
||||||
|
private async assertMemberOfGroup(currentUserId: number, groupId: number): Promise<SensorGroup> { |
||||||
|
const group = await this.groupRepo.findOne({ where: { id: groupId }, relations: ['users'] }); |
||||||
|
if (!group) throw new NotFoundException('SensorGroup not found'); |
||||||
|
|
||||||
|
const isMember = group.users?.some((u) => u.id === currentUserId); |
||||||
|
if (!isMember) throw new ForbiddenException('No permission on this group'); |
||||||
|
|
||||||
|
return group; |
||||||
|
} |
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Sensor
|
||||||
|
async createSensor(currentUserId: number, dto: CreateSensorDto): Promise<SensorResponseDto> { |
||||||
|
const group = await this.assertMemberOfGroup(currentUserId, dto.groupId); |
||||||
|
const sensor = this.sensorRepo.create({ |
||||||
|
name: dto.name, |
||||||
|
unit: dto.unit, |
||||||
|
group, |
||||||
|
}); |
||||||
|
const saved = await this.sensorRepo.save(sensor); |
||||||
|
|
||||||
|
return { |
||||||
|
id: saved.id, |
||||||
|
name: saved.name, |
||||||
|
unit: saved.unit ?? undefined, |
||||||
|
groupId: group.id, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async findSensorsInGroup(currentUserId: number, groupId: number): Promise<SensorResponseDto[]> { |
||||||
|
await this.assertMemberOfGroup(currentUserId, groupId); |
||||||
|
const sensors = await this.sensorRepo.find({ |
||||||
|
where: { group: { id: groupId } }, |
||||||
|
order: { id: 'DESC' }, |
||||||
|
}); |
||||||
|
|
||||||
|
return sensors.map((s) => ({ |
||||||
|
id: s.id, |
||||||
|
name: s.name, |
||||||
|
unit: s.unit ?? undefined, |
||||||
|
groupId, |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
private async assertMemeberOfSensor(currentUserId: number, sensorId: number): Promise<Sensor> { |
||||||
|
const sensor = await this.sensorRepo.findOne({ |
||||||
|
where: { id: sensorId }, |
||||||
|
relations: ['group', 'group.users'], |
||||||
|
}); |
||||||
|
if (!sensor) throw new NotFoundException('Sensor not found'); |
||||||
|
|
||||||
|
const isMember = sensor.group?.users?.some((u) => u.id === currentUserId); |
||||||
|
if (!isMember) throw new ForbiddenException('No permission on this sensor'); |
||||||
|
|
||||||
|
return sensor; |
||||||
|
} |
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region SensorData
|
||||||
|
async createSensorData( |
||||||
|
currentUserId: number, |
||||||
|
sensorId: number, |
||||||
|
dto: CreateSensorDataDto, |
||||||
|
): Promise<SensorDataResponseDto> { |
||||||
|
const sensor = await this.assertMemeberOfSensor(currentUserId, sensorId); |
||||||
|
const recordedAt = dto.recordedAt ? new Date(dto.recordedAt) : new Date(); |
||||||
|
if (Number.isNaN(recordedAt.getTime())) throw new BadRequestException('Invalid recoredAt'); |
||||||
|
|
||||||
|
const data = this.dataRepo.create({ sensor, value: dto.value, recordedAt }); |
||||||
|
const saved = await this.dataRepo.save(data); |
||||||
|
|
||||||
|
return { |
||||||
|
id: saved.id, |
||||||
|
value: saved.value, |
||||||
|
recordedAt: saved.recordedAt, |
||||||
|
}; |
||||||
|
} |
||||||
|
//#endregion
|
||||||
|
async findAll(): Promise<Sensor[]> { |
||||||
|
return this.sensorRepo.find({ relations: ['group'] }); |
||||||
|
} |
||||||
|
|
||||||
|
async findOne(id: number): Promise<Sensor | null> { |
||||||
|
return this.sensorRepo.findOne({ where: { id }, relations: ['group'] }); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue