4.2.1 · Repository map + Bootstrap + Apollo Client + Routing/Auth gate
บทนี้รวม 4 หัวข้อที่ V1 แยกเป็น 4 บท (repository-map / bootstrap-build / apollo-checklinkurl / routing-auth-gate) เพราะเนื้อหาสัมพันธ์กันแน่น — เข้าใจโครงสร้าง repo ก่อน → ดู entry point ที่ boot → ดู Apollo Client ที่ต่อ backend → ดู routing ที่เปิด/ปิด auth gate
ภาพรวม repo — vtrc-rc-backoffice
vtrc-rc-backoffice เป็น backoffice หลักของระบบ VTRC (ต่างจาก vtrc-backoffice และ vtrc-backoffice-new ที่เป็น backoffice ของฟีเจอร์เฉพาะทำนอง module ใหม่) เปิดให้ admin/staff ของสำนักงานใหญ่และศูนย์บริการใช้งาน บริหาร user/role/news/policy/organization/career/course/leave/welfare/hospital/payment และอื่น ๆ
- Stack: React 16.14 + CRA 3.4.3 + AntD v4.15 + MUI v5.11 (ใช้น้อย) + Apollo v2 (
react-apollo@3.1) + Recoil 0.7 + react-router-dom 5 - Branch:
master(769 tracked files — branchdevelopมี 769 เท่ากันแต่ commits ต่างกัน) - Repo size: 871 ไฟล์ใน
src/pages/, 104 ไฟล์ในsrc/components/,src/graphql/queries.js4,108 บรรทัด +mutations.js2,208 บรรทัด - Production URL:
https://vtrcbackoffice.redcross.or.th/(และhttps://rc-vtrcbackoffice.redcross.or.th/สำหรับศูนย์บริการ) - Staging URL:
https://uat-vtrcbackoffice.redcross.or.th/(และhttps://uat-rc-vtrcbackoffice.redcross.or.th/) - Firebase Hosting:
https://backoffice-vtrc.web.app/(ใช้ endpoint คนละตัว —api.vtrcapp.com/api-dev/graphql)
โครงสร้าง src/
src/
├── index.js 33 บรรทัด entry point (ApolloProvider + RecoilRoot)
├── serviceWorker.js ~140 บรรทัด CRA default (unregister)
├── config/
│ ├── apollo.js 187 บรรทัด Apollo Client + checkLinkURL + errorLink
│ ├── routes.js 2,551 บรรทัด route table (route definitions + SiderLayout wiring)
│ ├── custom-antd.css ~13,000 บรรทัด AntD v4 override (compiled)
│ └── axios-bo.js, axios-pms.js, axios-ai.js, refreshToken.js, datasets*.js
├── containers/App/index.js 33 บรรทัด root router + auth gate (overlaps routes.js)
├── graphql/
│ ├── queries.js 4,108 บรรทัด 121 queries (gql operation names)
│ ├── mutations.js 2,208 บรรทัด 94 mutations
│ ├── WorkForce/queries.js ~1,500 บรรทัด ~40 WorkForce-specific queries
│ ├── WorkForce/mutations.js ~900 บรรทัด ~30 WorkForce-specific mutations
│ └── index.js re-export hub
├── pages/ 871 ไฟล์ feature screens (รวม `TheamCss` — typo)
├── components/ 104 ไฟล์ shell (SiderLayout, Header, Breadcrumb), form controls, modals
├── utils/ helpers (auth.js, apiKey.js, message.js, axios wrappers)
└── assets/ css (main.css, redesign.css), images, fontsไฟล์ legacy/backup — มากกว่า 75 ไฟล์ ห้ามแตะ
vtrc-rc-backoffice สะสมไฟล์ backup/snapshot จากการ deploy หลาย env และ refactor ที่ยังไม่ลบ — pattern เดียวกับ vtrc-web
| รูปแบบ | ตัวอย่าง |
|---|---|
_bk.js, _bk-<date>.js | queries_bk-061265.js, mutations_bk.js |
_prd.js, _pentest.js | snapshot ของ env prod / pentest |
_gql.js, _axios.js, _table.js | snapshot ของ refactor ทดลอง |
กฎเหล็ก — (1) อย่าแก้ไฟล์ที่ลงท้ายด้วยรูปแบบ backup (2) อย่า copy โค้ดจากไฟล์เหล่านี้เป็นต้นแบบ (3) อย่าลบทิ้งเองโดยไม่ตรวจ git history ก่อน
วิธีตรวจว่าไฟล์เป็นของจริง — เช็คว่าไม่มี suffix ผิดปกติ + ถูก import จาก routes.js + ถ้าเป็น query/mutation ต้องอยู่ใน queries.js/mutations.js ตัวจริง
Entry point — src/index.js
vtrc-rc-backoffice/src/index.js:1-33
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import { ApolloProvider } from 'react-apollo'
import App from './containers/App'
import * as serviceWorker from './serviceWorker'
import apolloClient from './config/apollo'
import { RecoilRoot } from 'recoil';
// const Root = () => (
// <ApolloProvider client={apolloClient} >
// <App />
// </ApolloProvider>
// )
class Root extends Component {
render() {
return (
<ApolloProvider client={apolloClient} >
<RecoilRoot>
<App />
</RecoilRoot>
</ApolloProvider>
);
}
}
ReactDOM.render(<Root />, document.getElementById('root'))
serviceWorker.unregister()บรรทัดที่สำคัญ:
- ลำดับการ mount:
ApolloProvider(หุ้มนอกสุด) →RecoilRoot→App(root router) Rootเป็น class component ที่ commented-out function version อยู่ด้านบน (index.js:10-14) — ห้ามลบ comment นั้น ผู้พัฒนาเก็บไว้เป็น referenceApolloProviderรับapolloClientจาก default export ของ./config/apollo— เป็น singleton ที่สร้างตอน importRecoilRootหุ้มไว้ใต้ Apollo ทำให้useRecoilState/useSetRecoilState/useRecoilValueใช้ได้ในทุก component ลูก
serviceWorker.unregister() — ผลกระทบ
CRA default — ไม่ลงทะเบียน service worker ทำให้ browser ไม่เก็บ cache ฝั่ง client — ผู้ใช้ต้อง refresh เพื่อรับเวอร์ชันใหม่เสมอ (ตรงข้ามกับแนว PWA) ผลคือ deploy ใหม่แล้ว user อาจเห็นโค้ดเก่าได้ถ้า browser cache ฝั่ง HTTP ยัง active (ดู nginx config ในส่วน Dockerfile)
App container — root router + auth gate
vtrc-rc-backoffice/src/containers/App/index.js:1-33
import React from 'react'
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
import { withApollo } from 'react-apollo'
import { Queries, Mutations } from '../../graphql'
import { isLoggedIn } from '../../utils/auth'
import SiderLayout from '../../components/SiderLayout'
import Login from '../../pages/Login'
import TheamCss from '../../pages/TheamCss'
import PathLogin from '../../pages/PathLogin'
import NotFound from '../../pages/NotFound'
import '../../config/custom-antd.css'
import 'antd/dist/antd.css'
import '../../assets/css/main.css'
import '../../assets/css/redesign.css'
function App () {
console.log = console.warn = console.error = () => { };
// console.log('login')
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/TheamCss" exact component={TheamCss} />
<Route path="/pathLogin/:token/:refreshToken" exact component={PathLogin}/>
{isLoggedIn() && <Route path="/" component={SiderLayout} />}
{!isLoggedIn() && <Redirect to="/login" />}
<Route component={NotFound} />
</Switch>
</BrowserRouter>
)
}
export default withApollo(App)สิ่งที่ต้องระวังใน App/index.js
console.log = console.warn = console.error = () => { };— ปิด console ทุกชนิดใน production ทำให้ debug ด้วยconsole.*ไม่ได้ ต้องใช้debuggerหรือ React DevTools ผลกระทบคือ error ฝั่ง client ไม่ถูก log ที่ browser console ทำให้นักพัฒนาไม่เห็น error จริงในการ debugไม่มี
PrivateRoutecomponent — auth gate เป็น conditional render:isLoggedIn() && <Route …/>คู่กับ!isLoggedIn() && <Redirect to="/login" />pattern นี้ทำให้ routes ทั้งหมด mount/unmount ทีเดียวกันเมื่อสถานะ login เปลี่ยนisLoggedIn()คืน boolean — ตรวจว่ามีทั้งtokenและrefreshTokenใน localStorage/sessionStorage (ดูsrc/utils/auth.js) ไม่ได้คืน JWT string และไม่ได้ validate token ฝั่ง client — เช็คแค่ "มีอยู่หรือไม่"TheamCss— typo ในชื่อจริง (ควรเป็นThemeCss) ห้ามแก้เพราะ path และ import อ้างชื่อนี้ทั้งระบบ เป็นหน้า dev tool สำหรับ preview CSSPathLoginroute/pathLogin/:token/:refreshToken— SSO bypass ที่รับ token ทั้งคู่ผ่าน URL path (ดูวิเคราะห์เชิงลึกใน บท 4.2.5 Security analysis)CSS imports —
custom-antd.css(override) →antd.css(AntD core) →main.css→redesign.cssลำดับสำคัญเพราะ cascade ฝั่ง browserwithApollo(App)— injectclientเข้าpropsของAppทำให้ component ลูกเรียกthis.props.client.query()/.mutate()ได้ (pattern class component) ส่วน function component ใช้useQuery/useMutationตรง ๆ ผ่าน hook ของreact-apollo(ไม่ต้องwithApollo)
Apollo Client — src/config/apollo.js
vtrc-rc-backoffice/src/config/apollo.js:1-187 เป็นไฟล์เดียวที่บริหาร GraphQL connection ทั้งหมด — checkLinkURL() + authLink + errorLink + uploadLink + apolloFetch + apolloClient
checkLinkURL() — env-based endpoint resolution
vtrc-rc-backoffice/src/config/apollo.js:14-36
export const checkLinkURL = () => {
if (window.location.href.includes("https://vtrcbackoffice.redcross.or.th/")
|| window.location.href.includes("https://rc-vtrcbackoffice.redcross.or.th/")) {
return "https://vtrcapi.redcross.or.th/api/graphql"
} else if (window.location.href.includes("https://uat-vtrcbackoffice.redcross.or.th/")) {
return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
// return "https://uat-vtrcapi.redcross.or.th/api/graphql";
} else if (window.location.href.includes("https://uat-rc-vtrcbackoffice.redcross.or.th/")) {
return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
// return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
// return "https://uat-vtrcapi.redcross.or.th/api/graphql";
} else if (window.location.href.includes("http://localhost:3000/") || window.location.href.includes("http://localhost:3001/")) {
return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
// return "https://uat-vtrcapi.redcross.or.th/api/graphql"
// return "http://localhost:8081/api/graphql"
// return "http://192.168.0.149:8081/api-uat/graphql"
} else if (window.location.href.includes("https://backoffice-vtrc.web.app")) {
return "https://api.vtrcapp.com/api-dev/graphql"
}
}5 env ที่ตรวจด้วย window.location.href.includes(...) ไม่ใช้ NODE_ENV:
| Origin | GraphQL endpoint | ใช้โดย |
|---|---|---|
https://vtrcbackoffice.redcross.or.th/ | https://vtrcapi.redcross.or.th/api/graphql | backoffice prod หลัก |
https://rc-vtrcbackoffice.redcross.or.th/ | https://vtrcapi.redcross.or.th/api/graphql | backoffice prod ศูนย์บริการ (ใช้ backend ตัวเดียวกับด้านบน) |
https://uat-vtrcbackoffice.redcross.or.th/ | https://uat-vtrcapi.redcross.or.th/api-uat/graphql | staging หลัก |
https://uat-rc-vtrcbackoffice.redcross.or.th/ | https://uat-vtrcapi.redcross.or.th/api-uat/graphql | staging ศูนย์บริการ |
http://localhost:3000/ หรือ http://localhost:3001/ | https://uat-vtrcapi.redcross.or.th/api-uat/graphql | dev local (ชี้ที่ staging backend) |
https://backoffice-vtrc.web.app | https://api.vtrcapp.com/api-dev/graphql | Firebase Hosting (env dev แยก) |
หมายเหตุสำคัญ — V1 ไม่ถูกต้อง: V1 เอกสารฉบับเดิมระบุว่ามี RSA encryption สำหรับ login ใน repo vtrc-rc-backoffice นี้ แต่การ grep JSEncrypt, setPublicKey, node-forge, jsencrypt, RSA, publicKey ใน src/ ของ repo นี้ ไม่พบ library RSA ใด ๆ — Login (pages/Login/index.js:55-66) ส่ง password: password เป็น plaintext ผ่าน Mutations.loginBackoffice ไม่มีการ encrypt ฝั่ง client (RSA encryption มีจริงใน vtrc-web และ vtrc-mobile แต่ไม่ใช่ repo นี้) — ความผิดพลาดนี้เกิดจาก V1 copy pattern จาก vtrc-web มาผิด ๆ
Comments ที่ต้องระวัง
apollo.js:21,apollo.js:25-26,apollo.js:29-31— มี commented-out endpoint หลายตัว (uat-vtrcapi.redcross.or.th/api/graphqlไม่มี-uat,http://localhost:8081/api/graphql,http://192.168.0.149:8081/...) — เหล่านี้คือ endpoint ทดลอง ห้าม uncomment โดยไม่ตรวจกับทีม backend
authLink — apiKey + Authorization header
vtrc-rc-backoffice/src/config/apollo.js:48-65
const authLink = setContext((_, { headers }) => {
const { token } = getToken()
const API_DEVICE_KEY = apiKeyNews()
if (token) {
return {
headers: {
...headers,
Authorization: `Bearer ${token}`,
apiKey: API_DEVICE_KEY
}
}
}
return {
headers: {
apiKey: API_DEVICE_KEY
}
}
})getToken()(จากutils/auth.js) — อ่านtokenจาก localStorage/sessionStorageapiKeyNews()(จากutils/apiKey.js) — คืน device key ที่ map กับAPP_TYPE=WEB_ADMIN(หรือค่าที่ custom สำหรับ backoffice)- header
apiKeyส่งทุก request แม้จะยังไม่ login — backend (vtrc-api) ใช้ค่านี้ resolveAPP_TYPEก่อนประมวลผล resolver - header
Authorization: Bearer {token}ส่งเฉพาะเมื่อtokenมีอยู่ — ทำให้หน้า login (Mutations.loginBackoffice) ทำงานได้โดยไม่ต้องส่ง bearer token
errorLink — refresh token + ระบบ error code
vtrc-rc-backoffice/src/config/apollo.js:67-169 ยาว 102 บรรทัดเพราะจัดการ error code 8+ แบบ:
extensions.code | Action | Redirect? |
|---|---|---|
TOKEN_EXPIRED | refresh ผ่าน Queries.refreshTokenBO แล้ว retry operation | ไม่ (ลอง refresh ก่อน) |
TOKEN_INVALID | alert "พบข้อผิดพลาด กรุณาติดต่อผู้ดูแลระบบ" → clearAllStorage() | /login |
INTERNAL_SERVER_ERROR | alert → clearAllStorage() | /login |
SESSION_EXPIRED, SESSION_INVALID | alert + clearAllStorage() | /login |
CANNOT_ACCESS | alert + clearAllStorage() (delay 1s) | /login |
AUTH_USER_OR_PASS_INVALID | alert message จาก backend | ไม่ (อยู่หน้า login) |
AUTH_LIMIT | alert + clearAllStorage() (delay 1s) | /login |
USER_OR_ORG_WRONG | alert message จาก backend | ไม่ |
DATA_USED | alert message จาก backend | ไม่ |
PROFILE_NOT_FOUND | alert + clearAllStorage() (delay 1s) | /login |
NOT_SSO_ID | (ทั้ง block ถูก comment ไว้ — ไม่ทำอะไรเลย) | ไม่ |
(default extensions.code อื่น ๆ) | alert message จาก backend | ไม่ |
Token refresh logic — apollo.js:67-105
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors && graphQLErrors[0].extensions.code === 'TOKEN_EXPIRED') {
return new Observable(async observer => {
const oldHeaders = operation.getContext().headers
const { errors, data } = await apolloClient.query({
query: Queries.refreshTokenBO,
variables: {
refreshToken: getToken().refreshToken,
token: getToken().token
},
});
console.log('result refreshTokenBO : appllo :', { errors, data })
if (!errors) {
setToken(data.refreshTokenBO)
operation.setContext({
headers: {
...oldHeaders,
Authorization: `Bearer ${data.refreshTokenBO.accessToken}`
}
})
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
}
forward(operation).subscribe(subscriber)
} else if (errors && errors[0].extensions.code === 'TOKEN_INVALID') {
alertMessage("error", "พบข้อผิดพลาด กรุณาติดต่อผู้ดูแลระบบ")
clearAllStorage()
window.location.href = '/login'
}
})
}
// ... ระบบอื่น ๆจุดที่น่าสนใจ:
- refresh ใช้ query ชื่อ
refreshTokenBO(มี suffixBO= BackOffice) ไม่ใช่refreshTokenธรรมดา — backend แยก flow refresh ระหว่าง user-side (refreshToken) และ backoffice (refreshTokenBO) - หลัง refresh สำเร็จ เรียก
setToken(data.refreshTokenBO)เก็บ token ใหม่ แล้ว set context ใหม่ก่อนforward(operation)retry - ถ้า refresh เองก็ fail ด้วย
TOKEN_INVALID→ logout ทันที console.log('result refreshTokenBO : appllo :', ...)ที่apollo.js:78— ทำงานจริง แต่ถูกปิดผลกระทบโดยApp/index.js:17(console.log = () => {}) ทำให้ไม่แสดงใน browser console- ไม่มี retry limit — ถ้า refresh loop (token หมด → refresh → token หมด → refresh → ...) ไม่มีกลไกตัด ต้องพึ่ง browser abort หรือ user ปิด tab
apolloClient — singleton + defaultOptions
vtrc-rc-backoffice/src/config/apollo.js:171-187
const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, uploadLink]),
cache: new InMemoryCache()
})
apolloClient.defaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore'
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all'
}
}
export default apolloClientสิ่งที่ต้องระวัง:
fetchPolicy: 'no-cache'— ทุก query ดึงจาก backend ทุกครั้ง ไม่ใช้ cache ฝั่ง Apollo ทำให้ UI อัปเดตเสมอแต่ network load สูง (เหมาะกับ backoffice ที่ต้องการข้อมูลใหม่)errorPolicy: 'ignore'สำหรับwatchQuery— error จาก GraphQL ไม่ทำให้ component crash แต่ก็ไม่เข้าerrorของuseQueryด้วย — error ถูก handle ที่errorLinkเท่านั้นerrorPolicy: 'all'สำหรับquery— error เข้าทั้งdataและerrorของ promise- link order:
errorLink → authLink → uploadLink— errorLink อยู่นอกสุดเพื่อ catch error ทั้งหมด, authLink แทรก header ก่อน uploadLink ส่ง request จริง
apolloFetch + uploadLink — สองทางเข้า
vtrc-rc-backoffice/src/config/apollo.js:38-46
const apolloFetch = createApolloFetch({ uri: checkLinkURL() })
const uploadLink = createUploadLink({ uri: checkLinkURL() })
apolloFetch.use(({ request, options }, next) => {
options.headers = {
apiKey: apiKeyNews(),
};
next();
});apolloFetch— ใช้สำหรับapolloClient.query({ fetchPolicy: 'no-cache' })บาง flow (เช่น refresh token) ที่ต้องการ raw fetch ไม่ผ่าน Apollo cacheuploadLink— จากapollo-upload-clientรองรับ file upload ผ่าน GraphQL multipartapolloFetchเพิ่มapiKeyheader เท่านั้น ไม่ได้เพิ่มAuthorization— ใช้สำหรับ query ที่ไม่จำเป็นต้อง login (เช่น refreshToken เอง)
Routing — src/config/routes.js + SiderLayout wiring
routes.js ยาว 2,551 บรรทัด — เป็นไฟล์ใหญ่ที่สุดใน src/config/ มีทั้ง import statements, route definitions (<Route path=... component={...} />), helper functions และ SiderLayout wiring
vtrc-rc-backoffice/src/config/routes.js:1-40 (import statements — เห็นการ mix module เก่าใหม่):
import React from "react";
import { Route, Switch } from "react-router-dom";
import NotFound from "../pages/NotFound";
import News from "../pages/News";
import FormNews from "../pages/News/form";
import Organization from "../pages/Organization";
import Formorganization from "../pages/Organization/form";
import Career from "../pages/Career";
import FormCareer from "../pages/Career/form";
import Manageuser from "../pages/ManageUser";
import ManageuserMenu from "../pages/ManageUser/menu";
import Course from "../pages/Course";
import CategoryCourse from "../pages/WorkForce/CategoryCourse";
import Prefix from "../pages/WorkForce/Prefix";
import FormCourse from "../pages/Course/form";
import Notification from "../pages/Notification";
import Policy from "../pages/Policy";
import PolicyForm from "../pages/Policy/form";
import PolicyCategory from "../pages/Policy/category";
import HospitalSearchForm from "../pages/HospitalHr/Search";
import HospitalForm from "../pages/HospitalHr/Form";
// ... 2,511 บรรทัดเพิ่มเติมโครงสร้าง routes.js
ไฟล์แบ่งเป็น 4 ส่วนใหญ่ ๆ:
- Imports (~400 บรรทัด) — import ทุก page component ที่จะ render ใน SiderLayout
- Route table (~1,200 บรรทัด) —
<Route exact path="/news" component={News} />ลำดับตาม feature - Helper functions (~500 บรรทัด) — function สำหรับ render route ตาม role/menu permission (
checkMenuPermission,renderRouteIfAccessible, etc.) - SiderLayout wiring (~400 บรรทัด) — mapping route → menu item → icon → breadcrumb
ลำดับ route ที่สำคัญ
| Route | Component | หมวด |
|---|---|---|
/news, /news/form/:id? | News, FormNews | News/announcement |
/organization, /organization/form/:id? | Organization, Formorganization | Organization master |
/career, /career/form/:id? | Career, FormCareer | Career master |
/manageuser, /manageuser/menu/:id | Manageuser, ManageuserMenu | User/role CRUD |
/course, /course/form/:id? | Course, FormCourse | Course master |
/notification | Notification | Notification blast |
/policy, /policy/form/:id?, /policy/category | Policy, PolicyForm, PolicyCategory | Policy document |
/hospitalhr/search, /hospitalhr/form/:id?, /hospitalhr/withdraw/... | HospitalSearchForm, HospitalForm, HospitalWithdraw* | Hospital HR |
/hospitalpaid/payments/... | HospitalPaidPayments, etc. | Hospital payment |
/payment/paid, /payment/cash, /payment/exportpayment/... | PaymentPagePaid, PaymentPagCash, PaymentPagExportPayment* | Payment workflow |
/WorkForce/... | (many) | WorkForce module (เริ่มปี 2022) |
/pms/... | (many) | PMS module |
/recruitment/... | (many) | Recruitment module |
PathLogin — SSO bypass route
vtrc-rc-backoffice/src/containers/App/index.js:24
<Route path="/pathLogin/:token/:refreshToken" exact component={PathLogin}/>route นี้รับ token และ refreshToken ผ่าน URL path (ไม่ใช่ query string) สำหรับ SSO จากระบบอื่น — component PathLogin อ่าน params, เก็บลง localStorage, redirect ไป / ทำให้ isLoggedIn() คืน true
ความเสี่ยงด้าน security: token ทั้งคู่ปรากฏใน browser history, server access log, referer header (ถ้ามี link ออก) — severity S3 ใน บท 4.2.5
TheamCss — typo route
vtrc-rc-backoffice/src/containers/App/index.js:23
<Route path="/TheamCss" exact component={TheamCss} />TheamCss (ควรเป็น ThemeCss) เป็น dev tool สำหรับ preview CSS — ไม่มี auth gate (mount ก่อน isLoggedIn() check) ผู้พัฒนาใช้ตรวจ styling โดยไม่ต้อง login ห้ามเผยแพร่ URL นี้เพราะ bypass auth
CRA build — Dockerfile + nginx
vtrc-rc-backoffice/Dockerfile:1-17
FROM node:16 AS stage-1
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --legacy-peer-deps
COPY . .
RUN CI=false GENERATE_SOURCEMAP=false NODE_OPTIONS=--max-old-space-size=4096 npm run build
FROM nginx:1.24.0 AS stage-2
COPY ./nginx/default.conf /etc/nginx/conf.d/
COPY --from=stage-1 /usr/src/app/build/ /usr/share/nginx/html/build/ขั้นตอนใน Docker
- stage-1 — base
node:16ลง dependency ด้วยnpm install --legacy-peer-deps(จำเป็นเพราะ peer dep conflict ระหว่างreact-apollo+@apollo/client+ MUI v5) แล้วnpm run buildปิด source map (GENERATE_SOURCEMAP=false) เพิ่ม heap 4GB (NODE_OPTIONS=--max-old-space-size=4096) เพราะ Webpack ของ CRA 3.4.3 out-of-memory ถ้าไม่เพิ่ม - stage-2 — base
nginx:1.24.0คัดลอกnginx/default.confและ static จากbuild/ไปที่/usr/share/nginx/html/build/ - nginx listen port 80 (ไม่ใช่ 3000) — SPA fallback ผ่าน
try_files $uri $uri/ /index.html
ข้อระวังเรื่อง Docker
- ไม่ใช้
serve -s— production image เป็น nginx ไม่ใช่ Node + serve COPY . .คัดลอกbuild/ที่มีอยู่แล้วมาด้วย ถ้า repo มีbuild/commit ไว้ทำให้ image ใหญ่และ build ช้า —build/ที่ commit อยู่ใน repo (จากการ deploy manual ครั้งเดือนที่แล้ว) มีขนาด ~130 MBCI=false— ปิด CRA warning-as-error ที่ CRA ทำตอน CI env (ไม่งั้น warning เรื่อง<forge>หรือ source map จะ fail build)- Firebase Hosting (
backoffice-vtrc.web.app) เป็นอีกช่องทาง deploy ที่checkLinkURL()รองรับแยกต่างหาก (ดูตาราง env ด้านบน)
npm scripts
vtrc-rc-backoffice/package.json (scripts section):
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}script ทั้งหมดมาจาก CRA default — ไม่มี custom script
Bundle size — ทำไมใหญ่
CRA ไม่มี built-in bundle analyzer แต่จากการประเมิน package.json dependencies:
| หมวด | Library | ขนาดประมาณการ |
|---|---|---|
| UI | antd@4.15 (full import) | ~1.2 MB |
| UI | @mui/*@5.11 (ใช้น้อย) | ~300 KB |
| State | recoil@0.7 | ~50 KB |
| GraphQL | react-apollo@3.1 + apollo-client + apollo-link-* + apollo-upload-client + apollo-cache-inmemory + apollo-fetch | ~280 KB |
| Date | moment@2.29 (with locale) | ~300 KB |
| Charts | chart.js@3 + react-chartjs-2 | ~250 KB |
| Report | stimulsoft-reports-js | ~5 MB |
| Excel | xlsx@0.18, exceljs | ~800 KB |
@react-pdf/renderer, react-pdf, pdfjs-dist | ~2 MB | |
| Crypto | crypto-js | ~120 KB |
| Misc | lodash, axios, qrcode, react-router-dom, ... | ~1 MB |
รวมประมาณ 11-13 MB (gzip ~3-4 MB) เพราะ:
- ไม่มี code splitting —
React.lazy+Suspenseแทบไม่มี Stimulsoft ถูก load ตั้งแต่ page load แม้ user จะไม่ได้ใช้ report - AntD v4 import แบบ full — ไม่ได้ใช้
babel-plugin-importสำหรับ tree-shake - MUI v5 อยู่ใน dependencies แต่ใช้แค่ไม่กี่ไฟล์ — น่าจะเริ่มทดลองแต่ยังไม่ migrate
- Stimulsoft ใหญ่มาก (~5 MB) แต่ใช้เฉพาะหน้า report — ควรแยก chunk ด้วย
React.lazy(() => import('./StimulsoftReport'))
.env — ไม่ขับ GraphQL
vtrc-rc-backoffice/.env (committed — เป็น debt S2 ใน บท 4.2.5) มีตัวแปรเช่น:
REACT_APP_END_POINT_RECRUITMENT_API_ATTACHFILEREACT_APP_END_POINT_EXPORTREACT_APP_AI_RECRUITMENT_APIREACT_APP_PMS_API_END_POINTREACT_APP_STIMUL_CONN_STRINGREACT_APP_PRIVATE_KEY
ไม่มี REACT_APP_API_URL และใน src/ ไม่มีโค้ดที่อ่าน REACT_APP_API_URL
GraphQL endpoint ของ Apollo หลักตัดสินด้วย checkLinkURL() ใน src/config/apollo.js ตาม window.location.href เท่านั้น (ดูตาราง env ด้านบน)
axios client สำหรับ microservice (axios-pms.js, axios-ai.js, axios-bo.js) อ่าน REACT_APP_* ของตัวเอง — คนละเรื่องกับ GraphQL
browserslist
vtrc-rc-backoffice/package.json (browserslist):
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}เป้าหมายครอบคลุมเบราว์เซอร์ที่มีส่วนแบ่ง > 0.2% ทั่วโลก ไม่รวม "dead" (IE, Opera Mini ฯลฯ)
เปรียบเทียบกับ vtrc-web
| ด้าน | vtrc-rc-backoffice | vtrc-web |
|---|---|---|
| Build system | CRA 3.4.3 (ไม่ eject) | Webpack 4 (eject แล้ว) |
| Webpack config | default ของ CRA | custom ใน config/webpack.config.dev.js |
| Node ใน Docker (build) | 16 | 14 |
| Static serve ใน Docker | nginx:1.24.0 | (ดู vtrc-web ch.6) |
console.* ปิด | ใช้ (App/index.js:17) | ใช้ |
| Bundle | ~11-13 MB | ~12 MB |
| Code splitting | แทบไม่มี | แทบไม่มี |
| Login | Mutations.loginBackoffice (plaintext, no RSA) | Mutations.login + RSA encrypt (JSEncrypt) |
| RSA encryption | ไม่มี | มี (utils/rsa.js, JSEncrypt) |
| Apollo Client | apollo-client@2.x + react-apollo@3.1 + apollo-link-* | เดียวกัน |
| State | Recoil 0.7 + class component this.state (mix) | class component this.state เกือบล้วน |
| AntD | v4.15 + custom override 13K บรรทัด | v3.x + custom override 21K บรรทัด |
| MUI | v5.11 (ใช้น้อย) | ไม่มี |
| GraphQL queries | 121 queries + 94 mutations (รวม WorkForce module แยก) | ดู vtrc-web ch.2 |
| SSO bypass route | /pathLogin/:token/:refreshToken | ไม่มี |
| Routing | react-router-dom 5 + routes.js 2,551 บรรทัด | react-router-dom 5 + routes.js 1,410 บรรทัด |
jsconfig.json absolute import | ไม่มี | มี |