1 · Repository map
บทนี้เป็นแผนที่ของ recruitment-api ครอบคลุม stack ที่ใช้จริง, โครงสร้างโฟลเดอร์, โมดูลทั้งหมด, entity ทั้งหมด, การจัดการไฟล์แนบ, การทำงานของ GCP integration และ Dockerfile — เพื่อให้อ่านบทถัดไป (core pipeline, persistence, integrations) ได้โดยไม่ต้องเปิดไฟล์ต้นทางซ้ำ
Identity ของ repository
| ฟิลด์ | ค่า | หลักฐาน |
|---|---|---|
| Repository | recruitment-api | workspace root |
| Package name | rest-structure | recruitment-api/package.json (branch: prod):2 |
| Canonical branch | prod (322 tracked files) | workspace rule table |
| Stack ที่ตรวจพบจริง | NestJS 9 + TypeORM ^0.3.10 + MSSQL (mssql ^9.1.1) + TypeScript 4.7 | recruitment-api/package.json (branch: prod):27-33,47,61,86 |
| Entry point | recruitment-api/src/main.ts (branch: prod):15 | — |
| Default port | 9330 (PORT=9330 ใน .env, ตรงกับ EXPOSE 9330 ใน Dockerfile) | recruitment-api/Dockerfile (branch: prod):25 |
| Global API prefix | ${BASE_PATH}api/v1 | recruitment-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.0 | Web framework หลัก | src/main.ts, src/app.module.ts |
| ORM + DB driver | typeorm ^0.3.10, mssql ^9.1.1 | TypeORM ต่อ MSSQL | src/app.module.ts:6,30-53, config/configuration.ts:11 |
| Config | @nestjs/config ^2.2.0 | โหลด env ทั้งหมดผ่าน configuration() factory | src/app.module.ts:26-29, config/configuration.ts |
| Auth | passport-jwt ^4.0.0, passport ^0.6.0, jsonwebtoken ^9.0.1 | Global JWT guard + self-managed JWT คู่ขนาน | libs/jwt/jwt-auth.guard.ts, src/modules/auth/auth.service.ts:8 |
| Validation | class-validator ^0.13.2, class-transformer ^0.5.1 | ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }) ที่ main.ts:40 | src/main.ts:40 |
| Swagger | @nestjs/swagger ^6.1.3 | Document endpoint ที่ ${BASE_PATH}docs | src/main.ts:5,47-48 |
puppeteer ^19.7.5 + ejs ^3.1.9 | render HTML template เป็น PDF สำหรับสัญญาจ้าง/ใบเปรียบเทียบผู้สมัคร/ประกาศผลสอบ | src/modules/contract/contract.service.ts:37,39, src/modules/jobRequest/jobRequest.service.ts:10,9 | |
| Excel | exceljs ^4.4.0 | export ตารางเปรียบเทียบผู้สมัครและข้อมูลสัญญา | src/modules/candidate/candidate.service.ts:53, src/modules/contract/contract.service.ts:42 |
nodemailer ^6.9.1 | ส่ง email ผ่าน SMTP Outlook สำหรับประกาศผล/สัญญาจ้าง/ลบข้อมูล | libs/utils/contractEmailService.ts:3,16-23 | |
| Rich text | draft-js ^0.11.7, draftjs-to-html ^0.9.1, draftjs-md-converter ^1.5.2 | เนื้อหาประกาศรับสมัครจาก back-office ที่ editor เป็น draft-js | package.json (branch: prod):39-41 |
| HTTP client | axios ^1.3.6 | เรียก workforce-api (mapping HRMI, ตรวจพนักงานสภากาชาด), และ vtrc-common | src/modules/contract/contract.service.ts:8,72-73, src/modules/candidate/candidate.service.ts:29 |
| Zip | adm-zip ^0.5.10 | รวมไฟล์แนบหลายไฟล์เป็น archive | src/modules/contract/contract.service.ts:41 |
| Crypto | crypto-js ^4.1.1 | ใช้ใน libs/utils/encrypt.util.ts | libs/utils/encrypt.util.ts |
| Logging | pino 7, pino-pretty 7 | logger ผ่าน I3GatewayLogger | libs/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.mdModule organization — 13 feature module
recruitment-api/src/app.module.ts (branch: prod):54-66 imports ทั้ง 13 module ตามลำดับ:
| # | Module | Entity หลัก | Service ไลน์ | หน้าที่ |
|---|---|---|---|---|
| 1 | JobRequestModule | jobRequestHd | 1,444 | คำขอเปิดรับสมัคร + ประวัติเปลี่ยนแปลง + PDF export + restructure history |
| 2 | DropdownModule | mDropdown | 24 | master lookup กลาง |
| 3 | UploadModule | — (เขียน disk) | 168 | base64 → local disk ที่ /uploads/<prefix>/<YYYY-MM-DD>/ |
| 4 | AppProfileModule | appProfileHd | 1,603 | โปรไฟล์กลางของผู้สมัคร 1 คน (edu/exp/capability/license/reference/personality) |
| 5 | JobListModule | — (อ่าน jobRequestHd+jobReqAdvertisement) | 195 | public listing + OTP request/validate + logout |
| 6 | CandidateModule | candidateHd | 3,037 | ใบสมัคร 1 ใบต่อ 1 ตำแหน่ง + PDF/Excel compare + AI compare |
| 7 | ShareModule | — | 37 | แชร์ประกาศงานไปโซเชียล (@Public()) |
| 8 | CheckApiOnModule | — | (เล็กมาก) | health check (@Public()) |
| 9 | BlacklistModule | tbBlacklist | 352 | บัญชีดำผู้สมัคร |
| 10 | AnnouncementModule | qualifiedHd | 2,645 | ประกาศผลสอบ/สัมภาษณ์ + ส่ง email + PDF |
| 11 | AuthModule | authData | 269 | login/refresh/revoke ของ candidate portal |
| 12 | ContractModule | contractHd | 1,895 | สัญญาจ้าง + HRMI mapping + email export-to-HRMI |
| 13 | DeleteHistoryModule | deleteHistory | 1,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:
NestFactory.create<NestExpressApplication>(AppModule, { logger: new I3GatewayLogger('System') })— เลือก Express adapter เพราะใช้useStaticAssetsของ NestExpress- อ่าน
portจากConfigService(fallback3000ถ้าไม่มี — แต่.envproduction ตั้ง9330) - คำนวณ prefix จาก
process.env.BASE_PATHต่อด้วยapi/v1(main.ts:24) app.setGlobalPrefix(prefix)— ทุก endpoint ขึ้นต้นด้วย${BASE_PATH}api/v1/app.useStaticAssets(...)สองครั้ง —/uploadsและ/uploads-migrateที่ prefix/recruitment-api/(main.ts:27-33) — บ่งชี้ว่ามีการ migrate ไฟล์เก่าจาก path หนึ่งไปอีก pathapp.disable('etag')— ปิด etag (เหมาะกับ static asset ที่เปลี่ยนบ่อย)bodyParser.json({ limit: '50mb' })และurlencoded({ limit: '50mb' })— รองรับ base64 ไฟล์แนบขนาดใหญ่ใน bodyuseGlobalFilters(new HttpExceptionFilter())(main.ts:37)useGlobalInterceptors(new LoggingInterceptor())(main.ts:38)useGlobalGuards(new JwtAuthGuard(reflector))(main.ts:39) — ทุก endpoint ต้องผ่าน JWT guard เว้นแต่ติด@Public()useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))(main.ts:40) — reject field ที่ไม่ได้ประกาศใน DTOenableCors({ origin: '*', allowedHeaders: '*', methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS' })(main.ts:41-45)SwaggerModule.setup('${BASE_PATH}docs', app, document)(main.ts:47-48) — เปิด Swagger UI โดยไม่มี env flag ปิดใน productionapp.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 | ชื่อตาราง | หน้าที่ | ไฟล์ |
|---|---|---|---|
JobRequestHd | jobRequestHd | คำขอเปิดรับสมัครหลัก (documentNo, recruitmentStatus, responsiblePerson) | jobRequestHd.entity.ts:16 |
JobReqPosition | (auto) | ตำแหน่งที่ขอเปิด — เก็บ manpowerNo, remainingManpower ที่ลดทุกครั้งที่มีการ hired | jobReqPosition.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 | ชื่อตาราง | หน้าที่ | ไฟล์ |
|---|---|---|---|
CandidateHd | candidateHd | ใบสมัคร 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 |
AppProfileHd | appProfileHd | โปรไฟล์กลางของผู้สมัคร 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 | ชื่อตาราง | หน้าที่ | ไฟล์ |
|---|---|---|---|
QualifiedHd | qualifiedHd | รอบประกาศผล (สอบข้อเขียน/สัมภาษณ์ 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 | ชื่อตาราง | หน้าที่ | ไฟล์ |
|---|---|---|---|
ContractHd | contractHd | เอกสารสัญญาจ้างหลัก | contractHd.entity.ts |
ContractEmployment | contractEmployment | รายละเอียดการจ้าง + mapping code HRMI ทั้งหมด (orgUnitCodeHRMI, positionCareerCodeHRMI, positionManageCodeHRMI, positionTechnicalCodeHRMI, employmentTypeHRMI, employeeLevelCodeHRMI) | contractEmployment.entity.ts |
ContractDt | contractDt | รายละเอียดย่อยของสัญญา (เงื่อนไข/ผลประโยชน์/สถานะสมรส/สถานะทหาร) | contractDt.entity.ts |
ContractHistory | (auto) | ประวัติเปลี่ยนสถานะสัญญา (draft/confirm/exportToHRMi/cancelExportToHRMi/decline) | contractHistory.entity.ts |
กลุ่ม Utility/Cross-cutting (8 entity):
| Entity class | ชื่อตาราง | หน้าที่ | ไฟล์ |
|---|---|---|---|
AuthData | authData | เก็บ token/refreshToken ต่อ email ของ candidate portal — server-side token store | authData.entity.ts |
TbBlacklist | tbBlacklist | บัญชีดำผู้สมัคร (เลขบัตรประชาชน + เหตุผล) | backlist.entity.ts (สังเกตว่าชื่อไฟล์พิมพ์ผิด — backlist แทน blacklist) |
DeleteHistory | deleteHistory | บันทึกคำขอลบข้อมูลส่วนบุคคลตาม PDPA | deleteHistory.entity.ts |
EmailConfigEntity | emailConfig | รายชื่อผู้รับแจ้งเตือนต่อประเภทเหตุการณ์ | emailConfig.entity.ts |
MDropdown | mDropdown | master lookup กลาง | mDropdown.entity.ts |
RcAttachfile | rcAttachFile | ไฟล์แนบ polymorphic ผ่าน relatedTable/relatedId | rcAttachFile.entity.ts |
PermissionConfigEntity | permissionConfig | ตั้งค่าสิทธิ์ menu (ใช้กับ JwtAuthGuard.checkRole) | permissionConfigEntity.entity.ts |
CheckEmpRedCrossEntity | checkEmpRedCross | ผลตรวจสอบว่าผู้สมัครเป็น/เคยเป็นพนักงานสภากาชาด | 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):
- controller รับ
UploadDtoที่มีattachFiles: [{ fileBase64, fileType, fileNameOriginal, fileSize, sort }] upload.service.ts:55-62validate MIME type ที่อนุญาต:image/jpeg,image/jpg,image/png,application/pdf,video/mp4เท่านั้น — type อื่น reject ทันที- คำนวณ path:
/uploads/${prefixPath}/${dirDate}/เช่น/uploads/candidate/2026-07-13/(upload.service.ts:65) - เรียก
base64ToFile(base64Str, extensionFile, dirUpload, email, idCard)(libs/common/base64-to-file.ts:7) base64ToFile:14-31สร้าง directoryprocess.cwd() + path + '/'ด้วยfsExtra.ensureDirSync()ถ้ายังไม่มี แล้วfs.writeFileSync(url, base64, 'base64')- ชื่อไฟล์สร้างจาก
base64-to-file.ts:18-24: prefix = 3 ส่วนของเลขบัตรประชาชนกลับด้าน (หลักที่ 2-3 + หลักที่ 5-6 + หลักที่ 11-13) +_+ random 6 ตัวอักษร +_+HHmmsss+.${extension}ตัวอย่าง:ba_dc5_321_aB3xY9_1430521.pdf - return
{ dirAttach, fileName }และupload.service.ts:107-122pack กลับเป็น 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:
- branch
cicd,develop,uat,prodจะทำbuild-test(npm test) +build-docker-image+push-image-to-registry(bitbucket-pipelines.yml:99-132) - ขั้น
push-image-to-registry(bitbucket-pipelines.yml:39-89) ใช้ imagegoogle/cloud-sdk:alpine - authentication ใช้
$GCP_KEY_FILEที่เป็น Bitbucket deployment variable (base64 encoded) ถอดรหัสเป็น./gcloud-api-key.json(bitbucket-pipelines.yml:47) — ไม่ใช่vtrc-gcp-sa-key.jsonที่ commit ไว้ gcloud auth activate-service-account --key-file gcloud-api-key.json(bitbucket-pipelines.yml:49)- login GCR ผ่าน
cat ./gcloud-api-key.json | docker login -u _json_key --password-stdin https://$GCP_REGISTRY_HOSTNAME(bitbucket-pipelines.yml:53) - 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 name | env prefix | database | synchronize | สถานะการใช้จริง |
|---|---|---|---|---|
main | DB_* | recruitment | true เฉพาะ NODE_ENV === 'dev' (configuration.ts:18-19) | ใช้จริงหนักสุด — ทุก entity ของ recruitment |
ai | DB_AI_* | recruitment_ai | false (configuration.ts:34-35) | ใช้จริง — raw query/query builder จาก candidate.repositories.ts, jobList.repositories.ts, appProfile.repositories.ts สำหรับผลวิเคราะห์ AI |
vtrc | VTRC_DB_* | vtrc (user hrmiworkforce) | false | ประกาศไว้ แต่ ไม่พบ @InjectEntityManager('vtrc') ใน source tree |
workforce | WORKFORCE_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 และ loggingENV 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 สำหรับ dependencyCOPY . .แล้วRUN yarn build— compile TypeScript ผ่านnest build(package.json:13)EXPOSE 9330CMD ["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-apicanonical อยู่ที่ branchprod(322 tracked files) — default branchmasterอาจ empty/sparse เสมอ ต้อง checkoutprodก่อนอ่านหรือแก้โค้ด - 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 module | 13 | src/app.module.ts:54-66 |
| Entity class | 37 (38 ไฟล์รวม base.ts) | src/entities/ count |
| TypeORM connection ประกาศ / ใช้จริง | 4 / 2 | src/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 ใน CandidateController | 22 | candidate.controller.ts (22 method) |
| Default port | 9330 | .env, Dockerfile:25 |
| Body size limit | 50 MB | main.ts:35-36 |
| ประเภทไฟล์แนบที่อนุญาต | 5 (jpeg, jpg, png, pdf, mp4) | upload.service.ts:55 |
ไป บท 2 Core pipeline →