4.1.2 · Routing/auth gate + Component patterns + GraphQL catalog
Routing ทำงาน 2 ชั้น
ชั้นที่ 1 (App/index.js) ตัดสินว่า login หรือไม่ (ดู 4.1.1) ชั้นที่ 2 (routes.js + SiderLayout) วาดเมนูและ render route ภายใน shell
vtrc-web/src/utils/auth.js:91-97
export const isLoggedIn = () => {
if (localStorage.getItem('isRemember')) {
return !!localStorage.getItem('token') && !!localStorage.getItem('refreshToken')
} else {
return !!sessionStorage.getItem('token') && !!sessionStorage.getItem('refreshToken')
}
}isLoggedIn() เช็คแค่ว่ามี token+refreshToken ครบ — ไม่ตรวจ validity ถ้า token หมดอายุที่ server ผู้ใช้ยังผ่าน gate ได้ แต่ query จะ fail ด้วย TOKEN_EXPIRED แล้ว errorLink จะ refresh หรือ redirect ไป /login
routes.js — route entry structure
vtrc-web/src/config/routes.js:145-153
/* Struct {
key: String, 'unique key',
label!: String 'display name on menu',
icon!: String 'AntD icon',
path!: String 'path for routing',
isShow!: Boolean 'if it is true display on menu',
component: React Node 'component to render',
children?: Array 'sub route for routing'
} */mapRoutes() วน routes แบบ recursive → flatten เป็น <Route render={props => <route.component {...props} {...route.props} />} exact> ทุกตัว — ทุก route ใช้ exact
isShow, key (เช่น isPmsEvaluate) เป็นเพียง flag ฝั่ง UI — ใช้ toggle การแสดงเมนู แต่ไม่ใช่ auth gate จริง! ตัวอย่าง isShow: false ซ่อนเมนู แต่พิมพ์ path ตรงๆ ยังเข้าได้ การคุมสิทธิ์จริงอยู่ที่ฝั่ง server ผ่าน @auth(accessRole, accessMenu) directive ของ vtrc-api (ดู Volume 3.1.2) — ผู้ใช้ที่เจาะ route โดยไม่มีสิทธิ์จะเห็นหน้าว่างเพราะ query ตอบ FORBIDDEN
กฎเหล็ก — ใช้ isShow/key เพื่อ toggle UI ได้ แต่ห้ามคิดว่าเป็น gate ป้องกันการเข้าถึงจริง ถ้าต้องการป้องกันจริงที่ frontend ต้องเขียน <PrivateRoute> ที่เช็ค role ก่อน render (ปัจจุบันไม่มีในระบบ)
SiderLayout (src/components/user/SiderLayout/index.js:1-1226) เป็น shell ที่ดึง profile+role ผ่าน Queries.getProfileByEmpID ทุกครั้งที่ reload, วาดเมนูซ้าย+header+content, เรียก renderAllRoutes() ขนาดใหญ่ (1,226 บรรทัด) ทำให้แก้ไขยาก — logic เลือกเมนูกระจายหลายจุด (route entry + permission map + UI render)
Route clusters (path prefix โดยประมาณ) — PMS /pms/* ใหญ่ที่สุด (~25 route), Leave /leave/* (~15), Meeting /meetingManagement/* (~15), Hospital /hospital/* (~12), Welfare/Fund (~10), Profile (~10), Contact/About (~10)
Component + page architecture
จากการสแกน src/ — withApollo(class ...) ใช้ใน 250+ ไฟล์ (~95%), Form.create() (AntD v3 HOC) ~40% ของ page, function component มีแค่ 1 ตัว (App/index.js) <Query>/<Mutation> render props และ useQuery/useMutation hooks ไม่พบเลยในระบบ — vtrc-web เป็น class component + HOC ทั้งหมด
Pattern มาตรฐาน
class SlipPage extends Component {
state = { slip: null, loading: false }
componentDidMount() { this.fetchSlip() }
fetchSlip = async () => {
const { data, errors } = await this.props.client.query({
query: Queries.fetchSlip,
variables: { month: this.state.month, year: this.state.year, ... },
})
if (errors) return // errorLink จัดการแล้ว
this.setState({ slip: data.fetchSlip })
}
render() { /* ... */ }
}
export default withApollo(SlipPage)withApollo HOC ฉีด this.props.client — เรียก query/mutate ผ่าน .query()/.mutate() คืน promise error handling อยู่ที่ errorLink component แค่เช็ค errors แล้ว return state จัดการใน this.state เท่านั้น — ไม่มี Redux/Recoil/Context/MobX ในระบบเลย
ถ้ามี form ใช้ Form.create() (AntD v3 HOC) ครอบ — ลำดับ HOC สำคัญ: withApollo(Form.create({name})(Component)) ถูก, สลับกันผิด withApollo จะไม่ทำงาน
Layout pattern อื่นที่พบ — multi-tab form (LeaveStudies/Form/Tab1..Tab8/, container เก็บ state รวมแล้วส่ง props ลง tab), section sub-component แบบ flat (Recruit/Form/sector/secProfile.js ฯลฯ)
Component reuse ที่สำคัญ — SiderLayout/ (shell), LoginForm/, FormInputAddress/, ModalConfirmForm/, input wrapper ทั้งชุดอยู่ใน components/user/from/ (สะกดผิดจาก Form — ห้ามแก้ชื่อโฟลเดอร์ มี import path ทั้งระบบอ้างถึง)
ห้ามใช้ hooks (useState/useEffect) หรือ function component เมื่อสร้าง page ใหม่ — ผิด pattern ทั้งระบบ, ห้าม upgrade React 17/18, Apollo v3, หรือ AntD ข้าม major โดยไม่ได้รับมอบหมาย เพราะ react-apollo@3.1.5 ใช้ withApollo ที่ deprecated ใน Apollo v3, AntD v3 ใช้ LocaleProvider+Form.create ที่หายไปใน v4
ตัวอย่างหน้าสำคัญ 3 หน้า
| หน้า | ไฟล์ | ขนาด | จุดที่ต้องระวัง |
|---|---|---|---|
| Login | pages/user/Login/index.js | 650 บรรทัด | 2 ขั้น (verify username ผ่าน axios ไป SSO ก่อน → GraphQL); เก็บ password เข้ารหัส AES ด้วย key hardcoded สำหรับ "remember password" (ดู 4.1.3 S-3); ใช้ React.PureComponent ตัวเดียวในระบบ; RSA public key hardcoded สำหรับเข้ารหัส csId |
| Slip | pages/user/Slip/index.js | 1,528 บรรทัด | ต้องมี profileKey จาก fetchProfile ก่อนเรียก fetchSlip; สลับ tab (Slip/Tax/Salary Cert) ผ่าน state เดียว; แสดงปีเป็น พ.ศ. ผ่าน helper changeYear() เท่านั้น |
| Leave Form | pages/user/Leave/Form/index.js | 2,410 บรรทัด (ใหญ่ที่สุดใน repo) | dynamic field name ("documentType"+index); ปุ่ม "บันทึกร่าง"/"ยื่นเรื่องลา" ใช้ handleSubmit เดียวกัน แยกด้วย state status; มีไฟล์ backup 8 ไฟล์ในโฟลเดอร์เดียวกัน — ตรวจ routes.js ก่อนแก้เสมอ |
รูปแบบร่วมของทั้ง 3 หน้า — withApollo HOC ทุกตัว, มี modal หลายตัวซ้อนกัน, มี console.log เหลืออยู่ (แต่ไม่แสดงเพราะ console ถูก override), ใช้ ModalErrors component ร่วม
GraphQL catalog — 138 queries + 41 mutations
src/graphql/
├── queries.js 4,493 บรรทัด — 138 query definitions
├── mutations.js 1,447 บรรทัด — 41 mutation definitions
└── index.js re-export { Queries, Mutations }ทุกหน้า import ผ่าน import { Queries, Mutations } from '../graphql' — ไม่ import ตรงจากไฟล์ สัดส่วน queries:mutations ≈ 3.37:1 (read-heavy สอดคล้องกับ employee portal)
Cluster สรุป (ดูรายชื่อเต็มในซอร์ส queries.js/mutations.js)
| Cluster | ตัวอย่าง query/mutation |
|---|---|
| Auth & onboarding | refreshToken, verifyUser, generateCaptcha, login, changePassword |
| Profile | fetchProfile, fetchEmployeeProfile, fetchEmployeeProfileHistory |
| Slip & tax | fetchSlip, fetchUrlSlip, fetchUrlTax, createTaxDraft |
| News/policy/document | fetchListPost, fetchListNotification, fetchPolicyLastVersion |
| Hospital | fetchListWDHospital, createWDHospital, fetchApproverMedical |
| Leave | fetchLeaveDocumentList, createLeaveDocument, mutipleApproveCancelLeaveDocumentAsFlow |
| Leave studies | fetchListStudyLeaveRequest, updateStudyLeaveRequest |
| Welfare/Fund | fetchAllProvidentFundDetail, createFundApplication, createFundBenefitPerson |
| Master data/dropdown | dropdownListProvince, dropdownListAmphur, dropdownListDistrict |
| Delegate | createDelegateApprover, revokeDelegateApprover |
| Salary cert | fetchSalaryCert, fetchUrlSalaryCertForBank |
| Conicle + KCMH doc | fetchUrlConicleAllPolicy, fetchListKCMHDocument |
| PMS | fetchMasterConfigTypePMS, fetchCountListAllApproving |
Typos ที่ติดอยู่ใน schema — เปลี่ยนไม่ได้ (breaking change)
| Typo | ที่ควรเป็น |
|---|---|
fetchListDocumen | fetchListDocument |
fetchLeaveDocumentDetailDraf | fetchLeaveDocumentDetailDraft |
detchProfileListDetail | fetchProfileListDetail |
dropDownstudyLeaveYear | dropDownStudyLeaveYear (s ตัวเล็ก) |
กฎเหล็ก — ถ้าเจอ query/mutation ที่ดูสะกดผิด ให้ใช้ตามที่เป็น ห้ามแก้ชื่อ เพราะ schema ฝั่ง vtrc-api เผยแพร่แล้ว การแก้จะทำให้ request fail
utils/string.js มี toGqlTypes/toGqlArgs/toGqlFields ที่ import ใน queries.js แต่ไม่ได้ใช้จริง (ดู 4.1.1) — มี bug ในตัวเอง (toGqlFields เทียบ n !== fields.length - 1 ผิดเพราะ for...in วน key ไม่ใช่ index) ห้ามนำไปใช้โดยไม่ทดสอบ
Pattern การเรียกใช้
const { data, errors } = await this.props.client.query({
query: Queries.fetchSlip,
variables: { month, year, identityCard, profileKey },
})
if (errors) return // errorLink จัดการแล้ว
this.props.client.mutate({ mutation: Mutations.createLeaveDocument, variables: {...} })
.then(({ data, errors }) => { if (errors) return; alertMessage('success', 'บันทึกสำเร็จ') })ตอนเพิ่ม query ใหม่ — เพิ่ม const + เพิ่มใน export default {} ท้ายไฟล์ + cross-check schema ฝั่ง vtrc-api ว่ามีจริง