7.7 · Page deep dive — Login, Slip, Leave
บทนี้เจาะลึก 3 หน้าที่ใช้บ่อยที่สุด ทั้งในแง่ traffic และในแง่ความซับซ้อน เพื่อให้เห็นว่า pattern ในบท 6.6 ถูกใช้จริงอย่างไร
ถ้าจะแก้หน้าอื่น ให้ดู 3 ตัวอย่างนี้เป็นไข่แก้ว
หน้าที่ 1 — Login
src/pages/user/Login/index.js:1-650 — เป็นหน้าที่ซับซ้อนที่สุดใน repo เพราะมีหลายขั้นตอน (verify username → captcha → password → SSO check) และมีหลาย state ที่ต้อง sync
โครงสร้าง
class Login extends React.PureComponent {
state = {
check: "login", // 'login' | 'verify' | 'success'
checkVerifyPassword: null,
visibleCheckusername: false,
username: "",
checkCaptcha: false,
valueRecaptCha: '',
showModalErrors: false,
showCaptcha: false,
organization: getOrganization(),
showModalVersion: null,
showModalCheck:false,
modalRead:false,
modalLine: false,
loadingFullPage: false,
showTextModal: "",
btnTextModal: "",
showFooter: true,
showModalErrorNameEng: false
}Flow หลัก — 2 ขั้น
ผู้ใช้เข้าหน้า /login
│
▼
ขั้นที่ 1: ใส่ username + captcha
(component: BeforeLoginForm)
│
│ onSubmitCheck() (บรรทัด 203)
│ ├─ เรียก axios POST /SSOService/checkEmpLogin (บรรทัด 223)
│ └─ ถ้า SSO verify ผ่าน → เรียก Queries.verifyEmpCodeLogin (บรรทัด 235)
▼
ขั้นที่ 2: ใส่ password
(component: LoginForm)
│
│ onSubmit(password, remember) (บรรทัด 91)
│ ├─ เรียก Mutations.login (บรรทัด 99)
│ ├─ เก็บ token ผ่าน setToken() (บรรทัด 127)
│ ├─ เก็บ sessionID, appUsername (บรรทัด 128-129)
│ ├─ เรียก fetchProfile() (บรรทัด 133)
│ ├─ ส่ง event log (บรรทัด 142)
│ └─ redirect ไป /course (ถ้า IsTrial) หรือ /news (บรรทัด 144-148)
▼
เข้าสู่ระบบสำเร็จสิ่งที่ต้องระวัง
1. เรียกผ่าน axios ก่อน GraphQL (บรรทัด 223-229)
ขั้นที่ 1 ไม่ได้เรียก GraphQL โดยตรง แต่เรียก ${checkLinkURL()}SSOService/checkEmpLogin ผ่าน axios ก่อน — ถ้า SSO ตอบ isVerify == "success" แล้วค่อยเรียก GraphQL verifyEmpCodeLogin
ถ้า SSO ล่ม ผู้ใช้จะเจอ error แบบ network error (ไม่ใช่ GraphQL error) ทำให้ errorLink ไม่จับ
2. เก็บ password ลง localStorage แบบ AES (บรรทัด 124-125)
userNameEncrypt(this.state.username)
passwordEncrypt(password.password)passwordEncrypt ใน utils/auth.js:37-47 ใช้ CryptoJS.AES.encrypt ด้วย key และ IV ที่ hardcoded เดียวกันทุกรอบ ("6A45soD%9eB#") — ทุกคนที่เปิด bundle จะเห็น key นี้ได้ จึงไม่ใช่การเข้ารหัสที่ปลอดภัยจริง
เป็นฟีเจอร์ "จดจำรหัสผ่าน" ที่ใช้ภายใน browser เท่านั้น — รายละเอียดใน บท 6.9
3. ใช้ React.PureComponent ไม่ใช่ Component
Login เป็น component เดียวที่ใช้ PureComponent — ทำ shallow compare ของ state/props ก่อน re-render ถ้าจะเพิ่ม state ที่เป็น object ซ้อน ต้องระวังว่า mutate ตรง ๆ จะไม่ trigger re-render
4. มีหลาย modal ที่ทับซ้อน
state มี flag แยกสำหรับ modal หลายตัว: showModalErrors, showModalVersion, showModalCheck, modalRead, modalLine, showModalErrorNameEng — ถ้า modal เปิดพร้อมกันผิดทาง อาจเป็นเพราะ flag ทับซ้อน ตรวจที่ละตัว
5. RSA public key hardcoded (บรรทัด 173-181)
sshEncrypt() ใช้ RSA public key ที่ฝังใน source code เพื่อเข้ารหัส csId (client session id) ก่อนส่งไป server — เป็นการป้องกัน replay attack แบบง่าย ๆ
หน้าที่ 2 — Slip (สลิปเงินเดือน)
src/pages/user/Slip/index.js:1-1528 — ดูสลิปเงินเดือนรายเดือน + เลือกปี/เดือน + กรอกเลขบัตรประชาชนเพื่อยืนยัน
โครงสร้าง state
class Slip extends Component {
state = {
detailSlip: null, // ข้อมูลสลิปจาก fetchSlip
visibleModal: false,
selectMonth: '',
selectYear: moment().year(),
visibleModalCardNo: false, // modal กรอกเลขบัตร
ModalCardNoPreLoad: false,
CardNoOk: false,
CardNoValue: null,
CardNoFetch: '',
preLoad: true,
preLoadDataSlip: true,
showModalErrors: false,
textPayrollSlip: null,
detailSlipTax: null,
organizationShow: false,
selectOrganization: [],
profileKey: [],
isCanAccessIncomeDocOnly: false,
organizationName: null,
tax6Month: null,
payDate: null,
listUrlTax: [],
textMonthOrYear: 'เดือน',
ListUrlTaxKey: '',
switchShowMonthOrSalarCert: 'Month',
detaSalaryCert: null,
dropDownSalaryCert: [],
linkPDFSalary: null,
// ... อีกหลาย fields
}Flow หลัก
componentDidMount()
│
│ fetchProfile() (บรรทัด 72)
│ ├─ Queries.fetchProfile
│ ├─ เก็บ profileKey + employeeCodes[0]
│ └─ เก็บ organizationName จาก workProfile
▼
ผู้ใช้เลือกเดือน + ปี ใน dropdown
│
│ เปิด modal ให้กรอกเลขบัตรประชาชน
│ (visibleModalCardNo: true)
▼
ผู้ใช้กรอกเลขบัตร + กดยืนยัน
│
│ fetchSlip(monthValue) (บรรทัด 103)
│ ├─ Queries.fetchSlip พร้อม variables
│ │ { month, year, identityCard, profileKey }
│ ├─ ถ้าสำเร็จ → setState detailSlip
│ ├─ ถ้า 'Identity Card is incorrect' → แสดง modal ใหม่
│ └─ ถ้า 'PASSWORD_DOESNT_MATCH' → เปิด modal ใหม่
▼
แสดงสลิป (รายรับ-รายจ่าย-ภาษี)
│
│ ปุ่มดาวน์โหลด PDF → Queries.fetchUrlSlipสิ่งที่ต้องระวัง
1. profileKey ต้องมีก่อนเรียก fetchSlip
fetchSlip ส่ง profileKey ไปด้วย — ถ้า fetchProfile ยังไม่ return profileKey จะเป็น empty array ทำให้ query fail
ป้องกันโดยเช็ค preLoadDataSlip หรือ disable ปุ่มจนกว่า profile จะโหลดเสร็จ
2. มีหลาย tab (Slip, Tax, Salary Cert)
switchShowMonthOrSalarCert: 'Month',
detaSalaryCert: null,
dropDownSalaryCert: [],state switchShowMonthOrSalarCert สลับระหว่าง 'Month' (slip), 'SalaryCert' (หนังสือรับรองเงินเดือน) — ใช้ tab เดียวกันแต่ข้อมูลคนละอย่าง ต้องระวังอย่า query ผิดตอนสลับ tab
3. การแสดงผลเป็น B.E. ทั้งหมด
ปีใน dropdown และบนหน้าจอใช้ moment().year() ซึ่งเป็น C.E. — แต่ตอนแสดงผลแปลงเป็น B.E. ผ่าน changeYear() ใน utils/formatdate.js
ถ้าจะแก้ปีที่แสดง ต้องแก้ที่ helper changeYear หรือ changeFormatdateSlip — อย่า +543 ตรง ๆ ใน component
4. มี console.log กระจายอยู่
บรรทัด 119, 141, 148, 161, 169 — เป็น debug log ที่เหลือจากการพัฒนา แต่เนื่องจาก App/index.js:15 ปิด console ทั้งหมดไว้ จึงไม่แสดงใน production
หน้าที่ 3 — Leave Form (ยื่นใบลา)
src/pages/user/Leave/Form/index.js:1-2410 — เป็น page ที่ใหญ่ที่สุดใน repo มีหลาย tab + form validation + multi-step submit
โครงสร้าง
class Withdraw extends Component {
state = {
loading: false,
dataTalbeUploads: [ // ตารางไฟล์แนบ
{
documentType: null,
description: null,
attachFile: null,
statusFile: false
},
],
columns: [ // column spec ของตาราง
{
title: 'ประเภทเอกสาร',
dataIndex: 'documentType',
// ... render function ที่ใช้ getFieldDecorator
},
// ...
],
// ... อีกหลายสิบ fields
}Flow หลัก
componentDidMount()
│
│ โหลดข้อมูล: profile, leaveType dropdown, leaveDay, workingDay
▼
ผู้ใช้กรอก form (หลาย field)
│
│ ตรวจสอบทีละ field ผ่าน getFieldDecorator
│ ├─ leaveType (required)
│ ├─ startDate, endDate
│ ├─ เหตุผลการลา
│ ├─ approver (เลือกจาก dropdown)
│ └─ CC email (เพิ่มได้หลาย)
▼
ผู้ใช้กด "ยื่นเรื่องลา" หรือ "บันทึกร่าง"
│
│ handleSubmit(e) (บรรทัด 1291)
│ ├─ this.props.form.validateFields(callback)
│ ├─ ถ้า validation ผ่าน → ส่ง mutation
│ │ ├─ Mutations.createLeaveDocument (บรรทัด 1170)
│ │ │ สำหรับ 'DRAFT' (บันทึกร่าง)
│ │ └─ Mutations.updateLeaveDocument
│ │ สำหรับ 'SUBMIT' (ยื่นเรื่องจริง)
│ └─ ถ้าสำเร็จ → redirect ไป /leave/search
▼
กลับไปหน้า listสิ่งที่ต้องระวัง
1. ใช้ dynamic field name ในตาราง
this.props.form.getFieldDecorator("documentType"+index, {ชื่อ field เป็น "documentType" + index — ถ้าเพิ่ม/ลบ row ต้องเช็คว่าลำดับยังตรง ไม่งั้น validation จะผิด
2. มี 2 ปุ่ม submit ใช้ handler ตัวเดียวกัน
<Button
type="primary"
className="mx-2 btn-white-red"
htmlType="submit"
onClick={ async () => {
await this.setState({
// ...
})
await this.handleSubmit
}}
disabled={ preLoadOnsubmit }
>
บันทึกร่าง
</Button>
<Button
type="primary"
className="ml-2 color-white"
// ...
await this.handleSubmit
}}
>
ยื่นเรื่องลา
</Button>ทั้ง "บันทึกร่าง" และ "ยื่นเรื่องลา" ใช้ handleSubmit ตัวเดียวกัน — แต่เปลี่ยนพฤติกรรมผ่าน state ModalConfirmFormStatus ('D' สำหรับ draft, 'S' สำหรับ submit)
ถ้าจะเพิ่มปุ่มที่ 3 (เช่น "บันทึกชั่วคราว") ต้องเพิ่ม status ใหม่ใน handleSubmit ด้วย
3. ใช้ component จาก from/ โฟลเดอร์
import FromInput from '../../../../components/user/from/Input'ระวังสะกดผิด — from (f) ไม่ใช่ Form (F)
4. มีไฟล์ backup 4 ไฟล์ในโฟลเดอร์เดียวกัน
src/pages/user/Leave/Form/
├── index.js ← ตัวจริง
├── index_bk.js ← backup เก่า
├── index_bk-to-prd.js ← snapshot ก่อน deploy prod
├── detail.js ← detail ของ leave ที่ยื่นแล้ว
├── detail_bk.js ← backup
├── detail_bk-prd.js ← snapshot prod
├── detailDarf.js ← draft detail (s สะกดผิดจาก Draft)
├── detailDarf_bk.js ← backup
└── detailDarf-bk-pro.js ← snapshot prod-opt (ใช้ - แทน _)ก่อนแก้ไฟล์ใด ตรวจใน routes.js ก่อนว่า import ตัวไหนจริง — ส่วนใหญ่เป็น index.js และ detail.js
5. handleSubmit ใหญ่มาก
handler เดียวยาวหลายร้อยบรรทัด มีหลาย branch ตาม ModalConfirmFormStatus — ถ้าจะแก้ logic อ่านทั้ง handler ก่อน อย่าแก้เฉพาะบรรทัดที่ดูเกี่ยวข้อง เพราะอาจมี side effect ใน branch อื่น
รูปแบบทั่วไปที่เห็นได้จาก 3 หน้านี้
รูปแบบที่ใช้ซ้ำ
| รูปแบบ | Login | Slip | Leave |
|---|---|---|---|
class ... extends Component | PureComponent | Component | Component |
withApollo HOC | ✓ | ✓ | ✓ |
Form.create HOC | — | — | ✓ |
componentDidMount เรียก query | — | ✓ | ✓ |
ใช้ this.props.client.query | ✓ | ✓ | ✓ |
ใช้ this.props.client.mutate | ✓ | — | ✓ |
| มี modal หลายตัว | ✓ | ✓ | ✓ |
มี console.log เหลืออยู่ | ✓ | ✓ | ✓ |
มี ModalErrors component | ✓ | ✓ | ✓ |
รูปแบบที่ควรยึดตามเมื่อสร้างหน้าใหม่
- เริ่มจาก
class ... extends Component - ใช้
withApolloHOC - state ใน
this.state— อย่าใช้ Redux/Context componentDidMountสำหรับ initial fetch- ใช้
Queries.fetchX/Mutations.createXจากsrc/graphql/ - error ส่งให้ errorLink จัดการ (อย่า catch เอง)
- ใช้
alertMessage('success'/'error', msg)สำหรับ toast - ถ้ามี form → ใช้
Form.create+getFieldDecorator