260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -48,14 +48,14 @@
|
||||
|
||||
### **2.4 ข้อตกลงในการตั้งชื่อ (Naming Conventions)**
|
||||
|
||||
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
|
||||
| :-------------------- | :----------------- | :--------------------------------- |
|
||||
| Classes | PascalCase | UserService |
|
||||
| Property | snake_case | user_id |
|
||||
| Variables & Functions | camelCase | getUserInfo |
|
||||
| Files & Folders | kebab-case | user-service.ts |
|
||||
| Environment Variables | UPPERCASE | DATABASE_URL |
|
||||
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
|
||||
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
|
||||
| :----------------------- | :------------------ | :--------------------------------- |
|
||||
| Classes | PascalCase | UserService |
|
||||
| Property | snake_case | user_id |
|
||||
| Variables & Functions | camelCase | getUserInfo |
|
||||
| Files & Folders | kebab-case | user-service.ts |
|
||||
| Environment Variables | UPPERCASE | DATABASE_URL |
|
||||
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
|
||||
|
||||
ใช้คำเต็ม — ไม่ใช้อักษรย่อ — ยกเว้นคำมาตรฐาน (เช่น API, URL, req, res, err, ctx)
|
||||
|
||||
@@ -165,13 +165,13 @@ const testScenarios = {
|
||||
|
||||
### **3.1.1 NestJS 11 Patterns (Updated 2026-03-16)**
|
||||
|
||||
| Pattern | คำอธิบาย |
|
||||
| :--- | :--- |
|
||||
| Pattern | คำอธิบาย |
|
||||
| :-------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`import type` สำหรับ decorated signatures** | เมื่อ `isolatedModules` + `emitDecoratorMetadata` เปิดอยู่ ต้องใช้ `import type` สำหรับ interface ที่ใช้ใน decorated parameter (เช่น `@Req() req: RequestWithUser`) |
|
||||
| **Shared `RequestWithUser` interface** | ใช้ `src/common/interfaces/request-with-user.interface.ts` แทนการประกาศ local interface ในแต่ละ controller — ห้ามใช้ `req: any` |
|
||||
| **`@nestjs/mapped-types` ถูกลบออก** | DTO utility types (`PartialType`, `OmitType`, `IntersectionType`) ต้อง import จาก `@nestjs/swagger` เท่านั้น |
|
||||
| **Express v5** | `@nestjs/platform-express` v11 ใช้ Express 5 — path parameter syntax เปลี่ยน (`:id` → `:id` ยังใช้ได้ แต่ wildcard `*` → `*name`) |
|
||||
| **Swagger version** | Swagger doc version ต้องตรงกับ project version ปัจจุบัน (`1.8.1`) |
|
||||
| **Shared `RequestWithUser` interface** | ใช้ `src/common/interfaces/request-with-user.interface.ts` แทนการประกาศ local interface ในแต่ละ controller — ห้ามใช้ `req: any` |
|
||||
| **`@nestjs/mapped-types` ถูกลบออก** | DTO utility types (`PartialType`, `OmitType`, `IntersectionType`) ต้อง import จาก `@nestjs/swagger` เท่านั้น |
|
||||
| **Express v5** | `@nestjs/platform-express` v11 ใช้ Express 5 — path parameter syntax เปลี่ยน (`:id` → `:id` ยังใช้ได้ แต่ wildcard `*` → `*name`) |
|
||||
| **Swagger version** | Swagger doc version ต้องตรงกับ project version ปัจจุบัน (`1.8.1`) |
|
||||
|
||||
```typescript
|
||||
// ✅ ถูกต้อง — NestJS 11 pattern
|
||||
@@ -428,34 +428,34 @@ Unified Workflow Engine (Core Architecture)
|
||||
|
||||
### **3.13 เเทคโนโลยีที่ใช้ (Technology Stack)**
|
||||
|
||||
| ส่วน | Library/Tool | หมายเหตุ |
|
||||
| ----------------------- | ---------------------------------------------------- | -------------------------------------- |
|
||||
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
|
||||
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
|
||||
| **Database** | `MariaDB 11.8` | ฐานข้อมูลหลัก |
|
||||
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
|
||||
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
|
||||
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
|
||||
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
|
||||
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
|
||||
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
|
||||
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
|
||||
| ส่วน | Library/Tool | หมายเหตุ |
|
||||
| ----------------------- | ---------------------------------------------------- | -------------------------------------------- |
|
||||
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
|
||||
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
|
||||
| **Database** | `MariaDB 11.8` | ฐานข้อมูลหลัก |
|
||||
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
|
||||
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
|
||||
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
|
||||
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
|
||||
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
|
||||
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
|
||||
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
|
||||
| **Scheduling** | `@nestjs/schedule` | 📬สำหรับ Cron Jobs (เช่น แจ้งเตือน Deadline) |
|
||||
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
|
||||
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
|
||||
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
|
||||
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
|
||||
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
|
||||
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
|
||||
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
|
||||
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
|
||||
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
|
||||
| **File Processing** | `clamscan` | 🦠 Virus scanning |
|
||||
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
|
||||
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
|
||||
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
|
||||
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
|
||||
| **Compression** | `compression` | 📦 JSON compression |
|
||||
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
|
||||
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
|
||||
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
|
||||
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
|
||||
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
|
||||
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
|
||||
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
|
||||
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
|
||||
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
|
||||
| **File Processing** | `clamscan` | 🦠 Virus scanning |
|
||||
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
|
||||
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
|
||||
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
|
||||
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
|
||||
| **Compression** | `compression` | 📦 JSON compression |
|
||||
|
||||
### **3.14 Security Testing:**
|
||||
|
||||
@@ -511,13 +511,11 @@ Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บส
|
||||
1. User สร้างเอกสาร → เลือก routing template
|
||||
2. System สร้าง routing instances ตาม template
|
||||
3. สำหรับแต่ละ routing step:
|
||||
|
||||
- กำหนด due date (จาก expected_days)
|
||||
- ส่ง notification ไปยังองค์กรผู้รับ
|
||||
- อัพเดทสถานะเป็น SENT
|
||||
|
||||
4. เมื่อองค์กรผู้รับดำเนินการ:
|
||||
|
||||
- อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED
|
||||
- บันทึก processed_by และ processed_at
|
||||
- ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี)
|
||||
@@ -731,27 +729,23 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
สำหรับ Next.js App Router เราจะใช้ State Management แบบ Simplified โดยแบ่งเป็น 3 ระดับหลัก:
|
||||
|
||||
- 4.10.1. **Server State (สถานะข้อมูลจากเซิร์ฟเวอร์)**
|
||||
|
||||
- **เครื่องมือ:** **TanStack Query (React Query)**
|
||||
- **ใช้เมื่อ:** จัดการข้อมูลที่ดึงมาจาก NestJS API ทั้งหมด
|
||||
- **ครอบคลุม:** รายการ correspondences, rfas, drawings, users, permissions
|
||||
- **ประโยชน์:** จัดการ Caching, Re-fetching, Background Sync อัตโนมัติ
|
||||
|
||||
- 4.10.2. **Form State (สถานะของฟอร์ม):**
|
||||
|
||||
- **เครื่องมือ:** **React Hook Form** + **Zod** (สำหรับ validation)
|
||||
- **ใช้เมื่อ:** จัดการฟอร์มที่ซับซ้อนทั้งหมด
|
||||
- **ครอบคลุม:** ฟอร์มสร้าง/แก้ไข RFA, Correspondence, Circulation
|
||||
- **รวมฟีเจอร์:** Auto-save drafts ลง LocalStorage
|
||||
|
||||
- 4.10.3. **UI State (สถานะ UI ชั่วคราว):**
|
||||
|
||||
- **เครื่องมือ:** **useState**, **useReducer** (ใน Component) หรือ **Zustand** (สำหรับ Global Client State เช่น Preferences, Auth)
|
||||
- **ใช้เมื่อ:** จัดการสถานะเฉพาะ Component หรือข้อมูลที่แชร์ทั้งแอปโดยไม่พึ่งพาเซิร์ฟเวอร์
|
||||
- **ครอบคลุม:** Modal เปิด/ปิด, Dropdown state, Loading states, Themes, Sidebar
|
||||
|
||||
- **ยกเลิกการใช้:**
|
||||
|
||||
- ❌ Context API สำหรับ Server State (ใช้ React Query แทน)
|
||||
|
||||
- **ตัวอย่าง Implementation:**
|
||||
@@ -868,15 +862,15 @@ updateRFA(@Param('id') id: string) {
|
||||
|
||||
## 🔗 **7. แนวทางการบูรณาการ Full Stack (Full Stack Integration Guidelines)**
|
||||
|
||||
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
|
||||
| :----------------------- | :------------------------- | :---------------------------- | :------------------------------------- |
|
||||
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
|
||||
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
|
||||
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
|
||||
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
|
||||
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
|
||||
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
|
||||
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
|
||||
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
|
||||
| :------------------------- | :------------------------- | :----------------------------- | :------------------------------------- |
|
||||
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
|
||||
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
|
||||
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
|
||||
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
|
||||
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
|
||||
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
|
||||
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
|
||||
|
||||
## 🗂️ **8. ข้อตกลงเฉพาะสำหรับ DMS (LCBP3-DMS)**
|
||||
|
||||
@@ -886,17 +880,17 @@ updateRFA(@Param('id') id: string) {
|
||||
|
||||
บันทึกการดำเนินการ CRUD และการจับคู่ทั้งหมดลงในตาราง audit_logs
|
||||
|
||||
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
|
||||
| :----------- | :------------- | :----------------------------------------------- |
|
||||
| audit_id | BIGINT | Primary Key |
|
||||
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
|
||||
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
|
||||
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
|
||||
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
|
||||
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
|
||||
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
|
||||
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
|
||||
| created_at | TIMESTAMP | Timestamp (UTC) |
|
||||
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
|
||||
| :------------ | :------------- | :----------------------------------------------- |
|
||||
| audit_id | BIGINT | Primary Key |
|
||||
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
|
||||
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
|
||||
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
|
||||
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
|
||||
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
|
||||
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
|
||||
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
|
||||
| created_at | TIMESTAMP | Timestamp (UTC) |
|
||||
|
||||
### 📂**8.2 การจัดการไฟล์ (File Handling)**
|
||||
|
||||
|
||||
@@ -361,10 +361,7 @@ describe('DocumentNumberingService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DocumentNumberingService,
|
||||
{ provide: RedisLock, useValue: mockRedisLock },
|
||||
],
|
||||
providers: [DocumentNumberingService, { provide: RedisLock, useValue: mockRedisLock }],
|
||||
}).compile();
|
||||
|
||||
service = module.get(DocumentNumberingService);
|
||||
@@ -421,10 +418,7 @@ describe('Correspondence API (e2e)', () => {
|
||||
// src/modules/monitoring/logger/winston.config.ts
|
||||
export const winstonConfig = {
|
||||
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
@@ -463,14 +457,14 @@ async approve(@Param('id') id: string, @CurrentUser() user: User) {
|
||||
|
||||
Backend codebase has **zero** `any` types remaining. Key techniques used:
|
||||
|
||||
| Pattern | Solution |
|
||||
|---------|----------|
|
||||
| JWT `expiresIn` branded type | `import type { StringValue } from 'ms'`; cast `as StringValue` |
|
||||
| CASL `detectSubjectType` callback | Type param as `object`, internal cast via `Record<string, unknown>` |
|
||||
| CASL `ability.can()` params | Export `Actions`/`Subjects` types from `ability.factory.ts`, cast explicitly |
|
||||
| TypeORM nullable column clearing | Use `undefined` instead of `null as any` for optional (`?:`) properties |
|
||||
| Test mock objects | Use `as unknown as InterfaceType` or `as Partial<Entity> as Entity` |
|
||||
| TypeScript legacy decorators | `target: any` is unavoidable — whitelisted per TS spec limitation |
|
||||
| Pattern | Solution |
|
||||
| --------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| JWT `expiresIn` branded type | `import type { StringValue } from 'ms'`; cast `as StringValue` |
|
||||
| CASL `detectSubjectType` callback | Type param as `object`, internal cast via `Record<string, unknown>` |
|
||||
| CASL `ability.can()` params | Export `Actions`/`Subjects` types from `ability.factory.ts`, cast explicitly |
|
||||
| TypeORM nullable column clearing | Use `undefined` instead of `null as any` for optional (`?:`) properties |
|
||||
| Test mock objects | Use `as unknown as InterfaceType` or `as Partial<Entity> as Entity` |
|
||||
| TypeScript legacy decorators | `target: any` is unavoidable — whitelisted per TS spec limitation |
|
||||
|
||||
> **Exceptions:** Only `target: any` in legacy TS decorators (`circuit-breaker.decorator.ts`, `retry.decorator.ts`) remains — this is a TypeScript language constraint, not a code quality issue.
|
||||
|
||||
@@ -487,7 +481,7 @@ Backend codebase has **zero** `any` types remaining. Key techniques used:
|
||||
|
||||
## 🔄 Update History
|
||||
|
||||
| Version | Date | Changes |
|
||||
| ------- | ---------- | ---------------------------------- |
|
||||
| 1.5.0 | 2025-12-01 | Initial backend guidelines created |
|
||||
| Version | Date | Changes |
|
||||
| ------- | ---------- | ------------------------------------------------------------------- |
|
||||
| 1.5.0 | 2025-12-01 | Initial backend guidelines created |
|
||||
| 1.8.1 | 2026-03-20 | Added `any` type elimination techniques, enforced 0 remaining `any` |
|
||||
|
||||
@@ -144,9 +144,7 @@ export function ResponsiveTable({ data }: { data: Correspondence[] }) {
|
||||
<Card key={item.id}>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
เลขที่เอกสาร
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">เลขที่เอกสาร</div>
|
||||
<div className="font-medium">{item.doc_number}</div>
|
||||
<div className="text-sm text-muted-foreground">เรื่อง</div>
|
||||
<div>{item.title}</div>
|
||||
@@ -367,11 +365,7 @@ export default apiClient;
|
||||
```typescript
|
||||
// lib/services/correspondence.service.ts
|
||||
import apiClient from '@/lib/api/client';
|
||||
import type {
|
||||
Correspondence,
|
||||
CreateCorrespondenceDto,
|
||||
SearchCorrespondenceDto,
|
||||
} from '@/types/dto/correspondence';
|
||||
import type { Correspondence, CreateCorrespondenceDto, SearchCorrespondenceDto } from '@/types/dto/correspondence';
|
||||
|
||||
export const correspondenceService = {
|
||||
async getAll(params: SearchCorrespondenceDto): Promise<Correspondence[]> {
|
||||
@@ -389,10 +383,7 @@ export const correspondenceService = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: Partial<CreateCorrespondenceDto>
|
||||
): Promise<Correspondence> {
|
||||
async update(id: string, dto: Partial<CreateCorrespondenceDto>): Promise<Correspondence> {
|
||||
const { data } = await apiClient.put(`/correspondences/${id}`, dto);
|
||||
return data;
|
||||
},
|
||||
@@ -441,24 +432,20 @@ export function DynamicForm({ schemaName, onSubmit }: DynamicFormProps) {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{Object.entries(schema.schema_definition.properties).map(
|
||||
([key, prop]: [string, Record<string, unknown>]) => (
|
||||
<FormField
|
||||
key={key}
|
||||
control={form.control}
|
||||
name={key}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{prop.title || key}</FormLabel>
|
||||
<FormControl>
|
||||
{renderFieldByType(prop.type, field)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{Object.entries(schema.schema_definition.properties).map(([key, prop]: [string, Record<string, unknown>]) => (
|
||||
<FormField
|
||||
key={key}
|
||||
control={form.control}
|
||||
name={key}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{prop.title || key}</FormLabel>
|
||||
<FormControl>{renderFieldByType(prop.type, field)}</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button type="submit">บันทึก</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -525,24 +512,17 @@ export function FileUploadZone({
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
|
||||
transition-colors
|
||||
${
|
||||
isDragActive
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-muted-foreground/25'
|
||||
}
|
||||
${isDragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
||||
hover:border-primary hover:bg-primary/5
|
||||
`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{isDragActive
|
||||
? 'วางไฟล์ที่นี่...'
|
||||
: 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
|
||||
{isDragActive ? 'วางไฟล์ที่นี่...' : 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์,{' '}
|
||||
{maxSize / 1024 / 1024}MB/ไฟล์)
|
||||
รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์, {maxSize / 1024 / 1024}MB/ไฟล์)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -583,9 +563,7 @@ describe('CorrespondenceForm', () => {
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: 'Test Correspondence' })
|
||||
);
|
||||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ title: 'Test Correspondence' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,11 +52,11 @@ related:
|
||||
|
||||
### Test Distribution
|
||||
|
||||
| Level | Coverage | Speed | Purpose |
|
||||
| ----------- | -------- | ---------- | ---------------------------- |
|
||||
| Unit | 60% | Fast (ms) | ทดสอบตรรกะแต่ละ Function |
|
||||
| Level | Coverage | Speed | Purpose |
|
||||
| ----------- | -------- | ---------- | ------------------------------- |
|
||||
| Unit | 60% | Fast (ms) | ทดสอบตรรกะแต่ละ Function |
|
||||
| Integration | 30% | Medium (s) | ทดสอบการทำงานร่วมกันของ Modules |
|
||||
| E2E | 10% | Slow (m) | ทดสอบ User Journey ทั้งหมด |
|
||||
| E2E | 10% | Slow (m) | ทดสอบ User Journey ทั้งหมด |
|
||||
|
||||
---
|
||||
|
||||
@@ -94,9 +94,7 @@ describe('CorrespondenceService', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<CorrespondenceService>(CorrespondenceService);
|
||||
repository = module.get<Repository<Correspondence>>(
|
||||
getRepositoryToken(Correspondence)
|
||||
);
|
||||
repository = module.get<Repository<Correspondence>>(getRepositoryToken(Correspondence));
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
@@ -106,9 +104,7 @@ describe('CorrespondenceService', () => {
|
||||
{ id: '2', title: 'Test 2' },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(repository, 'find')
|
||||
.mockResolvedValue(mockCorrespondences as any);
|
||||
jest.spyOn(repository, 'find').mockResolvedValue(mockCorrespondences as any);
|
||||
|
||||
const result = await service.findAll();
|
||||
expect(result).toEqual(mockCorrespondences);
|
||||
@@ -570,9 +566,7 @@ describe('CorrespondenceForm', () => {
|
||||
vi.advanceTimersByTime(30000);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(saveDraft).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: 'Draft Test' })
|
||||
);
|
||||
expect(saveDraft).toHaveBeenCalledWith(expect.objectContaining({ title: 'Draft Test' }));
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
@@ -620,9 +614,7 @@ describe('useCorrespondences', () => {
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
vi.mocked(correspondenceService.getAll).mockRejectedValue(
|
||||
new Error('API Error')
|
||||
);
|
||||
vi.mocked(correspondenceService.getAll).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => useCorrespondences('project-1'), {
|
||||
wrapper,
|
||||
@@ -792,9 +784,7 @@ describe('Security - SQL Injection', () => {
|
||||
.expect(200);
|
||||
|
||||
// Should not execute malicious SQL
|
||||
const tableExists = await repository.query(
|
||||
`SHOW TABLES LIKE 'correspondences'`
|
||||
);
|
||||
const tableExists = await repository.query(`SHOW TABLES LIKE 'correspondences'`);
|
||||
expect(tableExists).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -908,26 +898,22 @@ describe('Security - Authentication', () => {
|
||||
### Critical Paths (Must have 100% coverage)
|
||||
|
||||
1. **Authentication & Authorization**
|
||||
|
||||
- Login/Logout flow
|
||||
- Token refresh
|
||||
- RBAC permission checks
|
||||
|
||||
2. **Document Numbering**
|
||||
|
||||
- Concurrent number generation
|
||||
- Format validation
|
||||
- Counter increment logic
|
||||
|
||||
3. **File Upload**
|
||||
|
||||
- Two-phase upload
|
||||
- Virus scanning
|
||||
- File type validation
|
||||
- Orphan cleanup
|
||||
|
||||
4. **Workflow Engine**
|
||||
|
||||
- State transitions
|
||||
- Permission checks at each step
|
||||
- Notification triggers
|
||||
@@ -1118,10 +1104,7 @@ export class CorrespondenceFactory {
|
||||
};
|
||||
}
|
||||
|
||||
static createMany(
|
||||
count: number,
|
||||
overrides?: Partial<Correspondence>
|
||||
): Correspondence[] {
|
||||
static createMany(count: number, overrides?: Partial<Correspondence>): Correspondence[] {
|
||||
return Array(count)
|
||||
.fill(null)
|
||||
.map(() => this.create(overrides));
|
||||
@@ -1206,18 +1189,15 @@ describe('[ClassName/FeatureName]', () => {
|
||||
### Key Metrics
|
||||
|
||||
1. **Coverage Percentage**
|
||||
|
||||
- Track via CodeCov/Coveralls
|
||||
- Enforce minimum thresholds in CI
|
||||
|
||||
2. **Test Execution Time**
|
||||
|
||||
- Unit tests: < 5 seconds
|
||||
- Integration tests: < 30 seconds
|
||||
- E2E tests: < 5 minutes
|
||||
|
||||
3. **Flaky Test Rate**
|
||||
|
||||
- Target: < 1% flaky tests
|
||||
- Track and fix flaky tests immediately
|
||||
|
||||
|
||||
@@ -110,8 +110,8 @@ git pull --rebase
|
||||
git log
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🧩 SECTION 3 – ทำงานกับ Branch
|
||||
|
||||
### ดู branch ทั้งหมด
|
||||
@@ -231,3 +231,5 @@ git clone ssh://git@git.np-dms.work:2222/np-dms/lcbp3.git
|
||||
## 📌 END
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -24,22 +24,22 @@ This document outlines the step-by-step implementation plan to integrate UUIDv7
|
||||
|
||||
### Affected Tables (14)
|
||||
|
||||
| # | Table | PK Column | UUID Index |
|
||||
|---|-------|-----------|------------|
|
||||
| 1 | organizations | id | idx_organizations_uuid |
|
||||
| 2 | projects | id | idx_projects_uuid |
|
||||
| 3 | contracts | id | idx_contracts_uuid |
|
||||
| 4 | users | user_id | idx_users_uuid |
|
||||
| 5 | correspondences | id | idx_correspondences_uuid |
|
||||
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
|
||||
| 7 | circulations | id | idx_circulations_uuid |
|
||||
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
|
||||
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
|
||||
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
|
||||
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
|
||||
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
|
||||
| 13 | attachments | id | idx_attachments_uuid |
|
||||
| 14 | notifications | id | idx_notifications_uuid |
|
||||
| # | Table | PK Column | UUID Index |
|
||||
| --- | ------------------------- | --------- | ---------------------------------- |
|
||||
| 1 | organizations | id | idx_organizations_uuid |
|
||||
| 2 | projects | id | idx_projects_uuid |
|
||||
| 3 | contracts | id | idx_contracts_uuid |
|
||||
| 4 | users | user_id | idx_users_uuid |
|
||||
| 5 | correspondences | id | idx_correspondences_uuid |
|
||||
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
|
||||
| 7 | circulations | id | idx_circulations_uuid |
|
||||
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
|
||||
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
|
||||
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
|
||||
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
|
||||
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
|
||||
| 13 | attachments | id | idx_attachments_uuid |
|
||||
| 14 | notifications | id | idx_notifications_uuid |
|
||||
|
||||
### Excluded Tables (Shared-PK / Junction — inherit UUID from parent)
|
||||
|
||||
@@ -116,22 +116,22 @@ export class Correspondence extends UuidBaseEntity {
|
||||
|
||||
### Entities to Update
|
||||
|
||||
| Entity File | Table |
|
||||
|-------------|-------|
|
||||
| `organization.entity.ts` | organizations |
|
||||
| `project.entity.ts` | projects |
|
||||
| `contract.entity.ts` | contracts |
|
||||
| `user.entity.ts` | users |
|
||||
| `correspondence.entity.ts` | correspondences |
|
||||
| `correspondence-revision.entity.ts` | correspondence_revisions |
|
||||
| `circulation.entity.ts` | circulations |
|
||||
| `shop-drawing.entity.ts` | shop_drawings |
|
||||
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
|
||||
| `contract-drawing.entity.ts` | contract_drawings |
|
||||
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
|
||||
| Entity File | Table |
|
||||
| ------------------------------------ | ------------------------- |
|
||||
| `organization.entity.ts` | organizations |
|
||||
| `project.entity.ts` | projects |
|
||||
| `contract.entity.ts` | contracts |
|
||||
| `user.entity.ts` | users |
|
||||
| `correspondence.entity.ts` | correspondences |
|
||||
| `correspondence-revision.entity.ts` | correspondence_revisions |
|
||||
| `circulation.entity.ts` | circulations |
|
||||
| `shop-drawing.entity.ts` | shop_drawings |
|
||||
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
|
||||
| `contract-drawing.entity.ts` | contract_drawings |
|
||||
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
|
||||
| `asbuilt-drawing-revision.entity.ts` | asbuilt_drawing_revisions |
|
||||
| `attachment.entity.ts` | attachments |
|
||||
| `notification.entity.ts` | notifications |
|
||||
| `attachment.entity.ts` | attachments |
|
||||
| `notification.entity.ts` | notifications |
|
||||
|
||||
---
|
||||
|
||||
@@ -186,7 +186,7 @@ async findByUuid(uuid: string): Promise<CorrespondenceDto> {
|
||||
```typescript
|
||||
// Response DTO exposes uuid, hides id
|
||||
export class CorrespondenceResponseDto {
|
||||
uuid: string; // ✅ Public identifier
|
||||
uuid: string; // ✅ Public identifier
|
||||
correspondenceNumber: string;
|
||||
// id: number; // ❌ Never expose INT id
|
||||
}
|
||||
@@ -249,19 +249,20 @@ async findByUuidOrId(identifier: string): Promise<Entity> {
|
||||
|
||||
#### Remaining Issues
|
||||
|
||||
| File | Field | Entity | Issue |
|
||||
|------|-------|--------|-------|
|
||||
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
|
||||
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
|
||||
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
|
||||
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
|
||||
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
|
||||
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
|
||||
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
|
||||
| File | Field | Entity | Issue |
|
||||
| ------------------------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------ |
|
||||
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
|
||||
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
|
||||
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
|
||||
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
|
||||
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
|
||||
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
|
||||
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
|
||||
|
||||
#### Fix Strategy (same pattern as Drawing Search fix)
|
||||
|
||||
For each affected backend DTO:
|
||||
|
||||
1. Add `projectUuid?: string` / `organizationUuid?: string` field
|
||||
2. Controller resolves UUID → INT id via respective service's `findOneByUuid()`
|
||||
3. Frontend sends UUID string directly (remove `parseInt`)
|
||||
@@ -296,19 +297,19 @@ For each affected backend DTO:
|
||||
|
||||
## Implementation Order (Priority)
|
||||
|
||||
| Order | Task | Effort | Status |
|
||||
|-------|------|--------|--------|
|
||||
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
|
||||
| 2 | Install `uuid` package | XS | ✅ Done |
|
||||
| 3 | Update 14 entity files with uuid column | M | ✅ Done |
|
||||
| 4 | Create ParseUuidPipe | S | ✅ Done |
|
||||
| 5 | Update controllers to use UUID params | L | ✅ Done |
|
||||
| 6 | Update services with findByUuid methods | L | ✅ Done |
|
||||
| 7 | Update DTOs to expose uuid, hide id | M | ✅ Done |
|
||||
| 8 | Update frontend API calls & routes | L | ✅ Done |
|
||||
| 9 | Drawing search: projectUuid migration | S | ✅ Done (2026-03-18) |
|
||||
| 10 | FK reference UUID migration (Correspondence, User, Numbering) | M | ❌ Pending (see Phase 5.4) |
|
||||
| 11 | Write unit + integration tests | M | ❌ Pending |
|
||||
| Order | Task | Effort | Status |
|
||||
| ----- | ------------------------------------------------------------- | ------ | -------------------------- |
|
||||
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
|
||||
| 2 | Install `uuid` package | XS | ✅ Done |
|
||||
| 3 | Update 14 entity files with uuid column | M | ✅ Done |
|
||||
| 4 | Create ParseUuidPipe | S | ✅ Done |
|
||||
| 5 | Update controllers to use UUID params | L | ✅ Done |
|
||||
| 6 | Update services with findByUuid methods | L | ✅ Done |
|
||||
| 7 | Update DTOs to expose uuid, hide id | M | ✅ Done |
|
||||
| 8 | Update frontend API calls & routes | L | ✅ Done |
|
||||
| 9 | Drawing search: projectUuid migration | S | ✅ Done (2026-03-18) |
|
||||
| 10 | FK reference UUID migration (Correspondence, User, Numbering) | M | ❌ Pending (see Phase 5.4) |
|
||||
| 11 | Write unit + integration tests | M | ❌ Pending |
|
||||
|
||||
**Estimated Remaining Effort:** ~2-3 days for FK migration + ~2 days for tests
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
## 📖 คู่มือการพัฒนา (Implementation Guides)
|
||||
|
||||
### 1. [FullStack JS Guidelines](./05-01-fullstack-js-guidelines.md)
|
||||
|
||||
**แนวทางการพัฒนาภาพรวมทั้งระบบ (v1.8.1 — includes NestJS 11 Patterns)**
|
||||
|
||||
- โครงสร้างโปรเจกต์ (Monorepo-like focus)
|
||||
- Naming Conventions & Code Style
|
||||
- Secrets & Environment Management
|
||||
@@ -58,7 +60,9 @@
|
||||
- Double-Lock Mechanism for Numbering
|
||||
|
||||
### 2. [Backend Guidelines](./05-02-backend-guidelines.md)
|
||||
|
||||
**แนวทางการพัฒนา NestJS 11 Backend**
|
||||
|
||||
- Modular Architecture Detail
|
||||
- DTO Validation & Transformer
|
||||
- TypeORM Best Practices & Optimistic Locking
|
||||
@@ -66,7 +70,9 @@
|
||||
- BullMQ for Background Jobs
|
||||
|
||||
### 3. [Frontend Guidelines](./05-03-frontend-guidelines.md)
|
||||
|
||||
**แนวทางการพัฒนา Next.js 16 Frontend**
|
||||
|
||||
- App Router Patterns
|
||||
- Shadcn/UI & Tailwind Styling
|
||||
- TanStack Query for Data Fetching
|
||||
@@ -74,7 +80,9 @@
|
||||
- API Client Interceptors (Auth & Idempotency)
|
||||
|
||||
### 4. [Document Numbering System](../01-Requirements/business-rules/01-02-02-doc-numbering-rules.md)
|
||||
|
||||
**รายละเอียดการนำระบบออกเลขที่เอกสารไปใช้งาน**
|
||||
|
||||
- Table Schema: Templates, Counters, Audit
|
||||
- Double-Lock Strategy (Redis Redlock + Database VersionColumn)
|
||||
- Reservation Flow (Phase 1: Reserve, Phase 2: Confirm)
|
||||
@@ -95,13 +103,13 @@
|
||||
|
||||
## 🛠️ Technology Stack Recap
|
||||
|
||||
| Layer | Primary Technology | Secondary/Supporting |
|
||||
| ------------ | ------------------ | -------------------- |
|
||||
| **Backend** | NestJS 11 (Express v5) | TypeORM, BullMQ |
|
||||
| **Frontend** | Next.js 16.2.0 (React 19.2.4) | Shadcn/UI, Tailwind 4.2.2 |
|
||||
| **Database** | MariaDB 11.8 | Redis 7 (Cache/Lock) |
|
||||
| **Search** | Elasticsearch | - |
|
||||
| **Testing** | Jest, Vitest | Playwright |
|
||||
| Layer | Primary Technology | Secondary/Supporting |
|
||||
| ------------ | ----------------------------- | ------------------------- |
|
||||
| **Backend** | NestJS 11 (Express v5) | TypeORM, BullMQ |
|
||||
| **Frontend** | Next.js 16.2.0 (React 19.2.4) | Shadcn/UI, Tailwind 4.2.2 |
|
||||
| **Database** | MariaDB 11.8 | Redis 7 (Cache/Lock) |
|
||||
| **Search** | Elasticsearch | - |
|
||||
| **Testing** | Jest, Vitest | Playwright |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user