Skip to content

7.8 · State, styling, locale (พ.ศ.)

บทนี้เล่าว่า vtrc-web จัดการ state อย่างไร ใช้ CSS แบบไหน และทำไมการจัดการปี พ.ศ. จึงกระจายอยู่หลายที่

ถ้าผู้ใช้บอกว่า "ปีที่แสดงผิด" หรือ "หน้านี้ไม่เหมือนหน้าอื่น" — บทนี้คือจุดเริ่มต้น


State — local เท่านั้น ไม่มี global

vtrc-web ไม่มี Redux, Recoil, MobX, Context API หรือ state library ใด ๆ state ทั้งหมดอยู่ใน 3 ที่:

ที่ใช้เก็บอะไรscope
this.state ใน componentstate ของหน้าcomponent
localStoragetoken, refreshToken, profile, noti counter, remember flagข้าม tab + ข้ามวัน
sessionStoragetoken (ถ้าไม่ remember), tab-specific flagtab ปัจจุบัน

ไม่มี "global loading"

แต่ละ page จัด loading state ของตัวเองผ่าน this.state.loading หรือ this.state.preLoad — ไม่มี spinner กลาง

ไม่มี "global error banner"

error ทั้งหมดผ่าน alertMessage(type, msg) ใน utils/message.js หรือ message.error() ของ AntD — เป็น toast ที่หายไปเอง

localStorage / sessionStorage — key ที่ใช้บ่อย

keyที่เก็บตัวอย่างค่า
tokenJWT ปัจจุบันeyJhbGc...
refreshTokenrefresh tokeneyJhbGc...
isRememberflag remember me'true'
appUsernameusername ที่ login'sso-12345'
sessionIDsession idUUID
lsidlogin session idUUID
organizationorg id'3F3BF3AD-...'
IsTrialflag ทดลองใช้'true'
objectValuePFprofile object cacheJSON string
objectValueEMemployee object cacheJSON string
profileDetailPWencrypted passwordAES encrypted
profileDetailUNencrypted usernameAES encrypted
expiredDateวันที่ password จะหมดอายุISO date
localtokenclient id (random)UUID
reCaptchaCheckflag captcha''

helpers สำหรับจัดการ storage:

helperที่มาหน้าที่
setToken({accessToken, refreshToken}, isRemember)utils/auth.js:4เก็บ token ตาม isRemember
getToken()utils/auth.js:23อ่าน token + refreshToken
clearAllStorage()utils/auth.js:99logout — ล้างทั้งสอง storage
setWithExpiry(key, value, ttl)utils/storage.jsเก็บ key พร้อม expiry (ms)
getWithExpiry(key)utils/storage.jsอ่าน key + return null ถ้าหมดอายุ

กฎเหล็ก

  • ห้ามอ่าน localStorage.getItem('token') ตรง ๆ — ใช้ getToken() เสมอ เพราะต้องเลือก localStorage หรือ sessionStorage
  • ห้ามเก็บ object โดยไม่ JSON.stringifylocalStorage เก็บ string เท่านั้น

Styling — ผสม 4 แบบ

vtrc-web ใช้ CSS ผสมจากหลายแหล่ง ทำให้บางครั้ง style ทับซ้อนกัน

1. global CSS (จาก SCSS)

src/assets/sass/main.scss:1-1764 เป็น source — compile ด้วย Prepros (manual, ไม่ได้อยู่ใน webpack) ออกมาเป็น src/assets/css/main.css:1-4565

14:23:vtrc-web/src/assets/sass/main.scss
html {
  position: relative;
  height: 100%;
}

body {
  font-size: 16px;
  color: rgba(0, 0, 0, 0.65);
  font-family: "Kanit-Medium";
}

import ครั้งเดียวใน index.js — ใช้ได้ทุกหน้า

2. AntD override (compiled CSS)

