# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
## Batch 10: Controllers 91-100

**วันที่ตรวจสอบ:** 2026-05-08  
**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers

---

## Controllers ที่ตรวจสอบในชุดนี้

1. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts)
2. [SocketController.ts](src/controllers/SocketController.ts)
3. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts)
4. [ApiManageController.ts](src/controllers/ApiManageController.ts)
5. [ImportDataController.ts](src/controllers/ImportDataController.ts)
6. [ExRetirementController.ts](src/controllers/ExRetirementController.ts)
7. [IssuesController.ts](src/controllers/IssuesController.ts)
8. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts)
9. [MyController.ts](src/controllers/MyController.ts)
10. [MainController.ts](src/controllers/MainController.ts)

---

## รายการปัญหาที่พบ

### 1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop

**File & Location:** [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts:159-182) - `syncByProfileIds()` method

**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle

**Root Cause:**
```typescript
for (const profileId of profileIds) {
  try {
    const success = await this.keycloakAttributeService.syncOnOrganizationChange(
      profileId,
      profileType,
    );
    // ...
  } catch (error: any) {
    result.failed++;
    result.details.push({ profileId, status: "failed", error: error.message });
  }
}
```

แม้ว่าจะมี `try-catch` ภายใน loop แต่การ catch error แล้วเพียงแค่บันทึกผลลัพธ์ อาจไม่เพียงพอสำหรับบางกรณี:
- หาก `syncOnOrganizationChange` มี Promise rejection ที่ไม่ถูก handle อย่างถูกต้องภายใน service
- หากเกิด error ระหว่างการทำงานของ loop ที่ไม่ใช่จาก `syncOnOrganizationChange` เช่น จาก `result.details.push()`
- Error ที่เกิดขึ้นอาจเป็น unhandled rejection หาก service ไม่ return Promise อย่างถูกต้อง

**Recommended Fix:**
```typescript
@Post("sync-profiles-batch")
async syncByProfileIds(
  @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
) {
  const { profileIds, profileType } = request;

  if (!profileIds || profileIds.length === 0) {
    throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
  }

  if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
    throw new HttpError(
      HttpStatus.BAD_REQUEST,
      "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
    );
  }

  const result = {
    total: profileIds.length,
    success: 0,
    failed: 0,
    details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
  };

  // เพิ่ม timeout protection และ error handling ที่ดีขึ้น
  const SYNC_TIMEOUT = 30000; // 30 วินาทีต่อ profile

  for (const profileId of profileIds) {
    try {
      // เพิ่ม Promise.race เพื่อป้องกันการ hang
      const syncPromise = this.keycloakAttributeService.syncOnOrganizationChange(
        profileId,
        profileType,
      );
      
      const timeoutPromise = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT)
      );

      const success = await Promise.race([syncPromise, timeoutPromise]) as boolean;

      if (success) {
        result.success++;
        result.details.push({ profileId, status: "success" });
      } else {
        result.failed++;
        result.details.push({
          profileId,
          status: "failed",
          error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID",
        });
      }
    } catch (error: any) {
      result.failed++;
      // เพิ่ม validation ก่อน push เพื่อป้องกัน crash จาก invalid data
      const errorMessage = error?.message || String(error);
      result.details.push({ 
        profileId, 
        status: "failed", 
        error: errorMessage.substring(0, 500) // จำกัดความยาว
      });
      
      // Log error สำหรับ monitoring
      console.error(`[KeycloakSync] Failed to sync profile ${profileId}:`, error);
    }
  }

  return new HttpSuccess({
    message: "Batch sync เสร็จสิ้น",
    ...result,
  });
}
```

---

### 2. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Large Loop

**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:219-364) - `UploadFileSqlOfficer()` method

**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle

**Root Cause:**
```typescript
@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
  const OFFICER = await this.OFFICERRepo.find();
  let rowCount = 0;
  // ... 
  for await (const item of OFFICER) {
    rowCount++;
    // ... การประมวลผลข้อมูล ...
    await this.profileRepo.save(profile);
  }
  return new HttpSuccess();
}
```

