Skip to content

1 · Repository map

บทนี้เป็นแผนที่ของ recruitment-api ครอบคลุม stack ที่ใช้จริง, โครงสร้างโฟลเดอร์, โมดูลทั้งหมด, entity ทั้งหมด, การจัดการไฟล์แนบ, การทำงานของ GCP integration และ Dockerfile — เพื่อให้อ่านบทถัดไป (core pipeline, persistence, integrations) ได้โดยไม่ต้องเปิดไฟล์ต้นทางซ้ำ

Identity ของ repository

ฟิลด์ค่าหลักฐาน
Repositoryrecruitment-apiworkspace root
Package namerest-structurerecruitment-api/package.json (branch: prod):2
Canonical branchprod (322 tracked files)workspace rule table
Stack ที่ตรวจพบจริงNestJS 9 + TypeORM ^0.3.10 + MSSQL (mssql ^9.1.1) + TypeScript 4.7recruitment-api/package.json (branch: prod):27-33,47,61,86
Entry pointrecruitment-api/src/main.ts (branch: prod):15
Default port9330 (PORT=9330 ใน .env, ตรงกับ EXPOSE 9330 ใน Dockerfile)recruitment-api/Dockerfile (branch: prod):25
Global API prefix${BASE_PATH}api/v1recruitment-api/src/main.ts (branch: prod):24,26

หมายเหตุสำคัญ: workspace rule เดิมเรียก repo นี้ว่า "NestJS + Sequelize/Mongoose" แต่ในไฟล์ package.json ไม่มี dependency ของ sequelize, mongoose หรือ mysql2 สำหรับ production เลย — ORM ที่ใช้จริงคือ TypeORM ต่อ Microsoft SQL Server (MSSQL) ผ่าน package mssql ส่วน mysql ^2.18.1 ที่ประกาศไว้เป็น dependency residual ที่ไม่พบการ import ใน src/ ระหว่างการสำรวจรอบนี้ — ระวังการอ้างถึง stack ผิดจากเอกสารเก่า

Tech stack ที่ใช้จริง

recruitment-api/package.json (branch: prod):26-87 ครอบคลุม dependency ทั้งหมด กลุ่มที่ทำงานจริงตามการ import ในโค้ด:

กลุ่มPackageใช้ทำอะไรหลักฐานการใช้
Framework@nestjs/core ^9.0.0, @nestjs/platform-express ^9.0.0Web framework หลักsrc/main.ts, src/app.module.ts
ORM + DB drivertypeorm ^0.3.10, mssql ^9.1.1TypeORM ต่อ MSSQLsrc/app.module.ts:6,30-53, config/configuration.ts:11
Config@nestjs/config ^2.2.0โหลด env ทั้งหมดผ่าน configuration() factorysrc/app.module.ts:26-29, config/configuration.ts
Authpassport-jwt ^4.0.0, passport ^0.6.0, jsonwebtoken ^9.0.1Global JWT guard + self-managed JWT คู่ขนานlibs/jwt/jwt-auth.guard.ts, src/modules/auth/auth.service.ts:8
Validationclass-validator ^0.13.2, class-transformer ^0.5.1ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }) ที่ main.ts:40src/main.ts:40
Swagger@nestjs/swagger ^6.1.3Document endpoint ที่ ${BASE_PATH}docssrc/main.ts:5,47-48
PDFpuppeteer ^19.7.5 + ejs ^3.1.9render HTML template เป็น PDF สำหรับสัญญาจ้าง/ใบเปรียบเทียบผู้สมัคร/ประกาศผลสอบsrc/modules/contract/contract.service.ts:37,39, src/modules/jobRequest/jobRequest.service.ts:10,9
Excelexceljs ^4.4.0export ตารางเปรียบเทียบผู้สมัครและข้อมูลสัญญาsrc/modules/candidate/candidate.service.ts:53, src/modules/contract/contract.service.ts:42
Emailnodemailer ^6.9.1ส่ง email ผ่าน SMTP Outlook สำหรับประกาศผล/สัญญาจ้าง/ลบข้อมูลlibs/utils/contractEmailService.ts:3,16-23
Rich textdraft-js ^0.11.7, draftjs-to-html ^0.9.1, draftjs-md-converter ^1.5.2เนื้อหาประกาศรับสมัครจาก back-office ที่ editor เป็น draft-jspackage.json (branch: prod):39-41
HTTP clientaxios ^1.3.6เรียก workforce-api (mapping HRMI, ตรวจพนักงานสภากาชาด), และ vtrc-commonsrc/modules/contract/contract.service.ts:8,72-73, src/modules/candidate/candidate.service.ts:29
Zipadm-zip ^0.5.10รวมไฟล์แนบหลายไฟล์เป็น archivesrc/modules/contract/contract.service.ts:41
Cryptocrypto-js ^4.1.1ใช้ใน libs/utils/encrypt.util.tslibs/utils/encrypt.util.ts
Loggingpino 7, pino-pretty 7logger ผ่าน I3GatewayLoggerlibs/log/logs.middleware.ts