src/config/custom-antd.css:1-21190 — override style ของ AntD v3 แบบ compiled แล้ว (21,190 บรรทัด) — ใหญ่ที่สุดใน repo

ไม่ใช่ source — เป็น CSS ที่ compile จาก .less ของ AntD แล้ว modify ทับ ห้ามแก้ที่นี่โดยตรง ให้แก้ที่ .less source (ถ้ามี) หรือเพิ่ม override class

3. utility CSS สั้น ๆ

src/config/styles.css:1-128 — utility class เล็ก ๆ เช่น .card-bg-green, .btn-white-red, .color-red

4. styled-components (ใช้น้อย)

บางหน้าใช้ styled-components สำหรับ scoped style — ส่วนใหญ่ใน Login page:

38:51:vtrc-web/src/pages/user/Login/index.js
const BackgroundDiv = styled.div`
	display: flex;
	justify-content: center;
	align-items: center;
`

const FormDiv = styled.div`
	padding: 50px 100px;
	box-shadow: 30px 40px 40px 1px rgba(0, 0, 0, 0.15), 0 0px 0px rgba(0, 0, 0, 0.25);
	border-radius 20px;
	background-color: #fff;
`

ข้อควรระวังเกี่ยวกับ styling

  • ลำดับความสำคัญ: AntD default → custom-antd.cssmain.cssstyles.css → styled-components inline
  • ถ้าจะ override AntD component ให้ใช้ className + target ใน main.css หรือ styles.css
  • ห้ามแก้ custom-antd.css โดยตรง — ใหญ่เกินไป และเป็น output จาก build

fonts

src/assets/fonts/
├── Kanit-Medium.*       (primary body font)
├── Kanit-Regular.*
├── THSarabunPSK.*       (alternative Thai font)
└── ...

ค่า default คือ "Kanit-Medium" (main.scss:22) — ใช้ Kanit เป็นหลัก


Locale — พ.ศ. และภาษาไทย

การแสดงปีเป็น พ.ศ. ทำได้ 3 ทาง และกระจายอยู่หลายจุดในโค้ด

1. moment.js locale ไทย

src/index.js:10-45:

10:12:vtrc-web/src/index.js
import moment from 'moment';
import 'moment/locale/th';

moment.locale('th');

ผล: moment().format('LL') ได้ 10 กรกฎาคม 2569 โดยอัตโนมัติ — moment.js แปลงเดือนเป็นไทยและปีเป็น พ.ศ. ให้

2. override moment.format (ถูกปิดไว้)

18:33:vtrc-web/src/index.js
// override format ของ moment ให้แสดงปีเป็น พ.ศ.
// moment.fn.format = function (f) {
//   if (!f) return _format.call(this);
//   // แปลง YYYY → พ.ศ. โดยบวก 543
//   return _format.call(this, f.replace(/YYYY/g, this.year() + 543));
// };

// const _year = moment.fn.year;

// override year ให้เป็น พ.ศ.
// moment.fn.year = function (y) {
//   if (typeof y === 'undefined') {
//     return _year.call(this) + 543;
//   }
//   return _year.call(this, y - 543);
// };

override ทั้งคู่ถูก comment ไว้ — ปัจจุบันใช้ helper function แทน

3. helper function ใน utils/formatdate.js

src/utils/formatdate.js:1-442 มี helper 22 ตัว ส่วนใหญ่ใช้ manual +543:

222:224:vtrc-web/src/utils/formatdate.js
export const changeYear = (year) => {
  return year + 543;
};
66:92:vtrc-web/src/utils/formatdate.js
export const changeFormatdate = (date) => {

  let monthThai = [];
  monthThai["01"] = "มกราคม";
  monthThai["02"] = "กุมภาพันธ์";
  // ... 12 เดือน
  if (date) {
    const newdate =
      moment(date).format("D") +
      " " +
      monthThai[moment(date).format("MM")] +
      " " +
      (parseInt(moment(date).format("YYYY")) + 543).toString();
    return newdate;
  }
};