**ปัญหาที่พบ:**
1. **ไม่มี try-catch รอบ loop** - หากเกิด error ระหว่างการประมวลผล เช่น:
   - Database connection lost
   - Invalid data format
   - Constraint violation
   - Memory overflow
   
   จะทำให้เกิด Unhandled Exception และ **Process Crash**

2. **ไม่มี Error Recovery** - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup

3. **Loading all data at once** - `await this.OFFICERRepo.find()` โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory

4. **No transaction management** - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้

**Recommended Fix:**
```typescript
@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  
  let rowCount = 0;
  let successCount = 0;
  let failedCount = 0;
  const errors: Array<{row: number, citizenId: string, error: string}> = [];
  
  try {
    // ใช้ pagination แทนการโหลดทั้งหมด
    const BATCH_SIZE = 500;
    let offset = 0;
    let hasMore = true;
    
    while (hasMore) {
      const OFFICER = await queryRunner.manager.find(OFFICER, {
        take: BATCH_SIZE,
        skip: offset,
        order: { id: 'ASC' }
      });
      
      if (OFFICER.length === 0) {
        hasMore = false;
        break;
      }
      
      for (const item of OFFICER) {
        rowCount++;
        
        try {
          let type_: any = null;
          let level_: any = null;
          const profile = new Profile();

          const existingProfile = await queryRunner.manager.findOne(Profile, {
            where: { citizenId: item.CIT.toString() },
          });

          if (existingProfile) {
            // ข้ามกรณีมีข้อมูลอยู่แล้ว
            continue;
          }

          // ... การประมวลผลข้อมูลเดิม ...
          
          // ใช้ queryRunner.manager.save แทน this.profileRepo.save
          await queryRunner.manager.save(profile);
          successCount++;
          
        } catch (itemError: any) {
          failedCount++;
          errors.push({
            row: rowCount,
            citizenId: item.CIT?.toString() || 'unknown',
            error: itemError?.message || String(itemError)
          });
          // Log แต่ไม่หยุดการทำงาน
          console.error(`[UploadOfficer] Error at row ${rowCount}:`, itemError);
        }
      }
      
      offset += BATCH_SIZE;
      
      // Commit ทุกๆ batch เพื่อป้องกัน transaction ใหญ่เกินไป
      await queryRunner.commitTransaction();
      await queryRunner.startTransaction();
    }
    
    // Commit transaction สุดท้าย
    await queryRunner.commitTransaction();
    
    return new HttpSuccess({
      message: "อัปโหลดข้อมูลเสร็จสิ้น",
      total: rowCount,
      success: successCount,
      failed: failedCount,
      errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก
    });
    
  } catch (error: any) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถอัปโหลดข้อมูลได้: ${error?.message || 'Unknown error'}`
    );
  } finally {
    await queryRunner.release();
  }
}
```

---

### 3. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Employee Upload

**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:369-496) - `UploadFileSQL()` method

**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle

**Root Cause:**
เหมือนกับปัญหาข้างต้น แต่สำหรับการอัปโหลดข้อมูลลูกจ้างประจำ มีความเสี่ยงเช่นเดียวกัน:
- ไม่มี try-catch ใน loop
- ไม่มี transaction management
- ไม่มี error recovery

**Recommended Fix:**
ใช้ pattern เดียวกับข้อ 2 โดยใช้ QueryRunner สำหรับ transaction management

---

### 4. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Temp Employee Upload

**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:501-633) - `UploadFileSQLTemp()` method

**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle

**Root Cause:**
```typescript
if (item.CIT.toString() == "1101801164891") {
  continue;
}
const existingProfile = await this.profileEmpRepo.findOne({
  where: { employeeClass: "TEMP", citizenId: item.CIT.toString() },
});
if (existingProfile) {
  profile.id = existingProfile.id;
} else {
  continue;
}
```

**ปัญหาเพิ่มเติม:**
1. **Hardcoded citizenId check** - มีการ hardcode เงื่อนไข `item.CIT.toString() == "1101801164891"` ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ
2. **การ skip ที่ไม่ชัดเจน** - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่
3. **ไม่มี error handling** เหมือนปัญหาก่อนหน้า

**Recommended Fix:**
```typescript
@Post("uploadProfile-EmployeeTemp")
async UploadFileSQLTemp(@Request() request: { user: Record<string, any> }) {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  
  let rowCount = 0;
  let successCount = 0;
  let failedCount = 0;
  const errors: Array<{row: number, citizenId: string, error: string}> = [];
  
  try {
    const BATCH_SIZE = 500;
    let offset = 0;
    let hasMore = true;
    
    while (hasMore) {
      const EMPLOYEE = await queryRunner.manager.find(EMPLOYEETEMP, {
        take: BATCH_SIZE,
        skip: offset,
        order: { id: 'ASC' }
      });
      
      if (EMPLOYEE.length === 0) {
        hasMore = false;
        break;
      }
      
      for (const item of EMPLOYEE) {
        rowCount++;
        
        try {
          // เอา hardcode check ออก หรือเปลี่ยนเป็น configurable
          // if (item.CIT.toString() === "1101801164891") {
          //   continue;
          // }
          
          const existingProfile = await queryRunner.manager.findOne(ProfileEmployee, {
            where: { 
              employeeClass: "TEMP", 
              citizenId: item.CIT.toString() 
            },
          });

          let profile: ProfileEmployee;
          
          if (existingProfile) {
            profile = existingProfile;
          } else {
            // สร้าง profile ใหม่ถ้าไม่พบ
            profile = new ProfileEmployee();
            profile.employeeClass = "TEMP";
          }

          // ... การประมวลผลข้อมูลเดิม ...
          
          await queryRunner.manager.save(profile);
          successCount++;
          
        } catch (itemError: any) {
          failedCount++;
          errors.push({
            row: rowCount,
            citizenId: item.CIT?.toString() || 'unknown',
            error: itemError?.message || String(itemError)
          });
          console.error(`[UploadEmployeeTemp] Error at row ${rowCount}:`, itemError);
        }
      }
      
      offset += BATCH_SIZE;
      await queryRunner.commitTransaction();
      await queryRunner.startTransaction();
    }
    
    await queryRunner.commitTransaction();
    
    return new HttpSuccess({
      message: "อัปโหลดข้อมูลลูกจ้างชั่วคราวเสร็จสิ้น",
      total: rowCount,
      success: successCount,
      failed: failedCount,
      errors: errors.slice(0, 100)
    });
    
  } catch (error: any) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถอัปโหลดข้อมูลลูกจ้างชั่วคราวได้: ${error?.message || 'Unknown error'}`
    );
  } finally {
    await queryRunner.release();
  }
}
```

---

### 5. 🟡 HIGH - ExRetirementController.ts - Unhandled External API Error

**File & Location:** [ExRetirementController.ts](src/controllers/ExRetirementController.ts:148-173) - `getToken()` function

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  // ...
  try {
    const formData = new FormData();
    formData.append("ClientID", ClientID);
    formData.append("ClientSecret", ClientSecret);
    const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    const token = res.data.token;
    TokenCache.set(cacheKey, token);
    return token;
  } catch (error) {
    return Promise.reject({ message: "Error occurred", error });
  }
}
```

**ปัญหา:**
1. **Generic error handling** - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป
2. **ไม่มี retry logic** - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ
3. **No timeout** - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด

**Recommended Fix:**
```typescript
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  const cacheKey = `${ClientID}:${ClientSecret}`;
  
  const cachedToken = TokenCache.get(cacheKey);
  if (cachedToken) {
    return cachedToken;
  }

  const MAX_RETRIES = 3;
  const TIMEOUT = 10000; // 10 วินาที
  let lastError: any;

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const formData = new FormData();
      formData.append("ClientID", ClientID);
      formData.append("ClientSecret", ClientSecret);
      
      const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
        headers: {
          "Content-Type": "application/json",
        },
        timeout: TIMEOUT,
      });
      
      const token = res.data.token;
      if (!token) {
        throw new Error('Token not found in response');
      }
      
      TokenCache.set(cacheKey, token);
      return token;
      
    } catch (error: any) {
      lastError = error;
      
      // ไม่ retry หากเป็น client error (4xx)
      if (error.response?.status >= 400 && error.response?.status < 500) {
        break;
      }
      
      // Retry หากเป็น server error หรือ network error
      if (attempt < MAX_RETRIES) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  // Log error สำหรับ monitoring
  console.error(`[ExRetirement] Failed to get token after ${MAX_RETRIES} attempts:`, lastError);
  
  throw new Error(`ไม่สามารถขอ Token ได้: ${lastError?.message || 'Unknown error'}`);
}
```

---

### 6. 🟡 HIGH - ApiWebServiceController.ts - Potential Null Reference

**File & Location:** [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts:67-78) - `listAttribute()` method

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
if (system == "organization") {
  tbMain = "OrgRoot";
  const revision = await this.orgRevisionRepository.findOne({
    select: ["id"],
    where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
  });
  condition = `OrgRoot.orgRevisionId = "${revision?.id}"`;
}
```

