> For the complete documentation index, see [llms.txt](https://docs-epc.gitbook.io/ncs-documents/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-epc.gitbook.io/ncs-documents/migration/migrate-your-databases-to-postgresql-database-as-a-service.md).

# Migrate your databases to PostgreSQL Database as a Service

เนื่องจากการย้าย Database จากระบบภายนอก (On-premises หรือ Provider อื่น) มายัง PostgreSQL Database ของ NCS จำเป็นต้องมีการโอนถ่ายข้อมูลผ่าน Logical Dump เพื่อความเข้ากันได้ของระบบสูงสุด จึงแนะนำให้ใช้วิธีมาตรฐานในการดำเนินการ

## Prerequisite

* มี PostgreSQL Database ต้นทาง ที่พร้อมสำหรับการ Export ข้อมูล
* มี PostgreSQL Database Instance ใน Project บน NCS
  * ควรกำหนดขนาดพื้นที่จัดเก็บข้อมูลและทรัพยากรของระบบให้เหมาะสมกับปริมาณข้อมูลและการใช้งานของฐานข้อมูลเดิม
* เครื่องที่ใช้ดำเนินการ (Migration Workstation) ต้องติดตั้ง `postgresql-client` ซึ่งมี `pg_dump`, `pg_restore`, `psql`
  * ควรใช้ Version เดียวกับ PostgreSQL ปลายทาง และไม่เก่ากว่า Server ต้นทาง
* ตั้งค่า Port ของ Database บน NCS เป็น 5432 ให้รองรับการเชื่อมต่อจาก IP ของเครื่องที่ใช้ดำเนินการ ซึ่งเป็นค่าเริ่มต้นของ NCS อยู่แล้ว

{% hint style="warning" %}
ช่วงเวลาที่ทำตาม Document นี้ ควรอยู่ในช่วงการปรับปรุงระบบ (Maintenance Phase)
{% endhint %}

## Instruction

{% stepper %}
{% step %}

## เตรียมความพร้อมของข้อมูล

เปิด Read-only บนต้นทางเพื่อ Freeze ข้อมูล:

```bash
psql -h [source_host] -U [username] -d postgres -c "ALTER SYSTEM SET default_transaction_read_only = on;"

psql -h [source_host] -U [username] -d postgres -c "SELECT pg_reload_conf();"
```

{% hint style="warning" %}
คำสั่งนี้จะกำหนดให้ Database Server อยู่ในโหมด **Read-only** ส่งผลให้ไม่สามารถเพิ่ม แก้ไข หรือลบข้อมูลได้ จนกว่าจะมีการเปลี่ยนค่ากลับ จึงควรดำเนินการในช่วง Maintenance และตรวจสอบให้แน่ใจว่าไม่มี Application หรือผู้ใช้งานกำลังเขียนข้อมูลอยู่
{% endhint %}

ตรวจสอบว่าเปิดสำเร็จ:

```bash
psql -h [source_host] -U [username] -d postgres -c "SHOW default_transaction_read_only;"
```

**ตัวอย่าง Output:**

```
 default_transaction_read_only
--------------------------------
 on
```

{% endstep %}

{% step %}

## สั่งสร้างไฟล์ Backup จาก Database ต้นทาง (Export)

ก่อนเริ่ม Dump ข้อมูล ควรตรวจสอบชื่อ Database บนเครื่องต้นทางก่อนเสมอ เพื่อป้องกันการ Dump ผิด Database

ใช้คำสั่งนี้เพื่อแสดงรายการ Database ทั้งหมดบน PostgreSQL ต้นทาง:

```bash
psql -h [source_host] -U [username] -d postgres -c "\l"
```

คำสั่งนี้จะเชื่อมต่อไปยัง Database เริ่มต้นชื่อ `postgres` แล้วแสดงรายการ Database ทั้งหมดที่ User มีสิทธิ์มองเห็น จากนั้นให้ตรวจสอบชื่อ Database ที่ต้องการ Migrate ให้ถูกต้องก่อนนำไปใช้ในคำสั่ง `pg_dump`

ใช้คำสั่ง `pg_dump` เพื่อ Export ข้อมูลและ Schema ออกมาเป็นไฟล์ Backup ในรูปแบบ Custom Format โดยกำหนดตัวเลือก `-Fc`

```bash
pg_dump -h [source_host] -U [username] -Fc -d [db_name] -f nipa_migration.dump
```

รายละเอียด Parameter:

* `source_host`: Hostname หรือ IP Address ของ PostgreSQL ต้นทาง
* `username`: Database User ของ Database เดิม
* `db_name`: ชื่อ Database ที่ต้องการดึงข้อมูลมา
* `-Fc`: กำหนดรูปแบบไฟล์ Dump เป็น Custom Format
* `-f nipa_migration.dump`: กำหนดชื่อไฟล์ Backup ที่ต้องการสร้าง

{% hint style="warning" %}
ห้ามใช้ `pg_dumpall --roles-only` แล้วนำไป Restore เข้า NCS DBaaS โดยตรง เนื่องจากไฟล์ที่ได้จะมีคำสั่ง `ALTER ROLE` ของทุก Role ซึ่งทำให้เกิดการเขียนทับ Role ภายในที่ NCS ใช้จัดการ Instance เช่น postgres หรือ Bootstrap User
{% endhint %}

หลังจากรัน `pg_dump` เสร็จ ให้ตรวจสอบว่าไฟล์ Dump ถูกสร้างขึ้นเรียบร้อย และมีขนาดไฟล์ที่เหมาะสม

```bash
ls -lh nipa_migration.dump
```

**ตัวอย่าง Output:**

```
-rw-rw-r-- 1 nc-user nc-user 2.8M Jul  9 14:37 nipa_migration.dump
```

จากตัวอย่าง Output แปลว่าไฟล์ `nipa_migration.dump` ถูกสร้างสำเร็จ โดยมีขนาด `2.8M` และถูกแก้ไขล่าสุดวันที่ `Jul 9 14:37`

หากไฟล์มีขนาดเล็กผิดปกติ เช่น ไม่กี่ KB หรือเป็น `0 byte` ควรตรวจสอบ Error ระหว่างการ Dump หรือเช็คว่าเลือก Database ถูกต้องหรือไม่

#### กรณีมีหลาย Database ที่ต้องการ Migrate พร้อมกัน

หากมีหลาย Database ที่ต้องการ Migrate ให้ใช้ `pg_dump` แยกทีละ Database เนื่องจากไม่สามารถใช้ `pg_dumpall` ได้

สามารถใช้ Loop เพื่อ Dump หลาย Database ได้ดังนี้:

```bash
for db in customer_db order_db payment_db; do
  pg_dump -h [source_host] -p [source_port] -U [username] -d "$db" -Fc -f "${db}.dump"
done
```

คำสั่งนี้จะวน Dump Database ตามรายชื่อที่กำหนดไว้ในตัวแปร `db` ได้แก่ `customer_db`, `order_db` และ `payment_db`

หลังจากรันสำเร็จ จะได้ไฟล์ Dump แยกตามชื่อ Database เช่น:

```
customer_db.dump
order_db.dump
payment_db.dump
```

ไฟล์ Dump แต่ละไฟล์ต้องนำไป Restore แยกกันตามขั้นตอน Restore โดยต้องสร้าง Database, Schema และ Database User บน NCS ให้ครบทุกชื่อก่อนเริ่ม Restore

#### 2.1 ตรวจสอบไฟล์ Dump ก่อน Restore (Pre-restore Validation)

ก่อนนำไฟล์ Dump ไป Restore จริง ควรตรวจสอบเนื้อหาภายในไฟล์ก่อนว่า Object สำคัญถูก Dump มาครบ เช่น Schema, Extension, Table, View, Function และ Trigger

ใช้คำสั่ง `pg_restore -l` เพื่ออ่านรายการ Object ทั้งหมดภายในไฟล์ Dump โดยคำสั่งนี้จะไม่เขียนข้อมูลใด ๆ ลง Database ปลายทาง จึงปลอดภัยสำหรับการตรวจสอบก่อน Restore จริง

```bash
pg_restore -l nipa_migration.dump > dump_contents.txt
cat dump_contents.txt
```

คำสั่งนี้จะอ่าน Table of Contents จากไฟล์ `nipa_migration.dump` แล้วบันทึกผลลัพธ์ลงไฟล์ `dump_contents.txt` จากนั้นใช้ `cat` เพื่อเปิดดูรายการ Object ทั้งหมด

**ตัวอย่าง Output**&#x20;

```
8; 2615 16510 SCHEMA - analytics sourceuser
2; 3079 16390 EXTENSION - pgcrypto
3; 3079 16427 EXTENSION - uuid-ossp
225; 1259 16511 TABLE analytics daily_revenue sourceuser
221; 1259 16462 TABLE public bookings sourceuser
217; 1259 16438 TABLE public customers sourceuser
223; 1259 16485 TABLE public payments sourceuser
219; 1259 16451 TABLE public rooms sourceuser
224; 1259 16504 VIEW public booking_summary sourceuser
272; 1255 16502 FUNCTION public set_updated_at() sourceuser
3366; 2620 16503 TRIGGER public customers trg_customers_updated_at sourceuser
```

จากตัวอย่าง Output แปลว่าไฟล์ Dump มี Object สำคัญหลายประเภท เช่น:

* Schema ชื่อ `analytics`
* Extension ชื่อ `pgcrypto`
* Extension ชื่อ `uuid-ossp`
* Table ใน Schema `analytics` เช่น `analytics.daily_revenue`
* Table ใน Schema `public` เช่น `bookings`, `customers`, `payments`, `rooms`
* View ชื่อ `public.booking_summary`
* Function ชื่อ `public.set_updated_at()`
* Trigger ชื่อ `trg_customers_updated_at` บน Table `public.customers`

นับจำนวน `TABLE DATA` ที่พบในไฟล์ Dump เพื่อดูว่ามีข้อมูลของกี่ตารางถูก Dump เข้ามา

```bash
grep -c "TABLE DATA" dump_contents.txt
```

ผลลัพธ์ที่ได้ควรนำไปเทียบกับจำนวนตารางจริงที่ต้นทาง โดยต้องนับรวมทุก Schema ไม่ใช่เฉพาะ Schema `public`

หากจำนวน `TABLE DATA` น้อยกว่าที่คาดไว้ อาจเกิดจากบาง Table ไม่มีข้อมูล, User ที่ใช้ Dump ไม่มีสิทธิ์เข้าถึงบาง Schema/Table หรือ Dump มาไม่ครบ

หากไฟล์ Dump มีการใช้ Extension เช่น `pgcrypto` หรือ `uuid-ossp` ตามตัวอย่างข้างต้น ต้องตรวจสอบว่า PostgreSQL ปลายทางบน NCS รองรับ Extension เหล่านี้หรือไม่ก่อน Restore จริง

หาก Extension ไม่พร้อมใช้งาน อาจทำให้ Restore Fail ในขั้นตอน `CREATE EXTENSION`

```bash
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c \
  "SELECT * FROM pg_available_extensions WHERE name IN ('pgcrypto', 'uuid-ossp');"
```

คำสั่งนี้ใช้เชื่อมต่อไปยัง Database ปลายทาง แล้วตรวจสอบว่า Extension ที่ไฟล์ Dump ต้องใช้มีอยู่ในรายการ Extension ที่ PostgreSQL ปลายทางรองรับหรือไม่

**ตัวอย่าง Output:**

```
   name    | default_version | installed_version |                     comment
-----------+-----------------+--------------------+---------------------------------------------------
 uuid-ossp | 1.1             | 1.1                | generate universally unique identifiers (UUIDs)
 pgcrypto  | 1.3             | 1.3                | cryptographic functions
```

จากตัวอย่าง Output แปลว่า PostgreSQL ปลายทางรองรับ Extension ทั้ง `uuid-ossp` และ `pgcrypto`

#### 2.1.1 เก็บ Row Count จาก Source ไว้เป็น Baseline

หลังจากตรวจสอบ Object ในไฟล์ Dump แล้ว ควรเก็บจำนวนแถวของ Table สำคัญจาก Database ต้นทางไว้เป็น Baseline เพื่อใช้เปรียบเทียบกับ Database ปลายทางหลัง Restore เสร็จ

ต้องระบุทุกตารางที่ต้องการตรวจสอบ ที่พบจาก TOC ในไฟล์ Dump

```bash
psql -h [source_host] -U [username] -d [db_name] -c \
  "SELECT 'public.bookings' AS table_name, COUNT(*) FROM public.bookings
   UNION ALL SELECT 'public.customers', COUNT(*) FROM public.customers
   UNION ALL SELECT 'analytics.daily_revenue', COUNT(*) FROM analytics.daily_revenue;"
```

คำสั่งนี้ใช้เชื่อมต่อไปยัง Database ต้นทาง แล้วนับจำนวนแถวของแต่ละ Table ที่ต้องการตรวจสอบ โดยผลลัพธ์จะแสดงชื่อ Table และจำนวน Record ของแต่ละ Table

**ตัวอย่าง Output:**

```
       table_name        | count
--------------------------+-------
 public.customers         | 10000
 public.bookings          | 50000
 analytics.daily_revenue  |    31
(3 rows)
```

ให้บันทึกค่า Row Count เหล่านี้ไว้ เพื่อนำไปเปรียบเทียบกับ Database ปลายทางหลัง Restore เสร็จ หากจำนวนแถวไม่ตรงกัน อาจแปลว่าข้อมูลถูก Restore มาไม่ครบ หรือมีข้อมูลเปลี่ยนแปลงระหว่างช่วงเวลาที่ทำ Dump

#### 2.1.2 เก็บ Checksum จาก Source ไว้เป็น Baseline

นอกจาก Row Count แล้ว ควรเก็บค่า Checksum ของข้อมูลจาก Table สำคัญไว้ด้วย เพื่อใช้ตรวจสอบว่าข้อมูลภายใน Table ตรงกันระหว่าง Source และ Target หรือไม่

PostgreSQL ไม่มีคำสั่ง `CHECKSUM TABLE` แบบ MySQL จึงใช้ `md5(string_agg(...))` แทน โดยแนวคิดคือแปลงข้อมูลใน Table เป็นข้อความ เรียงลำดับข้อมูลให้แน่นอน แล้วคำนวณค่า MD5 ออกมาเป็นค่า Checksum

ควรรันแยกทีละตาราง โดยเฉพาะ Table สำคัญหรือ Table ที่ใช้ยืนยันความถูกต้องของข้อมูล

```bash
psql -h [source_host] -U [username] -d [db_name] -c \
  "SELECT md5(string_agg(t::text, '' ORDER BY t::text)) FROM (SELECT * FROM public.customers) t;"
```

คำสั่งนี้ใช้คำนวณค่า Checksum ของข้อมูลทั้งหมดใน Table `public.customers` จาก Database ต้นทาง โดยใช้ `ORDER BY t::text` เพื่อให้ลำดับข้อมูลคงที่ก่อนนำไปคำนวณค่า MD5

**ตัวอย่าง Output:**

```
               md5
----------------------------------
 5e2c037955c70c88118f65c3a0b3370b
(1 row)
```

ให้บันทึกค่า Checksum นี้ไว้ แล้วหลัง Restore เสร็จให้รันคำสั่งเดียวกันบน Database ปลายทาง เพื่อนำค่า MD5 มาเปรียบเทียบกัน

{% hint style="info" %}
บันทึกค่า Checksum ของทุกตารางไว้ จะใช้เทียบกับปลายทางใน Step 5
{% endhint %}
{% endstep %}

{% step %}

## เตรียม Database ปลายทางบน NCS ก่อน Restore

ก่อนนำเข้า ต้องเตรียม Database ปลายทางบน NCS ให้พร้อม 2 อย่าง:

1. สร้าง Database Schema เปล่า บนหน้า NCS (section Database Schema → CREATE) ให้ชื่อตรงกับต้นทาง [(วิธีการสร้าง Database Schema)](https://docs-epc.gitbook.io/ncs-documents/database-as-a-service/sql-database/manage-sql-database-schema/create-database-schema#id-3.-database-schema#id-3.-database-schema)
2. สร้างหรือผูก Database User เข้ากับ Database Schema นั้น (tab Access Management → CREATE) [(วิธีการสร้าง User ใหม่)](https://docs-epc.gitbook.io/ncs-documents/database-as-a-service/sql-database/manage-sql-database-user/create-database-user) และ [วิธีการแก้ไขสิทธิ์การเข้าถึง Database Schema](https://docs-epc.gitbook.io/ncs-documents/database-as-a-service/sql-database/manage-sql-database-user/edit-access#id-3.-edit-access)

{% hint style="warning" %}
Database Schema ที่สร้างใหม่ผ่านปุ่ม CREATE จะไม่มี Database User ผูกอยู่โดยอัตโนมัติ
{% endhint %}

### 3.1 กรณีต้องการอัปเดตเฉพาะข้อมูลใหม่ โดยไม่แตะข้อมูลเดิม (Insert-Only Sync)

หาก Database ปลายทางมีข้อมูลอยู่แล้วและไม่ต้องการให้ถูกแก้ไขหรือลบ ต้องการเพียงเพิ่มแถวใหม่ที่ยังไม่มีในปลายทาง ให้ Dump แบบระบุเฉพาะข้อมูล (`--data-only`) พร้อมสั่งให้สร้างเป็นคำสั่ง `INSERT` ที่ข้ามแถวซ้ำอัตโนมัติ แทนการ Dump แบบเต็มใน Step 2:

```bash
pg_dump -h [source_host] -U [username] -d [db_name] \
  --data-only \
  --inserts \
  --on-conflict-do-nothing \
  --table=public.customers \
  --table=public.bookings \
  -f nipa_data_update.sql
```

* `--data-only`: ไม่รวมคำสั่ง `CREATE TABLE`/`DROP TABLE` เพื่อไม่ให้กระทบ Schema เดิมที่ปลายทาง
* `--inserts`: สร้างเป็นคำสั่ง `INSERT INTO` ทีละแถว แทนการใช้ `COPY` (จำเป็นสำหรับให้ `--on-conflict-do-nothing` ทำงานได้)
* `--on-conflict-do-nothing`: เติม `ON CONFLICT DO NOTHING` ต่อท้ายทุกคำสั่ง `INSERT` หากแถวชนกับ Primary/Unique Key เดิมที่ปลายทาง จะข้ามแถวนั้นไปโดยไม่ Error และไม่ทับข้อมูลเดิม
* `--table`: ระบุเฉพาะตารางที่ต้องการ Sync ข้อมูล (ใส่ได้หลายตัว)

{% hint style="warning" %}
วิธีนี้ตรวจสอบความซ้ำของข้อมูลจาก Primary Key/Unique Key เท่านั้น หากตารางปลายทางไม่มี Unique Key ที่มีความหมาย ข้อมูลอาจถูกมองว่าเป็นแถวใหม่ทั้งหมดและถูก Insert ซ้ำได้ ควรตรวจสอบ Schema ของตารางก่อนใช้งานเสมอ
{% endhint %}

ตรวจสอบไฟล์ก่อน Import ว่ามีแต่ `INSERT ... ON CONFLICT DO NOTHING` ไม่มี DDL หลุดมา:

```bash
grep -E "DROP TABLE|CREATE TABLE|INSERT INTO" nipa_data_update.sql | head -5
```

Import เข้าปลายทางด้วย Database User ปกติได้เลย (ไม่จำเป็นต้องมีสิทธิ์พิเศษ เพราะไม่มี DDL):

```bash
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -f nipa_data_update.sql
```

{% endstep %}

{% step %}

## นำข้อมูลเข้าสู่ Database บน NCS

```bash
pg_restore -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] --clean --if-exists --no-owner --no-privileges -j 4 nipa_migration.dump
```

* `--no-owner --no-privileges`: ข้าม Owner/Privilege เดิมของต้นทาง เพราะ User บน NCS อาจไม่ตรงกับต้นทาง
* `--clean --if-exists`: ให้ Restore ทับ Database เดิมที่มีข้อมูล/Schema อยู่แล้วได้โดยไม่ต้อง Drop Database เอง

{% hint style="info" %}
กรณี Database ต้นทางมีหลาย Schema คำสั่งนี้ใช้ได้เหมือนเดิมโดยไม่ต้องแก้ไข เพราะ `pg_dump` ดึงทุก Schema มาให้อัตโนมัติเป็นค่าเริ่มต้น
{% endhint %}

ตรวจสอบทันทีหลัง Restore ว่า Object ถูกสร้างครบ:

```bash
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c "\dt public.*"
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c "\dt analytics.*"
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c "\dv"
```

**ตัวอย่าง Output จริง:**

```
            List of tables
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+---------
 public | bookings  | table | db-user
 public | customers | table | db-user
 public | payments  | table | db-user
 public | rooms     | table | db-user
(4 rows)

               List of tables
  Schema   |     Name      | Type  |  Owner
-----------+---------------+-------+---------
 analytics | daily_revenue | table | db-user
(1 row)
```

{% endstep %}

{% step %}

## ตรวจสอบความถูกต้องและตั้งค่าเพิ่มเติม

เทียบ Row Count กับ Baseline ที่เก็บไว้ใน Step 2.1 — ต้องตรงกันทุกตาราง:

```bash
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c \
  "SELECT 'public.customers' AS table_name, COUNT(*) FROM public.customers
   UNION ALL SELECT 'analytics.daily_revenue', COUNT(*) FROM analytics.daily_revenue;"
```

```bash
psql -h [nipa_db_endpoint] -U [nipa_db_user] -d [db_name] -c \
  "SET timezone = 'UTC'; SELECT md5(string_agg(t::text, '' ORDER BY t::text)) FROM (SELECT * FROM public.customers) t;"
```

{% hint style="info" %}
ต้องรัน `SET` และ `SELECT` ในคำสั่งเดียวกันเสมอ เพราะการตั้งค่าด้วย `SET` มีผลแค่ Session/Connection นั้นๆ
{% endhint %}

**ตัวอย่างผลการทดสอบจริง:**

```
ก่อนตั้ง timezone = 'UTC':  customers checksum = c0fd14ebc79c23704fbb8c2b8addbd2a   (ไม่ตรงกับ Baseline)
หลังตั้ง timezone = 'UTC':  customers checksum = 5e2c037955c70c88118f65c3a0b3370b   (ตรงกับ Baseline)
```

{% hint style="warning" %}
หาก Row Count ตรงแต่ Checksum ไม่ตรง อย่าเพิ่งสรุปว่าข้อมูลเสียหาย ให้ตรวจสอบ Timezone ของทั้งสองฝั่งก่อนเป็นอันดับแรก โดยเฉพาะตารางที่มี Column ประเภท `timestamptz`
{% endhint %}

ตรวจสอบเพิ่มเติมอื่นๆ:

* เข้าใช้งาน Database บน NCS ผ่าน CLI (`psql`) เพื่อตรวจสอบจำนวนตารางและข้อมูล เทียบ Row Count กับต้นทางให้ตรงกัน
* ตรวจสอบว่า Extension ที่ต้นทางใช้งาน มีการติดตั้งและ Enable บน Database ใหม่ครบถ้วน
* ตรวจสอบฟีเจอร์ใหม่ (หาก Database Instance สร้างหลัง 01/04/2025) เช่น การสร้าง Replica หรือการจัดการ Monitoring User
  {% endstep %}

{% step %}

## เชื่อมต่อระบบเดิมกับ Database ใหม่บน NCS

เมื่อตรวจสอบความถูกต้องแล้ว ให้ดำเนินการเปลี่ยนถ่ายระบบดังนี้:

* ปิดโหมด Read-Only บน Database ต้นทาง (หากยังต้องใช้งานต่อ) หรือปิดในกรณีที่ทำ Rollback:

```bash
psql -h [source_host] -U [username] -d postgres -c "ALTER SYSTEM SET default_transaction_read_only = off;"
psql -h [source_host] -U [username] -d postgres -c "SELECT pg_reload_conf();"
```

* อัปเดต Connection String ใน Application (Host, Database User, Password) ให้มาชี้ที่ Database ใหม่บน NCS
* ตรวจสอบการทำงานของระบบว่าสามารถ อ่าน-เขียน (Read-Write) ข้อมูลได้ตามปกติ รวมถึงฟีเจอร์ที่พึ่งพา Trigger/Function/View ด้วย
  {% endstep %}
  {% endstepper %}
