จาก CRUD ในไฟล์ สู่ระบบ POS ร้านหมูกระทะ
ทั้ง 5 บทคือโปรเจกต์เดียวที่ค่อย ๆ โตขึ้น: เริ่มจากเก็บข้อมูลใน ArrayList + ไฟล์ → ย้ายไปฐานข้อมูล SQLite ผ่าน JDBC → แยกโค้ดเป็นชั้น (Layer) → ใส่หน้าจอ Swing → ทำ flow ขายจริง (เปิดโต๊ะ / สั่ง / เช็คบิล)
แก่นที่ต้องจำ 5 ข้อ (ถ้าจำอะไรไม่ได้เลย จำอันนี้)
Create · Read · Update · Delete — งานพื้นฐานของทุกระบบข้อมูล ทุกบททำเรื่องนี้ซ้ำในรูปแบบที่ดีขึ้น
แต่ละชั้นมีหน้าที่เดียว แก้ชั้นหนึ่งไม่กระทบชั้นอื่น (UI → Service → DAO → DB)
กฎ 3 ชั้นของ POS — constraint อยู่ DB, "ห้ามเปิดโต๊ะซ้ำ" อยู่ service
?แยกข้อมูลออกจากคำสั่ง SQL กัน SQL injection
หลังแก้ข้อมูลให้อ่านจาก DB ใหม่เสมอ ไม่จำค่าไว้เองในหน้าจอ
GUI รอเหตุการณ์ (กดปุ่ม) แล้วเรียก handler ไม่ได้รันจากบนลงล่างแบบ console
CRUD ผู้ใช้ด้วย Swing + ArrayList + ไฟล์
โปรแกรม User Management: หน้าจอ Swing ที่เพิ่ม/แก้/ลบผู้ใช้ได้ ข้อมูลอยู่ใน ArrayList<User> และบันทึกลงไฟล์ user.dat ด้วย Serialization ยังไม่มีฐานข้อมูล
ที่มา: โค้ด week7 (UserManagement) ในเครื่อง — repo week7-crud บน GitLab เปิดไม่ได้ (private / มีแค่ initial commit)
บทนี้ทำให้เราเรียนรู้
- CRUD คืออะไร และทำ CRUD ครบวงจรบนหน้าจอเดียว
- แยก "ข้อมูล" (Model:
User) ออกจาก "ตัวจัดการข้อมูล" (UserService) และ "หน้าจอ" (UserFrame) - เก็บ object ลงไฟล์ (Serialization) เพื่อให้ปิดโปรแกรมแล้วข้อมูลไม่หาย
- JTable ไม่ได้เก็บข้อมูลเอง — มันถาม TableModel
- เห็นข้อจำกัดของการเก็บในไฟล์ ซึ่งเป็นเหตุผลที่บทถัดไปย้ายไปใช้ฐานข้อมูล
Concept หลัก
CRUD พื้นฐาน
Create เพิ่ม · Read อ่าน · Update แก้ · Delete ลบ ในบทนี้ map เป็นเมธอด addUser, getUsers/getUser, updateUser, deleteUser
Model class + Encapsulation OOP
User มีฟิลด์ private (id, login, name, password, gender, role) เข้าถึงผ่าน getter/setter และมี toString() ไว้พิมพ์ดู
Service แบบ static โครงสร้าง
UserService ถือ static ArrayList และ lastId เพื่อแจก id อัตโนมัติ (lastId++) ใช้ static {} block ใส่ข้อมูลตั้งต้น (admin, user1, user2)
สังเกต: ตอนนี้ service ทำทั้ง "เก็บข้อมูล" และ "อ่าน/เขียนไฟล์" ปนกัน — บท Week 10 จะแยกเป็น DAO
Serialization ไฟล์
คลาสต้อง implements Serializable แล้วใช้ ObjectOutputStream เขียนทั้ง list ลงไฟล์ / ObjectInputStream อ่านกลับ
// writeFile(): เขียนทั้งก้อนทับไฟล์เดิม oos.writeObject(userList); // readFile(): อ่านทั้งก้อนกลับมา userList = (ArrayList<User>) ois.readObject();
JTable + TableModel Swing
JTable เป็นแค่ "หน้าตา" มันจะถาม model ว่ามีกี่แถว (getRowCount) กี่คอลัมน์ (getColumnCount) และช่องนี้แสดงอะไร (getValueAt) — บทนี้ใช้ AbstractTableModel ที่ดึงจาก list โดยตรง
โหมดฟอร์ม: เพิ่ม vs แก้ไข UI logic
ปุ่ม Add เปิดฟอร์มว่าง / ปุ่ม Edit เอาแถวที่เลือกมาเติมฟอร์ม (fillForm) ปุ่ม Save ต้องรู้ว่ากำลังเพิ่มหรือแก้ เพื่อเรียก add หรือ update และใช้ enabledForm(true/false) เปิด–ปิดช่องกรอก
แบบทดสอบท้ายบท
SQLite + Letos + Java JDBC พื้นฐาน
สร้างฐานข้อมูลไฟล์เดียว moogata-basic.db มีตาราง menu_category ตารางเดียว ทำ CRUD ใน Letos ก่อน แล้วทำคำสั่งเดิมผ่าน Java JDBC — เขียน 2 แบบ: try-catch-finally และ try-with-resources
ที่มา: mookata_jdbc.md, jdbc-sql-sequence.md, resource-management-comparison.md · โค้ดของเรา: repo week9 (คลาส ...Finally)
บทนี้ทำให้เราเรียนรู้
- SQLite คือฐานข้อมูลในไฟล์เดียว — Letos กับ Java เปิด "ไฟล์เดียวกัน" ไม่ได้คุยกันเอง
- SQL พื้นฐาน: CREATE TABLE (DDL) และ SELECT/INSERT/UPDATE/DELETE (DML)
- ลำดับการใช้ JDBC 6 ขั้น และความต่างของ
executeQueryvsexecuteUpdate - ResultSet อ่านทีละแถวด้วย cursor (
next()) - การปิด resource ให้ถูกต้อง (finally vs try-with-resources)
Concept หลัก
SQLite = Embedded Database DB
ทั้งฐานข้อมูลอยู่ในไฟล์ .db เดียว ไม่ต้องมี server / user / รหัสผ่าน (ต่างจาก MySQL) Letos (ชื่อเดิม SQLiteStudio) คือโปรแกรมเปิดดู/แก้ไฟล์นั้น
Letos → moogata-basic.db ← SQLite JDBC ← Java
Constraint ในตาราง DB
PRIMARY KEY ระบุแถวไม่ซ้ำ · AUTOINCREMENT ให้ DB แจกเลขเอง · NOT NULL ห้ามว่าง · UNIQUE ห้ามซ้ำ
CREATE TABLE menu_category (
category_id INTEGER PRIMARY KEY AUTOINCREMENT,
category_name TEXT NOT NULL UNIQUE
);DDL vs DML SQL
DDL = คำสั่งโครงสร้าง (CREATE TABLE) · DML = คำสั่งข้อมูล (SELECT / INSERT / UPDATE / DELETE)
WHERE สำคัญมาก SQL
UPDATE/DELETE ที่ไม่มี WHERE จะกระทบ ทุกแถว และเลข id ที่ลบแล้วไม่จำเป็นต้องถูกใช้ซ้ำ จึงไม่ควรเดาเลข id ถัดไป
JDBC + Driver Java
JDBC คือ API มาตรฐานของ Java ที่ใช้คุยกับฐานข้อมูล ตัวแปลจริงคือ Driver (sqlite-jdbc) ที่ Maven โหลดให้จาก pom.xml — ไม่ต้องเพิ่ม JAR เอง
executeQuery vs executeUpdate Java
executeQuery() ใช้กับ SELECT ได้ ResultSet · executeUpdate() ใช้กับ INSERT/UPDATE/DELETE ได้ int = จำนวนแถวที่เปลี่ยน (ไม่ใช่ id ใหม่ — ถ้าอยากได้ id ใช้ getGeneratedKeys())
ได้ 0 แถว ≠ SQL ผิด (SQL ผิดจะเกิด SQLException)
PreparedStatement + ? ความปลอดภัย
? คือตำแหน่งของ ค่า ใส่ด้วย setString(1, …) (นับจาก 1) ไม่ต้องใส่ quote รอบ ? และใช้แทนชื่อตาราง/คอลัมน์ไม่ได้
Working directory กับดัก
URL jdbc:sqlite:moogata-basic.db เป็น path สัมพัทธ์ → หาไฟล์จากโฟลเดอร์ที่รันโปรแกรม ถ้าวางผิดที่ SQLite จะ สร้างไฟล์เปล่าใหม่ แล้วเจอ no such table
ResultSet เริ่มอยู่ ก่อนแถวแรก ต้องเรียก next() ก่อนถึงจะอ่าน getInt/getString ได้
| category_id | category_name |
|---|
ปิด resource: 2 แบบ
try-catch-finally แบบหลักของคาบ
1) ประกาศตัวแปรเป็น null ก่อน try 2) เปิดใน try 3) finally ตรวจ != null แล้วปิดย้อนลำดับ ResultSet → Statement → Connection 4) แต่ละ close มี try/catch ของตัวเอง 5) ห้าม return ใน finally
Connection conn = null; PreparedStatement st = null; try { conn = DriverManager.getConnection(url); ... } catch (SQLException e) { ... } finally { if (st != null) try { st.close(); } catch ... }
try-with-resources แบบย่อ
ประกาศ resource ในวงเล็บ try (...) Java ปิดให้อัตโนมัติ ย้อนลำดับ ถ้าตอนปิดพลาดด้วย error จะถูกเก็บเป็น suppressed exception
try (Connection c = DriverManager.getConnection(url); PreparedStatement st = c.prepareStatement(sql)) { ... } catch (SQLException e) { ... }
close() เอง และ finally ไม่รับประกันว่าจะรันถ้า JVM ถูกยุติ (System.exit()) · ถ้า Letos ค้าง transaction (ยังไม่ Commit) Java จะเจอ database is lockedjdbc:sqlite:moogata-basic.db เป็น path สัมพัทธ์ ถ้าหาไฟล์ไม่เจอ SQLite จะสร้างไฟล์เปล่าให้ (ตอนเชื่อมต่อจึงไม่ error) แต่จะไปพังตอน queryแบบทดสอบท้ายบท
Layer Pattern + DAO + Service (ล็อกอิน)
ฐานข้อมูลร้าน BONUS SUKI 3 ตาราง (menu_category, menu_item, employee) แล้วแบ่งโค้ดเป็นชั้น: helper → model → dao → service ทำทีละ "ช่วง" 7 ช่วง แต่ละช่วงจบด้วย commit
ที่มา: Lab 8 README.md · โค้ดของเรา: repo week (commit ช่วง 1–7)
บทนี้ทำให้เราเรียนรู้
- ทำไมต้องแบ่งเลเยอร์ และแต่ละชั้นรับผิดชอบอะไร
- ออกแบบตารางมีความสัมพันธ์: PK, FK, 1:N, constraint, normalization
- OOP ที่ใช้จริง: encapsulation, constructor overloading, static, interface, generics
- Pattern: Layered, DAO, Singleton/shared connection, Row mapping
- วางกฎธุรกิจ (login) ไว้ที่ service ไม่ใช่ที่ DAO หรือ UI
login() แล้วดูว่าวิ่งผ่านชั้นไหนบ้าง interactiveแต่ละชั้นทำอะไร
helper / DatabaseHelper ช่วง 3
ศูนย์กลาง Connection: ฟิลด์ static Connection conn ใช้ร่วมทั้งแอป, constructor เป็น private (ห้าม new), getConnect() สร้างครั้งแรกครั้งเดียว และเปิด PRAGMA foreign_keys = ON
synchronized กันสองเธรดสร้าง connection ซ้อนกัน (race condition) — แต่ไม่ได้คุม SQL ที่ DAO รันหลังจากนั้น
model / Employee ช่วง 5
"กล่องข้อมูล" พนักงาน 1 คน ฟิลด์ private + getter/setter, constructor 3 แบบ (ครบ / ไม่มี id / เปล่า) = overloading และ fromRS(ResultSet) เป็นจุดเดียวที่แปลงแถว → object (Row mapping)
dao / Dao<T> + EmployeeDao ช่วง 6
Dao<T> คือ interface = สัญญาว่าทุกตารางต้องมี get, getAll, save, update, delete ส่วน EmployeeDao implements Dao<Employee> คือคนเขียน SQL จริง
public interface Dao<T> { T get(int id); List<T> getAll(); T save(T obj); T update(T obj); int delete(T obj); }
service / EmployeeService ช่วง 7
"เคาน์เตอร์" ที่ UI จะเรียก ใส่ กฎธุรกิจ เช่น login: ขอข้อมูลจาก DAO แล้วตัดสินเอง
Employee e = employeeDao.getByName(name); if (e != null && e.getPassword().equals(pw)) return e; return null;
<T> ทำให้สัญญาเดียวใช้ได้กับทุกโมเดล (generics) และ service เรียกเมธอดชื่อเดิมได้ไม่ว่าจะเป็นตารางไหน (polymorphism)ทฤษฎีที่ใช้
PK / FK / 1:N DB
menu_item.category_id ชี้ไป menu_category (หมวด 1 มีเมนูได้หลายรายการ) · ON DELETE RESTRICT = ห้ามลบหมวดที่ยังมีเมนูอ้างอิง → เจอ FOREIGN KEY constraint failed ไม่ใช่บั๊ก
Normalization DB
แยกข้อมูลที่ซ้ำหลายแถวไปไว้ตารางใหม่ — ชื่อหมวดเก็บที่เดียวใน menu_category แก้ที่เดียวถูกทั้งระบบ
Interface + Polymorphism + Generics OOP
<T> ทำให้เขียนสัญญาครั้งเดียวใช้กับทุกโมเดล: Dao<Employee>, Dao<Menu> · @Override ให้ compiler ฟ้องถ้าพิมพ์ชื่อเมธอดผิด
static OOP
ของคลาส ไม่ผูกกับ object — เรียก DatabaseHelper.getConnect() ได้เลยไม่ต้อง new · ส่วน service ต้อง new EmployeeDao() เพราะเรียก instance method
ใครเป็นเจ้าของ Connection resource
DAO ยืม connection จาก helper จึงปิดแค่ Statement/ResultSet ของตัวเอง ส่วน main ปิด connection ร่วมครั้งเดียวตอนจบ (DatabaseHelper.close())
รหัสผ่าน plain text ความปลอดภัย
lab เทียบด้วย equals เพื่อฝึกเท่านั้น ระบบจริงต้องเก็บ hash (เช่น bcrypt + salt) แล้วเทียบ hash
2) ทำไม
menu_item เก็บแค่ category_id? → normalization แก้ชื่อหมวดที่เดียว3)
getAll(where, order) อันตรายยังไง? → ต่อ string ตรง ๆ ถ้าเอาค่าที่ผู้ใช้กรอกมาใส่ = SQL injection4) กฎ "มา 4 จ่าย 3" ควรอยู่ชั้นไหน? → service (กฎธุรกิจ)
แบบทดสอบท้ายบท
หน้าจอจัดการพนักงานด้วย Java Swing + Login
เพิ่มชั้น UI บนหลังบ้านเดิม (DB / DAO / Service) ทำ 2 แบบที่หลังบ้านเหมือนกัน: MoogataLab ฟอร์มรวม (ฟอร์มบน ตารางล่าง) และ MoogataLabDialog (หน้าหลักมีตาราง ฟอร์มเปิดเป็น dialog)
ที่มา: lab11-88b208.gitlab.io
บทนี้ทำให้เราเรียนรู้
- โปรแกรม GUI เป็น event-driven และทำงานบน EDT
- ใช้ component หลักของ Swing และตั้งชื่อ control ให้เป็นระบบ
- JTable + DefaultTableModel: แยกหน้าตากับข้อมูล และรีเฟรชด้วยการอ่าน DB ใหม่
- หน้าจอคุยกับ service เท่านั้น ห้ามแตะ SQL
- กฎสิทธิ์ (role) และ trade-off ระหว่างฟอร์มรวมกับ dialog
ทฤษฎี 7 เรื่อง
Swing GUI
อยู่ใน javax.swing.* มากับ JDK ไม่ต้องเพิ่ม dependency ใน pom.xml · ใน NetBeans ห้ามเลือก Absolute Layout
Container กับ Component GUI
JFrame → contentPane → ปุ่ม / ช่องกรอก / JScrollPane → JTable (JTable ต้องอยู่ใน JScrollPane ถึงจะเห็นหัวตารางและเลื่อนได้)
Event-driven GUI
โปรแกรมรอเหตุการณ์: กดปุ่ม → ActionEvent → ActionListener → เมธอด handler → กลับไปรอ · main แค่เปิดหน้าต่าง
EDT (Event Dispatch Thread) GUI
การวาดและ event ทั้งหมดทำบนเธรดเดียว เปิดหน้าต่างด้วย SwingUtilities.invokeLater และห้ามทำงานหนักบน EDT (หน้าจอจะค้าง)
Modal vs Non-modal Dialog
super(owner, title, true) → dialog ล็อกหน้าหลัก ต้องปิด dialog ก่อนถึงจะกลับไปกดหน้าหลักได้
JTable + DefaultTableModel GUI
JTable = หน้าตา, Model = ข้อมูล · รีเฟรชแบบ "ล้างแล้วเติมใหม่ทั้งชุด" จาก DB (showEmployees()) และตั้งให้ตารางแก้ไขในช่องไม่ได้
JOptionPane GUI
showMessageDialog (ERROR / WARNING) แจ้งผล · showConfirmDialog (YES_NO) ถามยืนยันก่อนลบ
ตั้งชื่อ control convention
ขึ้นต้นตามชนิด: lblLabel, txtTextField, btnButton, tblTable, cmbComboBox, scrScrollPane · รหัสผ่านอ่านด้วย new String(txtPassword.getPassword())
บัญชีจำลอง: manager (role 1 ผู้จัดการ) · cashier (role 2 พนักงาน) รหัส 1234
ฟอร์มรวม vs Dialog (trade-off)
editingId · ขวา: หน้าหลักมีแค่ตาราง ฟอร์มแยกไปอยู่ใน dialog ที่ล็อกหน้าหลักไว้| id | ชื่อ | role |
|---|
ลองกด btnAdd แล้วคลิกตารางด้านหลัง (modal จะไม่ยอม) · เอาติ๊ก modal ออกแล้วลองใหม่ · ลองบันทึกชื่อว่าง / ชื่อซ้ำ / ยกเลิก
| ส่วนที่ 1 · ฟอร์มรวม (MoogataLab) | ส่วนที่ 2 · Dialog (MoogataLabDialog) | |
|---|---|---|
| หน้าตา | ฟอร์มด้านบน ตารางด้านล่าง ในหน้าเดียว | หน้าหลักมีแค่ตาราง + ปุ่ม เพิ่ม/แก้ไข/ลบ/รีเฟรช |
| แยกโหมดเพิ่ม/แก้ | ใช้ตัวแปร editingId ในหน้าหลัก (มีสถานะซ่อนอยู่) | dialog มี 2 constructor (เพิ่ม / แก้ไข) — หน้าหลักไม่มีสถานะซ่อน |
| ส่งผลกลับ | บันทึกแล้วรีเฟรชตารางทันที | dialog บอกผลผ่าน isSaved() → true ค่อยรีเฟรช, กดยกเลิกตารางไม่เปลี่ยน |
| บันทึกผิด | แจ้ง error บนหน้าเดิม | แจ้ง error และ ไม่ปิด dialog |
แบบทดสอบท้ายบท
เปิดโต๊ะ · สั่งอาหาร · เช็คบิล
ร้าน BONUS SUKI: บิลมีชีวิต (เกิดตอนเปิดโต๊ะ จบตอนเช็คบิล) และ โต๊ะมีสถานะ (ว่าง → ใช้งาน → ว่าง) เพิ่มตาราง dining_table, bill, bill_item และหน้าจอ 3 หน้า: PosFrame, OrderFrame, CheckoutDialog
ที่มา: pos-flow-2dfbeb.gitlab.io
บทนี้ทำให้เราเรียนรู้
- สถานะต้องเปลี่ยนตาม กฎ (state machine) ไม่ใช่ใครจะตั้งค่าก็ได้
- สั่งหลายรอบ = INSERT ทีละแถวลง
bill_item - Snapshot ปกป้องบิลเก่าจากการแก้ราคาเมนูทีหลัง
- กฎ 3 ชั้น: DB คุมโครงสร้าง, service คุมกฎธุรกิจ, UI คุมการแสดงผล
- Transaction (ACID) — และรู้ว่าโค้ด lab ยังไม่ได้ห่อ transaction (ช่องโหว่ที่ตั้งใจให้เห็น)
State diagram ด้านบนซิงก์กับร้านจำลอง: เลือกโต๊ะแล้วกรอบสีเน้นคือสถานะตอนนี้ · เปิดโต๊ะ/เช็คบิลแล้วลูกศรจะกะพริบ · เส้นประคือส่วนที่เป็นแบบฝึกหัด
ทฤษฎี 9 เรื่อง
State machine กฎสถานะ
สถานะเปลี่ยนได้เฉพาะตามเหตุการณ์ที่อนุญาต โต๊ะ: ว่าง → ใช้งาน → (รอชำระ) → ว่าง · บิล: เปิด → (รอชำระ) → ปิด · บางเหตุการณ์เปลี่ยน 2 ตารางพร้อมกัน (เปิดโต๊ะ = สร้างบิล + โต๊ะเป็นใช้งาน)
Snapshot ข้อมูล
bill_item เก็บ ชื่อและราคา ณ ตอนสั่ง ไว้เอง ไม่ดึงจาก menu_item ทุกครั้ง → แก้ราคาเมนูทีหลัง บิลเก่าไม่เปลี่ยน
Transaction (ACID) ความถูกต้อง
การกระทำที่ต้องเกิดคู่กัน (เปิดโต๊ะ, เช็คบิล) ต้องสำเร็จทั้งหมดหรือย้อนทั้งหมด · lab ยังไม่ห่อ — แบบฝึกหัดให้แก้ด้วย setAutoCommit(false) → commit() / rollback()
PK / FK / 1:N + constraint DB
โต๊ะ 1 → บิลหลายใบ · บิล 1 → รายการหลายแถว · เมนู 1 → ถูกสั่งได้หลายครั้ง · พนักงาน 1 → เปิดบิลได้หลายใบ · ใช้ NOT NULL, UNIQUE, CHECK, DEFAULT
DAO + PreparedStatement ชั้นข้อมูล
DAO 4 ตัวตามแบบ EmployeeDao เมธอดเด่น: updateStatus, getOpenByTableId, close, save, findByBillId · SQL อยู่ที่ DAO ชั้นเดียว
กฎ 3 ชั้น สถาปัตยกรรม
DB คุม โครงสร้าง (FK, CHECK) · Service คุม กฎธุรกิจ (SC-4 ห้ามเปิดโต๊ะที่ใช้งานอยู่, จำนวนคน ≥ 1) · UI คุม การแสดงผล · ทดลองแล้วว่า SQL ล้วน ๆ ไม่กันการเปิดโต๊ะซ้ำ จึงต้องมีกฎที่ service
Swing event-driven UI
refreshTables() ระบายสีปุ่มโต๊ะจากข้อมูลจริง · tableClicked() แยกทางตามสถานะ · JTable เป็นหน้าตา ข้อมูลอยู่ที่ model · DocumentListener คิดเงินทอนทันทีที่พิมพ์
เงินกับ double กับดัก
double เก็บทศนิยมฐาน 10 ได้ไม่ตรง (0.1 + 0.2 = ) ต้องจัดรูปแบบ 2 ตำแหน่งตอนแสดง (ระบบจริงใช้ BigDecimal หรือเก็บเป็นสตางค์) · Single source of truth: อ่านยอดจาก DB ใหม่ทุกครั้ง ไม่จำเอง
กฎตั้งชื่อ control ใน NetBeans convention
lbl/txt/btn/tbl/scr · JTable ต้องอยู่ใน JScrollPane · ดับเบิลคลิกปุ่มเพื่อผูก event · ข้อมูลที่เปลี่ยนได้ให้เติมตอนรัน ไม่พิมพ์ใน Properties · คลาส ThaiFont กันภาษาไทยเป็นกล่อง □□□
แผนผังข้อมูล (ERD)
bill_item คือ snapshot (คัดลอกชื่อและราคามาเก็บเอง) · ตารางใหม่คือ dining_table, bill, bill_item3 หน้าจอ + กฎเบื้องหลัง
| หน้าจอ | หน้าที่ | กฎ |
|---|---|---|
| PosFrame | ปุ่ม 6 โต๊ะ สีตามสถานะ กดโต๊ะว่างเพื่อเปิด | SC-4 ห้ามเปิดโต๊ะที่ใช้งานอยู่ — อยู่ใน TableService |
| OrderFrame | เมนูซ้าย บิลขวา สั่งได้หลายรอบ บันทึกลง DB ทุกครั้ง | บิลปิดแล้วสั่งเพิ่มไม่ได้ · ราคาเป็น snapshot · ปิดโปรแกรมกลางบิล รายการไม่หาย |
| CheckoutDialog | modal dialog ใส่เงินรับ เห็นเงินทอนทันที | ปุ่มยืนยันล็อกจนเงินพอ · ปิดบิล + คืนโต๊ะในจุดเดียว (BillService.checkout) · บิลว่างเช็คไม่ได้ |
TableService (ตรวจกฎ) → BillDao + TableDao → ฐานข้อมูล → รีเฟรชหน้าจอSequence เช็คบิล: พิมพ์เงิน → คิดทอนสด → ยืนยัน →
BillService ปิดบิลและคืนโต๊ะ → แจ้งผลแบบทดสอบท้ายบท
ข้อสอบรวม 50 ข้อ
ปรนัย 4 ตัวเลือกครอบคลุมทั้ง 5 บท สุ่มลำดับข้อและตัวเลือกทุกครั้ง ทำไปแล้วออกไปอ่านบทอื่นได้ คำตอบถูกบันทึกไว้ กลับมาทำต่อได้จากปุ่มมุมขวาล่าง
Flashcards
กดการ์ดเพื่อพลิกดูคำอธิบาย เลือกบทหรือสุ่มลำดับได้
หน้านี้ใช้สีแบบไหน ทำไมอ่านสบายตา
โทน "กระดาษอุ่น" สำหรับธีมสว่าง และ "ถ่านอุ่น" สำหรับธีมมืด ใช้สีเน้นสีเดียว (teal หม่น) ที่เหลือเป็นเทาอมน้ำตาล ค่าด้านล่างอ่านจากธีมที่ใช้อยู่ตอนนี้ ลองสลับธีมที่แถบซ้ายแล้วดูตัวเลขเปลี่ยน
Palette ()
Contrast ratio (WCAG)
ตัวหนังสือทั่วไปควรได้อย่างน้อย 4.5:1 (AA) · 7:1 ขึ้นไปเป็นระดับ AAA
หลักที่ใช้
ไม่ใช้ขาวสนิท / ดำสนิท
พื้นขาว #FFF กับตัวดำ #000 ตัดกันแรงเกินไป ทำให้ตาล้าเวลาอ่านนาน ใช้ขาวนวลแบบกระดาษ และตัวอักษรเทาอมน้ำตาลเข้มแทน ธีมมืดก็ใช้เทาถ่าน ไม่ใช่ดำ เพื่อลดอาการตัวหนังสือ "เรือง" (halation)
สีเน้นสีเดียว
teal หม่นใช้กับลิงก์ ปุ่มหลัก และสิ่งที่กำลังเลือกเท่านั้น สายตาเลยรู้ทันทีว่าอะไรสำคัญ ส่วนสีเขียว/ส้ม/แดงเก็บไว้สื่อ "สถานะ" (ถูก / เตือน / ผิด, โต๊ะว่าง / ใช้งาน) อย่างเดียว
ระยะบรรทัดเผื่อภาษาไทย
line-height 1.8 เพราะภาษาไทยมีสระบน–ล่างและวรรณยุกต์ซ้อน ถ้าบรรทัดชิดจะเบียดกัน และจำกัดความกว้างย่อหน้าราว 70 ตัวอักษร ให้ตาไม่ต้องกวาดไกล
ฟอนต์
IBM Plex Sans Thai (หัวกลมอ่านง่ายบนจอ) ถ้าออฟไลน์จะใช้ Sukhumvit Set / Noto Sans Thai ของเครื่องแทน · โค้ดใช้ JetBrains Mono แยก 0/O, 1/l ชัด