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