Skip to content

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

โครงสร้าง

54:77:vtrc-web/src/pages/user/Login/index.js
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)

js
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

30:70:vtrc-web/src/pages/user/Slip/index.js
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)

58:60:vtrc-web/src/pages/user/Slip/index.js
		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

โครงสร้าง

37:80:vtrc-web/src/pages/user/Leave/Form/index.js
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 ในตาราง

58:vtrc-web/src/pages/user/Leave/Form/index.js
                              this.props.form.getFieldDecorator("documentType"+index, {

ชื่อ field เป็น "documentType" + index — ถ้าเพิ่ม/ลบ row ต้องเช็คว่าลำดับยังตรง ไม่งั้น validation จะผิด

2. มี 2 ปุ่ม submit ใช้ handler ตัวเดียวกัน

2353:2381:vtrc-web/src/pages/user/Leave/Form/index.js
                                          <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/ โฟลเดอร์

8:vtrc-web/src/pages/user/Leave/Form/index.js
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 หน้านี้

รูปแบบที่ใช้ซ้ำ

รูปแบบLoginSlipLeave
class ... extends ComponentPureComponentComponentComponent
withApollo HOC
Form.create HOC
componentDidMount เรียก query
ใช้ this.props.client.query
ใช้ this.props.client.mutate
มี modal หลายตัว
มี console.log เหลืออยู่
มี ModalErrors component

รูปแบบที่ควรยึดตามเมื่อสร้างหน้าใหม่

  1. เริ่มจาก class ... extends Component
  2. ใช้ withApollo HOC
  3. state ใน this.state — อย่าใช้ Redux/Context
  4. componentDidMount สำหรับ initial fetch
  5. ใช้ Queries.fetchX / Mutations.createX จาก src/graphql/
  6. error ส่งให้ errorLink จัดการ (อย่า catch เอง)
  7. ใช้ alertMessage('success'/'error', msg) สำหรับ toast
  8. ถ้ามี form → ใช้ Form.create + getFieldDecorator

ขั้นตอนถัดไป

ไป บท 6.8 State, styling, locale (พ.ศ.)