**ปัญหา:**
1. **revision อาจเป็น null** - หากไม่พบ revision ที่ตรงตามเงื่อนไข `revision?.id` จะเป็น `undefined`
2. **SQL Injection vulnerability** - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error
3. **ไม่มี error handling** - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception

**Recommended Fix:**
```typescript
@Get("/:system/:code")
async listAttribute(
  @Request() request: RequestWithUserWebService,
  @Path("system")
  system: SystemCode,
  @Path("code") code: string,
  @Query("page") page: number = 1,
  @Query("pageSize") pageSize: number = 100,
): Promise<HttpSuccess | HttpError> {
  try {
    const apiName = await this.apiNameRepository.findOne({
      where: { code },
      select: ["id", "code", "methodApi", "system", "isActive"],
      relations: ["apiAttributes"],
      order: {
        apiAttributes: {
          ordering: "ASC",
        },
      },
    });

    if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ");
    }
    
    await isPermissionRequest(request, apiName.id);
    
    const offset = (page - 1) * pageSize;
    const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`);

    let tbMain: string = "";
    let condition: string = "1=1";
    let revisionId: string | null = null;
    
    if (system == "registry") {
      tbMain = "Profile";
    } else if (system == "registry_emp") {
      tbMain = "ProfileEmployee";
      condition = `ProfileEmployee.employeeClass = "PERM"`;
    } else if (system == "registry_temp") {
      tbMain = "ProfileEmployee";
      condition = `ProfileEmployee.employeeClass = "TEMP"`;
    } else if (system == "organization") {
      tbMain = "OrgRoot";
      const revision = await this.orgRevisionRepository.findOne({
        select: ["id"],
        where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
      });
      
      if (!revision) {
        throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
      }
      
      revisionId = revision.id;
      condition = `OrgRoot.orgRevisionId = :revisionId`;
    } else if (system == "position") {
      tbMain = "PosMaster";
      const revision = await this.orgRevisionRepository.findOne({
        select: ["id"],
        where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
      });
      
      if (!revision) {
        throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
      }
      
      revisionId = revision.id;
      condition = `PosMaster.orgRevisionId = :revisionId`;
    }

    const repo = AppDataSource.getRepository(tbMain);
    const metadata = repo.metadata;

    const relationMap: Record<string, string> = {};
    metadata.relations.forEach((rel) => {
      relationMap[rel.inverseEntityMetadata.name] = rel.propertyName;
    });

    let propertyOtherKey: any[] = [];
    propertyOtherKey = [
      ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)),
    ];

    const queryBuilder = repo.createQueryBuilder(tbMain);

    if (propertyOtherKey.length > 0) {
      propertyOtherKey.forEach((tb) => {
        const relationName = relationMap[tb];
        if (relationName) {
          queryBuilder.leftJoin(
            `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`,
            tb,
          );
        }
      });
    }

    let pk: string = "";
    const primaryColumns = metadata.primaryColumns;
    primaryColumns.forEach((col) => {
      pk = col.propertyName;
      if (!propertyKey.includes(`${tbMain}.${pk}`)) {
        propertyKey.push(`${tbMain}.${pk}`);
      }
    });

    // ใช้ parameterized query แทน string interpolation
    const queryParams: any = {};
    if (revisionId) {
      queryParams.revisionId = revisionId;
    }

    const [items, total] = await queryBuilder
      .select(propertyKey)
      .where(condition, queryParams)
      .orderBy(propertyKey[0], "ASC")
      .skip(offset)
      .take(pageSize)
      .getManyAndCount();

    const data = items.map((item) => {
      const { [pk]: removedPk, ...x } = item;
      return x;
    });

    // save api history after query success
    const history = {
      headerApi: JSON.stringify({
        host: request.headers.host,
        "x-api-key": request.headers["x-api-key"],
        connection: request.headers.connection,
        accept: request.headers.accept,
      }),
      tokenApi: Array.isArray(request.headers["x-api-key"])
        ? request.headers["x-api-key"][0] || ""
        : request.headers["x-api-key"] || "",
      requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`,
      responseApi: "OK",
      ipApi: request.ip,
      codeApi: code,
      apiKeyId: request.user.id,
      apiNameId: apiName.id,
      createdFullName: request.user.name,
      lastUpdateFullName: request.user.name,
    };
    
    try {
      await this.apiHistoryRepository.save(history);
    } catch (historyError) {
      // Log แต่ไม่ให้กระทบต่อ response
      console.error('[ApiWebService] Failed to save history:', historyError);
    }

    return new HttpSuccess({ data: data, total });
    
  } catch (error: any) {
    if (error instanceof HttpError) {
      throw error;
    }
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `เกิดข้อผิดพลาด: ${error?.message || 'Unknown error'}`
    );
  }
}
```

---

### 7. 🟡 MEDIUM - ApiManageController.ts - Missing Transaction Error Handling

**File & Location:** [ApiManageController.ts](src/controllers/ApiManageController.ts:464-518) - `createApi()` method

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
@Post("")
async createApi(
  @Request() req: RequestWithUser,
  @Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    this.validateSuperAdminRole(req.user);
    // ...
    await queryRunner.commitTransaction();
    return new HttpSuccess(apiName.id);
  } catch (error) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(...);
  } finally {
    await queryRunner.release();
  }
}
```

**ปัญหา:**
1. **validateSuperAdminRole อยู่นอก try-catch** - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak
2. **ไม่ validate req.user** ก่อนเรียก `validateSuperAdminRole` - หาก `req.user` เป็น null หรือ undefined จะเกิด error

**Recommended Fix:**
```typescript
@Post("")
async createApi(
  @Request() req: RequestWithUser,
  @Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    // Validate request ก่อน
    if (!req.user) {
      throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้");
    }
    
    this.validateSuperAdminRole(req.user);

    const code = this.generateApiCode();
    const postData = {
      name: apiData.name,
      code,
      pathApi: this.createApiPath(apiData.system as SystemCode, code),
      methodApi: apiData.methodApi || "GET",
      system: apiData.system || "registry",
      isActive: apiData.isActive || false,
      createdUserId: req.user?.sub,
      createdFullName: req.user?.name || "",
    };

    const apiName = await queryRunner.manager.getRepository(ApiName).save(postData);

    if (apiData.apiAttributes?.length) {
      let orderingCounter = 0;
      const attributesToSave = apiData.apiAttributes.flatMap((attr) =>
        attr.propertyKey.map((propertyKey) => ({
          apiNameId: apiName.id,
          tbName: attr.tbName,
          propertyKey,
          ordering: orderingCounter++,
          createdUserId: req.user?.sub,
          createdFullName: req.user?.name || "",
        })),
      );

      await queryRunner.manager.getRepository(ApiAttribute).save(attributesToSave);
    }

    await queryRunner.commitTransaction();
    return new HttpSuccess(apiName.id);
  } catch (error) {
    await queryRunner.rollbackTransaction();
    
    if (error instanceof HttpError) {
      throw error;
    }
    
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      error instanceof Error ? error.message : "เกิดข้อผิดพลาด ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่ในภายหลัง",
    );
  } finally {
    // Ensure release is called even if rollback fails
    try {
      await queryRunner.release();
    } catch (releaseError) {
      console.error('[ApiManage] Failed to release queryRunner:', releaseError);
    }
  }
}
```

---

### 8. 🟡 MEDIUM - DevelopmentRequestController.ts - Unhandled Promise in Parallel Operations

**File & Location:** [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts:349-365) - `newDevelopmentRequest()` method

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
if (body.developmentProjects != null) {
  await Promise.all(
    body.developmentProjects.map(async (x) => {
      let developmentProject = new DevelopmentProject();
      developmentProject.name = x;
      developmentProject.createdUserId = req.user.sub;
      developmentProject.createdFullName = req.user.name;
      developmentProject.lastUpdateUserId = req.user.sub;
      developmentProject.lastUpdateFullName = req.user.name;
      developmentProject.createdAt = new Date();
      developmentProject.lastUpdateUpdatedAt = new Date();
      developmentProject.developmentRequestId = data.id;
      await this.developmentProjectRepository.save(developmentProject, { data: req });
      setLogDataDiff(req, { before, after: developmentProject });
    }),
  );
}
```