โครงสร้างโฟลเดอร์

recruitment-api/
├── src/
│   ├── main.ts                      bootstrap + global middleware
│   ├── app.module.ts                root module: 4 TypeORM connection + 13 feature module
│   ├── app.controller.ts            root ping endpoint
│   ├── app.service.ts
│   ├── dto/                         ไฟล์ DTO ทั้งหมด (รวมกันกลาง)
│   ├── entities/                    38 entity ไฟล์ (37 entity class + base.ts)
│   └── modules/                     13 feature module
│       ├── jobRequest/                คำขอเปิดรับสมัคร (jobRequestHd)
│       ├── jobList/                    ประกาศรับสมัครเผยแพร่ (ส่วนใหญ่ @Public())
│       ├── candidate/                 ใบสมัครผู้สมัคร (candidateHd) — service ใหญ่สุด 3,037 บรรทัด
│       ├── appProfile/                โปรไฟล์กลางของผู้สมัคร (แยกจาก candidateHd)
│       ├── announcement/              ประกาศผลสอบ/สัมภาษณ์ (qualifiedHd)
│       ├── contract/                  สัญญาจ้าง (contractHd/contractEmployment)
│       ├── blacklist/                 บัญชีดำผู้สมัคร
│       ├── deleteHistory/             คำขอลบข้อมูลส่วนบุคคลตาม PDPA
│       ├── auth/                      JWT/refresh token ของ candidate portal
│       ├── dropdown/                  master lookup กลาง (mDropdown)
│       ├── share/                     แชร์ประกาศงานไปโซเชียล (@Public())
│       ├── checkApiOn/                health check (@Public())
│       └── uploadFiles/               base64 → local disk
├── libs/
│   ├── jwt/                           jwt.strategy.ts + jwt-auth.guard.ts
│   ├── common/                        8 utility (base64-to-file, remove-file, date-service, ...)
│   ├── utils/                         10 utility (running.util, audit.util, otpService.util, 4 email service)
│   ├── pdfTemplate/                   ejs template (สัญญาจ้าง, ประกาศผล, ใบเปรียบเทียบ, เอกสารลบบัญชี)
│   ├── log/                           I3GatewayLogger และ logs.middleware
│   ├── exception/                     HttpExceptionFilter
│   ├── interceptors/                  LoggingInterceptor
│   ├── constant/                      country list (สัญชาติ/ที่อยู่)
│   └── views/                         (ใช้กับ ejs render)
├── config/
│   └── configuration.ts               TypeORM connection factory 4 ชุด + apiDeviceKey
├── sql/                               manual SQL migration แยกตาม ticket (RC-xxx, AU-xxx, UAT-xxx, AI-xxx, Del-xxx)
├── assets/images/                     men.png / women.png (default avatar)
├── docs/                              Swagger documentConfig
├── test/                              jest spec
├── Dockerfile                         node:20-alpine + chromium
├── docker-compose.yml                 (ว่าง — มีแค่ skeleton services:)
├── docker-compose-dev.yml             azure-sql-edge บน port 1433 สำหรับ dev
├── bitbucket-pipelines.yml            CI/CD → GCP Container Registry
├── vtrc-gcp-sa-key.json               GCP service account key (commit ไว้ — sensitive)
├── package.json / yarn.lock
└── README.md

