Peace 1 month ago
parent a33d102393
commit b294df9cf5
  1. 17
      backend/src/sensors/dto/create-sensor-data.dto.ts
  2. 14
      backend/src/sensors/dto/create-sensor-group.dto.ts
  3. 19
      backend/src/sensors/dto/create-sensor.dto.ts
  4. 12
      backend/src/sensors/dto/sensor-data-response.dto.ts
  5. 12
      backend/src/sensors/dto/sensor-group-response.dto.ts
  6. 15
      backend/src/sensors/dto/sensor-response.dto.ts
  7. 3
      backend/src/sensors/entities/sensor-group.entity.ts
  8. 3
      backend/src/sensors/entities/sensor.entity.ts
  9. 18
      backend/src/sensors/sensors.controller.spec.ts
  10. 24
      backend/src/sensors/sensors.controller.ts
  11. 7
      backend/src/sensors/sensors.module.ts
  12. 18
      backend/src/sensors/sensors.service.spec.ts
  13. 148
      backend/src/sensors/sensors.service.ts
  14. 3
      backend/src/users/users.controller.ts

@ -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;
}

@ -11,6 +11,9 @@ export class SensorGroup extends TimestampedEntity {
@Column({ unique: true })
name: string;
@Column()
description: string;
@ManyToMany(() => User, (user) => user.sensorGroups)
users: User[];

@ -11,6 +11,9 @@ export class Sensor extends TimestampedEntity {
@Column({ unique: true })
name: string;
@Column()
unit: string;
@ManyToOne(() => SensorGroup, (group) => group.sensors, { onDelete: 'CASCADE' }) // FK Onwer
group: SensorGroup;

@ -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);
}
}

@ -3,8 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { SensorGroup } from './entities/sensor-group.entity';
import { Sensor } from './entities/sensor.entity';
import { SensorData } from './entities/sensor-data.entity';
import { SensorsService } from './sensors.service';
import { SensorsController } from './sensors.controller';
import { User } from 'src/users/entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([SensorGroup, Sensor, SensorData])],
imports: [TypeOrmModule.forFeature([SensorGroup, Sensor, SensorData, User])],
providers: [SensorsService],
controllers: [SensorsController],
})
export class SensorsModule {}

@ -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'] });
}
}

@ -3,7 +3,7 @@ import { UsersService } from './users.service';
import { ChangePasswordDto } from './dto/change-password.dto';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthRequest } from 'src/common/interfaces/auth-request.interface';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { UserInfoResponseDto } from './dto/user-info-response.dto';
@ApiTags('사용자')
@ -14,6 +14,7 @@ export class UsersController {
@Patch('password')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: '비밀번호 변경' })
@ApiOkResponse({ description: '성공', type: UserInfoResponseDto })
async changePassword(
@Request() req: AuthRequest,

Loading…
Cancel
Save