260223:1415 20260223 nextJS & nestJS Best pratices
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s

This commit is contained in:
admin
2026-02-23 14:15:06 +07:00
parent c90a664f53
commit ef16817f38
164 changed files with 24815 additions and 311 deletions

View File

@@ -0,0 +1,353 @@
# ADR-001: Unified Workflow Engine
**Status:** Accepted
**Date:** 2025-11-30
**Decision Makers:** Development Team, System Architect
**Related Documents:**
- [System Architecture](../02-architecture/02-01-system-architecture.md)
- [Unified Workflow Requirements](../01-requirements/01-03.6-unified-workflow.md)
---
## Context and Problem Statement
LCBP3-DMS ต้องจัดการเอกสารหลายประเภท (Correspondences, RFAs, Circulations) โดยแต่ละประเภทมี Workflow การเดินเอกสารที่แตกต่างกัน:
- **Correspondence Routing:** ส่งเอกสารระหว่างองค์กร มีการ Forward, Reply
- **RFA Approval Workflow:** ส่งขออนุมัติ มีขั้นตอน Review → Approve → Respond
- **Circulation Workflow:** เวียนเอกสารภายในองค์กร มีการ Assign ผู้รับเพื่อพิจารณา
### Key Problems
1. **Code Duplication:** หากสร้างตาราง Routing แยกกันสำหรับแต่ละประเภทเอกสาร จะมี Logic ซ้ำซ้อน
2. **Complexity:** การ Maintain หลาย Workflow Systems ทำให้ซับซ้อน
3. **Inconsistency:** State Management และ History Tracking อาจไม่สอดคล้องกัน
4. **Scalability:** เมื่อเพิ่มประเภทเอกสารใหม่ ต้องสร้าง Workflow System ใหม่
5. **Versioning:** การแก้ไข Workflow กระทบเอกสารที่กำลังดำเนินการอยู่
---
## Decision Drivers
- **DRY Principle:** Don't Repeat Yourself - ลดการเขียน Code ซ้ำ
- **Maintainability:** ง่ายต่อการ Maintain และ Debug
- **Flexibility:** รองรับการเปลี่ยนแปลง Workflow ในอนาคต
- **Traceability:** ติดตามประวัติการเปลี่ยนสถานะได้ชัดเจน
- **Performance:** ประมวลผล Workflow ได้เร็วและมีประสิทธิภาพ
---
## Considered Options
### Option 1: Hard-coded Workflow per Document Type
**แนวทาง:** สร้างตาราง `correspondence_routings`, `rfa_approvals`, `circulation_routings` แยกกัน
**Pros:**
- ✅ เข้าใจง่าย straightforward
- ✅ Query performance ดี (table-specific indexes)
- ✅ Schema ชัดเจนสำหรับแต่ละ type
**Cons:**
- ❌ Code duplication มาก
- ❌ ยากต่อการเพิ่ม Document Type ใหม่
- ❌ Inconsistent state management
- ❌ ไม่มี Workflow versioning mechanism
- ❌ ยากต่อการ reuse common workflows
### Option 2: Generic Workflow Engine with Hard-coded State Machines
**แนวทาง:** สร้าง Workflow Engine แต่ Hard-code State Machine ไว้ใน Code
**Pros:**
- ✅ Centralized workflow logic
- ✅ Reusable workflow components
- ✅ Better maintainability
**Cons:**
- ❌ ต้อง Deploy ใหม่ทุกครั้งที่แก้ Workflow
- ❌ ไม่ยืดหยุ่นสำหรับ Business Users
- ❌ Versioning ยังซับซ้อน
### Option 3: **DSL-Based Unified Workflow Engine** ⭐ (Selected)
**แนวทาง:** สร้าง Workflow Engine ที่ใช้ JSON-based DSL (Domain Specific Language) เพื่อ Define Workflows
**Pros:**
-**Single Source of Truth:** Workflow logic อยู่ใน Database
-**Versioning Support:** เก็บ Workflow Definition versions ได้
-**Runtime Flexibility:** แก้ Workflow ได้โดยไม่ต้อง Deploy
-**Reusability:** Workflow templates สามารถใช้ซ้ำได้
-**Consistency:** State management เป็นมาตรฐานเดียวกัน
-**Audit Trail:** ประวัติครบถ้วนใน `workflow_history`
-**Scalability:** เพิ่ม Document Type ใหม่ได้ง่าย
**Cons:**
- ❌ Initial development complexity สูง
- ❌ ต้องเขียน DSL Parser และ Validator
- ❌ Performance overhead เล็กน้อย (parse JSON)
- ❌ Learning curve สำหรับทีม
---
## Decision Outcome
**Chosen Option:** Option 3 - DSL-Based Unified Workflow Engine
### Rationale
เลือก Unified Workflow Engine เนื่องจาก:
1. **Long-term Maintainability:** แม้จะมี complexity ในการพัฒนา แต่ในระยะยาวจะลดภาระการ Maintain
2. **Business Flexibility:** Business Users สามารถปรับ Workflow ได้ (ผ่าน Admin UI ในอนาคต)
3. **Consistency:** สถานะและประวัติเป็นมาตรฐานเดียวกันทุก Document Type
4. **Scalability:** เตรียมพร้อมสำหรับ Document Types ใหม่ๆ ในอนาคต
5. **Versioning:** รองรับการแก้ไข Workflow โดยไม่กระทบ In-progress documents
---
## Implementation Details
### Database Schema
```sql
-- Workflow Definitions (Templates)
CREATE TABLE workflow_definitions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
version INT NOT NULL,
entity_type ENUM('correspondence', 'rfa', 'circulation'),
definition JSON NOT NULL, -- DSL Configuration
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (name, version)
);
-- Workflow Instances (Running Workflows)
CREATE TABLE workflow_instances (
id INT PRIMARY KEY AUTO_INCREMENT,
definition_id INT NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id INT NOT NULL,
current_state VARCHAR(50) NOT NULL,
context JSON, -- Runtime data
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
FOREIGN KEY (definition_id) REFERENCES workflow_definitions(id)
);
-- Workflow History (Audit Trail)
CREATE TABLE workflow_history (
id INT PRIMARY KEY AUTO_INCREMENT,
instance_id INT NOT NULL,
from_state VARCHAR(50),
to_state VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
actor_id INT NOT NULL,
metadata JSON,
transitioned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (instance_id) REFERENCES workflow_instances(id),
FOREIGN KEY (actor_id) REFERENCES users(user_id)
);
```
### DSL Example
```json
{
"name": "CORRESPONDENCE_ROUTING",
"version": 1,
"entity_type": "correspondence",
"states": [
{
"name": "DRAFT",
"type": "initial",
"allowed_transitions": ["SUBMIT"]
},
{
"name": "SUBMITTED",
"type": "intermediate",
"allowed_transitions": ["RECEIVE", "RETURN", "CANCEL"]
},
{
"name": "RECEIVED",
"type": "intermediate",
"allowed_transitions": ["REPLY", "FORWARD", "CLOSE"]
},
{
"name": "CLOSED",
"type": "final"
}
],
"transitions": [
{
"action": "SUBMIT",
"from": "DRAFT",
"to": "SUBMITTED",
"guards": [
{
"type": "permission",
"permission": "correspondence.submit"
},
{
"type": "validation",
"rules": ["hasRecipient", "hasAttachment"]
}
],
"effects": [
{
"type": "notification",
"template": "correspondence_submitted",
"recipients": ["originator", "assigned_reviewer"]
},
{
"type": "update_entity",
"field": "submitted_at",
"value": "{{now}}"
}
]
}
]
}
```
### NestJS Module Structure
```typescript
// workflow-engine.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([
WorkflowDefinition,
WorkflowInstance,
WorkflowHistory,
]),
],
providers: [
WorkflowEngineService,
WorkflowDefinitionService,
WorkflowInstanceService,
DslParserService,
StateValidator,
TransitionExecutor,
],
exports: [WorkflowEngineService],
})
export class WorkflowEngineModule {}
// workflow-engine.service.ts
@Injectable()
export class WorkflowEngineService {
async createInstance(
definitionId: number,
entityType: string,
entityId: number
): Promise<WorkflowInstance> {
const definition = await this.getActiveDefinition(definitionId);
const initialState = this.dslParser.getInitialState(definition.definition);
return this.instanceRepo.save({
definition_id: definitionId,
entity_type: entityType,
entity_id: entityId,
current_state: initialState,
});
}
async executeTransition(
instanceId: number,
action: string,
actorId: number
): Promise<void> {
const instance = await this.instanceRepo.findOne(instanceId);
const definition = await this.definitionRepo.findOne(
instance.definition_id
);
// Validate transition
const transition = this.stateValidator.validateTransition(
definition.definition,
instance.current_state,
action
);
// Execute guards
await this.checkGuards(transition.guards, instance, actorId);
// Update state
await this.transitionExecutor.execute(instance, transition, actorId);
// Record history
await this.recordHistory(instance, transition, actorId);
}
}
```
---
## Consequences
### Positive
1.**Unified State Management:** สถานะทุก Document Type จัดการโดย Engine เดียว
2.**No Code Changes for Workflow Updates:** แก้ Workflow ผ่าน JSON DSL
3.**Complete Audit Trail:** ประวัติครบถ้วนใน `workflow_history`
4.**Versioning Support:** In-progress documents ใช้ Workflow Version เดิม
5.**Reusable Templates:** สามารถ Clone Workflow Template ได้
6.**Future-proof:** พร้อมสำหรับ Document Types ใหม่
### Negative
1.**Initial Complexity:** ต้องสร้าง DSL Parser, Validator, Executor
2.**Learning Curve:** ทีมต้องเรียนรู้ DSL Structure
3.**Performance:** เพิ่ม overhead เล็กน้อยจากการ parse JSON
4.**Debugging:** ยากกว่า Hard-coded logic เล็กน้อย
5.**Testing:** ต้อง Test ทั้ง Engine และ Workflow Definitions
### Mitigation Strategies
- **Complexity:** สร้าง UI Builder สำหรับ Workflow Design ในอนาคต
- **Learning Curve:** เขียน Documentation และ Examples ที่ชัดเจน
- **Performance:** ใช้ Redis Cache สำหรับ Workflow Definitions
- **Debugging:** สร้าง Workflow Visualization Tool
- **Testing:** เขียน Comprehensive Unit Tests สำหรับ Engine
---
## Compliance
เป็นไปตาม:
- [Backend Plan Section 2.4.1](../../docs/2_Backend_Plan_V1_4_5.md) - Unified Workflow Engine
- [Requirements 3.6](../01-requirements/01-03.6-unified-workflow.md) - Unified Workflow Specification
---
## Notes
- Workflow DSL จะถูก Validate ด้วย JSON Schema ก่อน Save
- Admin UI สำหรับจัดการ Workflow จะพัฒนาใน Phase 2
- ต้องมี Migration Tool สำหรับ Workflow Definition Changes
- พิจารณาใช้ BPMN 2.0 Notation ในอนาคต (ถ้าต้องการ Visual Workflow Designer)
---
## Related ADRs
- [ADR-002: Document Numbering Strategy](./ADR-002-document-numbering-strategy.md) - ใช้ Workflow Engine trigger Document Number Generation
- [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md) - Permission Guards ใน Workflow Transitions
---
## References
- [NestJS State Machine Example](https://docs.nestjs.com/techniques/queues)
- [Workflow Patterns](http://www.workflowpatterns.com/)
- [JSON Schema Specification](https://json-schema.org/)

View File

@@ -0,0 +1,977 @@
# ADR-002: Document Numbering Strategy
**Status:** Accepted
**Date:** 2025-12-18
**Decision Makers:** Development Team, System Architect
**Related Documents:**
- [System Architecture](../02-architecture/02-01-system-architecture.md)
- [Document Numbering Requirements](../01-requirements/01-03.11-document-numbering.md)
---
## Context and Problem Statement
LCBP3-DMS ต้องสร้างเลขที่เอกสารอัตโนมัติสำหรับ Correspondence, RFA, Transmittal และ Drawing โดยเลขที่เอกสารต้อง:
1. **Unique:** ไม่ซ้ำกันในระบบ
2. **Sequential:** เรียงตามลำดับเวลา
3. **Meaningful:** มีโครงสร้างที่อ่านเข้าใจได้ (เช่น `LCBP3-C2-RFI-ROW-0029-A`)
4. **Configurable:** สามารถปรับรูปแบบได้ตาม Project/Organization/Document Type
5. **Concurrent-safe:** ป้องกัน Race Condition เมื่อมีหลาย Request พร้อมกัน
### Key Challenges
1. **Race Condition:** เมื่อมี 2+ requests พร้อมกัน อาจได้เลขเดียวกัน
2. **Performance:** ต้องรวดเร็วแม้มี concurrent requests (50-100 req/sec)
3. **Flexibility:** รองรับรูปแบบเลขที่หลากหลายตามชนิดเอกสาร
4. **Discipline Support:** เลขที่ต้องรวม Discipline Code (GEN, STR, ARC, etc.)
5. **Transmittal Logic:** เลขที่ Transmittal เปลี่ยนตามผู้รับ (To Owner vs To Contractor)
6. **Year Reset:** Counter ต้อง reset ตาม ปี พ.ศ. หรือ ค.ศ.
---
## Decision Drivers
- **Data Integrity:** เลขที่ต้องไม่ซ้ำกันเด็ดขาด (Mission-Critical)
- **Performance:** Generate เลขที่ได้เร็ว (<500ms normal, <2s p95, <5s p99)
- **Scalability:** รองรับ 50-100 concurrent requests/second
- **Maintainability:** ง่ายต่อการ Config และ Debug
- **Flexibility:** รองรับ Template-based format สำหรับแต่ละ document type
- **Auditability:** บันทึก history ของทุก generated number
- **Security:** ป้องกัน abuse ด้วย rate limiting
---
## Considered Options
### Option 1: Database AUTO_INCREMENT
**แนวทาง:** ใช้ MySQL AUTO_INCREMENT column
**Pros:**
- ✅ Simple implementation
- ✅ Database handles uniqueness
- ✅ Very fast performance
**Cons:**
- ❌ ไม่ Configurable (รูปแบบเลขที่ fixed)
- ❌ ยากต่อการ Partition by Project/Type/Discipline/Year
- ❌ ไม่รองรับ Custom format (เช่น `LCBP3-RFA-2025-0001`)
- ❌ Reset ตาม Year ทำได้ยาก
### Option 2: Application-Level Counter (Single Lock)
**แนวทาง:** ใช้ Redis INCR สำหรับ Counter
**Pros:**
- ✅ Fast performance (Redis in-memory)
- ✅ Configurable format
- ✅ Easy to partition (different Redis keys)
**Cons:**
- ❌ Single Point of Failure (ถ้า Redis down)
- ❌ ไม่มี Persistence ถ้า Redis crash (ถ้าไม่ใช้ AOF/RDB)
- ❌ Difficult to audit (ไม่มี history ใน DB)
### Option 3: **Double-Lock Mechanism (Redis + Database)** ⭐ (Selected)
**แนวทาง:** ใช้ Redis Distributed Lock + Database Optimistic Locking + Version Column
**Pros:**
-**Guaranteed Uniqueness:** Double-layer protection
-**Fast Performance:** Redis lock prevents most conflicts (<500ms)
-**Audit Trail:** Counter history + audit log in database
-**Configurable Format:** Template-based generation
-**Resilient:** Fallback to DB pessimistic lock if Redis unavailable
-**Partition Support:** Different counters per Project/Type/SubType/Discipline/Year
-**Transmittal Logic:** Support recipient-based counting
**Cons:**
- ❌ More complex implementation
- ❌ Slightly slower than pure Redis (but still fast)
- ❌ Requires both Redis and DB
---
## Decision Outcome
**Chosen Option:** Option 3 - Double-Lock Mechanism (Redis + Database)
### Rationale
เลือก Double-Lock เนื่องจาก:
1. **Mission-Critical:** เลขที่เอกสารต้องถูกต้อง 100% (ไม่ยอมรับการซ้ำ)
2. **Performance + Safety:** Balance ระหว่างความเร็วและความปลอดภัย
3. **Auditability:** มี Counter history + Audit log ใน Database
4. **Flexibility:** รองรับ Template-based format สำหรับทุก document type
5. **Resilience:** ถ้า Redis มีปัญหา ยัง Fallback ไปใช้ DB Lock ได้
---
## Implementation Details
### Database Schema
```sql
-- Format Templates
CREATE TABLE document_number_formats (
id INT PRIMARY KEY AUTO_INCREMENT,
project_id INT NOT NULL,
correspondence_type_id INT NULL COMMENT 'Specific Type ID, or NULL for Project Default', -- CHANGED: Allow NULL
format_template VARCHAR(100) NOT NULL COMMENT 'e.g. {PROJECT}-{TYPE}-{YEAR}-{SEQ:4}',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE,
-- Note: Application logic must enforce single default format per project
UNIQUE KEY unique_format (project_id, correspondence_type_id)
) ENGINE=InnoDB COMMENT='Template configurations for document numbering';
-- Counter Table with Optimistic Locking
CREATE TABLE document_number_counters (
project_id INT NOT NULL,
correspondence_type_id INT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT NOT NULL DEFAULT 0, -- 0 = no recipient (RFA)
sub_type_id INT DEFAULT 0,
rfa_type_id INT DEFAULT 0,
discipline_id INT DEFAULT 0,
reset_scope VARCHAR(20) NOT NULL,
last_number INT DEFAULT 0 NOT NULL,
version INT DEFAULT 0 NOT NULL,
created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (
project_id,
originator_organization_id,
recipient_organization_id,
correspondence_type_id,
sub_type_id,
rfa_type_id,
discipline_id,
reset_scope
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Running Number Counters';
-- Audit Trail
CREATE TABLE document_number_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
document_id INT DEFAULT NULL COMMENT 'FK to documents (set after doc creation)',
generated_number VARCHAR(255) NOT NULL,
counter_key VARCHAR(500) NOT NULL COMMENT 'Redis lock key used',
template_used VARCHAR(255) NOT NULL,
sequence_number INT NOT NULL,
user_id INT NOT NULL,
ip_address VARCHAR(45),
retry_count INT DEFAULT 0,
lock_wait_ms INT DEFAULT 0 COMMENT 'Time spent waiting for lock',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_audit_number (generated_number),
INDEX idx_audit_user (user_id, created_at),
INDEX idx_audit_created (created_at)
) ENGINE=InnoDB COMMENT='Audit trail for all generated document numbers';
```
### Token Types Reference
> [!IMPORTANT]
> **Updated to align with Requirements Specification**
>
> This ADR now uses token names from [03.11-document-numbering.md](../01-requirements/01-03.11-document-numbering.md) for consistency.
รองรับ Token ทั้งหมด:
| Token | Description | Example Value | Database Source |
| -------------- | ------------------------- | ------------------------------ | --------------------------------------------------------------------- |
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` |
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
| `{SEQ:n}` | Running number (n digits) | `0001`, `0029`, `0985` | `document_number_counters.last_number + 1` |
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `current_year + 543` |
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `current_year` |
| `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` |
> [!WARNING]
> **Deprecated Token Names (DO NOT USE)**
>
> The following tokens were used in earlier drafts but are now **deprecated**:
> - ~~`{ORG}`~~ → Use `{ORIGINATOR}` or `{RECIPIENT}` (explicit roles)
> - ~~`{TYPE}`~~ → Use `{CORR_TYPE}`, `{SUB_TYPE}`, or `{RFA_TYPE}` (context-specific)
> - ~~`{CATEGORY}`~~ → Not used in current system
>
> **Always refer to**: [03.11-document-numbering.md](../01-requirements/01-03.11-document-numbering.md) as source of truth
### Format Resolution Strategy (Fallback Logic)
The system resolves the numbering format using the following priority:
1. **Specific Format:** Search for a record matching both `project_id` and `correspondence_type_id`.
2. **Default Format:** If not found, search for a record with matching `project_id` where `correspondence_type_id` is `NULL`.
3. **System Fallback:** If neither exists, use the hardcoded system default: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`.
| Priority | Scenario | Template Source | Counter Scope (Key) | Reset Behavior |
| -------- | --------------------- | --------------------------------------------------- | ----------------------------- | ----------------------------------- |
| 1 | Specific Format Found | Database (project_id, type_id) | Specific Type (type_id) | Based on reset_sequence_yearly flag |
| 2 | Default Format Found | Database (project_id, type_id=NULL) | Shared Counter (type_id=NULL) | Based on reset_sequence_yearly flag |
| 3 | Fallback (No Config) | System Default: {ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE} | Shared Counter (type_id=NULL) | Reset Yearly (Default: True) |
### Format Examples by Document Type
#### 1. Correspondence (หนังสือราชการ)
**Letter:**
```
Template: {ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-0001-2568
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)
```
> **Note**: `{CORR_TYPE}` ไม่แสดงใน template เพื่อความกระชับ แต่ยังใช้ `correspondence_type_id` ใน Counter Key เพื่อแยก counter
**Other Types (RFI, MEMO, etc.):**
```
Template: {ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}
Example (RFI): คคง.-สคฉ.3-0042-2568
Example (MEMO): คคง.-ผรม.1-0001-2568
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)
```
> **Note**: แต่ละ type มี counter แยกกันผ่าน `correspondence_type_id`
#### 2. Transmittal
**Standard Format:**
```
Template: {ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-21-0117-2568
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)
```
**Token Breakdown:**
- `คคง.` = `{ORIGINATOR}` - ผู้ส่ง
- `สคฉ.3` = `{RECIPIENT}` - ผู้รับหลัก (TO)
- `21` = `{SUB_TYPE}` - หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 21=...)
- `0117` = `{SEQ:4}` - Running number
- `2568` = `{YEAR:B.E.}` - ปี พ.ศ.
> **Note**: `{CORR_TYPE}` ไม่แสดงใน template (เหมือน LETTER) เพื่อความกระชับ
#### 3. RFA (Request for Approval)
```
Template: {PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV}
Example: LCBP3-C2-RFA-TER-RPT-0001-A
Counter Key: (project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)
```
**Token Breakdown:**
- `LCBP3-C2` = `{PROJECT}` - รหัสโครงการ
- `RFA` = `{CORR_TYPE}` - ประเภทเอกสาร (**แสดง**ในtemplate สำหรับ RFA เท่านั้น)
- `TER` = `{DISCIPLINE}` - รหัสสาขา (TER=Terminal, STR=Structure, GEO=Geotechnical)
- `RPT` = `{RFA_TYPE}` - ประเภท RFA (RPT=Report, SDW=Shop Drawing, MAT=Material)
- `0001` = `{SEQ:4}` - Running number
- `A` = `{REV}` - Revision code
> **RFA Workflow**: เป็น Project-level document (ไม่ระบุ `recipient_organization_id` ใน counter key → NULL)
> **Workflow Path**: CONTRACTOR → CONSULTANT → OWNER
#### 4. Drawing
```
Template: {PROJECT}-{DISCIPLINE}-{CATEGORY}-{SEQ:4}-{REV}
Example: LCBP3-STR-DRW-0001-A
Counter Key: project_id + doc_type_id + discipline_id + category + year
```
### NestJS Service Implementation (Simplified)
```typescript
// File: backend/src/modules/document-numbering/document-numbering.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import Redlock from 'redlock';
import Redis from 'ioredis';
interface NumberingContext {
projectId: number;
docTypeId: number;
subTypeId?: number;
disciplineId?: number;
recipientType?: 'OWNER' | 'CONTRACTOR' | 'CONSULTANT' | 'OTHER';
year?: number;
userId: number;
ipAddress: string;
}
@Injectable()
export class DocumentNumberingService {
private readonly logger = new Logger(DocumentNumberingService.name);
constructor(
@InjectRepository(DocumentNumberCounter)
private counterRepo: Repository<DocumentNumberCounter>,
@InjectRepository(DocumentNumberConfig)
private configRepo: Repository<DocumentNumberConfig>,
@InjectRepository(DocumentNumberAudit)
private auditRepo: Repository<DocumentNumberAudit>,
private redis: Redis,
private redlock: Redlock
) {}
async generateNextNumber(context: NumberingContext): Promise<string> {
const year = context.year || new Date().getFullYear() + 543; // พ.ศ.
const subTypeId = context.subTypeId || 0; // Fallback for NULL
const disciplineId = context.disciplineId || 0; // Fallback for NULL
// Build Redis lock key
const lockKey = this.buildLockKey(
context.projectId,
context.docTypeId,
subTypeId,
disciplineId,
context.recipientType,
year
);
// Retry with exponential backoff (Scenario 2 & 3)
return this.retryWithBackoff(
async () => await this.generateNumberWithLock(
lockKey,
context,
year,
subTypeId,
disciplineId
),
5, // Max 5 retries
1000 // Initial delay 1s
);
}
private async generateNumberWithLock(
lockKey: string,
context: NumberingContext,
year: number,
subTypeId: number,
disciplineId: number
): Promise<string> {
let lock: any;
const lockStartTime = Date.now();
try {
// Scenario 1: Redis Unavailable - Fallback to DB lock
try {
// Step 1: Acquire Redis Distributed Lock (TTL: 5 seconds)
lock = await this.redlock.acquire([lockKey], 5000);
} catch (redisError) {
this.logger.warn(`Redis lock failed, falling back to DB lock: ${redisError.message}`);
// Fallback: Use SELECT ... FOR UPDATE (Pessimistic Lock)
return await this.generateWithDatabaseLock(context, year, subTypeId, disciplineId);
}
const lockWaitMs = Date.now() - lockStartTime;
// Step 2: Query current counter with version
let counter = await this.counterRepo.findOne({
where: {
project_id: context.projectId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
recipient_type: context.recipientType || null,
year: year,
},
});
// Initialize counter if not exists
if (!counter) {
counter = this.counterRepo.create({
project_id: context.projectId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
recipient_type: context.recipientType || null,
year: year,
last_number: 0,
version: 0,
});
await this.counterRepo.save(counter);
}
const currentVersion = counter.version;
const nextNumber = counter.last_number + 1;
// Step 3: Update counter with Optimistic Lock check (Scenario 3)
const result = await this.counterRepo
.createQueryBuilder()
.update(DocumentNumberCounter)
.set({
last_number: nextNumber,
version: () => 'version + 1',
})
.where({
project_id: context.projectId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
recipient_type: context.recipientType || null,
year: year,
version: currentVersion, // Optimistic lock check
})
.execute();
if (result.affected === 0) {
throw new ConflictException('Counter version conflict - retrying...');
}
// Step 4: Generate formatted number
const config = await this.getConfig(
context.projectId,
context.docTypeId,
subTypeId,
disciplineId
);
const formattedNumber = await this.formatNumber(config.template, {
...context,
year,
sequenceNumber: nextNumber,
});
// Step 5: Audit logging
await this.auditRepo.save({
generated_number: formattedNumber,
counter_key: lockKey,
template_used: config.template,
sequence_number: nextNumber,
user_id: context.userId,
ip_address: context.ipAddress,
retry_count: 0,
lock_wait_ms: lockWaitMs,
});
this.logger.log(`Generated: ${formattedNumber} (wait: ${lockWaitMs}ms)`);
return formattedNumber;
} finally {
// Step 6: Release Redis lock
if (lock) {
await lock.release();
}
}
}
private async formatNumber(template: string, data: any): Promise<string> {
// Token replacement logic
const tokens = {
'{PROJECT}': await this.getProjectCode(data.projectId),
'{ORIGINATOR}': await this.getOriginatorOrgCode(data.originatorOrgId),
'{RECIPIENT}': await this.getRecipientOrgCode(data.recipientOrgId),
'{CORR_TYPE}': await this.getCorrespondenceTypeCode(data.corrTypeId),
'{SUB_TYPE}': await this.getSubTypeCode(data.subTypeId),
'{RFA_TYPE}': await this.getRfaTypeCode(data.rfaTypeId),
'{DISCIPLINE}': await this.getDisciplineCode(data.disciplineId),
'{SEQ:4}': data.sequenceNumber.toString().padStart(4, '0'),
'{SEQ:5}': data.sequenceNumber.toString().padStart(5, '0'),
'{YEAR:B.E.}': data.year.toString(),
'{YEAR:A.D.}': (data.year - 543).toString(),
'{REV}': data.revisionCode || 'A',
};
let result = template;
for (const [token, value] of Object.entries(tokens)) {
result = result.replace(new RegExp(token, 'g'), value);
}
return result;
}
private buildLockKey(...parts: Array<number | string | null | undefined>): string {
return `doc_num:${parts.filter(p => p !== null && p !== undefined).join(':')}`;
}
// Scenario 2: Lock Acquisition Timeout - Exponential Backoff
private async retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number,
initialDelay: number
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable =
error instanceof ConflictException ||
error.code === 'ECONNREFUSED' || // Scenario 4
error.code === 'ETIMEDOUT'; // Scenario 4
if (!isRetryable || attempt === maxRetries) {
if (attempt === maxRetries) {
throw new ServiceUnavailableException(
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
);
}
throw error;
}
const delay = initialDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
this.logger.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
}
}
}
// Scenario 1: Fallback to Database Lock
private async generateWithDatabaseLock(
context: NumberingContext,
year: number,
subTypeId: number,
disciplineId: number
): Promise<string> {
return await this.counterRepo.manager.transaction(async (manager) => {
// Pessimistic lock: SELECT ... FOR UPDATE
const counter = await manager
.createQueryBuilder(DocumentNumberCounter, 'counter')
.setLock('pessimistic_write')
.where({
project_id: context.projectId,
doc_type_id: context.docTypeId,
sub_type_id: subTypeId,
discipline_id: disciplineId,
recipient_type: context.recipientType || null,
year: year,
})
.getOne();
const nextNumber = (counter?.last_number || 0) + 1;
// Update counter
await manager.save(DocumentNumberCounter, {
...counter,
last_number: nextNumber,
});
// Format and return
const config = await this.getConfig(context.projectId, context.docTypeId, subTypeId, disciplineId);
return await this.formatNumber(config.template, {
...context,
year,
sequenceNumber: nextNumber,
});
});
}
}
```
### Algorithm Flow
```mermaid
sequenceDiagram
participant Client
participant Service as Numbering Service
participant Redis
participant DB as MariaDB
participant Audit
Client->>Service: generateNextNumber(context)
Service->>Redis: ACQUIRE Lock (key, TTL=5s)
alt Redis Available
Redis-->>Service: Lock Success
Service->>DB: SELECT counter (with version)
DB-->>Service: current_number, version
Service->>DB: UPDATE counter SET last_number=X, version=version+1<br/>WHERE version=old_version
alt Update Success (No Conflict)
DB-->>Service: Success (1 row affected)
Service->>Service: Format Number with Template
Service->>Audit: Log generated number + metadata
Service->>Redis: RELEASE Lock
Service-->>Client: Document Number
else Version Conflict (Scenario 3)
DB-->>Service: Failed (0 rows affected)
Service->>Redis: RELEASE Lock
Service->>Service: Retry with Exponential Backoff (2x)
Note over Service: If still fail after 2 retries:<br/>Return 409 Conflict
end
else Redis Unavailable (Scenario 1)
Redis-->>Service: Connection Error
Service->>DB: BEGIN TRANSACTION
Service->>DB: SELECT ... FOR UPDATE (Pessimistic Lock)
DB-->>Service: Counter (locked)
Service->>DB: UPDATE counter
Service->>DB: COMMIT
Service-->>Client: Document Number (slower but works)
end
alt Lock Timeout (Scenario 2)
Redis-->>Service: Lock Acquisition Timeout
Service->>Service: Retry 5 times with backoff<br/>(1s, 2s, 4s, 8s, 16s)
Note over Service: If all retries fail:<br/>Return 503 Service Unavailable
end
```
---
## Error Handling Scenarios
### Scenario 1: Redis Unavailable
**Trigger:** Redis connection error, Redis down
**Fallback:**
- ใช้ Database-only locking (`SELECT ... FOR UPDATE`)
- Log warning และแจ้ง ops team
- ระบบยังใช้งานได้แต่ performance ลดลง (slower)
### Scenario 2: Lock Acquisition Timeout
**Trigger:** หลาย requests แย่งชิง lock พร้อมกัน
**Retry Logic:**
- Retry 5 ครั้งด้วย exponential backoff: 1s, 2s, 4s, 8s, 16s (รวม ~31 วินาที)
- หลัง 5 ครั้ง: Return HTTP 503 "Service Temporarily Unavailable"
- Frontend: แสดง "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
### Scenario 3: Version Conflict After Lock
**Trigger:** Optimistic lock version mismatch
**Retry Logic:**
- Retry 2 ครั้ง (reload counter + retry transaction)
- หลัง 2 ครั้ง: Return HTTP 409 Conflict
- Frontend: แสดง "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
### Scenario 4: Database Connection Error
**Trigger:** Database connection timeout, connection pool exhausted
**Retry Logic:**
- Retry 3 ครั้งด้วย exponential backoff: 1s, 2s, 4s
- หลัง 3 ครั้ง: Return HTTP 500 "Internal Server Error"
- Frontend: แสดง "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ"
---
## Performance Requirements
### Response Time Targets
| Metric | Target | Description |
| ---------------- | ---------- | ----------------------------------- |
| Normal Operation | <500ms | Under normal load, no conflicts |
| 95th Percentile | <2 seconds | Including retry scenarios |
| 99th Percentile | <5 seconds | Extreme cases with multiple retries |
### Throughput Targets
| Load Level | Target | Notes |
| ----------- | ----------- | ----------------------------- |
| Normal Load | 50 req/sec | Typical office hours |
| Peak Load | 100 req/sec | Construction deadline periods |
### Availability
- **Uptime:** ≥99.5% (exclude planned maintenance)
- **Maximum Downtime:** ≤3.6 hours/month
---
## Monitoring & Alerting
### Metrics to Track
1. **Lock Acquisition Metrics:**
- Lock wait time (p50, p95, p99)
- Lock acquisition success rate
- Lock timeout count
2. **Counter Generation:**
- Generation latency (p50, p95, p99)
- Generation success rate
- Retry count distribution
3. **System Health:**
- Redis connection status
- Database connection pool usage
- Error rate by scenario (1-4)
### Alert Conditions
| Severity | Condition | Action |
| ---------- | ---------------------------- | ------------------ |
| 🔴 Critical | Redis unavailable >1 minute | Page ops team |
| 🔴 Critical | Lock failures >10% in 5 min | Page ops team |
| 🟡 Warning | Lock failures >5% in 5 min | Alert ops team |
| 🟡 Warning | Avg lock wait time >1 second | Alert ops team |
| 🟡 Warning | Retry count >100/hour | Review system load |
### Dashboard Panels
- Real-time lock acquisition success rate (%)
- Lock wait time percentiles chart
- Counter generation rate (per minute)
- Error rate breakdown by type
- Redis/Database health indicators
---
## Security Considerations
### Authorization
- เฉพาะ **authenticated users** สามารถ request document number
- เฉพาะ **Project Admin** สามารถแก้ไข template
- เฉพาะ **Super Admin** สามารถ reset counter
### Rate Limiting
Prevent abuse และ resource exhaustion:
| Scope | Limit | Window |
| -------------- | ------------- | -------- |
| Per User | 10 requests | 1 minute |
| Per IP Address | 50 requests | 1 minute |
| Global | 5000 requests | 1 minute |
**Implementation:** ใช้ Redis-based rate limiter middleware
### Audit & Compliance
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
- เก็บ audit log อย่างน้อย **7 ปี** (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
- บันทึก: user, IP, timestamp, generated number, retry count
---
## Consequences
### Positive
1.**Zero Duplicate Risk:** Double-lock + DB constraint guarantees uniqueness
2.**High Performance:** Redis lock + optimistic locking (<500ms normal)
3.**Complete Audit Trail:** All counters + generated numbers in database
4.**Highly Configurable:** Template-based for all document types
5.**Partition Support:** Separate counters per Project/Type/SubType/Discipline/Recipient/Year
6.**Resilient:** Multiple fallback strategies for all failure scenarios
7.**Transmittal Logic:** Supports recipient-based numbering
8.**Security:** Rate limiting + authorization + audit logging
### Negative
1.**Complexity:** Requires coordination between Redis and Database
2.**Dependencies:** Requires both Redis and DB healthy for optimal performance
3.**Retry Logic:** May retry causing delays under high contention
4.**Monitoring Overhead:** Need comprehensive monitoring for all scenarios
### Mitigation Strategies
- **Redis Dependency:** Use Redis Persistence (AOF) + Replication + Fallback to DB
- **Complexity:** Encapsulate all logic in `DocumentNumberingService`
- **Retry Delays:** Exponential backoff limits max delay time
- **Monitoring:** Automated dashboards + alerting for all critical metrics
---
## Testing Strategy
### Unit Tests
```typescript
describe('DocumentNumberingService - Concurrent Generation', () => {
it('should generate unique numbers for 100 concurrent requests', async () => {
const context: NumberingContext = {
projectId: 1,
docTypeId: 2, // RFA
disciplineId: 3, // STR
year: 2568,
userId: 1,
ipAddress: '192.168.1.1',
};
const promises = Array(100)
.fill(null)
.map(() => service.generateNextNumber(context));
const results = await Promise.all(promises);
// Check uniqueness
const unique = new Set(results);
expect(unique.size).toBe(100);
// Check format
results.forEach(num => {
expect(num).toMatch(/^LCBP3-C2-RFI-STR-\d{4}-A$/);
});
});
it('should use correct format for Transmittal To Owner', async () => {
const number = await service.generateNextNumber({
projectId: 1,
docTypeId: 3, // Transmittal
recipientType: 'OWNER',
year: 2568,
userId: 1,
ipAddress: '192.168.1.1',
});
expect(number).toMatch(/^คคง\.-สคฉ\.3-03-21-\d{4}-2568$/);
});
it('should fallback to DB lock when Redis unavailable', async () => {
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Redis down'));
const number = await service.generateNextNumber(context);
expect(number).toBeDefined();
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('falling back to DB lock'));
});
it('should retry on version conflict and succeed', async () => {
let attempt = 0;
jest.spyOn(counterRepo, 'createQueryBuilder').mockImplementation(() => {
attempt++;
return {
update: () => ({
set: () => ({
where: () => ({
execute: async () => ({
affected: attempt === 1 ? 0 : 1, // Fail first, succeed second
}),
}),
}),
}),
} as any;
});
const result = await service.generateNextNumber(context);
expect(result).toBeDefined();
expect(attempt).toBe(2);
});
it('should throw 503 after max lock acquisition retries', async () => {
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Lock timeout'));
await expect(service.generateNextNumber(context))
.rejects
.toThrow(ServiceUnavailableException);
});
});
```
### Load Testing
```yaml
# artillery.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 50 # 50 requests/second
name: 'Normal Load'
- duration: 30
arrivalRate: 100 # 100 requests/second
name: 'Peak Load'
scenarios:
- name: 'Generate Document Numbers - RFA'
weight: 40
flow:
- post:
url: '/api/v1/rfa'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
doc_type_id: 2
discipline_id: 3
- name: 'Generate Document Numbers - Transmittal'
weight: 30
flow:
- post:
url: '/api/v1/transmittals'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
doc_type_id: 3
recipient_type: 'OWNER'
- name: 'Generate Document Numbers - Correspondence'
weight: 30
flow:
- post:
url: '/api/v1/correspondences'
json:
title: 'Load Test {{ $randomString() }}'
project_id: 1
doc_type_id: 1
expect:
- statusCode: 200
- statusCode: 201
- contentType: json
ensure:
p95: 2000 # 95th percentile < 2 seconds
p99: 5000 # 99th percentile < 5 seconds
maxErrorRate: 0.1 # < 0.1% errors
```
---
## Compliance
เป็นไปตาม:
- ✅ [Requirements 3.11](../01-requirements/01-03.11-document-numbering.md) - Document Numbering Management (v1.6.2)
- ✅ [Implementation Guide](../03-implementation/03-04-document-numbering.md) - DocumentNumberingModule (v1.6.1)
- ✅ [Operations Guide](../04-operations/04-08-document-numbering-operations.md) - Monitoring & Troubleshooting
- ✅ [Security Best Practices](../02-architecture/security-architecture.md) - Rate Limiting, Audit Logging
---
## Related ADRs
- [ADR-001: Unified Workflow Engine](./ADR-001-unified-workflow-engine.md) - Workflow triggers number generation
- [ADR-005: Redis Usage Strategy](./ADR-005-redis-usage-strategy.md) - Redis lock implementation details
- [ADR-006: Audit Logging Strategy](./ADR-006-audit-logging-strategy.md) - Comprehensive audit requirements
---
## References
- [Redlock Algorithm](https://redis.io/topics/distlock) - Distributed locking with Redis
- [TypeORM Optimistic Locking](https://typeorm.io/entities#version-column) - Version column usage
- [Distributed Lock Patterns](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) - Martin Kleppmann's analysis
- [Redis Persistence](https://redis.io/topics/persistence) - AOF and RDB strategies
- [Rate Limiting Patterns](https://redis.io/glossary/rate-limiting/) - Redis-based rate limiting
---
## Version History
| Version | Date | Changes |
| ------- | ---------- | ------------------------------------------------------------------------------------------------- |
| 1.0 | 2025-11-30 | Initial decision |
| 2.0 | 2025-12-02 | Updated with comprehensive error scenarios, monitoring, security, and all token types |
| 3.0 | 2025-12-17 | Aligned with Requirements v1.6.2: updated counter schema, token definitions, Number State Machine |

View File

@@ -0,0 +1,291 @@
# ADR-005: Technology Stack Selection
**Status:** Accepted
**Date:** 2025-11-30
**Decision Makers:** Development Team, CTO
**Related Documents:**
- [System Architecture](../02-architecture/02-01-system-architecture.md)
- [FullStack JS Guidelines](../03-implementation/03-01-fullftack-js-v1.7.0.md)
---
## Context and Problem Statement
LCBP3-DMS ต้องเลือก Technology Stack สำหรับพัฒนา Document Management System ที่:
- รองรับ Multi-user Concurrent Access
- จัดการเอกสารซับซ้อนพร้อม Workflow
- Deploy บน QNAP Server (ข้อจำกัดด้าน Infrastructure)
- พัฒนาโดย Small Team (1-3 developers)
- Maintain ได้ในระยะยาว (5+ years)
---
## Decision Drivers
- **Development Speed:** พัฒนาได้เร็ว (6-12 months MVP)
- **Maintainability:** Maintain และ Scale ได้ง่าย
- **Team Expertise:** ทีมมีประสบการณ์ TypeScript/JavaScript
- **Infrastructure Constraints:** Deploy บน QNAP Container Station
- **Community Support:** มี Community และ Documentation ดี
- **Future-proof:** Technology ยังได้รับการ Support อย่างน้อย 5 ปี
---
## Considered Options
### Option 1: Traditional Stack (PHP + Laravel + Vue)
**Pros:**
- ✅ Mature ecosystem
- ✅ Good documentation
- ✅ Familiar to many developers
**Cons:**
- ❌ Team ไม่มีประสบการณ์ PHP
- ❌ Separate language for frontend/backend
- ❌ Less TypeScript support
### Option 2: Java Stack (Spring Boot + React)
**Pros:**
- ✅ Enterprise-grade
- ✅ Strong typing
- ✅ Excellent tooling
**Cons:**
- ❌ Team ไม่มีประสบการณ์ Java
- ❌ Higher resource usage (QNAP limitation)
- ❌ Slower development cycle
### Option 3: **Full Stack TypeScript (NestJS + Next.js)** ⭐ (Selected)
**Pros:**
-**Single Language:** TypeScript ทั้ง Frontend และ Backend
-**Team Expertise:** ทีมมีประสบการณ์ Node.js/TypeScript
-**Modern Architecture:** Modular, Scalable, Maintainable
-**Rich Ecosystem:** NPM packages มากมาย
-**Fast Development:** Code sharing, Type safety
-**QNAP Compatible:** Docker deployment support
**Cons:**
- ❌ Runtime Performance ต่ำกว่า Java/Go
- ❌ Package.json dependency management complexity
---
## Decision Outcome
**Chosen Option:** Option 3 - Full Stack TypeScript (NestJS + Next.js)
### Selected Technologies
#### Backend Stack
| Component | Technology | Rationale |
| :----------------- | :-------------- | :--------------------------------------------- |
| **Runtime** | Node.js 20 LTS | Stable, modern features, long-term support |
| **Framework** | NestJS | Modular, TypeScript-first, similar to Angular |
| **Language** | TypeScript 5.x | Type safety, better DX |
| **ORM** | TypeORM | TypeScript support, migrations, repositories |
| **Database** | MariaDB 11.8 | JSON support, virtual columns, QNAP compatible |
| **Validation** | class-validator | Decorator-based, integrates with NestJS |
| **Authentication** | Passport + JWT | Standard, well-supported |
| **Authorization** | CASL | Flexible RBAC implementation |
| **Documentation** | Swagger/OpenAPI | Auto-generated from decorators |
| **Testing** | Jest | Built-in with NestJS |
#### Frontend Stack
| Component | Technology | Rationale |
| :-------------------- | :------------------ | :------------------------------------- |
| **Framework** | Next.js 14+ | App Router, SSR/SSG, React integration |
| **UI Library** | React 19 | Industry standard, large ecosystem |
| **Language** | TypeScript 5.x | Consistency with backend |
| **Styling** | Tailwind CSS | Utility-first, fast development |
| **Component Library** | shadcn/ui | Accessible, customizable, TypeScript |
| **State Management** | TanStack Query | Server state management |
| **Form Handling** | React Hook Form | Performance, ต้ validation with Zod |
| **Testing** | Vitest + Playwright | Fast unit tests, reliable E2E |
#### Infrastructure
| Component | Technology | Rationale |
| :------------------- | :---------------------- | :------------------------------- |
| **Containerization** | Docker + Docker Compose | QNAP Container Station |
| **Reverse Proxy** | Nginx Proxy Manager | UI-based SSL management |
| **Database** | MariaDB 11.8 | Robust, JSON support |
| **Cache** | Redis 7 | Session, locks, queue management |
| **Search** | Elasticsearch 8 | Full-text search |
| **Workflow** | n8n | Visual workflow automation |
| **Git** | Gitea | Self-hosted, lightweight |
---
## Architecture Decisions
### Backend Architecture: Modular Monolith
**Chosen:** Modular Monolith (Not Microservices)
**Rationale:**
- ✅ Easier to develop and deploy initially
- ✅ Lower infrastructure overhead (QNAP limitation)
- ✅ Simpler debugging and testing
- ✅ Can split into microservices later if needed
- ✅ Modules communicate via Event Emitters (loosely coupled)
**Module Structure:**
```
backend/src/
├── common/ # Shared utilities
├── modules/
│ ├── auth/
│ ├── user/
│ ├── project/
│ ├── correspondence/
│ ├── rfa/
│ ├── workflow-engine/
│ └── ...
```
### Frontend Architecture: Server-Side Rendering (SSR)
**Chosen:** Next.js with App Router (SSR + Client Components)
**Rationale:**
- ✅ Better SEO (if needed in future)
- ✅ Faster initial page load
- ✅ Flexibility (SSR + CSR)
- ✅ Built-in routing and API routes
- ✅ Image optimization
---
## Development Workflow
### Monorepo Structure
```
lcbp3-dms/
├── backend/ # NestJS
├── frontend/ # Next.js
├── docs/ # Documentation
├── specs/ # Specifications
└── docker-compose.yml
```
**Chosen:** Separate repositories (Not Monorepo)
**Rationale:**
- ✅ ง่ายต่อการ Deploy แยกกัน
- ✅ สิทธิ์ Git แยกได้ (Frontend team / Backend team)
- ✅ CI/CD pipeline ง่ายกว่า
- ❌ Cons: Shared types ต้องจัดการแยก
---
## Database Decisions
### ORM vs Query Builder
**Chosen:** TypeORM (ORM)
**Rationale:**
- ✅ Type-safe entities
- ✅ Migration management
- ✅ Relationship mapping
- ✅ Query Builder available when needed
- ❌ Cons: Learning curve for complex queries
### Database Choice
**Chosen:** MariaDB 11.8 (Not PostgreSQL)
**Rationale:**
- ✅ QNAP supports MariaDB หนึ่งของโจทย์
- ✅ JSON support (MariaDB 10.2+)
- ✅ Virtual columns for JSON indexing
- ✅ Familiar MySQL syntax
- ❌ Cons: ฟีเจอร์บางอย่างไม่เท่า PostgreSQL
---
## Styling Decision
### CSS Framework
**Chosen:** Tailwind CSS (Not Bootstrap, Material-UI)
**Rationale:**
- ✅ Utility-first, fast development
- ✅ Small bundle size (purge unused)
- ✅ Highly customizable
- ✅ Works well with shadcn/ui
- ✅ TypeScript autocomplete support
---
## Consequences
### Positive
1.**Single Language:** TypeScript ลด Context Switching
2.**Code Sharing:** Share types/interfaces ระหว่าง Frontend/Backend
3.**Fast Development:** Modern tooling, hot reload
4.**Type Safety:** Catch errors at compile time
5.**Rich Ecosystem:** NPM packages มากมาย
6.**Good DX:** Excellent developer experience
### Negative
1.**Runtime Performance:** ช้ากว่า Compiled languages
2.**Dependency Management:** NPM dependency hell
3.**Memory Usage:** Node.js ใช้ RAM มากกว่า PHP
4.**Package Updates:** Breaking changes บ่อย
### Mitigation Strategies
- **Performance:** ใช้ Redis caching, Database indexing
- **Dependencies:** Lock versions, use `pnpm` for deduplication
- **Memory:** Monitor และ Optimize, Set Node.js memory limits
- **Updates:** Test thoroughly before upgrading major versions
---
## Compliance
เป็นไปตาม:
- [FullStack JS Guidelines](../03-implementation/03-01-fullftack-js-v1.7.0.md)
- [Backend Guidelines](../03-implementation/03-02-backend-guidelines.md)
- [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md)
---
## Related ADRs
- [ADR-007: Deployment Strategy](./ADR-007-deployment-strategy.md) - Docker deployment details
---
## References
- [NestJS Documentation](https://docs.nestjs.com/)
- [Next.js Documentation](https://nextjs.org/docs)
- [TypeORM Documentation](https://typeorm.io/)
- [State of JavaScript 2024](https://stateofjs.com/)

View File

@@ -0,0 +1,438 @@
# ADR-006: Redis Usage and Caching Strategy
**Status:** Accepted
**Date:** 2025-11-30
**Decision Makers:** Development Team, System Architect
**Related Documents:**
- [System Architecture](../02-architecture/02-01-system-architecture.md)
- [Performance Requirements](../01-requirements/01-06-non-functional.md)
---
## Context and Problem Statement
LCBP3-DMS ต้องการ High Performance ในการ:
- Check Permissions (ทุก Request)
- Document Numbering (Concurrent Safe)
- Master Data Access (ถูกเรียกบ่อยมาก)
- Session Management
- Background Job Queue
**Challenges:**
- Database queries ช้า (แม้มี indexing)
- Concurrent access ต้องมี Locking mechanism
- Permission checking ต้องเร็ว (< 10ms)
- Master data แทบไม่เปลี่ยน แต่ถูก query บ่อย
---
## Decision Drivers
- **Performance:** Response time < 200ms (p90)
- **Scalability:** รองรับ 100+ concurrent users
- **Consistency:** Data consistency with database
- **Reliability:** Cache must not cause data loss
- **Cost-Effectiveness:** ใช้ Resource น้อยที่สุด
---
## Considered Options
### Option 1: No Caching (Database Only)
**Pros:**
- ✅ Simple, no cache invalidation
- ✅ Always consistent
**Cons:**
- ❌ Slow permission checks (JOIN tables)
- ❌ High DB load
- ❌ No distributed locking
### Option 2: Application-Level In-Memory Cache
**Pros:**
- ✅ Very fast (local memory)
- ✅ No external dependency
**Cons:**
- ❌ Not shared across instances
- ❌ No distributed locking
- ❌ Cache invalidation issues
### Option 3: **Redis as Distributed Cache + Lock** ⭐ (Selected)
**Pros:**
-**Fast:** In-memory, < 1ms access
-**Distributed:** Shared across instances
-**Locking:** Redis locks for concurrency
-**Pub/Sub:** Cache invalidation broadcasting
-**Queue:** BullMQ for background jobs
**Cons:**
- ❌ External dependency
- ❌ Requires Redis cluster for HA
---
## Decision Outcome
**Chosen Option:** Redis as Distributed Cache + Lock Provider
---
## Redis Usage Patterns
### 1. Distributed Locking (Redlock)
**Use Cases:**
- Document Number Generation
- Critical Sections
**Implementation:**
```typescript
const lock = await redlock.acquire([lockKey], 3000); // 3sec TTL
try {
// Critical section
} finally {
await lock.release();
}
```
**Configuration:**
- TTL: 2-5 seconds
- Retry: Exponential backoff, max 3 retries
---
### 2. Permission Caching
**Cache Structure:**
```typescript
// Key: user:{user_id}:permissions
// Value: JSON array of CASL rules
// TTL: 30 minutes
await redis.set(
`user:${userId}:permissions`,
JSON.stringify(abilityRules),
'EX',
1800
);
```
**Invalidation Strategy:**
- Role changed → Invalidate all users with that role
- User assignment changed → Invalidate that user
- Permission modified → Invalidate all affected roles
---
### 3. Master Data Caching
**Cached Data:**
- Organizations (TTL: 1 hour)
- Projects (TTL: 1 hour)
- Correspondence Types (TTL: 24 hours)
- RFA Status Codes (TTL: 24 hours)
- Roles & Permissions (TTL: 30 minutes)
**Cache Pattern:**
```typescript
async getOrganizations(): Promise<Organization[]> {
const cacheKey = 'master:organizations';
let cached = await redis.get(cacheKey);
if (!cached) {
const organizations = await this.orgRepo.find({ where: { is_active: true } });
await redis.set(cacheKey, JSON.stringify(organizations), 'EX', 3600);
return organizations;
}
return JSON.parse(cached);
}
```
**Invalidation:**
- On CREATE/UPDATE/DELETE → Invalidate immediately
- Publish event to Redis Pub/Sub for multi-instance sync
---
### 4. Session Management
**Structure:**
```typescript
// Key: session:{session_id}
// Value: User session data
// TTL: 8 hours
interface SessionData {
user_id: number;
username: string;
organization_id: number;
last_activity: Date;
}
```
**Refresh Strategy:**
- Update `last_activity` on every request
- Extend TTL if activity within last 1 hour
---
### 5. Rate Limiting
**Implementation:**
```typescript
const key = `rate_limit:${userId}:${endpoint}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, 3600); // 1 hour window
}
if (current > limit) {
throw new TooManyRequestsException();
}
```
**Limits:**
- File Upload: 50 req/hour per user
- Search: 500 req/hour per user
- Anonymous: 100 req/hour per IP
---
### 6. Background Job Queue (BullMQ)
**Queues:**
1. **Email Queue:** Send email notifications
2. **Notification Queue:** LINE Notify
3. **Indexing Queue:** Elasticsearch indexing
4. **Cleanup Queue:** Delete temp files
5. **Report Queue:** Generate PDF reports
**Configuration:**
```typescript
const emailQueue = new Queue('email', {
connection: redisConnection,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: 100, // Keep last 100
removeOnFail: 500,
},
});
```
---
## Cache Invalidation Strategy
### 1. Time-Based Expiration (TTL)
| Data Type | TTL | Rationale |
| :------------- | :--------- | :---------------------------- |
| Permissions | 30 minutes | Balance freshness/performance |
| Master Data | 1 hour | Rarely changes |
| Session | 8 hours | Match JWT expiration |
| Search Results | 15 minutes | Data changes frequently |
### 2. Event-Based Invalidation
**Pattern:**
```typescript
@Injectable()
export class CacheInvalidationService {
async invalidateUserPermissions(userId: number): Promise<void> {
await this.redis.del(`user:${userId}:permissions`);
// Broadcast to other instances
await this.redis.publish(
'cache:invalidate',
JSON.stringify({
pattern: 'user:permissions',
userId,
})
);
}
async invalidateMasterData(entity: string): Promise<void> {
await this.redis.del(`master:${entity}`);
await this.redis.publish(
'cache:invalidate',
JSON.stringify({
pattern: 'master',
entity,
})
);
}
}
```
### 3. Write-Through Cache
**For Master Data:**
```typescript
async updateOrganization(id: number, dto: UpdateOrgDto): Promise<Organization> {
const org = await this.orgRepo.save({ id, ...dto });
// Invalidate cache immediately
await this.cache.invalidateMasterData('organizations');
return org;
}
```
---
## Redis Configuration
### Production Setup
```yaml
# docker-compose.yml
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--appendfsync everysec
--maxmemory 2gb
--maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 3s
retries: 3
```
**Key Settings:**
- `appendonly yes`: AOF persistence
- `appendfsync everysec`: Write every second (balance performance/durability)
- `maxmemory 2gb`: Limit memory usage
- `maxmemory-policy allkeys-lru`: Evict least recently used keys
---
### High Availability Considerations
**Future Improvements:**
1. **Redis Sentinel:** Auto-failover
2. **Redis Cluster:** Horizontal scaling
3. **Read Replicas:** Offload read queries
**Current:** Single Redis instance (sufficient for MVP)
---
## Monitoring
### Key Metrics
```typescript
@Injectable()
export class RedisMonitoringService {
@Cron('*/5 * * * *') // Every 5 minutes
async captureMetrics(): Promise<void> {
const info = await this.redis.info();
// Parse and log metrics
metrics.record({
'redis.memory.used': parseMemoryUsed(info),
'redis.memory.peak': parseMemoryPeak(info),
'redis.keyspace.hits': parseHits(info),
'redis.keyspace.misses': parseMisses(info),
'redis.connections.active': parseConnections(info),
});
}
}
```
**Alert Thresholds:**
- Memory usage > 80%
- Hit rate < 70%
- Connections > 90% of max
---
## Consequences
### Positive
1.**Fast Permission Check:** < 5ms (vs 50ms from DB)
2.**Reduced DB Load:** 70% reduction in queries
3.**Distributed Locking:** No race conditions
4.**Queue Management:** Background jobs reliable
5.**Scalability:** รองรับ Multi-instance deployment
### Negative
1.**Dependency:** Redis ต้อง Available เสมอ
2.**Memory Limit:** ต้อง Monitor และ Evict
3.**Complexity:** Cache invalidation ซับซ้อน
4.**Data Loss Risk:** ถ้า Redis crash (with AOF mitigates this)
### Mit Strategies
- **Dependency:** Health checks + Fallback to DB
- **Memory:** Set max memory + LRU eviction policy
- **Complexity:** Centralize invalidation logic
- **Data Loss:** Enable AOF persistence
---
## Compliance
เป็นไปตาม:
- [System Architecture Section 3.5](../02-architecture/02-01-system-architecture.md#redis)
- [Performance Requirements](../01-requirements/01-06-non-functional.md)
---
## Related ADRs
- [ADR-002: Document Numbering Strategy](./ADR-002-document-numbering-strategy.md) - Redis locks
- [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md) - Permission caching
---
## References
- [Redis Documentation](https://redis.io/docs/)
- [Redlock Algorithm](https://redis.io/topics/distlock)
- [BullMQ Documentation](https://docs.bullmq.io/)
- [Cache Invalidation Strategies](https://martinfowler.com/bliki/TwoHardThings.html)

View File

@@ -0,0 +1,388 @@
# ADR-008: Email & Notification Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Backend Team, System Architect
**Related Documents:** [Backend Guidelines](../03-implementation/03-02-backend-guidelines.md), [TASK-BE-011](../06-tasks/README.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS ต้องการส่งการแจ้งเตือนให้ผู้ใช้งานผ่านหลายช่องทาง (Email, LINE Notify, In-App) เมื่อมี Events สำคัญเกิดขึ้น เช่น Correspondence ได้รับการอนุมัติ, RFA ถูก Review, Workflow เปลี่ยนสถานะ
### ปัญหาที่ต้องแก้:
1. **Multi-Channel:** รองรับหลายช่องทางการแจ้งเตือน (Email, LINE, In-app)
2. **Reliability:** ทำอย่างไรให้การส่ง Email ไม่ Block main request
3. **Retry Logic:** จัดการ Email delivery failures อย่างไร
4. **Template Management:** จัดการ Email templates อย่างไรให้ Maintainable
5. **User Preferences:** ให้ User เลือก Channel ที่ต้องการได้อย่างไร
---
## Decision Drivers
-**Performance:** ส่ง Email ต้องไม่ทำให้ API Response ช้า
- 🔄 **Reliability:** Email ส่งไม่สำเร็จต้อง Retry ได้
- 🎨 **Branding:** Email template ต้องดูเป็นมืออาชีพ
- 🛠️ **Maintainability:** แก้ไข Template ได้ง่าย
- 📱 **Multi-Channel:** รองรับ Email, LINE, In-app notification
---
## Considered Options
### Option 1: Sync Email Sending (ส่งทันที ใน Request)
**Implementation:**
```typescript
await this.emailService.sendEmail({ to, subject, body });
return { success: true };
```
**Pros:**
- ✅ Simple implementation
- ✅ ง่ายต่อการ Debug
**Cons:**
- ❌ Block API response (slow)
- ❌ หาก SMTP server down จะ Timeout
- ❌ ไม่มี Retry mechanism
### Option 2: Async with Event Emitter (NestJS EventEmitter)
**Implementation:**
```typescript
this.eventEmitter.emit('correspondence.approved', { correspondenceId });
// Return immediately
return { success: true };
// Listener
@OnEvent('correspondence.approved')
async handleApproved(payload) {
await this.emailService.sendEmail(...);
}
```
**Pros:**
- ✅ Non-blocking (async)
- ✅ Decoupled
**Cons:**
- ❌ ไม่มี Retry หาก Event listener fail
- ❌ Lost jobs หาก Server restart
### Option 3: Message Queue (BullMQ + Redis)
**Implementation:**
```typescript
await this.emailQueue.add('send-email', {
to,
subject,
template,
context,
});
```
**Pros:**
- ✅ Non-blocking (async)
- ✅ Persistent (Store in Redis)
- ✅ Built-in Retry mechanism
- ✅ Job monitoring & management
- ✅ Scalable (Multiple workers)
**Cons:**
- ❌ Requires Redis infrastructure
- ❌ More complex setup
---
## Decision Outcome
**Chosen Option:** **Option 3 - Message Queue (BullMQ + Redis)**
### Rationale
1. **Performance:** ไม่ Block API response, ส่ง Email แบบ Async
2. **Reliability:** Persistent jobs ใน Redis, มี Retry mechanism
3. **Scalability:** สามารถ Scale workers แยกได้
4. **Monitoring:** ดู Job status, Failed jobs ได้
5. **Infrastructure:** Redis มีอยู่แล้วสำหรับ Locking และ Caching (ADR-006)
---
## Implementation Details
### 1. Email Queue Setup
```typescript
// File: backend/src/modules/notification/notification.module.ts
import { BullModule } from '@nestjs/bullmq';
@Module({
imports: [
BullModule.registerQueue({
name: 'email',
connection: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
},
}),
BullModule.registerQueue({
name: 'line-notify',
}),
],
providers: [NotificationService, EmailProcessor, LineNotifyProcessor],
})
export class NotificationModule {}
```
### 2. Queue Email Job
```typescript
// File: backend/src/modules/notification/notification.service.ts
@Injectable()
export class NotificationService {
constructor(
@InjectQueue('email') private emailQueue: Queue,
@InjectQueue('line-notify') private lineQueue: Queue
) {}
async sendEmailNotification(dto: SendEmailDto): Promise<void> {
await this.emailQueue.add(
'send-email',
{
to: dto.to,
subject: dto.subject,
template: dto.template, // e.g., 'correspondence-approved'
context: dto.context, // Template variables
},
{
attempts: 3, // Retry 3 times
backoff: {
type: 'exponential',
delay: 5000, // Start with 5s delay
},
removeOnComplete: {
age: 24 * 3600, // Keep completed jobs for 24h
},
removeOnFail: false, // Keep failed jobs for debugging
}
);
}
async sendLineNotification(dto: SendLineDto): Promise<void> {
await this.lineQueue.add('send-line', {
token: dto.token,
message: dto.message,
});
}
}
```
### 3. Email Processor (Worker)
```typescript
// File: backend/src/modules/notification/processors/email.processor.ts
import { Processor, Process } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import * as nodemailer from 'nodemailer';
import * as handlebars from 'handlebars';
import * as fs from 'fs/promises';
@Processor('email')
export class EmailProcessor {
private transporter: nodemailer.Transporter;
constructor() {
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
@Process('send-email')
async handleSendEmail(job: Job) {
const { to, subject, template, context } = job.data;
try {
// Load and compile template
const templatePath = `./templates/emails/${template}.hbs`;
const templateSource = await fs.readFile(templatePath, 'utf-8');
const compiledTemplate = handlebars.compile(templateSource);
const html = compiledTemplate(context);
// Send email
const info = await this.transporter.sendMail({
from: process.env.SMTP_FROM,
to,
subject,
html,
});
console.log('Email sent:', info.messageId);
return info;
} catch (error) {
console.error('Failed to send email:', error);
throw error; // Will trigger retry
}
}
}
```
### 4. Email Template (Handlebars)
```handlebars
<!-- File: backend/templates/emails/correspondence-approved.hbs -->
<html>
<head>
<style>
body { font-family: Arial, sans-serif; } .container { max-width: 600px;
margin: 0 auto; padding: 20px; } .header { background: #007bff; color:
white; padding: 20px; } .content { padding: 20px; } .button { background:
#007bff; color: white; padding: 10px 20px; text-decoration: none; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h1>Correspondence Approved</h1>
</div>
<div class='content'>
<p>สวัสดีคุณ {{userName}},</p>
<p>เอกสาร <strong>{{documentNumber}}</strong> ได้รับการอนุมัติแล้ว</p>
<p><strong>Subject:</strong> {{subject}}</p>
<p><strong>Approved by:</strong> {{approver}}</p>
<p><strong>Date:</strong> {{approvedDate}}</p>
<p>
<a href='{{documentUrl}}' class='button'>ดูเอกสาร</a>
</p>
</div>
</div>
</body>
</html>
```
### 5. Workflow Event → Email Notification
```typescript
// File: backend/src/modules/workflow/workflow.service.ts
async executeTransition(workflowId: number, action: string) {
// ... Execute transition logic
// Send notifications
await this.notificationService.notifyWorkflowTransition(
workflowId,
action,
currentUserId,
);
}
```
```typescript
// File: backend/src/modules/notification/notification.service.ts
async notifyWorkflowTransition(
workflowId: number,
action: string,
actorId: number,
) {
// Get users to notify
const users = await this.getRelevantUsers(workflowId);
for (const user of users) {
// In-app notification
await this.createNotification({
user_id: user.user_id,
type: 'workflow_transition',
title: `${action} completed`,
message: `Workflow ${workflowId} has been ${action}`,
link: `/workflows/${workflowId}`,
});
// Email (if enabled)
if (user.email_notifications_enabled) {
await this.sendEmailNotification({
to: user.email,
subject: `Workflow Update: ${action}`,
template: 'workflow-transition',
context: {
userName: user.first_name,
action,
workflowId,
documentUrl: `${process.env.FRONTEND_URL}/workflows/${workflowId}`,
},
});
}
// LINE Notify (if enabled)
if (user.line_notify_token) {
await this.sendLineNotification({
token: user.line_notify_token,
message: `[LCBP3-DMS] Workflow ${workflowId}: ${action}`,
});
}
}
}
```
---
## Consequences
### Positive Consequences
1.**Performance:** API responses ไม่ถูก Block โดยการส่ง Email
2.**Reliability:** Jobs ถูกเก็บใน Redis ไม่สูญหายหาก Server restart
3.**Retry:** Automatic retry สำหรับ Failed jobs
4.**Monitoring:** ดู Job status, Failed jobs ผ่าน Bull Board
5.**Scalability:** เพิ่ม Email workers ได้ตามต้องการ
6.**Multi-Channel:** รองรับ Email, LINE, In-app notification
### Negative Consequences
1.**Delayed Delivery:** Email ส่งแบบ Async อาจมี Delay เล็กน้อย
2.**Dependency on Redis:** หาก Redis down ก็ส่ง Email ไม่ได้
3.**Template Management:** ต้อง Maintain Handlebars templates แยก
### Mitigation Strategies
- **Redis Monitoring:** ตั้ง Alert หาก Redis down
- **Template Versioning:** เก็บ Email templates ใน Git
- **Fallback:** หาก Redis ล้ม อาจ Fallback เป็น Sync sending ชั่วคราว
- **Testing:** ใช้ Mailtrap/MailHog สำหรับ Testing ใน Development
---
## Related ADRs
- [ADR-006: Redis Caching Strategy](./ADR-006-redis-caching-strategy.md) - ใช้ Redis สำหรับ Queue
- [TASK-BE-011: Notification & Audit](../06-tasks/README.md)
---
## References
- [BullMQ Documentation](https://docs.bullmq.io/)
- [Nodemailer Documentation](https://nodemailer.com/)
- [Handlebars Documentation](https://handlebarsjs.com/)
---
**Last Updated:** 2025-12-01
**Next Review:** 2025-06-01

View File

@@ -0,0 +1,383 @@
# ADR-009: Database Migration & Deployment Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Backend Team, DevOps Team, System Architect
**Related Documents:** [TASK-BE-001](../06-tasks/TASK-BE-015-schema-v160-migration.md), [ADR-005: Technology Stack](./ADR-005-technology-stack.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS ต้องการกลยุทธ์การจัดการ Database Schema และ Data migrations ที่ปลอดภัยและเชื่อถือได้ เพื่อให้ Deploy ใหม่ได้โดยไม่ทำให้ Production data เสียหาย
### ปัญหาที่ต้องแก้:
1. **Schema Evolution:** จัดการการเปลี่ยนแปลง Schema ใน Production อย่างไร
2. **Zero-Downtime:** Deploy โดยไม่ต้อง Downtime ระบบได้หรือไม่
3. **Rollback:** หาก Migration ล้มเหลว จะ Rollback อย่างไร
4. **Data Safety:** ป้องกัน Data loss จาก Migration errors อย่างไร
5. **Team Collaboration:** หลายคน Develop พร้อมกัน จัดการ Migration conflicts อย่างไร
---
## Decision Drivers
- 🔒 **Data Safety:** ป้องกัน Data loss เป็นอันดับแรก
-**Zero Downtime:** Deploy ได้โดยไม่ต้อง Stop service
- 🔄 **Reversibility:** สามารถ Rollback ได้ถ้า Migration ล้ม
- 👥 **Team Collaboration:** หลายคน Work พร้อมกัน ไม่ Conflict
- 📊 **Auditability:** ต้องรู้ว่า Schema เป็น Version ไหน
---
## Considered Options
### Option 1: Synchronize Schema (TypeORM synchronize: true)
**Implementation:**
```typescript
TypeOrmModule.forRoot({
synchronize: true, // Auto-generate schema from entities
});
```
**Pros:**
- ✅ ง่ายที่สุด ไม่ต้องเขียน Migration
- ✅ เหมาะสำหรับ Development
**Cons:**
-**อันตราย** ใน Production (อาจ Drop columns/tables)
- ❌ ไม่มี Version control
- ❌ ไม่มี Rollback
- ❌ ไม่เหมาะสำหรับ Production
### Option 2: Manual SQL Scripts
**Implementation:**
- เขียน SQL scripts ด้วยมือ
- Execute โดย DBA
**Pros:**
- ✅ Full control
- ✅ Review ได้ละเอียด
**Cons:**
- ❌ Manual process (Error-prone)
- ❌ ไม่มี Automation
- ❌ ลืม Run migration ได้
- ❌ Tracking ยาก
### Option 3: TypeORM Migrations (Automated + Version Controlled)
**Implementation:**
```bash
npm run migration:generate -- MigrationName
npm run migration:run
npm run migration:revert
```
**Pros:**
- ✅ Version controlled (Git)
- ✅ Automatic tracking (`migrations` table)
- ✅ Rollback support
- ✅ Generated from Entity changes
- ✅ CI/CD integration
**Cons:**
- ❌ ต้องเขียน Migration files
- ❌ Requires discipline
---
## Decision Outcome
**Chosen Option:** **Option 3 - TypeORM Migrations + Blue-Green Deployment Strategy**
### Rationale
1. **Safety:** Migrations มี Version control และ Rollback mechanism
2. **Automation:** Run migrations auto ใน CI/CD pipeline
3. **Tracking:** ดู Migration history ได้จาก `migrations` table
4. **Team Collaboration:** Merge migrations ใน Git เหมือน Code
5. **Zero Downtime:** ใช้ Blue-Green deployment สำหรับ Breaking changes
---
## Implementation Details
### 1. TypeORM Configuration
```typescript
// File: backend/src/config/database.config.ts
export default {
type: 'mariadb',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
migrationsTableName: 'migrations',
synchronize: false, // NEVER true in production
logging: process.env.NODE_ENV === 'development',
};
```
### 2. Migration Workflow
```bash
# 1. Create new entity or modify existing
# 2. Generate migration
npm run migration:generate -- -n AddDisciplineIdToCorrespondences
# Output: src/migrations/1234567890-AddDisciplineIdToCorrespondences.ts
# 3. Review generated migration
# 4. Test migration locally
npm run migration:run
# 5. Test rollback
npm run migration:revert
# 6. Commit to Git
git add src/migrations/
git commit -m "feat: add discipline_id to correspondences"
```
### 3. Migration File Example
```typescript
// File: backend/src/migrations/1234567890-AddDisciplineIdToCorrespondences.ts
import {
MigrationInterface,
QueryRunner,
TableColumn,
TableForeignKey,
} from 'typeorm';
export class AddDisciplineIdToCorrespondences1234567890
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// Add column
await queryRunner.addColumn(
'correspondences',
new TableColumn({
name: 'discipline_id',
type: 'int',
isNullable: true,
})
);
// Add foreign key
await queryRunner.createForeignKey(
'correspondences',
new TableForeignKey({
columnNames: ['discipline_id'],
referencedTableName: 'disciplines',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
})
);
// Add index
await queryRunner.query(
'CREATE INDEX idx_correspondences_discipline_id ON correspondences(discipline_id)'
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Reverse order: index → FK → column
await queryRunner.query(
'DROP INDEX idx_correspondences_discipline_id ON correspondences'
);
const table = await queryRunner.getTable('correspondences');
const foreignKey = table.foreignKeys.find(
(fk) => fk.columnNames.indexOf('discipline_id') !== -1
);
await queryRunner.dropForeignKey('correspondences', foreignKey);
await queryRunner.dropColumn('correspondences', 'discipline_id');
}
}
```
### 4. CI/CD Pipeline Integration
```yaml
# File: .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Run migrations
run: npm run migration:run
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_PORT: ${{ secrets.DB_PORT }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
- name: Deploy application
run: |
# Deploy to server
pm2 restart app
```
### 5. Zero-Downtime Migration Strategy
**กรณี Non-Breaking Changes (เพิ่ม Column ใหม่):**
```bash
# Step 1: Add nullable column (Old code still works)
ALTER TABLE correspondences ADD COLUMN discipline_id INT NULL;
# Step 2: Deploy new code (Can use new column)
pm2 restart app
# Step 3: (Optional) Backfill data if needed
UPDATE correspondences SET discipline_id = X WHERE ...;
```
**กรณี Breaking Changes (ลบ Column, เปลี่ยนชนิด):**
**Blue-Green Deployment:**
```bash
# Step 1: Deploy "Green" (New version) พร้อม Migration
# - Database supports ทั้ง old + new schema
# - Run migration: Add new column, Keep old column
# Step 2: Route traffic to "Green"
# - Load balancer switches to new version
# Step 3: Verify "Green" works
# - Monitor errors, metrics
# Step 4: (After 24h) Cleanup old schema
# - Run migration: Drop old column
# - Shutdown "Blue" (Old version)
```
### 6. Migration Testing
```typescript
// File: backend/test/migrations/migration.spec.ts
describe('Migrations', () => {
let dataSource: DataSource;
beforeEach(async () => {
dataSource = await createTestDataSource();
});
it('should run all migrations successfully', async () => {
await dataSource.runMigrations();
// Verify tables exist
const tables = await dataSource.query('SHOW TABLES');
expect(tables).toContainEqual(
expect.objectContaining({ Tables_in_lcbp3: 'correspondences' })
);
});
it('should rollback all migrations successfully', async () => {
await dataSource.runMigrations();
await dataSource.undoLastMigration();
// Verify rollback worked
});
});
```
---
## Consequences
### Positive Consequences
1.**Version Control:** Migrations อยู่ใน Git มี History
2.**Automation:** CI/CD run migrations automatically
3.**Rollback:** สามารถ Revert migration ได้
4.**Audit Trail:** ดู Migration history ใน `migrations` table
5.**Zero Downtime:** สามารถ Deploy โดยไม่ Downtime (Blue-Green)
### Negative Consequences
1.**Discipline Required:** ต้อง Review migrations ก่อน Merge
2.**Complex Rollbacks:** Breaking changes ยาก Rollback
3.**Migration Conflicts:** หลายคน Develop อาจ Conflict (แก้ด้วย Rebase)
### Mitigation Strategies
- **Code Review:** Review migrations เหมือน Code
- **Testing:** Test migrations ใน Staging ก่อน Production
- **Backup:** Backup database ก่อน Run migration ใน Production
- **Monitoring:** Monitor migration execution time และ Errors
- **Documentation:** Document Breaking changes ชัดเจน
---
## Migration Best Practices
### DO:
- ✅ Test migrations ใน Development และ Staging
- ✅ Backup database ก่อน Production migration
- ✅ ใช้ Transactions (TypeORM มีอัตโนมัติ)
- ✅ เขียน `down()` migration สำหรับ Rollback
- ✅ ใช้ Nullable columns สำหรับ Non-breaking changes
### DON'T:
- ❌ Run `synchronize: true` ใน Production
- ❌ ลบ Column/Table โดยไม่ Deploy code ก่อน
- ❌ เปลี่ยน Data type โดยตรง (ใช้ New column แทน)
- ❌ Hardcode Values ใน Migration (ใช้ Environment variables)
---
## Related ADRs
- [ADR-005: Technology Stack](./ADR-005-technology-stack.md) - เลือกใช้ TypeORM
- [TASK-BE-001: Database Migrations](../06-tasks/TASK-BE-015-schema-v160-migration.md)
---
## References
- [TypeORM Migrations](https://typeorm.io/migrations)
- [Blue-Green Deployment](https://martinfowler.com/bliki/BlueGreenDeployment.html)
- [Zero-Downtime Migrations](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
---
**Last Updated:** 2025-12-01
**Next Review:** 2025-06-01

View File

@@ -0,0 +1,464 @@
# ADR-010: Logging & Monitoring Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Backend Team, DevOps Team
**Related Documents:** [Backend Guidelines](../03-implementation/03-02-backend-guidelines.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS ต้องการ Logging และ Monitoring ที่ดีเพื่อ:
- Debug ปัญหาใน Production
- ติดตาม Performance metrics
- Audit trail สำหรับ Security และ Compliance
- Alert เมื่อมี Errors หรือ Anomalies
### ปัญหาที่ต้องแก้:
1. **Structured Logging:** บันทึก Logs ในรูปแบบที่ค้นหาและวิเคราะห์ได้ง่าย
2. **Log Levels:** กำหนด Log levels ที่เหมาะสมสำหรับแต่ละสถานการณ์
3. **Performance Monitoring:** ติดตาม Response time, Database queries, Memory usage
4. **Error Tracking:** ติดตาม Errors และ Exceptions อย่างเป็นระบบ
5. **Centralized Logging:** รวม Logs จากหลาย Services ไว้ที่เดียว
---
## Decision Drivers
- 🔍 **Debuggability:** หา Root cause ของปัญหาได้เร็ว
- 📊 **Performance Insights:** ดู Metrics และ Bottlenecks
- 🚨 **Alerting:** แจ้งเตือนเมื่อมีปัญหา
- 📈 **Scalability:** รองรับ High-volume logs
- 💰 **Cost:** ไม่ต้องลงทุนมากในช่วงเริ่มต้น
---
## Considered Options
### Option 1: Console.log (Built-in)
**Pros:**
- ✅ Simple, ไม่ต้อง Setup
- ✅ ไม่มีค่าใช้จ่าย
**Cons:**
- ❌ ไม่มี Structure
- ❌ ไม่มี Log levels
- ❌ ไม่มี Log rotation
- ❌ ยากต่อการ Search/Filter
### Option 2: Winston (Structured Logging Library)
**Pros:**
- ✅ Structured logs (JSON format)
- ✅ Multiple transports (File, Console, HTTP)
- ✅ Log levels (error, warn, info, debug)
- ✅ Log rotation
- ✅ Mature library
**Cons:**
- ❌ ต้อง Configure transports
- ❌ Performance overhead (minimal)
### Option 3: Full Observability Stack (ELK/Datadog/New Relic)
**Pros:**
- ✅ Complete solution (Logs + Metrics + APM)
- ✅ Powerful query และ Visualization
- ✅ Built-in Alerting
**Cons:**
-**ค่าใช้จ่ายสูง**
- ❌ Complex setup
- ❌ Overkill สำหรับ MVP
---
## Decision Outcome
**Chosen Option:** **Option 2 (Winston) + Docker Logging + Future ELK Stack**
### Rationale
**Phase 1 (MVP):** Winston with File/Console outputs
- ✅ เพียงพอสำหรับ MVP
- ✅ Structured logs พร้อมสำหรับ ELK ในอนาคต
- ✅ ไม่มีค่าใช้จ่ายเพิ่ม
**Phase 2 (Production Scale):** Add ELK Stack (Elasticsearch, Logstash, Kibana)
- ✅ Centralized logging
- ✅ Search และ Visualization
- ✅ Open-source (ไม่มี Vendor lock-in)
---
## Implementation Details
### 1. Winston Configuration
```typescript
// File: backend/src/config/logger.config.ts
import * as winston from 'winston';
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: {
service: 'lcbp3-dms-backend',
environment: process.env.NODE_ENV,
},
transports: [
// Console output (for Development)
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta) : ''
}`;
})
),
}),
// File output (for Production)
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 5,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10485760,
maxFiles: 10,
}),
],
});
```
### 2. NestJS Logger Integration
```typescript
// File: backend/src/main.ts
import { Logger } from '@nestjs/common';
import { logger as winstonLogger } from './config/logger.config';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new WinstonLogger(winstonLogger),
});
// ...
}
```
### 3. Custom Winston Logger for NestJS
```typescript
// File: backend/src/common/logger/winston.logger.ts
import { LoggerService } from '@nestjs/common';
import { Logger as WinstonLoggerType } from 'winston';
export class WinstonLogger implements LoggerService {
constructor(private readonly logger: WinstonLoggerType) {}
log(message: string, context?: string) {
this.logger.info(message, { context });
}
error(message: string, trace?: string, context?: string) {
this.logger.error(message, { trace, context });
}
warn(message: string, context?: string) {
this.logger.warn(message, { context });
}
debug(message: string, context?: string) {
this.logger.debug(message, { context });
}
verbose(message: string, context?: string) {
this.logger.verbose(message, { context });
}
}
```
### 4. Request Logging Middleware
```typescript
// File: backend/src/common/middleware/request-logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { logger } from 'src/config/logger.config';
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
userAgent: req.headers['user-agent'],
ip: req.ip,
userId: (req as any).user?.user_id,
});
});
next();
}
}
```
### 5. Database Query Logging
```typescript
// File: backend/src/config/database.config.ts
export default {
// ...
logging:
process.env.NODE_ENV === 'development'
? 'all'
: ['error', 'warn', 'schema'],
logger: 'advanced-console',
maxQueryExecutionTime: 1000, // Warn if query > 1s
};
```
### 6. Error Logging in Exception Filter
```typescript
// File: backend/src/common/filters/global-exception.filter.ts
import { logger } from 'src/config/logger.config';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// ... get status, message
// Log error
logger.error('Exception occurred', {
error: exception,
statusCode: status,
path: request.url,
method: request.method,
userId: request.user?.user_id,
stack: exception instanceof Error ? exception.stack : null,
});
// Send response to client
response.status(status).json({ ... });
}
}
```
### 7. Log Levels Usage
```typescript
// ERROR: จับ Exceptions และ Errors
logger.error('Failed to create correspondence', { error, userId, documentId });
// WARN: สถานการณ์ผิดปกติ แต่ไม่ Error
logger.warn('Document numbering retry attempt 2/3', { template, counter });
// INFO: Business events สำคัญ
logger.info('Correspondence approved', { documentId, approvedBy });
// DEBUG: ข้อมูลละเอียดสำหรับ Development
logger.debug('Workflow transition guard check', { workflowId, guardResult });
// VERBOSE: ข้อมูลละเอียดมากๆ
logger.verbose('Cache hit', { key, ttl });
```
### 8. Performance Monitoring
```typescript
// File: backend/src/common/interceptors/performance.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { logger } from 'src/config/logger.config';
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
if (duration > 1000) {
logger.warn('Slow request detected', {
method: request.method,
url: request.url,
duration: `${duration}ms`,
});
}
})
);
}
}
```
---
## Log Format Example
### Development (Console)
```
2024-01-01 10:30:15 [info]: Correspondence approved { documentId: 123, approvedBy: 5 }
2024-01-01 10:30:16 [error]: Failed to send email { error: 'SMTP timeout', userId: 5 }
```
### Production (JSON File)
```json
{
"timestamp": "2024-01-01T10:30:15.123Z",
"level": "info",
"message": "Correspondence approved",
"service": "lcbp3-dms-backend",
"environment": "production",
"documentId": 123,
"approvedBy": 5
}
```
---
## Future: ELK Stack Integration
**Phase 2 Setup:**
```yaml
# docker-compose.yml
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
ports:
- '9200:9200'
logstash:
image: docker.elastic.co/logstash/logstash:8.11.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
depends_on:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- '5601:5601'
depends_on:
- elasticsearch
```
**Winston transport to Logstash:**
```typescript
import { LogstashTransport } from 'winston-logstash';
logger.add(
new LogstashTransport({
host: process.env.LOGSTASH_HOST,
port: parseInt(process.env.LOGSTASH_PORT),
})
);
```
---
## Consequences
### Positive Consequences
1.**Structured Logs:** ค้นหาและวิเคราะห์ได้ง่าย
2.**Performance Insights:** ดู Slow requests ได้
3.**Error Tracking:** ติดตาม Errors พร้อม Context
4.**Scalable:** พร้อมสำหรับ ELK Stack ในอนาคต
5.**Cost Effective:** ไม่มีค่าใช้จ่ายในช่วง MVP
### Negative Consequences
1.**Manual Log Search:** ใน Phase 1 ต้องค้นหา Logs ใน Files
2.**No Centralized Dashboard:** ต้องรอ Phase 2 (ELK)
3.**Log Rotation Management:** ต้อง Monitor disk space
### Mitigation Strategies
- **Docker Logging Driver:** ใช้ Docker log driver สำหรับ Log rotation
- **Log Aggregation:** ใช้ `docker logs` รวม Logs จากหลาย Containers
- **Monitoring:** Set up Disk space alerts
---
## Logging Best Practices
### DO:
- ✅ Log ทุก HTTP requests พร้อม Response time
- ✅ Log Business events สำคัญ (Approved, Rejected, Created)
- ✅ Log Errors พร้อม Stack trace และ Context
- ✅ ใช้ Structured logging (JSON format)
### DON'T:
- ❌ Log Sensitive data (Passwords, Tokens)
- ❌ Log ทุก Database query ใน Production
- ❌ Log Large payloads (> 1KB) ทั้งหมด
- ❌ ใช้ `console.log` แทน Logger
---
## Related ADRs
- [ADR-007: API Design & Error Handling](./ADR-007-api-design-error-handling.md)
- [ADR-005: Technology Stack](./ADR-005-technology-stack.md)
---
## References
- [Winston Documentation](https://github.com/winstonjs/winston)
- [NestJS Logging](https://docs.nestjs.com/techniques/logger)
- [ELK Stack](https://www.elastic.co/elastic-stack)
---
**Last Updated:** 2025-12-01
**Next Review:** 2025-06-01

View File

@@ -0,0 +1,399 @@
# ADR-011: Next.js App Router & Routing Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Frontend Team, System Architect
**Related Documents:** [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md), [ADR-005: Technology Stack](./ADR-005-technology-stack.md)
---
## Context and Problem Statement
Next.js มี 2 รูปแบบ Router หลัก: Pages Router (เก่า) และ App Router (ใหม่ใน Next.js 13+) ต้องเลือกว่าจะใช้แบบไหนสำหรับ LCBP3-DMS
### ปัญหาที่ต้องแก้:
1. **Routing Architecture:** ใช้ Pages Router หรือ App Router
2. **Server vs Client Components:** จัดการ Data Fetching อย่างไร
3. **Layout System:** จัดการ Shared Layouts อย่างไร
4. **Performance:** ทำอย่างไรให้ Initial Load เร็ว
5. **SEO:** ต้องการ SEO หรือไม่ (Dashboard ไม่ต้องการ)
---
## Decision Drivers
- 🚀 **Performance:** Initial load time และ Navigation speed
- 🎯 **Developer Experience:** ง่ายต่อการพัฒนาและบำรุงรักษา
- 📦 **Code Organization:** โครงสร้างโค้ดชัดเจน
- 🔄 **Future-Proof:** พร้อมสำหรับ Next.js รุ่นถัดไป
- 🎨 **Layout Flexibility:** จัดการ Nested Layouts ได้ง่าย
---
## Considered Options
### Option 1: Pages Router (Traditional)
**โครงสร้าง:**
```
pages/
├── _app.tsx
├── _document.tsx
├── index.tsx
├── correspondences/
│ ├── index.tsx
│ └── [id].tsx
└── api/
└── ...
```
**Pros:**
- ✅ Mature และ Stable
- ✅ Documentation ครบถ้วน
- ✅ Community ใหญ่
- ✅ ทีมคุ้นเคยแล้ว
**Cons:**
- ❌ ไม่รองรับ Server Components
- ❌ Layout System ซับซ้อน (ต้องใช้ HOC)
- ❌ Data Fetching ไม่ทันสมัย
- ❌ Not recommended for new projects
### Option 2: App Router (New - Recommended)
**โครงสร้าง:**
```
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── correspondences/
│ ├── layout.tsx # Nested layout
│ ├── page.tsx # List page
│ └── [id]/
│ └── page.tsx # Detail page
└── (auth)/
├── layout.tsx
└── login/
└── page.tsx
```
**Pros:**
- ✅ Server Components (Better performance)
- ✅ Built-in Layout System
- ✅ Streaming & Suspense support
- ✅ Better Data Fetching patterns
- ✅ Recommended by Next.js team
**Cons:**
- ❌ Newer (less community resources)
- ❌ Learning curve สำหรับทีม
- ❌ Some libraries ยังไม่รองรับ
### Option 3: Hybrid Approach
ใช้ App Router + Pages Router พร้อมกัน
**Pros:**
- ✅ Gradual migration
**Cons:**
- ❌ เพิ่มความซับซ้อน
- ❌ Confusing สำหรับทีม
---
## Decision Outcome
**Chosen Option:** **Option 2 - App Router**
### Rationale
1. **Future-Proof:** Next.js แนะนำให้ใช้ App Router สำหรับโปรเจกต์ใหม่
2. **Performance:** Server Components ช่วยลด JavaScript bundle size
3. **Better DX:** Layout System สะดวกกว่า
4. **Server Actions:** รองรับ Form submissions โดยไม่ต้องสร้าง API routes
5. **Learning Investment:** Team จะได้ Skill ที่ทันสมัย
---
## Implementation Details
### 1. Folder Structure
```
app/
├── (public)/ # Public routes (no auth)
│ ├── layout.tsx
│ └── login/
│ └── page.tsx
├── (dashboard)/ # Protected routes
│ ├── layout.tsx # Dashboard layout with sidebar
│ ├── page.tsx # Dashboard home
│ │
│ ├── correspondences/
│ │ ├── layout.tsx
│ │ ├── page.tsx # List
│ │ ├── new/
│ │ │ └── page.tsx # Create
│ │ └── [id]/
│ │ ├── page.tsx # Detail
│ │ └── edit/
│ │ └── page.tsx
│ │
│ ├── rfas/
│ ├── drawings/
│ └── settings/
├── api/ # API route handlers (minimal)
│ └── auth/
│ └── [...nextauth]/
│ └── route.ts
├── layout.tsx # Root layout
└── page.tsx # Root redirect
```
### 2. Root Layout
```typescript
// File: app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'LCBP3-DMS',
description: 'Document Management System for Laem Chabang Port Phase 3',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="th">
<body className={inter.className}>{children}</body>
</html>
);
}
```
### 3. Dashboard Layout (with Sidebar)
```typescript
// File: app/(dashboard)/layout.tsx
import { Sidebar } from '@/components/layout/sidebar';
import { Header } from '@/components/layout/header';
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// Server-side auth check
const session = await getServerSession();
if (!session) {
redirect('/login');
}
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
</div>
);
}
```
### 4. Server Component (Data Fetching)
```typescript
// File: app/(dashboard)/correspondences/page.tsx
import { CorrespondenceList } from '@/components/correspondences/list';
import { getCorrespondences } from '@/lib/api/correspondences';
export default async function CorrespondencesPage({
searchParams,
}: {
searchParams: { page?: string; status?: string };
}) {
// Fetch data on server
const correspondences = await getCorrespondences({
page: parseInt(searchParams.page || '1'),
status: searchParams.status,
});
return (
<div>
<h1 className="text-2xl font-bold mb-6">Correspondences</h1>
<CorrespondenceList data={correspondences} />
</div>
);
}
```
### 5. Client Component (Interactive)
```typescript
// File: components/correspondences/list.tsx
'use client'; // Client Component
import { useState } from 'react';
import { Correspondence } from '@/types';
export function CorrespondenceList({ data }: { data: Correspondence[] }) {
const [filter, setFilter] = useState('');
const filtered = data.filter((item) =>
item.subject.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Filter..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="border p-2 mb-4"
/>
<div>
{filtered.map((item) => (
<div key={item.id}>{item.subject}</div>
))}
</div>
</div>
);
}
```
### 6. Loading States
```typescript
// File: app/(dashboard)/correspondences/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<div className="h-8 bg-gray-200 rounded animate-pulse" />
<div className="h-64 bg-gray-200 rounded animate-pulse" />
</div>
);
}
```
### 7. Error Handling
```typescript
// File: app/(dashboard)/correspondences/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="p-4">
<h2 className="text-xl font-bold text-red-600">Something went wrong!</h2>
<p className="text-gray-600">{error.message}</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
>
Try again
</button>
</div>
);
}
```
---
## Routing Patterns
### Route Groups (Organization)
```
(public)/ # Public pages
(dashboard)/ # Protected dashboard
(auth)/ # Auth-related pages
```
### Dynamic Routes
```
[id]/ # Dynamic segment (e.g., /correspondences/123)
[...slug]/ # Catch-all (e.g., /docs/a/b/c)
```
### Parallel Routes & Intercepting Routes
```
@modal/ # Parallel route for modals
(.)/ # Intercept same level
```
---
## Consequences
### Positive Consequences
1.**Better Performance:** Server Components ลด Client JavaScript
2.**SEO-Friendly:** Server-side rendering out of the box
3.**Simpler Layouts:** Nested layouts ทำได้ง่าย
4.**Streaming:** Progressive rendering with Suspense
5.**Future-Proof:** ทิศทางของ Next.js และ React
### Negative Consequences
1.**Learning Curve:** ทีมต้องเรียนรู้ Server Components
2.**Limited Libraries:** บาง Libraries ยังไม่รองรับ Server Components
3.**Debugging:** ยากกว่า Pages Router เล็กน้อย
### Mitigation Strategies
- **Training:** จัด Workshop เรื่อง App Router และ Server Components
- **Documentation:** เขียน Internal docs สำหรับ Patterns ที่ใช้
- **Code Review:** Review code ให้ใช้ Server/Client Components ถูกต้อง
- **Gradual Adoption:** เริ่มจาก Simple pages ก่อน
---
## Related ADRs
- [ADR-005: Technology Stack](./ADR-005-technology-stack.md) - เลือกใช้ Next.js
- [ADR-012: UI Component Library](./ADR-012-ui-component-library.md) - Shadcn/UI
---
## References
- [Next.js App Router Documentation](https://nextjs.org/docs/app)
- [React Server Components](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)
---
**Last Updated:** 2025-12-01
**Next Review:** 2026-06-01

View File

@@ -0,0 +1,428 @@
# ADR-012: UI Component Library Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Frontend Team, UX Designer
**Related Documents:** [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md), [ADR-005: Technology Stack](./ADR-005-technology-stack.md)
---
## Context and Problem Statement
ต้องการ UI Component Library สำหรับสร้าง User Interface ที่สวยงาม สม่ำเสมอ และ Accessible
### ปัญหาที่ต้องแก้:
1. **Component Library:** ใช้ Library สำเร็จรูป หรือสร้างเอง
2. **Customization:** ปรับแต่งได้ง่ายเพียงใด
3. **Accessibility:** รองรับ ARIA และ Keyboard navigation
4. **Bundle Size:** ขนาดไฟล์ไม่ใหญ่เกินไป
5. **Developer Experience:** ใช้งานง่าย Documentation ครบ
---
## Decision Drivers
- 🎨 **Design Consistency:** UI สม่ำเสมอทั้งระบบ
-**Accessibility:** รองรับ WCAG 2.1 AA
- 🎯 **Customization:** ปรับแต่งได้ตามต้องการ
- 📦 **Bundle Size:** เล็กและ Tree-shakeable
-**Performance:** Render เร็ว
- 🛠️ **DX:** Developer Experience ดี
---
## Considered Options
### Option 1: Material-UI (MUI)
**Pros:**
- ✅ Component ครบชุด
- ✅ Documentation ดี
- ✅ Community ใหญ่
- ✅ Built-in theming
**Cons:**
- ❌ Bundle size ใหญ่
- ❌ Design opinionated (Material Design)
- ❌ Customization ยาก
- ❌ Performance overhead
### Option 2: Ant Design
**Pros:**
- ✅ Component ครบ (เน้น Enterprise)
- ✅ i18n support ดี
- ✅ Form components ครบ
**Cons:**
- ❌ Bundle size ใหญ่มาก
- ❌ Chinese-centric design
- ❌ Customization จำกัด
- ❌ TypeScript support ไม่ดีเท่าไร
### Option 3: Chakra UI
**Pros:**
- ✅ Accessibility ดี
- ✅ Customization ง่าย
- ✅ TypeScript first
- ✅ Dark mode built-in
**Cons:**
- ❌ Bundle size ค่อนข้างใหญ่
- ❌ CSS-in-JS overhead
- ❌ Performance issues with many components
### Option 4: Headless UI + Tailwind CSS
**Pros:**
- ✅ Full control over styling
- ✅ Lightweight
- ✅ Accessibility ดี
- ✅ No styling overhead
**Cons:**
- ❌ ต้องเขียน styles เอง
- ❌ Component library น้อย
- ❌ ใช้เวลาพัฒนานาน
### Option 5: Shadcn/UI + Tailwind CSS
**วิธีการ:** Copy components ที่ต้องการไปยัง Project
**Pros:**
-**Full ownership:** Components เป็นของเรา ไม่ใช่ dependency
-**Highly customizable:** แก้ไขได้เต็มที่
-**Accessibility:** ใช้ Radix UI Primitives
-**Bundle size:** เฉพาะที่ใช้เท่านั้น
-**Tailwind CSS:** Utility-first ง่ายต่อการ maintain
-**TypeScript:** Type-safe
-**Beautiful defaults:** Design ดูทันสมัย
**Cons:**
- ❌ ต้อง Copy components เอง
- ❌ Update ต้องทำด้วยตัวเอง
- ❌ ไม่มี `npm install` แบบ Library
---
## Decision Outcome
**Chosen Option:** **Option 5 - Shadcn/UI + Tailwind CSS**
### Rationale
1. **Ownership:** เป็นเจ้าของ Code 100% ปรับแต่งได้อย่างเต็มที่
2. **Bundle Size:** เล็กที่สุด (เฉพาะที่ใช้)
3. **Accessibility:** ใช้ Radix UI primitives ที่ทดสอบแล้ว
4. **Customization:** แก้ไขได้ตามต้องการ ไม่ติด Framework
5. **Tailwind CSS:** ทีมคุ้นเคยและใช้อยู่แล้ว
6. **Modern Design:** ดูสวยงามและทันสมัย
---
## Implementation Details
### 1. Setup Shadcn/UI
```bash
# Initialize shadcn/ui
npx shadcn-ui@latest init
# Select options:
# - TypeScript: Yes
# - Style: Default
# - Base color: Slate
# - CSS variables: Yes
```
```typescript
// File: components.json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
```
### 2. Add Components
```bash
# Add specific components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add input
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add table
# Components will be copied to components/ui/
```
### 3. Component Usage
```typescript
// File: app/correspondences/page.tsx
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
export default function CorrespondencesPage() {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Input placeholder="Search..." className="max-w-sm" />
<Button>Create New</Button>
</div>
<Card className="p-6">
<h2 className="text-xl font-bold">Correspondences</h2>
{/* Content */}
</Card>
</div>
);
}
```
### 4. Customize Components
```typescript
// File: components/ui/button.tsx
// สามารถแก้ไขได้เต็มที่เพราะเป็น Code ของเรา
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// Add custom variant
success: 'bg-green-600 text-white hover:bg-green-700',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
```
### 5. Theming with CSS Variables
```css
/* File: app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... more colors */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode colors */
}
}
```
### 6. Component Composition
```typescript
// File: components/correspondence/card.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
export function CorrespondenceCard({ correspondence }) {
return (
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle>{correspondence.subject}</CardTitle>
<Badge
variant={
correspondence.status === 'APPROVED' ? 'success' : 'default'
}
>
{correspondence.status}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{correspondence.description}
</p>
<div className="mt-4 flex gap-2">
<Button variant="outline" size="sm">
View
</Button>
<Button size="sm">Edit</Button>
</div>
</CardContent>
</Card>
);
}
```
---
## Component Inventory
### Core Components (มีอยู่ใน Shadcn/UI)
**Forms:**
- Button
- Input
- Textarea
- Select
- Checkbox
- Radio Group
- Switch
- Slider
- Label
**Data Display:**
- Table
- Card
- Badge
- Avatar
- Separator
**Feedback:**
- Alert
- Dialog
- Toast
- Progress
- Skeleton
**Navigation:**
- Tabs
- Dropdown Menu
- Command
- Popover
- Sheet (Drawer)
**Layout:**
- Accordion
- Collapsible
- Aspect Ratio
- Scroll Area
---
## Consequences
### Positive Consequences
1.**Full Control:** แก้ไข Components ได้เต็มที่
2.**Smaller Bundle:** เฉพาะที่ใช้เท่านั้น
3.**No Lock-in:** ไม่ติด Dependency
4.**Accessibility:** ใช้ Radix UI (tested)
5.**Beautiful Design:** ดูทันสมัยและสวยงาม
6.**TypeScript:** Type-safe
### Negative Consequences
1.**Manual Updates:** ต้อง Update components ด้วยตัวเอง
2.**Initial Setup:** ต้อง Copy components ที่ต้องการ
3.**No Official Support:** ไม่มี Package maintainer
### Mitigation Strategies
- **Documentation:** เขียนเอกสารว่า Components ไหนมา version ไหน
- **Changelog:** Track changes ที่ทำกับ Components
- **Testing:** เขียน Tests สำหรับ Custom components
- **Review Updates:** Check Shadcn/UI releases เป็นระยะ
---
## Related ADRs
- [ADR-005: Technology Stack](./ADR-005-technology-stack.md) - เลือกใช้ Tailwind CSS
- [ADR-011: Next.js App Router](./ADR-011-nextjs-app-router.md)
---
## References
- [Shadcn/UI Documentation](https://ui.shadcn.com/)
- [Radix UI Primitives](https://www.radix-ui.com/)
- [Tailwind CSS](https://tailwindcss.com/)
---
**Last Updated:** 2025-12-01
**Next Review:** 2026-06-01

View File

@@ -0,0 +1,497 @@
# ADR-013: Form Handling & Validation Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Frontend Team
**Related Documents:** [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS มี Forms จำนวนมาก (Create/Edit Correspondence, RFA, Drawings) ต้องการวิธีจัดการ Forms ที่มี Performance ดี Validation ชัดเจน และ Developer Experience สูง
### ปัญหาที่ต้องแก้:
1. **Form State Management:** จัดการ Form state อย่างไร
2. **Validation:** Validate client-side และ server-side อย่างไร
3. **Error Handling:** แสดง Error messages อย่างไร
4. **Performance:** Forms ขนาดใหญ่ไม่ช้า
5. **Type Safety:** Type-safe forms with TypeScript
---
## Decision Drivers
-**Type Safety:** TypeScript support เต็มรูปแบบ
-**Performance:** Re-render minimal
- 🎯 **DX:** Developer Experience ดี
- 📝 **Validation:** Schema-based validation
- 🔄 **Reusability:** Reuse validation schema
- 🎨 **Flexibility:** ปรับแต่งได้ง่าย
---
## Considered Options
### Option 1: Formik
**Pros:**
- ✅ Popular และ Mature
- ✅ Documentation ดี
- ✅ Yup validation
**Cons:**
- ❌ Performance issues (re-renders)
- ❌ Bundle size ใหญ่
- ❌ TypeScript support ไม่ดีมาก
- ❌ Not actively maintained
### Option 2: Plain React State
```typescript
const [formData, setFormData] = useState({});
```
**Pros:**
- ✅ Simple
- ✅ No dependencies
**Cons:**
- ❌ Boilerplate code มาก
- ❌ ต้องจัดการ Validation เอง
- ❌ Error handling ซับซ้อน
- ❌ Performance issues
### Option 3: React Hook Form + Zod
**Pros:**
-**Performance:** Uncontrolled components (minimal re-renders)
-**TypeScript First:** Full type safety
-**Small Bundle:** ~8.5kb
-**Schema Validation:** Zod integration
-**DX:** Clean API
-**Actively Maintained**
**Cons:**
- ❌ Learning curve (uncontrolled approach)
- ❌ Complex forms ต้องใช้ Controller
---
## Decision Outcome
**Chosen Option:** **Option 3 - React Hook Form + Zod**
### Rationale
1. **Performance:** Uncontrolled components = minimal re-renders
2. **Type Safety:** Zod schemas → TypeScript types → Runtime validation
3. **Bundle Size:** เล็กมาก (8.5kb)
4. **Developer Experience:** API สะอาด ใช้งานง่าย
5. **Validation Reuse:** Validation schema ใช้ร่วมกับ Backend ได้
---
## Implementation Details
### 1. Install Dependencies
```bash
npm install react-hook-form zod @hookform/resolvers
```
### 2. Define Zod Schema
```typescript
// File: lib/validations/correspondence.ts
import { z } from 'zod';
export const correspondenceSchema = z.object({
subject: z
.string()
.min(5, 'Subject must be at least 5 characters')
.max(255, 'Subject must not exceed 255 characters'),
description: z
.string()
.min(10, 'Description must be at least 10 characters')
.optional(),
document_type_id: z.number({
required_error: 'Document type is required',
}),
from_organization_id: z.number({
required_error: 'From organization is required',
}),
to_organization_id: z.number({
required_error: 'To organization is required',
}),
importance: z.enum(['NORMAL', 'HIGH', 'URGENT']).default('NORMAL'),
attachments: z.array(z.instanceof(File)).optional(),
});
// Export TypeScript type
export type CorrespondenceFormData = z.infer<typeof correspondenceSchema>;
```
### 3. Create Form Component
```typescript
// File: components/correspondences/create-form.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
correspondenceSchema,
type CorrespondenceFormData,
} from '@/lib/validations/correspondence';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
export function CreateCorrespondenceForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
} = useForm<CorrespondenceFormData>({
resolver: zodResolver(correspondenceSchema),
defaultValues: {
importance: 'NORMAL',
},
});
const onSubmit = async (data: CorrespondenceFormData) => {
try {
const response = await fetch('/api/correspondences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create');
// Success - redirect
window.location.href = '/correspondences';
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Subject */}
<div>
<Label htmlFor="subject">Subject *</Label>
<Input
id="subject"
{...register('subject')}
placeholder="Enter subject"
/>
{errors.subject && (
<p className="text-sm text-red-600 mt-1">{errors.subject.message}</p>
)}
</div>
{/* Description */}
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
{...register('description')}
placeholder="Enter description"
rows={4}
/>
{errors.description && (
<p className="text-sm text-red-600 mt-1">
{errors.description.message}
</p>
)}
</div>
{/* Document Type (Select) */}
<div>
<Label>Document Type *</Label>
<Select
onValueChange={(value) =>
setValue('document_type_id', parseInt(value))
}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Internal Letter</SelectItem>
<SelectItem value="2">External Letter</SelectItem>
</SelectContent>
</Select>
{errors.document_type_id && (
<p className="text-sm text-red-600 mt-1">
{errors.document_type_id.message}
</p>
)}
</div>
{/* Importance (Radio) */}
<div>
<Label>Importance</Label>
<div className="flex gap-4 mt-2">
<label className="flex items-center">
<input type="radio" value="NORMAL" {...register('importance')} />
<span className="ml-2">Normal</span>
</label>
<label className="flex items-center">
<input type="radio" value="HIGH" {...register('importance')} />
<span className="ml-2">High</span>
</label>
<label className="flex items-center">
<input type="radio" value="URGENT" {...register('importance')} />
<span className="ml-2">Urgent</span>
</label>
</div>
</div>
{/* Submit */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline">
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create'}
</Button>
</div>
</form>
);
}
```
### 4. Reusable Form Field Component
```typescript
// File: components/ui/form-field.tsx
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { UseFormRegister, FieldError } from 'react-hook-form';
interface FormFieldProps {
label: string;
name: string;
type?: string;
register: UseFormRegister<any>;
error?: FieldError;
required?: boolean;
placeholder?: string;
}
export function FormField({
label,
name,
type = 'text',
register,
error,
required = false,
placeholder,
}: FormFieldProps) {
return (
<div>
<Label htmlFor={name}>
{label} {required && <span className="text-red-600">*</span>}
</Label>
<Input
id={name}
type={type}
{...register(name)}
placeholder={placeholder}
className={error ? 'border-red-600' : ''}
/>
{error && <p className="text-sm text-red-600 mt-1">{error.message}</p>}
</div>
);
}
```
### 5. File Upload Handling
```typescript
// File: components/correspondences/file-upload.tsx
'use client';
import { useState } from 'react';
import { UseFormSetValue } from 'react-hook-form';
import { Button } from '@/components/ui/button';
interface FileUploadProps {
setValue: UseFormSetValue<any>;
fieldName: string;
}
export function FileUpload({ setValue, fieldName }: FileUploadProps) {
const [files, setFiles] = useState<File[]>([]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(e.target.files || []);
setFiles(selectedFiles);
setValue(fieldName, selectedFiles);
};
return (
<div>
<input
type="file"
multiple
onChange={handleFileChange}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button type="button" variant="outline" asChild>
<span>Choose Files</span>
</Button>
</label>
{files.length > 0 && (
<div className="mt-2 text-sm text-gray-600">
{files.map((file, i) => (
<div key={i}>{file.name}</div>
))}
</div>
)}
</div>
);
}
```
### 6. Server-Side Validation
```typescript
// File: app/api/correspondences/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { correspondenceSchema } from '@/lib/validations/correspondence';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate with same Zod schema
const validated = correspondenceSchema.parse(body);
// Create correspondence
// ...
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', issues: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
```
---
## Form Patterns
### Dynamic Fields
```typescript
import { useFieldArray } from 'react-hook-form';
const { fields, append, remove } = useFieldArray({
control,
name: 'items', // RFA items
});
// Add item
append({ description: '', quantity: 0 });
// Remove item
remove(index);
```
### Controlled Components
```typescript
import { Controller } from 'react-hook-form';
<Controller
name="discipline_id"
control={control}
render={({ field }) => (
<Select onValueChange={field.onChange} value={field.value}>
{/* Options */}
</Select>
)}
/>;
```
---
## Consequences
### Positive Consequences
1.**Performance:** Minimal re-renders (uncontrolled)
2.**Type Safety:** Full TypeScript support
3.**Validation Reuse:** Same schema for client & server
4.**Small Bundle:** ~8.5kb only
5.**Clean Code:** Less boilerplate
6.**Error Handling:** Built-in error states
### Negative Consequences
1.**Learning Curve:** Uncontrolled approach ต่างจาก Formik
2.**Complex Forms:** ต้องใช้ Controller บางครั้ง
### Mitigation Strategies
- **Documentation:** เขียน Form patterns และ Examples
- **Reusable Components:** สร้าง FormField wrapper
- **Code Review:** Review forms ให้ใช้ best practices
---
## Related ADRs
- [ADR-007: API Design & Error Handling](./ADR-007-api-design-error-handling.md)
- [ADR-012: UI Component Library](./ADR-012-ui-component-library.md)
---
## References
- [React Hook Form Documentation](https://react-hook-form.com/)
- [Zod Documentation](https://zod.dev/)
---
**Last Updated:** 2025-12-01
**Next Review:** 2026-06-01

View File

@@ -0,0 +1,404 @@
# ADR-014: State Management Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Frontend Team
**Related Documents:** [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md), [ADR-011: App Router](./ADR-011-nextjs-app-router.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS ต้องการจัดการ Global State เช่น User session, Notifications, UI preferences ต้องเลือก State Management solution ที่เหมาะสม
### ปัญหาที่ต้องแก้:
1. **Global State:** จัดการ State ที่ใช้ร่วมกันทั้งแอปอย่างไร
2. **Server State:** จัดการข้อมูลจาก API อย่างไร
3. **Performance:** หลีกเลี่ยง Unnecessary re-renders
4. **Type Safety:** Type-safe state management
5. **Bundle Size:** ไม่ทำให้ Bundle ใหญ่เกินไป
---
## Decision Drivers
-**Performance:** Minimal re-renders
- 📦 **Bundle Size:** เล็กที่สุด
- 🎯 **Simplicity:** เรียนรู้และใช้งานง่าย
-**Type Safety:** TypeScript support
- 🔄 **Server State:** จัดการ API data ได้ดี
---
## Considered Options
### Option 1: Redux Toolkit
**Pros:**
- ✅ Industry standard
- ✅ DevTools ดี
- ✅ Middleware support
**Cons:**
- ❌ Boilerplate มาก
- ❌ Bundle size ใหญ่ (~40kb)
- ❌ Complexity สูง
- ❌ Overkill สำหรับ App ส่วนใหญ่
### Option 2: React Context API
**Pros:**
- ✅ Built-in (no dependencies)
- ✅ Simple
**Cons:**
- ❌ Performance issues (re-render ทั้ง tree)
- ❌ ไม่เหมาะสำหรับ Complex state
- ❌ ต้องจัดการ Optimization เอง
### Option 3: Zustand
**Props:**
-**Lightweight:** ~1.2kb only
-**Simple API:** เรียนรู้ง่าย
-**Performance:** Selective re-renders
-**TypeScript:** Full support
-**No boilerplate**
-**DevTools support**
**Cons:**
- ❌ Smaller community กว่า Redux
### Option 4: React Query (TanStack Query) for Server State
**Pros:**
-**Specialized:** จัดการ Server state ได้ดีที่สุด
-**Caching:** Auto cache management
-**Refetching:** Auto refetch on focus
-**TypeScript:** Excellent support
**Cons:**
- ❌ เฉพาะ Server state (ต้องใช้คู่กับ Client state solution)
---
## Decision Outcome
**Chosen Option:** **Zustand (Client State) + TanStack Query (Server State) + React Hook Form + Zod (Form State)**
### Rationale
**For Client State (UI state, Preferences):**
- Use **Zustand** - lightweight และเรียนรู้ง่าย
**For Server State (API data):**
- Use **TanStack Query** (React Query) สำหรับ data fetching, caching, synchronization
- Server Components สำหรับ initial data loading
**For Form State:**
- Use **React Hook Form + Zod** สำหรับ type-safe form management
---
## Implementation Details
### 1. Install Zustand
```bash
npm install zustand
```
### 2. Create Global Store (User Session)
```typescript
// File: lib/stores/auth-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
user_id: number;
username: string;
email: string;
first_name: string;
last_name: string;
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
// Actions
setAuth: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
setAuth: (user, token) =>
set({
user,
token,
isAuthenticated: true,
}),
logout: () =>
set({
user: null,
token: null,
isAuthenticated: false,
}),
}),
{
name: 'auth-storage', // LocalStorage key
}
)
);
```
### 3. Use Store in Components
```typescript
// File: components/header.tsx
'use client';
import { useAuthStore } from '@/lib/stores/auth-store';
import { Button } from '@/components/ui/button';
export function Header() {
const { user, logout } = useAuthStore();
return (
<header className="flex justify-between items-center p-4">
<div>Welcome, {user?.first_name}</div>
<Button onClick={logout}>Logout</Button>
</header>
);
}
```
### 4. Notifications Store
```typescript
// File: lib/stores/notification-store.ts
import { create } from 'zustand';
interface Notification {
id: string;
type: 'success' | 'error' | 'info';
message: string;
}
interface NotificationState {
notifications: Notification[];
addNotification: (notification: Omit<Notification, 'id'>) => void;
removeNotification: (id: string) => void;
clearAll: () => void;
}
export const useNotificationStore = create<NotificationState>((set) => ({
notifications: [],
addNotification: (notification) =>
set((state) => ({
notifications: [
...state.notifications,
{ ...notification, id: Math.random().toString() },
],
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
clearAll: () => set({ notifications: [] }),
}));
```
### 5. Server State with Server Components
```typescript
// File: app/(dashboard)/correspondences/page.tsx
// Server Component - No state management needed!
import { getCorrespondences } from '@/lib/api/correspondences';
export default async function CorrespondencesPage() {
// Fetch directly on server
const correspondences = await getCorrespondences();
return (
<div>
<h1>Correspondences</h1>
{correspondences.map((item) => (
<div key={item.id}>{item.subject}</div>
))}
</div>
);
}
```
### 6. Client-Side Fetching (with TanStack Query)
```bash
npm install @tanstack/react-query
```
```typescript
// File: components/correspondences/correspondence-list.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { getCorrespondences } from '@/lib/api/correspondences';
export function CorrespondenceList() {
const { data, error, isLoading, refetch } = useQuery({
queryKey: ['correspondences'],
queryFn: getCorrespondences,
refetchInterval: 30000, // Auto refresh every 30s
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data</div>;
return (
<div>
{data?.map((item) => (
<div key={item.id}>{item.subject}</div>
))}
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
```
### 7. UI Preferences Store
```typescript
// File: lib/stores/ui-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UIState {
sidebarCollapsed: boolean;
theme: 'light' | 'dark';
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUIStore = create<UIState>()(
persist(
(set) => ({
sidebarCollapsed: false,
theme: 'light',
toggleSidebar: () =>
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
setTheme: (theme) => set({ theme }),
}),
{
name: 'ui-preferences',
}
)
);
```
---
## State Management Patterns
### When to Use Zustand (Client State)
✅ Use Zustand for:
- User authentication state
- UI preferences (theme, sidebar state)
- Notifications/Toasts
- Shopping cart (if applicable)
- Form wizard state
- Modal state (global)
### When to Use Server Components (Server State)
✅ Use Server Components for:
- Initial data loading
- Static/semi-static data
- SEO-important content
- Data that doesn't need real-time updates
### When to Use TanStack Query (Client-Side Server State)
✅ Use TanStack Query for:
- Real-time data (notifications count)
- Polling/Auto-refresh data
- User-specific data that changes often
- Optimistic UI updates
- Complex cache invalidation
- Paginated/infinite scroll data
---
## Consequences
### Positive Consequences
1.**Lightweight:** Zustand ~1.2kb
2.**Simple:** Easy to learn and use
3.**Performance:** Selective re-renders
4.**No Boilerplate:** Clean API
5.**Type Safe:** Full TypeScript support
6.**Persistent:** Easy LocalStorage persist
### Negative Consequences
1.**Smaller Ecosystem:** กว่า Redux
2.**Less Tooling:** DevTools ไม่ครบเท่า Redux
### Mitigation Strategies
- **Documentation:** Document common patterns
- **Code Examples:** Provide store templates
- **Testing:** Unit test stores thoroughly
---
## Related ADRs
- [ADR-011: Next.js App Router](./ADR-011-nextjs-app-router.md) - Server Components
- [ADR-007: API Design](./ADR-007-api-design-error-handling.md)
---
## References
- [Zustand Documentation](https://github.com/pmndrs/zustand)
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [React Hook Form Documentation](https://react-hook-form.com/)
---
**Last Updated:** 2026-02-20
**Next Review:** 2026-06-01

View File

@@ -0,0 +1,457 @@
# ADR-015: Deployment & Infrastructure Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** DevOps Team, System Architect
**Related Documents:** [ADR-005: Technology Stack](./ADR-005-technology-stack.md), [Operations Guide](../04-operations/)
---
## Context and Problem Statement
LCBP3-DMS ต้อง Deploy บน QNAP Container Station โดยใช้ Docker แต่ต้องเลือกกลย modularุทธ์การ Deploy, การจัดการ Environment, และการ Scale ที่เหมาะสม
### ปัญหาที่ต้องแก้:
1. **Container Orchestration:** ใช้ Docker Compose หรือ Kubernetes
2. **Environment Management:** จัดการ Environment Variables อย่างไร
3. **Deployment Strategy:** Blue-Green, Rolling Update, หรือ Recreate
4. **Scaling:** แผน Scale horizontal/vertical
5. **Persistence:** จัดการ Data persistence อย่างไร
---
## Decision Drivers
- 🎯 **Simplicity:** ง่ายต่อการ Deploy และ Maintain
- 🔒 **Security:** Secrets management ปลอดภัย
-**Zero Downtime:** Deploy ได้โดยไม่มี Downtime
- 📦 **Resource Efficiency:** ใช้ทรัพยากร QNAP อย่างคุ้มค่า
- 🔄 **Rollback Capability:** Rollback ได้เมื่อมีปัญหา
---
## Considered Options
### Option 1: Docker Compose (Single Server)
**Deployment:**
```yaml
version: '3.8'
services:
backend:
image: lcbp3-backend:latest
environment:
- NODE_ENV=production
env_file:
- .env.production
volumes:
- ./uploads:/app/uploads
- ./logs:/app/logs
depends_on:
- mariadb
- redis
networks:
- lcbp3-network
frontend:
image: lcbp3-frontend:latest
depends_on:
- backend
networks:
- lcbp3-network
mariadb:
image: mariadb:11.8
volumes:
- mariadb-data:/var/lib/mysql
networks:
- lcbp3-network
redis:
image: redis:7.2-alpine
volumes:
- redis-data:/data
networks:
- lcbp3-network
elasticsearch:
image: elasticsearch:8.11.0
volumes:
- elastic-data:/usr/share/elasticsearch/data
networks:
- lcbp3-network
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
ports:
- '80:80'
- '443:443'
depends_on:
- backend
- frontend
networks:
- lcbp3-network
volumes:
mariadb-data:
redis-data:
elastic-data:
networks:
lcbp3-network:
```
**Pros:**
- ✅ Simple และเข้าใจง่าย
- ✅ พอดีกับ QNAP Container Station
- ✅ Resource requirement ต่ำ
- ✅ Debugging ง่าย
**Cons:**
- ❌ Single point of failure
- ❌ ไม่มี Auto-scaling
- ❌ Service discovery manual
### Option 2: Kubernetes (k3s)
**Pros:**
- ✅ Auto-scaling
- ✅ Self-healing
- ✅ Service discovery
**Cons:**
- ❌ ซับซ้อนเกินความจำเป็น
- ❌ Resource overhead สูง
- ❌ Learning curve สูง
- ❌ Overkill สำหรับ Single server
---
## Decision Outcome
**Chosen Option:** **Docker Compose with Blue-Green Deployment Strategy**
### Rationale
1. **Appropriate Complexity:** เหมาะกับ Scale และทีมของโปรเจกต์
2. **QNAP Compatibility:** รองรับโดย QNAP Container Station
3. **Resource Efficiency:** ใช้ทรัพยากรน้อยกว่า K8s
4. **Team Familiarity:** ทีม DevOps คุ้นเคยกับ Docker Compose
5. **Easy Rollback:** Rollback ได้ง่ายด้วย Tagged images
---
## Implementation Details
### 1. Directory Structure
```
/volume1/lcbp3/
├── blue/
│ ├── docker-compose.yml
│ ├── .env.production
│ └── nginx.conf
├── green/
│ ├── docker-compose.yml
│ ├── .env.production
│ └── nginx.conf
├── nginx-proxy/
│ ├── docker-compose.yml
│ └── nginx.conf (routes to blue or green)
├── shared/
│ ├── uploads/
│ ├── logs/
│ └── backups/
└── volumes/
├── mariadb-data/
├── redis-data/
└── elastic-data/
```
### 2. Blue-Green Deployment Process
```bash
#!/bin/bash
# File: scripts/deploy.sh
CURRENT=$(cat /volume1/lcbp3/current)
TARGET=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue")
echo "Current environment: $CURRENT"
echo "Deploying to: $TARGET"
cd /volume1/lcbp3/$TARGET
# 1. Pull latest images
docker-compose pull
# 2. Start new environment
docker-compose up -d
# 3. Run database migrations
docker exec lcbp3-${TARGET}-backend npm run migration:run
# 4. Health check
for i in {1..30}; do
if curl -f http://localhost:${TARGET}_PORT/health; then
echo "Health check passed"
break
fi
sleep 2
done
# 5. Switch nginx to new environment
sed -i "s/$CURRENT/$TARGET/g" /volume1/lcbp3/nginx-proxy/nginx.conf
docker exec lcbp3-nginx nginx -s reload
# 6. Update current pointer
echo "$TARGET" > /volume1/lcbp3/current
# 7. Stop old environment (keep data)
cd /volume1/lcbp3/$CURRENT
docker-compose down
echo "Deployment complete: $TARGET is now active"
```
### 3. Environment Variables Management
```bash
# File: .env.production (NOT in Git)
NODE_ENV=production
# Database
DB_HOST=mariadb
DB_PORT=3306
DB_USERNAME=lcbp3_user
DB_PASSWORD=<secret>
DB_DATABASE=lcbp3_dms
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=<secret>
# JWT
JWT_SECRET=<secret>
JWT_EXPIRES_IN=7d
# File Storage
UPLOAD_PATH=/app/uploads
ALLOWED_FILE_TYPES=.pdf,.doc,.docx,.xls,.xlsx,.dwg
# Email
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=<secret>
SMTP_PASSWORD=<secret>
```
**Secrets Management:**
- Production `.env` files stored on QNAP only (NOT in Git)
- Use `docker-compose.override.yml` for local development
- Validate required env vars at application startup
### 4. Volume Management
```yaml
volumes:
# Persistent data (survives container recreation)
mariadb-data:
driver: local
driver_opts:
type: none
device: /volume1/lcbp3/volumes/mariadb-data
o: bind
# Shared uploads across blue/green
uploads:
driver: local
driver_opts:
type: none
device: /volume1/lcbp3/shared/uploads
o: bind
# Logs
logs:
driver: local
driver_opts:
type: none
device: /volume1/lcbp3/shared/logs
o: bind
```
### 5. NGINX Reverse Proxy
```nginx
# File: nginx-proxy/nginx.conf
upstream backend {
server lcbp3-blue-backend:3000; # Switch to green during deployment
}
upstream frontend {
server lcbp3-blue-frontend:3000;
}
server {
listen 80;
server_name lcbp3-dms.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name lcbp3-dms.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Backend API
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Timeouts for file uploads
client_max_body_size 50M;
proxy_read_timeout 300s;
}
# Health check
location /health {
proxy_pass http://backend/health;
access_log off;
}
}
```
---
## Scaling Strategy
### Vertical Scaling (Phase 1)
**Current Recommendation:**
- Backend: 2 CPU cores, 4GB RAM
- Frontend: 1 CPU core, 2GB RAM
- MariaDB: 2 CPU cores, 8GB RAM
- Redis: 1 CPU core, 2GB RAM
- Elasticsearch: 2 CPU cores, 4GB RAM
**Upgrade Path:**
- Increase CPU/RAM ตาม Load
- Monitor with Prometheus/Grafana
### Horizontal Scaling (Phase 2 - Future)
**If needed:**
- Load Balancer หน้า Backend (multiple replicas)
- Database Read Replicas
- Redis Cluster
- Elasticsearch Cluster
**Prerequisite:**
- Stateless application (sessions in Redis)
- Shared file storage (NFS/S3)
---
## Deployment Checklist
```markdown
### Pre-Deployment
- [ ] Backup database
- [ ] Tag Docker images
- [ ] Update .env file
- [ ] Review migration scripts
- [ ] Notify stakeholders
### Deployment
- [ ] Pull latest images
- [ ] Start target environment (blue/green)
- [ ] Run migrations
- [ ] Health check passes
- [ ] Switch NGINX proxy
- [ ] Verify application working
### Post-Deployment
- [ ] Monitor logs for errors
- [ ] Check performance metrics
- [ ] Verify all features working
- [ ] Stop old environment
- [ ] Update deployment log
```
---
## Consequences
### Positive Consequences
1.**Simple Deployment:** Docker Compose เข้าใจง่าย
2.**Zero Downtime:** Blue-Green Deployment ไม่มี Downtime
3.**Easy Rollback:** Rollback = Switch NGINX back
4.**Cost Effective:** ไม่ต้อง Kubernetes overhead
5.**QNAP Compatible:** ใช้ได้กับ Container Station
### Negative Consequences
1.**Manual Scaling:** ต้อง Scale manual
2.**Single Server:** ไม่มี High Availability
3.**Limited Auto-healing:** ต้อง Monitor และ Restart manual
### Mitigation Strategies
- **Monitoring:** Setup Prometheus + Alertmanager
- **Automated Backups:** Cron jobs สำหรับ Database backups
- **Documentation:** เขียน Runbook สำหรับ Common issues
- **Health Checks:** Implement comprehensive health endpoints
---
## Related ADRs
- [ADR-005: Technology Stack](./ADR-005-technology-stack.md)
- [ADR-009: Database Migration Strategy](./ADR-009-database-migration-strategy.md)
---
## References
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [Blue-Green Deployment](https://martinfowler.com/bliki/BlueGreenDeployment.html)
- [QNAP Container Station](https://www.qnap.com/en/software/container-station)
---
**Last Updated:** 2025-12-01
**Next Review:** 2026-06-01

View File

@@ -0,0 +1,451 @@
# ADR-016: Security & Authentication Strategy
**Status:** ✅ Accepted
**Date:** 2025-12-01
**Decision Makers:** Security Team, System Architect
**Related Documents:** [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md), [ADR-007: API Design](./ADR-007-api-design-error-handling.md)
---
## Context and Problem Statement
LCBP3-DMS จัดการเอกสารสำคัญของโปรเจกต์ ต้องการ Security strategy ที่ครอบคลุม Authentication, Authorization, Data protection, และ Security best practices
### ปัญหาที่ต้องแก้:
1. **Authentication:** ใช้วิธีไหนในการยืนยันตัวตน
2. **Session Management:** จัดการ Session อย่างไร
3. **Password Security:** เก็บ Password อย่างไรให้ปลอดภัย
4. **Data Encryption:** Encrypt ข้อมูลอย่างไร
5. **Security Headers:** HTTP Headers ที่ต้องมี
6. **Input Validation:** ป้องกัน Injection attacks
7. **Rate Limiting:** ป้องกัน Brute force attacks
---
## Decision Drivers
- 🔒 **Security First:** ความปลอดภัยเป็นสำคัญที่สุด
-**Industry Standards:** ใช้ Standard practices (OWASP)
- 🎯 **User Experience:** ไม่ซับซ้อนเกินไป
- 📝 **Audit Trail:** บันทึก Security events ทั้งหมด
- 🔄 **Token Refresh:** Session management ที่สะดวก
---
## Decision Outcome
### 1. Authentication Strategy
**Chosen:** **JWT (JSON Web Tokens) with HTTP-only Cookies**
```typescript
// File: src/auth/auth.service.ts
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService
) {}
async login(credentials: LoginDto): Promise<{ tokens }> {
const user = await this.validateUser(credentials);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = {
sub: user.user_id,
username: user.username,
roles: user.roles,
};
// Generate tokens
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m', // Short-lived
});
const refreshToken = this.jwtService.sign(payload, {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d', // Long-lived
});
// Store refresh token (hashed) in database
await this.storeRefreshToken(user.user_id, refreshToken);
return { accessToken, refreshToken };
}
private async validateUser(credentials: LoginDto) {
const user = await this.usersService.findByUsername(credentials.username);
if (!user) return null;
// Use bcrypt for password comparison
const isValid = await bcrypt.compare(
credentials.password,
user.password_hash
);
return isValid ? user : null;
}
}
```
### 2. Password Security
**Strategy:** **bcrypt with salt rounds = 12**
```typescript
import * as bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
// Hash password
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
// Verify password
async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
```
**Password Policy:**
- Minimum 8 characters
- Mix of uppercase, lowercase, numbers
- No common passwords (check against dictionary)
- Password history (last 5 passwords)
- Force change every 90 days (optional)
### 3. JWT Guard (Authorization)
```typescript
// File: src/common/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw new UnauthorizedException(info?.message || 'Unauthorized');
}
return user;
}
}
```
### 4. Data Encryption
**At Rest:**
- Database: Use MariaDB encryption at column level (for sensitive fields)
- Files: Encrypt before storing (AES-256)
```typescript
import * as crypto from 'crypto';
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(text: string): { encrypted: string; iv: string; tag: string } {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex'),
};
}
function decrypt(encrypted: string, iv: string, tag: string): string {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
```
**In Transit:**
- HTTPS only (TLS 1.3)
- HSTS enabled
- Certificate from trusted CA
### 5. Security Headers
```typescript
// File: src/main.ts
import helmet from 'helmet';
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
xssFilter: true,
noSniff: true,
})
);
// CORS
app.enableCors({
origin: process.env.FRONTEND_URL,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
```
### 6. Input Validation
**Strategy:** **Class-validator + Zod + Custom Sanitization**
```typescript
// DTO Validation
import { IsString, IsEmail, MinLength, Matches } from 'class-validator';
export class LoginDto {
@IsString()
@MinLength(3)
username: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;
}
// SQL Injection Prevention (TypeORM handles this)
// Use parameterized queries ALWAYS
// XSS Prevention
import * as sanitizeHtml from 'sanitize-html';
function sanitizeInput(input: string): string {
return sanitizeHtml(input, {
allowedTags: [], // No HTML tags
allowedAttributes: {},
});
}
```
### 7. Rate Limiting
```typescript
// File: src/common/guards/rate-limit.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
protected getTracker(req: Request): string {
// Track by IP + User ID (if authenticated)
return req.ip + (req.user?.user_id || '');
}
}
// Apply to login endpoint
@Controller('auth')
@UseGuards(CustomThrottlerGuard)
export class AuthController {
@Post('login')
@Throttle(5, 60) // 5 attempts per minute
async login(@Body() credentials: LoginDto) {
return this.authService.login(credentials);
}
}
```
### 8. Session Management
**Strategy:** **Stateless JWT + Refresh Token in Database**
```typescript
// Refresh token table
@Entity('refresh_tokens')
export class RefreshToken {
@PrimaryGeneratedColumn()
token_id: number;
@Column()
user_id: number;
@Column()
token_hash: string; // SHA-256 hash of token
@Column()
expires_at: Date;
@Column({ default: false })
is_revoked: boolean;
@CreateDateColumn()
created_at: Date;
}
// Token refresh endpoint
@Post('refresh')
async refresh(@Body('refreshToken') token: string) {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_REFRESH_SECRET,
});
// Check if token is revoked
const storedToken = await this.findRefreshToken(token);
if (!storedToken || storedToken.is_revoked) {
throw new UnauthorizedException('Invalid refresh token');
}
// Generate new access token
const newAccessToken = this.jwtService.sign({
sub: payload.sub,
username: payload.username,
roles: payload.roles,
});
return { accessToken: newAccessToken };
}
```
### 9. Audit Logging (Security Events)
```typescript
// Log all security-related events
await this.auditLogService.create({
user_id: user.user_id,
action: 'LOGIN_SUCCESS',
entity_type: 'auth',
ip_address: req.ip,
user_agent: req.headers['user-agent'],
});
// Track failed login attempts
await this.auditLogService.create({
action: 'LOGIN_FAILED',
entity_type: 'auth',
ip_address: req.ip,
details: { username: credentials.username },
});
```
---
## Security Checklist
### Application Security
- [x] JWT authentication with short-lived tokens
- [x] Password hashing with bcrypt (12 rounds)
- [x] HTTPS only (TLS 1.3)
- [x] Security headers (Helmet.js)
- [x] CORS properly configured
- [x] Input validation (class-validator)
- [x] SQL injection prevention (TypeORM)
- [x] XSS prevention (sanitize-html)
- [x] CSRF protection (SameSite cookies)
- [x] Rate limiting (Throttler)
### Data Security
- [x] Sensitive data encrypted at rest (AES-256)
- [x] Passwords hashed (bcrypt)
- [x] Secrets in environment variables (not in code)
- [x] Database credentials rotated regularly
- [x] Backup encryption enabled
### Access Control
- [x] 4-level RBAC implemented
- [x] Principle of least privilege
- [x] Role-based permissions
- [x] Session timeout (15 minutes)
- [x] Audit logging for all actions
### Infrastructure
- [x] Firewall configured
- [x] Intrusion detection (optional)
- [x] Regular security updates
- [x] Vulnerability scanning
- [x] Penetration testing (before go-live)
---
## Consequences
### Positive Consequences
1.**Secure by Design:** ใช้ Industry best practices
2.**OWASP Compliant:** ครอบคลุม OWASP Top 10
3.**Audit Trail:** บันทึก Security events ทั้งหมด
4.**Token-based:** Stateless และ Scalable
5.**Defense in Depth:** หลายชั้นการป้องกัน
### Negative Consequences
1.**Complexity:** Security measures เพิ่ม Complexity
2.**Performance:** Encryption/Hashing ใช้ CPU
3.**User Friction:** Password policy อาจรำคาญผู้ใช้
### Mitigation Strategies
- **Documentation:** เขียน Security guidelines ให้ทีม
- **Training:** อบรม Security awareness
- **Automation:** Automated security scans
- **Monitoring:** Real-time security monitoring
---
## Related ADRs
- [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md)
- [ADR-007: API Design & Error Handling](./ADR-007-api-design-error-handling.md)
- [ADR-015: Deployment & Infrastructure](./ADR-015-deployment-infrastructure.md)
---
## References
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [JWT Best Practices](https://curity.io/resources/learn/jwt-best-practices/)
- [NestJS Security](https://docs.nestjs.com/security/authentication)
---
**Last Updated:** 2025-12-01
**Next Review:** 2026-03-01 (Quarterly review)

View File

@@ -0,0 +1,360 @@
# Architecture Decision Records (ADRs)
**Version:** 1.7.0
**Last Updated:** 2025-12-18
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
---
## 📋 What are ADRs?
Architecture Decision Records (ADRs) เป็นเอกสารที่บันทึก **ประวัติการตัดสินใจทางสถาปัตยกรรมที่สำคัญ** ของโปรเจกต์ โดยร ะบุ:
- **Context**: เหตุผลที่ต้องตัดสินใจ
- **Options Considered**: ทางเลือกที่พิจารณา
- **Decision**: สิ่งที่เลือก และเหตุผล
- **Consequences**: ผลที่ตามมา (ดีและไม่ดี)
**วัตถุประสงค์:**
1. ทำให้ทีมเข้าใจ "ทำไม" นอกเหนือจาก "ทำอย่างไร"
2. ป้องกันการสงสัยว่า "ทำไมถึงออกแบบแบบนี้" ในอนาคต
3. ช่วยในการ Onboard สมาชิกใหม่
4. บันทึกประวัติศาสตร์การพัฒนาโปรเจกต์
---
## 📚 ADR Index
### Core Architecture Decisions
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------- | ---------- | ---------- | ------------------------------------------------------------------------- |
| [ADR-001](./ADR-001-unified-workflow-engine.md) | Unified Workflow Engine | ✅ Accepted | 2025-11-30 | ใช้ DSL-based Workflow Engine สำหรับ Correspondences, RFAs, และ Circulations |
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2025-11-30 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
| [ADR-003](./ADR-003-file-storage-approach.md) | Two-Phase File Storage Approach | ✅ Accepted | 2025-11-30 | Upload → Temp → Commit to Permanent เพื่อป้องกัน Orphan Files |
### Security & Access Control
| ADR | Title | Status | Date | Summary |
| ------------------------------------------- | ----------------------------- | ---------- | ---------- | ------------------------------------------------------------- |
| [ADR-004](./ADR-004-rbac-implementation.md) | RBAC Implementation (4-Level) | ✅ Accepted | 2025-11-30 | Hierarchical RBAC: Global → Organization → Project → Contract |
### Technology & Infrastructure
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------------ | ---------- | ---------- | ------------------------------------------------------------ |
| [ADR-005](./ADR-005-technology-stack.md) | Technology Stack Selection | ✅ Accepted | 2025-11-30 | Full Stack TypeScript: NestJS + Next.js + MariaDB + Redis |
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2025-11-30 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted | 2025-12-01 | TypeORM Migrations พร้อม Blue-Green Deployment |
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2025-12-01 | Docker Compose with Blue-Green Deployment on QNAP |
| [ADR-016](./ADR-016-security-authentication.md) | Security & Authentication Strategy | ✅ Accepted | 2025-12-01 | JWT + bcrypt + OWASP Security Best Practices |
### API & Integration
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ---------- | ---------- | --------------------------------------------------------------------------- |
| [ADR-007](./ADR-007-api-design-error-handling.md) | API Design & Error Handling | ✅ Accepted | 2025-12-01 | Standard REST API with Custom Error Format + NestJS Exception Filters |
| [ADR-008](./ADR-008-email-notification-strategy.md) | Email & Notification Strategy | ✅ Accepted | 2025-12-01 | BullMQ + Redis Queue สำหรับ Multi-channel Notifications (Email, LINE, In-app) |
### Observability
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ---------- | ---------- | ------------------------------------------------------------ |
| [ADR-010](./ADR-010-logging-monitoring-strategy.md) | Logging & Monitoring Strategy | ✅ Accepted | 2025-12-01 | Winston Structured Logging พร้อม Future ELK Stack Integration |
### Frontend Architecture
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------ | -------------------------------- | ---------- | ---------- | ----------------------------------------------------- |
| [ADR-011](./ADR-011-nextjs-app-router.md) | Next.js App Router & Routing | ✅ Accepted | 2025-12-01 | App Router with Server Components and Nested Layouts |
| [ADR-012](./ADR-012-ui-component-library.md) | UI Component Library (Shadcn/UI) | ✅ Accepted | 2025-12-01 | Shadcn/UI + Tailwind CSS for Full Component Ownership |
| [ADR-013](./ADR-013-form-handling-validation.md) | Form Handling & Validation | ✅ Accepted | 2025-12-01 | React Hook Form + Zod for Type-Safe Forms |
| [ADR-014](./ADR-014-state-management.md) | State Management Strategy | ✅ Accepted | 2025-12-01 | Zustand for Client State + Server Components |
---
## 🔍 ADR Categories
### 1. Business Logic & Workflows
- **ADR-001:** Unified Workflow Engine - ใช้ JSON DSL แทน Hard-coded routing tables
### 2. Data Integrity & Concurrency
- **ADR-002:** Document Numbering - Double-lock (Redis Redlock + DB Optimistic) เพื่อป้องกัน Race Condition
- 📋 [Requirements](../01-requirements/01-03.11-document-numbering.md)
- 📘 [Implementation Guide](../03-implementation/03-04-document-numbering.md)
- 📗 [Operations Guide](../04-operations/04-08-document-numbering-operations.md)
- **ADR-003:** File Storage - Two-phase เพื่อ Transaction safety
- **ADR-009:** Database Migration - TypeORM Migrations พร้อม Blue-Green Deployment
### 3. Security & Access Control
- **ADR-004:** RBAC - 4-level scope สำหรับ Fine-grained permissions
### 4. Infrastructure & Performance
- **ADR-005:** Technology Stack - TypeScript ecosystem
- **ADR-006:** Redis - Caching และ Distributed coordination
- **ADR-015:** Deployment - Docker Compose with Blue-Green Deployment
- **ADR-016:** Security - JWT Authentication + OWASP Best Practices
### 5. API & Integration
- **ADR-007:** API Design - REST API with Custom Error Format
- **ADR-008:** Notification - BullMQ Queue สำหรับ Multi-channel notifications
### 6. Observability & Monitoring
- **ADR-010:** Logging - Winston Structured Logging พร้อม Future ELK Stack
### 7. Frontend Architecture
- **ADR-011:** Next.js App Router - Server Components และ Nested Layouts
- **ADR-012:** UI Components - Shadcn/UI + Tailwind CSS
- **ADR-013:** Form Handling - React Hook Form + Zod Validation
- **ADR-014:** State Management - Zustand + Server Components
---
## 📖 How to Read ADRs
### ADR Structure
แต่ละ ADR มีโครงสร้างดังนี้:
1. **Status**: Accepted, Proposed, Deprecated, Superseded
2. **Context**: ปัญหาหรือสถานการณ์ที่ต้องตัดสินใจ
3. **Decision Drivers**: ปัจจัยที่มีผลต่อการตัดสินใจ
4. **Considered Options**: ทางเลือกที่พิจารณา (พร้อม Pros/Cons)
5. **Decision Outcome**: สิ่งที่เลือก และเหตุผล
6. **Consequences**: ผลที่ตามมา (Positive/Negative/Mitigation)
7. **Implementation Details**: รายละเอียดการ Implement (Code examples)
8. **Related ADRs**: ADR อื่นที่เกี่ยวข้อง
### Reading Tips
- เริ่มจาก **Context** เพื่อเข้าใจปัญหา
- ดู **Considered Options** เพื่อเข้าใจ Trade-offs
- อ่าน **Consequences** เพื่อรู้ว่าต้อง Maintain อย่างไร
- ดู **Related ADRs** เพื่อเข้าใจภาพรวม
---
## 🆕 Creating New ADRs
### When to Create an ADR?
สร้าง ADR เมื่อ:
- ✅ เลือก Technology/Framework หลัก
- ✅ ออกแบบ Architecture Pattern สำคัญ
- ✅ แก้ปัญหาซับซ้อนที่มีหลาย Alternatives
- ✅ Trade-offs ที่มีผลกระทบระยะยาว
- ✅ ตัดสินใจที่ยากจะ Revert (Irreversible decisions)
**ไม่ต้องสร้าง ADR สำหรับ:**
- ❌ การเลือก Library เล็กๆ ที่เปลี่ยนได้ง่าย
- ❌ Implementation details ที่ไม่กระทบ Architecture
- ❌ Coding style หรือ Naming conventions
### ADR Template
```markdown
# ADR-XXX: [Title]
**Status:** Proposed
**Date:** YYYY-MM-DD
**Decision Makers:** [Names]
**Related Documents:** [Links]
---
## Context and Problem Statement
[Describe the problem...]
---
## Decision Drivers
- [Driver 1]
- [Driver 2]
---
## Considered Options
### Option 1: [Name]
**Pros:**
- ✅ [Pro 1]
**Cons:**
- ❌ [Con 1]
---
## Decision Outcome
**Chosen Option:** [Option X]
### Rationale
[Why this option...]
---
## Consequences
### Positive
1. ✅ [Impact 1]
### Negative
1. ❌ [Risk 1]
---
## Related ADRs
- [ADR-XXX: Title](./ADR-XXX.md)
```
---
## 🔄 ADR Lifecycle
```mermaid
stateDiagram-v2
[*] --> Proposed: Create new ADR
Proposed --> Accepted: Team agrees
Proposed --> Rejected: Team disagrees
Accepted --> Deprecated: No longer relevant
Accepted --> Superseded: Replaced by new ADR
Deprecated --> [*]
Superseded --> [*]
Rejected --> [*]
```
### Status Definitions
- **Proposed**: รอการ Review และ Approve
- **Accepted**: ผ่านการ Review แล้ว กำลังใช้งาน
- **Deprecated**: เลิกใช้แล้ว แต่ยังอยู่ในระบบ
- **Superseded**: ถูกแทนที่โดย ADR อื่น
- **Rejected**: ไม่ผ่านการ Approve
---
## 📊 ADR Impact Map
```mermaid
graph TB
ADR001[ADR-001<br/>Unified Workflow] --> Corr[Correspondences]
ADR001 --> RFA[RFAs]
ADR001 --> Circ[Circulations]
ADR002[ADR-002<br/>Document Numbering] --> Corr
ADR002 --> RFA
ADR003[ADR-003<br/>File Storage] --> Attach[Attachments]
ADR003 --> Corr
ADR003 --> RFA
ADR004[ADR-004<br/>RBAC] --> Auth[Authentication]
ADR004 --> Guards[Guards]
ADR005[ADR-005<br/>Tech Stack] --> Backend[Backend]
ADR005 --> Frontend[Frontend]
ADR005 --> DB[(Database)]
ADR006[ADR-006<br/>Redis] --> Cache[Caching]
ADR006 --> Lock[Locking]
ADR006 --> Queue[Job Queue]
ADR006 --> ADR002
ADR006 --> ADR004
```
---
## 🔗 Related Documentation
- [System Architecture](../02-architecture/02-01-system-architecture.md) - สถาปัตยกรรมระบบโดยรวม
- [Data Model](../02-architecture/02-03-data-model.md) - โครงสร้างฐานข้อมูล
- [API Design](../02-architecture/02-02-api-design.md) - การออกแบบ API
- [Backend Guidelines](../03-implementation/03-02-backend-guidelines.md) - มาตรฐานการพัฒนา Backend
- [Frontend Guidelines](../03-implementation/03-03-frontend-guidelines.md) - มาตรฐานการพัฒนา Frontend
---
## 📝 Review Process
### Before Merging
1. สร้าง ADR ใน `specs/05-decisions/ADR-XXX-title.md`
2. Update ADR Index ใน `README.md` นี้
3. Link ADR ไปยัง Related Documents
4. Request Review จากทีม
5. อภิปรายและปรับแก้ตาม Feedback
6. Update Status เป็น "Accepted"
7. Merge to main branch
### Review Checklist
- ☐ Context ชัดเจน เข้าใจปัญหา
- ☐ มี Options อย่างน้อย 2-3 ทางเลือก
- ☐ Pros/Cons ครบถ้วน
- ☐ Decision Rationale มีเหตุผลรองรับ
- ☐ Consequences ระบุทั้งดีและไม่ดี
- ☐ Related ADRs linked ถูกต้อง
- ☐ Code examples (ถ้ามี) อ่านง่าย
---
## 🎯 Best Practices
### Writing Good ADRs
1. **Be Concise:** ไม่เกิน 3-4 หน้า (except code examples)
2. **Focus on "Why":** อธิบายเหตุผลมากกว่า "How"
3. **List Alternatives:** แสดงว่าพิจารณาหลายทางเลือก
4. **Be Honest:** ระบุ Cons และ Risks จริงๆ
5. **Use Diagrams:** Visualize ด้วย Mermaid diagrams
6. **Link References:** ใส่ Link ไปเอกสารอ้างอิง
### Common Mistakes
- ❌ เขียนยาวเกินไป (วนเวียน)
- ❌ ไม่มี Alternatives (แสดงว่าไม่ได้พิจารณา)
- ❌ Consequences ไม่จริงใจ (แต่งว่าดีอย่างเดียว)
- ❌ Implementation details มากเกินไป
- ❌ ไม่ Update เมื่อ Decision เปลี่ยน
---
## 📚 External Resources
- [ADR GitHub Organization](https://adr.github.io/)
- [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
- [ADR Tools](https://github.com/npryce/adr-tools)
- [Architecture Decision Records in Action](https://www.thoughtworks.com/insights/blog/architecture/architecture-decision-records-in-action)
---
## 📧 Contact
หากมีคำถามเกี่ยวกับ ADRs กรุณาติดต่อ:
- **System Architect:** Nattanin Peancharoen
- **Development Team Lead:** [Name]
---
**Version:** 1.7.0
**Last Review:** 2025-12-18