ตาราง helper ที่ใช้บ่อย

helperรูปแบบ outputใช้ใน
changeFormatdate(date)10 กรกฎาคม 2569หน้าส่วนใหญ่
changeFormatdateSlip(date)10 กรกฎาคม 2569 (UTC→local ก่อน)Slip
changeFormatFullDateTimeThai(date)10 กรกฎาคม 2569 เวลา 14:30detail ต่าง ๆ
changeFormatMonthYearThai(date)ตุลาคม 2569report
changeFormatdateThai(date)เหมือน changeFormatdateleave
changeOnlyMonthSlip(date)กรกฎาคม (เดือนอย่างเดียว)Slip dropdown
changeYear(year)2569 (เลขปีอย่างเดียว)คำนวณ
pushAge(birthDate)อายุเป็นปีprofile
pushAgeWork(startDate)อายุงานเป็นปีprofile
pushAgeDate(birthDate, deathDate)อายุโดยระบุวันสิ้นสุดกรณีเสียชีวิต
changeFormatdateStartEnd(start, end)10 ก.ค. 2569 - 15 ก.ค. 2569leave list
dateFormatNumber(values)25691010ใช้ใน query
disabledDate(current)disable วันที่ผ่านไปแล้วDatePicker

กฎเหล็กเกี่ยวกับ พ.ศ.

  1. ห้าม +543 ตรง ๆ ใน component — ใช้ helper changeFormatdate() หรือ changeYear()
  2. ห้ามใช้ moment().year() ตรง ๆ เพื่อแสดงผล — จะได้ C.E.
  3. ถ้าจะแสดงปีเป็น พ.ศ. ใน dropdown — ใช้ changeYear(year) ก่อน set เป็น options
  4. moment.js locale 'th' จะ format ให้เป็น พ.ศ. อัตโนมัติเมื่อใช้ .format('LL') หรือ .format('LLLL') — แต่ .format('YYYY') ยังได้ C.E.

ความซ้ำซ้อนใน helper

helpers หลายตัว duplicate logic (mapping เดือน 12 ตัวเขียนซ้ำในแต่ละ function) — ถ้าจะ refactor แนะนำให้สร้าง constant MONTH_TH กลาง แล้วให้ทุก helper import ใช้

ปัจจุบัน monthThai array ถูกประกาศซ้ำใน 7+ function:

js
let monthThai = [];
monthThai["01"] = "มกราคม";
monthThai["02"] = "กุมภาพันธ์";
// ...

Axios instances — หลายตัวสำหรับ REST services

นอกจาก Apollo (GraphQL) vtrc-web ยังใช้ axios สำหรับ REST services หลายตัว:

1. axios-bo.js — instance หลักพร้อม interceptor