**ปัญหา:**
1. **Unhandled Promise rejection** - หาก `save` หรือ `setLogDataDiff` ล้ม จะเกิด unhandled rejection
2. **ไม่มี error handling รายตัว** - หาก project หนึ่งล้ม ทั้ง batch จะล้ม
3. **No cleanup on partial failure** - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database

**Recommended Fix:**
```typescript
if (body.developmentProjects != null) {
  const savedProjects: DevelopmentProject[] = [];
  
  try {
    for (const projectName of body.developmentProjects) {
      try {
        let developmentProject = new DevelopmentProject();
        developmentProject.name = projectName;
        developmentProject.createdUserId = req.user.sub;
        developmentProject.createdFullName = req.user.name;
        developmentProject.lastUpdateUserId = req.user.sub;
        developmentProject.lastUpdateFullName = req.user.name;
        developmentProject.createdAt = new Date();
        developmentProject.lastUpdateUpdatedAt = new Date();
        developmentProject.developmentRequestId = data.id;
        
        const saved = await this.developmentProjectRepository.save(developmentProject, { data: req });
        savedProjects.push(saved);
        
        setLogDataDiff(req, { before: null, after: saved });
      } catch (projectError: any) {
        console.error(`[DevelopmentRequest] Failed to save project ${projectName}:`, projectError);
        // Continue with next project instead of failing entire request
      }
    }
  } catch (batchError: any) {
    console.error('[DevelopmentRequest] Error in projects batch:', batchError);
    // Clean up any successfully saved projects if needed
    if (savedProjects.length > 0) {
      try {
        await this.developmentProjectRepository.delete({ 
          developmentRequestId: data.id 
        });
      } catch (cleanupError) {
        console.error('[DevelopmentRequest] Failed to cleanup projects:', cleanupError);
      }
    }
    throw batchError;
  }
}
```

