260318:0931 Fix UUID and UTF
Build and Deploy / deploy (push) Failing after 9m41s

This commit is contained in:
admin
2026-03-18 09:31:49 +07:00
parent 3abef2c745
commit 56def2d323
21 changed files with 459 additions and 151 deletions
+1
View File
@@ -101,6 +101,7 @@ import { MigrationModule } from './modules/migration/migration.module';
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
charset: 'utf8mb4',
autoLoadEntities: true,
synchronize: false, // Production Ready: false
}),
@@ -45,7 +45,9 @@ export class FileStorageService {
*/
async upload(file: Express.Multer.File, userId: number): Promise<Attachment> {
const tempId = uuidv4();
const fileExt = path.extname(file.originalname);
// Fix: แปลงชื่อไฟล์จาก Latin1 → UTF-8 (Multer/busboy decodes as Latin1 by default)
const originalFilename = this.fixMulterFilename(file.originalname);
const fileExt = path.extname(originalFilename);
const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.tempDir, storedFilename);
@@ -62,7 +64,7 @@ export class FileStorageService {
// 3. สร้าง Record ใน Database
const attachment = this.attachmentRepository.create({
originalFilename: file.originalname,
originalFilename,
storedFilename: storedFilename,
filePath: tempPath, // เก็บ path ปัจจุบันไปก่อน
mimeType: file.mimetype,
@@ -197,6 +199,22 @@ export class FileStorageService {
return { stream, attachment };
}
/**
* แก้ปัญหา Multer/busboy ถอดรหัสชื่อไฟล์เป็น Latin1 แทน UTF-8
* ทำให้ภาษาไทยกลายเป็น mojibake (เช่น ผรม → 脿赂聹脿赂拢脿赂隆)
* วิธีแก้: แปลง latin1 bytes กลับเป็น UTF-8
*/
private fixMulterFilename(originalname: string): string {
try {
const decoded = Buffer.from(originalname, 'latin1').toString('utf8');
// ตรวจสอบว่า decoded string มี valid UTF-8 characters
// ถ้า originalname เป็น ASCII อยู่แล้ว ผลลัพธ์จะเหมือนเดิม
return decoded;
} catch {
return originalname;
}
}
private calculateChecksum(buffer: Buffer): string {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
+1
View File
@@ -7,6 +7,7 @@ export const databaseConfig: TypeOrmModuleOptions = {
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'Center#2025',
database: process.env.DB_DATABASE || 'lcbp3_dev',
charset: 'utf8mb4',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
synchronize: false,
@@ -35,13 +35,17 @@ import { RequirePermission } from '../../common/decorators/require-permission.de
import { Audit } from '../../common/decorators/audit.decorator';
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
import { User } from '../user/entities/user.entity';
import { ProjectService } from '../project/project.service';
@ApiTags('Drawings - AS Built')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/asbuilt')
export class AsBuiltDrawingController {
constructor(private readonly asBuiltDrawingService: AsBuiltDrawingService) {}
constructor(
private readonly asBuiltDrawingService: AsBuiltDrawingService,
private readonly projectService: ProjectService
) {}
@Post()
@ApiOperation({ summary: 'Create new AS Built Drawing' })
@@ -74,6 +78,10 @@ export class AsBuiltDrawingController {
@ApiResponse({ status: 200, description: 'List of AS Built Drawings' })
@RequirePermission('drawing.view')
async findAll(@Query() searchDto: SearchAsBuiltDrawingDto) {
const project = await this.projectService.findOneByUuid(
searchDto.projectUuid
);
searchDto.projectId = project.id;
return this.asBuiltDrawingService.findAll(searchDto);
}
@@ -22,6 +22,7 @@ import { RequirePermission } from '../../common/decorators/require-permission.de
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
import { ProjectService } from '../project/project.service';
@ApiTags('Contract Drawings')
@ApiBearerAuth()
@@ -29,7 +30,8 @@ import { User } from '../user/entities/user.entity';
@Controller('drawings/contract')
export class ContractDrawingController {
constructor(
private readonly contractDrawingService: ContractDrawingService
private readonly contractDrawingService: ContractDrawingService,
private readonly projectService: ProjectService
) {}
// Force rebuild for DTO changes
@@ -47,7 +49,11 @@ export class ContractDrawingController {
@Get()
@ApiOperation({ summary: 'Search Contract Drawings' })
@RequirePermission('document.view') // สิทธิ์ ID 31: ดูเอกสารทั่วไป
findAll(@Query() searchDto: SearchContractDrawingDto) {
async findAll(@Query() searchDto: SearchContractDrawingDto) {
const project = await this.projectService.findOneByUuid(
searchDto.projectUuid
);
searchDto.projectId = project.id;
return this.contractDrawingService.findAll(searchDto);
}
@@ -37,6 +37,7 @@ import { DrawingMasterDataController } from './drawing-master-data.controller';
// Modules
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
import { UserModule } from '../user/user.module';
import { ProjectModule } from '../project/project.module';
@Module({
imports: [
@@ -62,6 +63,7 @@ import { UserModule } from '../user/user.module';
]),
FileStorageModule,
UserModule,
ProjectModule,
],
providers: [
ShopDrawingService,
@@ -1,4 +1,4 @@
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
import { IsNumber, IsOptional, IsString, IsUUID, Min } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
@@ -6,10 +6,15 @@ import { Type } from 'class-transformer';
* DTO for searching/filtering AS Built Drawings
*/
export class SearchAsBuiltDrawingDto {
@ApiProperty({ description: 'Project ID' })
@ApiProperty({ description: 'Project UUID' })
@IsUUID()
projectUuid!: string;
@ApiPropertyOptional({ description: 'Project ID (resolved internally)' })
@Type(() => Number)
@IsNumber()
projectId!: number;
@IsOptional()
projectId?: number;
@ApiPropertyOptional({ description: 'Filter by Main Category ID' })
@Type(() => Number)
@@ -1,11 +1,14 @@
import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator';
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchContractDrawingDto {
@IsUUID()
projectUuid!: string;
@IsOptional()
@IsInt()
@Type(() => Number)
@IsNotEmpty()
projectId!: number; // จำเป็น: ใส่ !
projectId?: number;
@IsOptional()
@IsInt()
@@ -1,10 +1,14 @@
import { IsInt, IsOptional, IsString } from 'class-validator';
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchShopDrawingDto {
@IsUUID()
projectUuid!: string;
@IsOptional()
@IsInt()
@Type(() => Number)
projectId!: number; // จำเป็น: ใส่ !
projectId?: number;
@IsOptional()
@IsInt()
@@ -21,13 +21,17 @@ import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
import { Audit } from '../../common/decorators/audit.decorator'; // Import
import { ProjectService } from '../project/project.service';
@ApiTags('Shop Drawings')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/shop')
export class ShopDrawingController {
constructor(private readonly shopDrawingService: ShopDrawingService) {}
constructor(
private readonly shopDrawingService: ShopDrawingService,
private readonly projectService: ProjectService
) {}
@Post()
@ApiOperation({ summary: 'Create new Shop Drawing with initial revision' })
@@ -40,7 +44,11 @@ export class ShopDrawingController {
@Get()
@ApiOperation({ summary: 'Search Shop Drawings' })
@RequirePermission('drawing.view')
findAll(@Query() searchDto: SearchShopDrawingDto) {
async findAll(@Query() searchDto: SearchShopDrawingDto) {
const project = await this.projectService.findOneByUuid(
searchDto.projectUuid
);
searchDto.projectId = project.id;
return this.shopDrawingService.findAll(searchDto);
}