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 ใน component | state ของหน้า | component |
localStorage | token, refreshToken, profile, noti counter, remember flag | ข้าม tab + ข้ามวัน |
sessionStorage | token (ถ้าไม่ remember), tab-specific flag | tab ปัจจุบัน |
ไม่มี "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 | ที่เก็บ | ตัวอย่างค่า |
|---|---|---|
token | JWT ปัจจุบัน | eyJhbGc... |
refreshToken | refresh token | eyJhbGc... |
isRemember | flag remember me | 'true' |
appUsername | username ที่ login | 'sso-12345' |
sessionID | session id | UUID |
lsid | login session id | UUID |
organization | org id | '3F3BF3AD-...' |
IsTrial | flag ทดลองใช้ | 'true' |
objectValuePF | profile object cache | JSON string |
objectValueEM | employee object cache | JSON string |
profileDetailPW | encrypted password | AES encrypted |
profileDetailUN | encrypted username | AES encrypted |
expiredDate | วันที่ password จะหมดอายุ | ISO date |
localtoken | client id (random) | UUID |
reCaptchaCheck | flag 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:99 | logout — ล้างทั้งสอง 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.stringify—localStorageเก็บ 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
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:
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.css→main.css→styles.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:
import moment from 'moment';
import 'moment/locale/th';
moment.locale('th');ผล: moment().format('LL') ได้ 10 กรกฎาคม 2569 โดยอัตโนมัติ — moment.js แปลงเดือนเป็นไทยและปีเป็น พ.ศ. ให้
2. override moment.format (ถูกปิดไว้)
// 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:
export const changeYear = (year) => {
return year + 543;
};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:30 | detail ต่าง ๆ |
changeFormatMonthYearThai(date) | ตุลาคม 2569 | report |
changeFormatdateThai(date) | เหมือน changeFormatdate | leave |
changeOnlyMonthSlip(date) | กรกฎาคม (เดือนอย่างเดียว) | Slip dropdown |
changeYear(year) | 2569 (เลขปีอย่างเดียว) | คำนวณ |
pushAge(birthDate) | อายุเป็นปี | profile |
pushAgeWork(startDate) | อายุงานเป็นปี | profile |
pushAgeDate(birthDate, deathDate) | อายุโดยระบุวันสิ้นสุด | กรณีเสียชีวิต |
changeFormatdateStartEnd(start, end) | 10 ก.ค. 2569 - 15 ก.ค. 2569 | leave list |
dateFormatNumber(values) | 25691010 | ใช้ใน query |
disabledDate(current) | disable วันที่ผ่านไปแล้ว | DatePicker |
กฎเหล็กเกี่ยวกับ พ.ศ.
- ห้าม +543 ตรง ๆ ใน component — ใช้ helper
changeFormatdate()หรือchangeYear() - ห้ามใช้
moment().year()ตรง ๆ เพื่อแสดงผล — จะได้ C.E. - ถ้าจะแสดงปีเป็น พ.ศ. ใน dropdown — ใช้
changeYear(year)ก่อน set เป็น options - 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:
let monthThai = [];
monthThai["01"] = "มกราคม";
monthThai["02"] = "กุมภาพันธ์";
// ...Axios instances — หลายตัวสำหรับ REST services
นอกจาก Apollo (GraphQL) vtrc-web ยังใช้ axios สำหรับ REST services หลายตัว:
1. axios-bo.js — instance หลักพร้อม interceptor
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:
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.js | render text payroll slip (HTML) |
Notification/index.js | render ข้อความ notification |
News/index.js, DetailNews/index.js | render เนื้อหาข่าว |
Policy/index.js, PolicyDetail/index.js | render policy text |
PmsIndicators/form/... | render modal content |
Orientation/index.js | render 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 "ปีที่แสดงผิด"
- เช็คว่าใช้ helper อะไร (
changeFormatdatevsmoment().format('YYYY')) - เช็คว่าข้อมูลจาก server เป็น C.E. หรือ พ.ศ. แล้ว — ถ้า server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ
- เช็ค locale ของ moment —
.locale()ต้องเป็น'th'
ตอน debug "style ไม่ทำงาน"
- ตรวจ className ใน DevTools → ดูว่ามี CSS rule ไหน match
- ตรวจ specificity — AntD override มักจะ
!importantบ่อย - ตรวจ load order ใน
index.js—main.cssโหลดก่อนcustom-antd.cssหรือไม่
ตอนจะเก็บข้อมูลใน localStorage
- ใช้ helper
setWithExpiryถ้าต้องการ expiry - JSON.stringify object ทุกครั้ง
- อย่าเก็บข้อมูล sensitive โดยไม่ encrypt — localStorage ทุกคนเปิดเห็นได้ผ่าน DevTools
ตอนใช้ axios
- เลือก instance ที่เหมาะสม (
AxiosBO,axiosAPI,axios-i3) - ถ้าต้องการ refresh token อัตโนมัติ ใช้
AxiosBO()— มี interceptor ครบ - ถ้า endpoint ต้องการ apiKey แต่ไม่ bearer token ใช้ factory แบบ
AxiosBO(false)เพื่อข้าม apiKey header