---

### 9. 🟢 LOW - SocketController.ts - No Error Handling

**File & Location:** [SocketController.ts](src/controllers/SocketController.ts:6-24) - `notify()` method

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  sendWebSocket(
    "socket-notification",
    { success: !payload.error, message: payload.message },
    {
      roles: payload.roles || [],
      userId: payload.userId || [],
    },
  );
}
```

**ปัญหา:**
1. **ไม่มี try-catch** - หาก `sendWebSocket` throw error จะเป็น unhandled exception
2. **ไม่ return ค่า** - ไม่มีการ return HttpSuccess หรือ error response
3. **No validation** - ไม่ validate payload ก่อนใช้งาน

**Recommended Fix:**
```typescript
@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  try {
    // Validate payload
    if (!payload.message || typeof payload.message !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "message ต้องเป็น string ที่ไม่ว่างเปล่า");
    }
    
    // Validate userId and roles
    if (payload.userId && !Array.isArray(payload.userId) && typeof payload.userId !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "userId ต้องเป็น string หรือ array of strings");
    }
    
    if (payload.roles && !Array.isArray(payload.roles) && typeof payload.roles !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "roles ต้องเป็น string หรือ array of strings");
    }

    sendWebSocket(
      "socket-notification",
      { success: !payload.error, message: payload.message },
      {
        roles: payload.roles || [],
        userId: payload.userId || [],
      },
    );
    
    return new HttpSuccess({ 
      message: "ส่งการแจ้งเตือนสำเร็จ",
      notification: {
        type: "socket-notification",
        success: !payload.error,
        message: payload.message,
        roles: payload.roles || [],
        userId: payload.userId || [],
      }
    });
  } catch (error: any) {
    if (error instanceof HttpError) {
      throw error;
    }
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      `ไม่สามารถส่งการแจ้งเตือนได้: ${error?.message || 'Unknown error'}`
    );
  }
}
```

---

### 10. 🟢 LOW - IssuesController.ts - Missing Try-Catch

**File & Location:** [IssuesController.ts](src/controllers/IssuesController.ts:31-39) - `getIssues()` method

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
```typescript
@Get("lists")
async getIssues() {
  const issues = await this.issuesRepository.find({
    order: {
      createdAt: "DESC",
    },
  });
  return new HttpSuccess(issues);
}
```

**ปัญหา:**
- ไม่มี try-catch หาก database connection ล้มหรือ query มีปัญหา จะเกิด unhandled exception

**Recommended Fix:**
```typescript
@Get("lists")
async getIssues() {
  try {
    const issues = await this.issuesRepository.find({
      order: {
        createdAt: "DESC",
      },
    });
    return new HttpSuccess(issues);
  } catch (error: any) {
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถดึงรายการปัญหาได้: ${error?.message || 'Unknown error'}`
    );
  }
}
```

---

## สรุปสถิติ

| ระดับความรุนแรง | จำนวน | รายการ |
|---|---|---|
| 🔴 CRITICAL | 4 | 1, 2, 3, 4 |
| 🟡 HIGH | 2 | 5, 6 |
| 🟡 MEDIUM | 2 | 7, 8 |
| 🟢 LOW | 2 | 9, 10 |

---

## คำแนะนำการจัดลำดับการแก้ไข

### แก้ไขทันที (P0 - Critical)
1. **ImportDataController.ts** - ทั้ง 3 methods (`UploadFileSqlOfficer`, `UploadFileSQL`, `UploadFileSQLTemp`)
   - เพิ่ม transaction management
   - เพิ่ม try-catch ใน loops
   - เพิ่ม pagination แทนการโหลดทั้งหมด

### แก้ไขเร็วๆ นี้ (P1 - High)
2. **KeycloakSyncController.ts** - เพิ่ม timeout protection
3. **ExRetirementController.ts** - ปรับปรุง error handling และ retry logic
4. **ApiWebServiceController.ts** - แก้ null reference issue

### แก้ไขในภายหลัง (P2 - Medium)
5. **ApiManageController.ts** - ปรับปรุง transaction error handling
6. **DevelopmentRequestController.ts** - เพิ่ม error handling สำหรับ parallel operations

### แก้ไขเมื่อว่าง (P3 - Low)
7. **SocketController.ts** - เพิ่ม validation และ error handling
8. **IssuesController.ts** - เพิ่ม try-catch

---

## ข้อเสนอแนะเพิ่มเติม

1. **ใช้ Global Error Handler** - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling
2. **เพิ่ม Health Check** - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API)
3. **Circuit Breaker Pattern** - สำหรับการเรียก external API เพื่อป้องกัน cascade failures
4. **Graceful Shutdown** - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown
5. **Logging Strategy** - เพิ่ม structured logging สำหรับ monitoring และ debugging

---

## ไฟล์รายงานที่เกี่ยวข้อง

- [Batch 1-10 Analysis](../reports/) - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า
- [Security Audit Report](../reports/security-audit.md) - รายงานการตรวจสอบด้านความปลอดภัย