19:42:vtrc-web/src/config/axios-bo.js
export const AxiosBO = (setHeader = true) => {
   const defaultOptions = {}
    if (setHeader === true) {
        defaultOptions.headers = {
            "Content-Type": "application/json"
        }
    }

    const instance = axios.create(defaultOptions);

    instance.interceptors.request.use(function (config) {
        const { token } = getToken()
        config.headers.authorization = token ? `Bearer ${token}` : "";
         if(!config.url.includes("/meeting/secure/")){
            config.headers.apiKey = atob(apiKeyNews());
         }
        return config;
    });

มี interceptor สำหรับ refresh token เมื่อได้ 401 (บรรทัด 45-83) — เหมือน errorLink ของ Apollo แต่สำหรับ REST

2. axiosAPI.js — ใช้ใน Login (SSO)

src/config/axiosAPI.js — ใช้สำหรับ ${checkLinkURL()}SSOService/checkEmpLogin ใน Login (บรรทัด 223)

3. axios-i3.js — ใช้สำหรับ I3 services

src/config/axios-i3.js — ใช้ในบาง page สำหรับ I3 internal services

ข้อควรระวังเกี่ยวกับ axios

  • แต่ละ instance มี interceptor ต่างกัน — ถ้าจะ refresh token ต้องเลือก instance ที่มี interceptor ครบ
  • AxiosBO() เป็น factory function — ทุกครั้งที่เรียกสร้าง instance ใหม่ ทำให้มี overhead ถ้าเรียกบ่อย
  • axios-bo.js:38-40 มีเงื่อนไขพิเศษสำหรับ URL ที่มี /meeting/secure/ — ไม่แนบ apiKey ใน request

PWA — service worker unregister

src/index.js:93 เรียก serviceWorker.unregister() — webpack ยังสร้าง SW ไว้ (ผ่าน Workbox) แต่ฝั่งแอปไม่ register จึงไม่ทำงาน

ถ้าจะเปิด PWA จริง ต้องเปลี่ยนเป็น serviceWorker.register() และออกแบบ cache invalidation


Console override — ปิด console ทั้งระบบ

src/containers/App/index.js:15:

js
console.log = console.warn = console.error = () => { };

เป็น global side effect ที่ทำให้ debug ฝั่ง production ได้ยาก — ทุก console.* ถูกทำให้เป็น no-op

ถ้าจะ debug ใน browser ต้อง comment บรรทัดนี้ชั่วคราว หรือใช้ debugger statement + DevTools


dangerouslySetInnerHTML — ใช้หลายที่

grep พบการใช้ dangerouslySetInnerHTML ใน 20+ ไฟล์ — ส่วนใหญ่เพื่อ render markdown content หรือ HTML ที่ได้จาก server

ตัวอย่างที่พบบ่อย:

ไฟล์ใช้ทำอะไร
Slip/index.jsrender text payroll slip (HTML)
Notification/index.jsrender ข้อความ notification
News/index.js, DetailNews/index.jsrender เนื้อหาข่าว
Policy/index.js, PolicyDetail/index.jsrender policy text
PmsIndicators/form/...render modal content
Orientation/index.jsrender orientation content

ความเสี่ยง

ถ้า content มาจาก user input โดยไม่ sanitize → XSS risk รายละเอียดใน บท 6.9

helper markdown

src/utils/markDown.js:1-16 แปลง markdown → HTML ผ่าน draftjs — แต่ใช้น้อย ส่วนใหญ่ใช้ react-markdown ในหน้า Slip


เคล็ดลับตอนทำงานจริง

ตอน debug "ปีที่แสดงผิด"

  1. เช็คว่าใช้ helper อะไร (changeFormatdate vs moment().format('YYYY'))
  2. เช็คว่าข้อมูลจาก server เป็น C.E. หรือ พ.ศ. แล้ว — ถ้า server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ
  3. เช็ค locale ของ moment — .locale() ต้องเป็น 'th'

ตอน debug "style ไม่ทำงาน"

  1. ตรวจ className ใน DevTools → ดูว่ามี CSS rule ไหน match
  2. ตรวจ specificity — AntD override มักจะ !important บ่อย
  3. ตรวจ load order ใน index.jsmain.css โหลดก่อน custom-antd.css หรือไม่

ตอนจะเก็บข้อมูลใน localStorage

  1. ใช้ helper setWithExpiry ถ้าต้องการ expiry
  2. JSON.stringify object ทุกครั้ง
  3. อย่าเก็บข้อมูล sensitive โดยไม่ encrypt — localStorage ทุกคนเปิดเห็นได้ผ่าน DevTools

ตอนใช้ axios

  1. เลือก instance ที่เหมาะสม (AxiosBO, axiosAPI, axios-i3)
  2. ถ้าต้องการ refresh token อัตโนมัติ ใช้ AxiosBO() — มี interceptor ครบ
  3. ถ้า endpoint ต้องการ apiKey แต่ไม่ bearer token ใช้ factory แบบ AxiosBO(false) เพื่อข้าม apiKey header

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

ไป บท 6.9 Security + performance