Skip to content

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

javascript
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

javascript
/* 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 มาตรฐาน

jsx
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 หน้า

หน้าไฟล์ขนาดจุดที่ต้องระวัง
Loginpages/user/Login/index.js650 บรรทัด2 ขั้น (verify username ผ่าน axios ไป SSO ก่อน → GraphQL); เก็บ password เข้ารหัส AES ด้วย key hardcoded สำหรับ "remember password" (ดู 4.1.3 S-3); ใช้ React.PureComponent ตัวเดียวในระบบ; RSA public key hardcoded สำหรับเข้ารหัส csId
Slippages/user/Slip/index.js1,528 บรรทัดต้องมี profileKey จาก fetchProfile ก่อนเรียก fetchSlip; สลับ tab (Slip/Tax/Salary Cert) ผ่าน state เดียว; แสดงปีเป็น พ.ศ. ผ่าน helper changeYear() เท่านั้น
Leave Formpages/user/Leave/Form/index.js2,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 & onboardingrefreshToken, verifyUser, generateCaptcha, login, changePassword
ProfilefetchProfile, fetchEmployeeProfile, fetchEmployeeProfileHistory
Slip & taxfetchSlip, fetchUrlSlip, fetchUrlTax, createTaxDraft
News/policy/documentfetchListPost, fetchListNotification, fetchPolicyLastVersion
HospitalfetchListWDHospital, createWDHospital, fetchApproverMedical
LeavefetchLeaveDocumentList, createLeaveDocument, mutipleApproveCancelLeaveDocumentAsFlow
Leave studiesfetchListStudyLeaveRequest, updateStudyLeaveRequest
Welfare/FundfetchAllProvidentFundDetail, createFundApplication, createFundBenefitPerson
Master data/dropdowndropdownListProvince, dropdownListAmphur, dropdownListDistrict
DelegatecreateDelegateApprover, revokeDelegateApprover
Salary certfetchSalaryCert, fetchUrlSalaryCertForBank
Conicle + KCMH docfetchUrlConicleAllPolicy, fetchListKCMHDocument
PMSfetchMasterConfigTypePMS, fetchCountListAllApproving

Typos ที่ติดอยู่ใน schema — เปลี่ยนไม่ได้ (breaking change)

Typoที่ควรเป็น
fetchListDocumenfetchListDocument
fetchLeaveDocumentDetailDraffetchLeaveDocumentDetailDraft
detchProfileListDetailfetchProfileListDetail
dropDownstudyLeaveYeardropDownStudyLeaveYear (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 การเรียกใช้

js
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 ว่ามีจริง