Module organization — 13 feature module

recruitment-api/src/app.module.ts (branch: prod):54-66 imports ทั้ง 13 module ตามลำดับ:

#ModuleEntity หลักService ไลน์หน้าที่
1JobRequestModulejobRequestHd1,444คำขอเปิดรับสมัคร + ประวัติเปลี่ยนแปลง + PDF export + restructure history
2DropdownModulemDropdown24master lookup กลาง
3UploadModule— (เขียน disk)168base64 → local disk ที่ /uploads/<prefix>/<YYYY-MM-DD>/
4AppProfileModuleappProfileHd1,603โปรไฟล์กลางของผู้สมัคร 1 คน (edu/exp/capability/license/reference/personality)
5JobListModule— (อ่าน jobRequestHd+jobReqAdvertisement)195public listing + OTP request/validate + logout
6CandidateModulecandidateHd3,037ใบสมัคร 1 ใบต่อ 1 ตำแหน่ง + PDF/Excel compare + AI compare
7ShareModule37แชร์ประกาศงานไปโซเชียล (@Public())
8CheckApiOnModule(เล็กมาก)health check (@Public())
9BlacklistModuletbBlacklist352บัญชีดำผู้สมัคร
10AnnouncementModulequalifiedHd2,645ประกาศผลสอบ/สัมภาษณ์ + ส่ง email + PDF
11AuthModuleauthData269login/refresh/revoke ของ candidate portal
12ContractModulecontractHd1,895สัญญาจ้าง + HRMI mapping + email export-to-HRMI
13DeleteHistoryModuledeleteHistory1,105คำขอลบข้อมูลส่วนบุคคลตาม PDPA

recruitment-api/src/app.module.ts (branch: prod):69 ลงทะเบียน provider กลาง 4 ตัวที่อยู่นอก feature module: AppService, JwtStrategy, AuthDataService, AuthRepository — เพื่อให้ AuthDataService (ที่ใช้ jsonwebtoken โดยตรง) ถูก inject ได้จากหลาย module (เช่น candidate.service.ts เรียก authDataService.validateTokenOnlyVTRC())

Boot sequence

ลำดับการเริ่มต้นของ service ตามที่อ่านได้จาก recruitment-api/src/main.ts (branch: prod):15-54:

  1. NestFactory.create<NestExpressApplication>(AppModule, { logger: new I3GatewayLogger('System') }) — เลือก Express adapter เพราะใช้ useStaticAssets ของ NestExpress
  2. อ่าน port จาก ConfigService (fallback 3000 ถ้าไม่มี — แต่ .env production ตั้ง 9330)
  3. คำนวณ prefix จาก process.env.BASE_PATH ต่อด้วย api/v1 (main.ts:24)
  4. app.setGlobalPrefix(prefix) — ทุก endpoint ขึ้นต้นด้วย ${BASE_PATH}api/v1/
  5. app.useStaticAssets(...) สองครั้ง — /uploads และ /uploads-migrate ที่ prefix /recruitment-api/ (main.ts:27-33) — บ่งชี้ว่ามีการ migrate ไฟล์เก่าจาก path หนึ่งไปอีก path
  6. app.disable('etag') — ปิด etag (เหมาะกับ static asset ที่เปลี่ยนบ่อย)
  7. bodyParser.json({ limit: '50mb' }) และ urlencoded({ limit: '50mb' }) — รองรับ base64 ไฟล์แนบขนาดใหญ่ใน body
  8. useGlobalFilters(new HttpExceptionFilter()) (main.ts:37)
  9. useGlobalInterceptors(new LoggingInterceptor()) (main.ts:38)
  10. useGlobalGuards(new JwtAuthGuard(reflector)) (main.ts:39) — ทุก endpoint ต้องผ่าน JWT guard เว้นแต่ติด @Public()
  11. useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) (main.ts:40) — reject field ที่ไม่ได้ประกาศใน DTO
  12. enableCors({ origin: '*', allowedHeaders: '*', methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS' }) (main.ts:41-45)
  13. SwaggerModule.setup('${BASE_PATH}docs', app, document) (main.ts:47-48) — เปิด Swagger UI โดยไม่มี env flag ปิดใน production
  14. app.listen(port, '0.0.0.0', callback) (main.ts:49) — bind ทุก interface

38 entities ทั้งหมด

recruitment-api/src/entities/ (branch: prod) มีไฟล์ 38 ไฟล์ (37 class entity + base.ts ที่เป็น base class) ทุก entity ที่ extends BaseEntity จะมี field กลาง 7 ตัวจาก recruitment-api/src/entities/base.ts (branch: prod):3-27: id (PK auto-increment), createdAt, createdByName, createdBy, updatedAt, updatedBy, updatedByName, isDeleted (soft delete แบบ bit)

กลุ่ม Job Request (9 entity):

Entity classชื่อตารางหน้าที่ไฟล์
JobRequestHdjobRequestHdคำขอเปิดรับสมัครหลัก (documentNo, recruitmentStatus, responsiblePerson)jobRequestHd.entity.ts:16
JobReqPosition(auto)ตำแหน่งที่ขอเปิด — เก็บ manpowerNo, remainingManpower ที่ลดทุกครั้งที่มีการ hiredjobReqPosition.entity.ts
JobReqCapability(auto)ทักษะที่ต้องการjobReqCapability.entity.ts
JobReqCapabilityLang(auto)ทักษะแยกตามภาษา (th/en)jobReqCapabilityLang.entity.ts
JobReqDegree(auto)คุณวุฒิการศึกษาที่ต้องการjobReqDegree.entity.ts
JobReqField(auto)สาขาวิชาที่ต้องการjobReqField.entity.ts
JobReqProLicense(auto)ใบอนุญาตประกอบวิชาชีพที่ต้องการjobReqProLicense.entity.ts
JobReqHistory(auto)ประวัติการเปลี่ยนสถานะคำขอjobReqHistory.entity.ts
JobReqAdvertisement(auto)เนื้อหาประกาศ (draft-js JSON) + ช่วงเวลาเปิดรับjobReqAdvertisement.entity.ts
JobReqExtendHistory(auto)ประวัติขอขยายเวลารับสมัครjobReqExtendHistory.entity.ts
JobReqRestructureHistory(auto)ประวัติขอปรับโครงสร้างตำแหน่ง (เปลี่ยน org unit/position ระหว่างทาง)jobReqRestructureHistory.entity.ts

กลุ่ม Candidate + AppProfile (10 entity):

Entity classชื่อตารางหน้าที่ไฟล์
CandidateHdcandidateHdใบสมัคร 1 ใบต่อ 1 ตำแหน่ง (documentNo prefix RCC, status, applyPositionCode, appProfileId, client)candidateHd.entity.ts
CandidateHistory(auto)log กิจกรรมต่อใบสมัครcandidateHistory.entity.ts
CandidateStatus(auto)สถานะละเอียด (draft, decline, hired, inContract, blacklist, ...)candidateStatus.entity.ts
AppProfileHdappProfileHdโปรไฟล์กลางของผู้สมัคร 1 คน (profileKey, empCode ถ้าเป็นพนักงาน, email)appProfileHd.entity.ts
AppProfileEdu(auto)ประวัติการศึกษาappProfileEdu.entity.ts
AppProfileExp(auto)ประสบการณ์ทำงาน (มี isCurrent flag ที่ contract module ใช้หา position ปัจจุบัน)appProfileExp.entity.ts
AppProfileCapability(auto)ทักษะappProfileCapability.entity.ts
AppProfileCapabilityLang(auto)ทักษะแยกภาษาappProfileCapabilityLang.entity.ts
AppProfileProLicense(auto)ใบอนุญาตประกอบวิชาชีพappProfileProLicense.entity.ts
AppProfileReference(auto)บุคคลอ้างอิงappProfileReference.entity.ts
AppProfilePersonality(auto)บุคลิก/นิสัยappProfilePersonality.entity.ts

กลุ่ม Qualified/Announcement (4 entity):

Entity classชื่อตารางหน้าที่ไฟล์
QualifiedHdqualifiedHdรอบประกาศผล (สอบข้อเขียน/สัมภาษณ์ onsite/online) + วันที่/เวลา/สถานที่qualifiedHd.entity.ts
QualifiedCandidate(auto)ผลลัพธ์ต่อผู้สมัคร (ผ่าน/ไม่ผ่าน)qualifiedCandidate.entity.ts
QualifiedCandidateBackup(auto)snapshot สำรองก่อน overwrite (audit)qualifiedCandidateBackup.entity.ts
QualifiedHistory(auto)ประวัติการแก้ผลประกาศqualifiedHistory.entity.ts

กลุ่ม Contract (4 entity):

Entity classชื่อตารางหน้าที่ไฟล์
ContractHdcontractHdเอกสารสัญญาจ้างหลักcontractHd.entity.ts
ContractEmploymentcontractEmploymentรายละเอียดการจ้าง + mapping code HRMI ทั้งหมด (orgUnitCodeHRMI, positionCareerCodeHRMI, positionManageCodeHRMI, positionTechnicalCodeHRMI, employmentTypeHRMI, employeeLevelCodeHRMI)contractEmployment.entity.ts
ContractDtcontractDtรายละเอียดย่อยของสัญญา (เงื่อนไข/ผลประโยชน์/สถานะสมรส/สถานะทหาร)contractDt.entity.ts
ContractHistory(auto)ประวัติเปลี่ยนสถานะสัญญา (draft/confirm/exportToHRMi/cancelExportToHRMi/decline)contractHistory.entity.ts

กลุ่ม Utility/Cross-cutting (8 entity):

Entity classชื่อตารางหน้าที่ไฟล์
AuthDataauthDataเก็บ token/refreshToken ต่อ email ของ candidate portal — server-side token storeauthData.entity.ts
TbBlacklisttbBlacklistบัญชีดำผู้สมัคร (เลขบัตรประชาชน + เหตุผล)backlist.entity.ts (สังเกตว่าชื่อไฟล์พิมพ์ผิด — backlist แทน blacklist)
DeleteHistorydeleteHistoryบันทึกคำขอลบข้อมูลส่วนบุคคลตาม PDPAdeleteHistory.entity.ts
EmailConfigEntityemailConfigรายชื่อผู้รับแจ้งเตือนต่อประเภทเหตุการณ์emailConfig.entity.ts
MDropdownmDropdownmaster lookup กลางmDropdown.entity.ts
RcAttachfilercAttachFileไฟล์แนบ polymorphic ผ่าน relatedTable/relatedIdrcAttachFile.entity.ts
PermissionConfigEntitypermissionConfigตั้งค่าสิทธิ์ menu (ใช้กับ JwtAuthGuard.checkRole)permissionConfigEntity.entity.ts
CheckEmpRedCrossEntitycheckEmpRedCrossผลตรวจสอบว่าผู้สมัครเป็น/เคยเป็นพนักงานสภากาชาดcheckRedcross.entity.ts

File upload — ไฟล์แนบเก็บที่ไหน

recruitment-api/src/modules/uploadFiles/upload.controller.ts (branch: prod):13,20 เปิด endpoint POST /upload (private ตาม default) และ GET /upload/:id (@Public()) โดย payload เป็น base64 inline ใน JSON body — ไม่ใช้ multipart/form-data เหมือน benefit-api หรือ NestJS multer pattern ทั่วไป

ลำดับการจัดเก็บ (recruitment-api/src/modules/uploadFiles/upload.service.ts (branch: prod):28-130):

  1. controller รับ UploadDto ที่มี attachFiles: [{ fileBase64, fileType, fileNameOriginal, fileSize, sort }]
  2. upload.service.ts:55-62 validate MIME type ที่อนุญาต: image/jpeg, image/jpg, image/png, application/pdf, video/mp4 เท่านั้น — type อื่น reject ทันที
  3. คำนวณ path: /uploads/${prefixPath}/${dirDate}/ เช่น /uploads/candidate/2026-07-13/ (upload.service.ts:65)
  4. เรียก base64ToFile(base64Str, extensionFile, dirUpload, email, idCard) (libs/common/base64-to-file.ts:7)
  5. base64ToFile:14-31 สร้าง directory process.cwd() + path + '/' ด้วย fsExtra.ensureDirSync() ถ้ายังไม่มี แล้ว fs.writeFileSync(url, base64, 'base64')
  6. ชื่อไฟล์สร้างจาก base64-to-file.ts:18-24: prefix = 3 ส่วนของเลขบัตรประชาชนกลับด้าน (หลักที่ 2-3 + หลักที่ 5-6 + หลักที่ 11-13) + _ + random 6 ตัวอักษร + _ + HHmmsss + .${extension} ตัวอย่าง: ba_dc5_321_aB3xY9_1430521.pdf
  7. return { dirAttach, fileName } และ upload.service.ts:107-122 pack กลับเป็น comma-separated string หลาย field (fileNameMulti, filePathWithFileNameMulti, fileTypeMulti, fileSizeMulti, sortMulti)

Static asset ถูก mount ที่ main.ts:27-33 ด้วย app.useStaticAssets(join(process.cwd(), '/uploads'), { prefix: '/recruitment-api/' }) — หมายความว่า URL สาธารณะของไฟล์จะเป็น https://${BASE_PATH}/recruitment-api/uploads/candidate/2026-07-13/... ส่วน prefix ที่ตั้งไว้ที่ app.setGlobalPrefix('${BASE_PATH}api/v1') ใช้กับ REST endpoint เท่านั้น ไม่รวม static asset

ลบไฟล์ผ่าน POST /upload/remove_file_from_path (upload.controller.ts:30-33) เรียก removeFile(body, header) (upload.service.ts:149-167) ที่ verify JWT และ check decodedToken.iss === 'redcross.or.th:vtrc' ก่อนเรียก removeFileFromPath(param.filePath) (libs/common/remove-file.ts) — เฉพาะ token ที่ vtrc-api ออกให้เท่านั้นที่ลบไฟล์ได้

การจัดการ polymorphic attach

recruitment-api/src/entities/rcAttachFile.entity.ts เก็บไฟล์แนบของหลายตารางผ่านสองฟิลด์ relatedTable (ชื่อตารางเช่น 'candidateHd', 'jobRequestHd', 'contractHd') และ relatedId (PK ของแถวนั้น) — pattern polymorphic association เดียวกับ mAttachfile ของ benefit-api แต่ชื่อตารางต่างกัน (rcAttachFile ไม่ share กับ mAttachfile)

ตัวอย่างการ pack ข้อมูลกลับเข้า attachFile object ใน candidate create — candidate.service.ts:459-479 สร้าง object { relatedTable: 'candidateHd', sort, fileName, fileType, fileSize, filePath: '${dirAttach.replace('/uploads','')}/${fileName}', description, documentType, ...StampAudit(...) } แล้ว cascade save ผ่าน entityManager.save(CandidateHd, hd) (candidate.service.ts:530)

GCP integration — HRMI export ผ่าน Bitbucket Pipeline

recruitment-api/vtrc-gcp-sa-key.json (branch: prod) เป็น GCP service account key ที่ commit ไว้ใน repo (sensitive — ไม่ควร log เนื้อหา) ไฟล์นี้ใช้ในกระบวนการ deploy ผ่าน bitbucket-pipelines.yml ไม่ใช่ใน runtime ของ NestJS service เอง

recruitment-api/bitbucket-pipelines.yml (branch: prod):39-89 อธิบาย flow การ push image ไป GCP Container Registry:

  1. branch cicd, develop, uat, prod จะทำ build-test (npm test) + build-docker-image + push-image-to-registry (bitbucket-pipelines.yml:99-132)
  2. ขั้น push-image-to-registry (bitbucket-pipelines.yml:39-89) ใช้ image google/cloud-sdk:alpine
  3. authentication ใช้ $GCP_KEY_FILE ที่เป็น Bitbucket deployment variable (base64 encoded) ถอดรหัสเป็น ./gcloud-api-key.json (bitbucket-pipelines.yml:47) — ไม่ใช่ vtrc-gcp-sa-key.json ที่ commit ไว้
  4. gcloud auth activate-service-account --key-file gcloud-api-key.json (bitbucket-pipelines.yml:49)
  5. login GCR ผ่าน cat ./gcloud-api-key.json | docker login -u _json_key --password-stdin https://$GCP_REGISTRY_HOSTNAME (bitbucket-pipelines.yml:53)
  6. tag image สองชุด: commit-short-hash + :latest แล้ว push ไป $GCP_REGISTRY_HOSTNAME/$GCP_PROJECT_ID/$GCP_IMAGE_NAME--$BITBUCKET_BRANCH (bitbucket-pipelines.yml:56-68)

ความสัมพันธ์ระหว่าง vtrc-gcp-sa-key.json (commit) กับ $GCP_KEY_FILE (Bitbucket variable): สันนิษฐานว่าไฟล์ที่ commit คือ source ของค่าที่ encode แล้วเก็บใน Bitbucket — เป็น pattern เดียวกับที่พบใน vtrc-api/vtrc-gcp-sa-key.json (workspace rule) — ทั้งคู่เป็น debt ด้านความปลอดภัย (ดู V1 scorecard SEC-RECRUIT-01)

ในแง่ HRMI export ที่ runtime: recruitment-api ไม่ได้ ส่งข้อมูล HRMI ตรงไป GCP ผ่าน HTTP — แต่ส่ง email แจ้งทีมฐานว่ามีบุคลากรใหม่ให้บันทึกเข้าระบบ HRMI (contract.service.ts:1618,1640 เรียก contractEmailService.sendEmail(emailDataContract, 'export')) การเชื่อมต่อ GCP เกิดแค่ในขั้น deploy (push docker image) ไม่เกี่ยวกับ business flow ของ HRMI

Database topology

recruitment-api/src/app.module.ts (branch: prod):30-53 ประกาศ TypeORM connection 4 ชุด, recruitment-api/config/configuration.ts (branch: prod):11-74 เป็น factory ของแต่ละ connection:

Connection nameenv prefixdatabasesynchronizeสถานะการใช้จริง
mainDB_*recruitmenttrue เฉพาะ NODE_ENV === 'dev' (configuration.ts:18-19)ใช้จริงหนักสุด — ทุก entity ของ recruitment
aiDB_AI_*recruitment_aifalse (configuration.ts:34-35)ใช้จริง — raw query/query builder จาก candidate.repositories.ts, jobList.repositories.ts, appProfile.repositories.ts สำหรับผลวิเคราะห์ AI
vtrcVTRC_DB_*vtrc (user hrmiworkforce)falseประกาศไว้ แต่ ไม่พบ @InjectEntityManager('vtrc') ใน source tree
workforceWORKFORCE_DB_*dbWorkforce (user hrmiworkforce)falseประกาศไว้ แต่ ไม่พบ @InjectEntityManager('workforce') ใน source tree

autoLoadEntities เปิดเฉพาะ main connection (configuration.ts:18) — ai, vtrc, workforce ตั้ง autoLoadEntities: false และ synchronize: false เพราะเป็น schema ของ service อื่น (read-only จากมุม recruitment-api)

การเปลี่ยนแปลง schema ใน production ไม่ได้ผ่าน TypeORM migration แต่ผ่าน manual SQL scripts ใน sql/ folder (สคริปต์ข้ามหลายปี ตั้งแต่ sql/prod/old/20130912-AuthTable.sql จนถึงสคริปต์ ticket ล่าสุด RC-xxx, AU-xxx, UAT-xxx, AI-xxx, Del-xxx) — เป็น debt ด้าน database change management (ไม่ใช่ TypeORM migration หรือ Flyway/Liquibase)

Dockerfile + docker-compose analysis

recruitment-api/Dockerfile (branch: prod):1-27:

  • Base image: node:20-alpine — Node.js 20 LTS
  • ENV TZ="Asia/Bangkok" — timezone สำหรับ puppeteer และ logging
  • ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium — ใช้ chromium ของระบบ ไม่โหลดของ puppeteer เอง
  • apk add --no-cache nmap chromium harfbuzz freetype ttf-freefont nss — ชุดเดียวกับ benefit-api สำหรับ puppeteer; การมี nmap แปลกเล็กน้อย (เครื่องมือสแกน port) — ไม่พบการอ้างถึง nmap ใน source tree ระหว่างการสำรวจรอบนี้ (UNVERIFIED ว่าจงใจหรือเป็น leftover)
  • COPY package.json yarn.lock ./ แล้ว RUN yarn install --frozen-lockfile — build layer caching สำหรับ dependency
  • COPY . . แล้ว RUN yarn build — compile TypeScript ผ่าน nest build (package.json:13)
  • EXPOSE 9330
  • CMD ["yarn", "start:prod"]package.json:18 คือ node dist/src/main

recruitment-api/docker-compose.yml (branch: prod):1-3 ว่างเปล่าแทบจะทั้งไฟล์ (มีแค่ version: "3.8" และ services: skeleton) — ใช้จริงใน production ผ่าน Kubernetes หรือ Cloud Run ไม่ใช่ docker-compose

recruitment-api/docker-compose-dev.yml (branch: prod):1-15 สำหรับ local dev เท่านั้น — มี service database เดียวที่ใช้ mcr.microsoft.com/azure-sql-edge:latest เปิด port 1433, password abcABC123, mount /var/opt/mssql และเชื่อม external network vtrc-api_default (เพื่อให้ dev environment ของ recruitment-api คุยกับ vtrc-api ใน compose อื่นได้)

Workspace rule ที่ต้องระวัง

  • Branch default: workspace rule ระบุว่า recruitment-api canonical อยู่ที่ branch prod (322 tracked files) — default branch master อาจ empty/sparse เสมอ ต้อง checkout prod ก่อนอ่านหรือแก้โค้ด
  • File backup pattern: หลีกเลี่ยงการแก้ไฟล์ที่ลงท้ายด้วย _bk, _bk.js, _copy, _bk.ts, app.service.ts_bk — workspace rule ห้ามแตะ
  • Build artifact: vtrc-api/api/src/dist/, centralize-api/dist/, */dist/ ห้ามแก้ — recruitment-api/dist/ จะถูกล้างทุกครั้งตอน prebuild (package.json:12)
  • yarn.lock: ใช้ yarn เท่านั้น (ไม่ใช่ npm) เพราะ Dockerfile ใช้ yarn install --frozen-lockfile และมี yarn.lock — workspace rule ระบุไว้ชัด
  • GCP service account key: recruitment-api/vtrc-gcp-sa-key.json อยู่ใน "Never log, never exfiltrate, never paste into chat" — เป็น debt ที่ต้อง rotate แต่ห้าม expose

ตัวเลขที่ควรจำ

ตัวเลขค่าที่มา
Feature module13src/app.module.ts:54-66
Entity class37 (38 ไฟล์รวม base.ts)src/entities/ count
TypeORM connection ประกาศ / ใช้จริง4 / 2src/app.module.ts:30-53
Service ที่ใหญ่สุดcandidate.service.ts (3,037) → announcement.service.ts (2,645) → contract.service.ts (1,895) → jobRequest.service.ts (1,444) → appProfile.service.ts (1,603)wc -l
Endpoint ใน CandidateController22candidate.controller.ts (22 method)
Default port9330.env, Dockerfile:25
Body size limit50 MBmain.ts:35-36
ประเภทไฟล์แนบที่อนุญาต5 (jpeg, jpg, png, pdf, mp4)upload.service.ts:55

ไป บท 2 Core pipeline