๋ณธ ํ๋ก์ ํธ๋ ์ฑ๋ ํ์ด๋ฐ์ด์ (Channel Foundation, CF) ์ ํ๋ก ํธ์๋ ๋ ํฌ์งํ ๋ฆฌ์ ๋๋ค. ํน์ ์๋น์ค๊ฐ ์๋๋ผ, ์น ํ๋ก์ ํธ๋ฅผ ์์ํ ๋ ๊ฐ์ ธ๊ฐ ์ฐ๋ React ๊ธฐ๋ฐ ํ๋ก ํธ์๋ ํ์ด๋ฐ์ด์ (์คํํฐํท) ์ ์งํฅํฉ๋๋ค.
๋น์ฆ๋์ค ๋ก์ง์ ์ต์ํํ๊ณ , ๋ค์ ์์ญ์ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ด ๋ชฉ์ ์ ๋๋ค.
- ๋ผ์ฐํ , ์๋ฒ ํต์ (HTTP/์ธ์ฆ), ์ ์ญ ์ํ ๋ฑ ๊ณตํต ๊ธฐ๋ฐ ๊ตฌ์ฑ
- ์๋ฒ OpenAPI ์คํ โ ํ๋ก ํธ ์คํ(ํ์ ยท์ฟผ๋ฆฌยทAPI ํด๋ผ์ด์ธํธ) ์๋ํ
- ๋ชจ๋ ธ๋ ํฌ ํด๋ ๊ตฌ์กฐ์ Feature-Sliced Design (FSD) ์ค๊ณ ๊ท๊ฒฉ
๊ธฐ์ ์คํ์ ์ด๊ณ ์ ๋น๋ ์ฑ๋ฅ๊ณผ ๊ทน๋ํ๋ DX(Developer Experience)๋ฅผ ์งํฅํ๋ React 19.2 + TypeScript 6 + Vite 8 ๊ธฐ๋ฐ์ ๋๋ค.
pnpm Workspaces + Turborepo ๊ธฐ๋ฐ์ ๋ชจ๋
ธ๋ ํฌ๋ก ๊ตฌ์ฑ๋๋ฉฐ, Feature-Sliced Design (FSD) ์ค๊ณ ๊ท๊ฒฉ๊ณผ ๋จ์ผ ๊ณต์ ํจํค์ง(@bx/shared) ์ํคํ
์ฒ๋ฅผ ๋ฐ๋ฆ
๋๋ค. PCยท๋ชจ๋ฐ์ผยท๊ด๋ฆฌ์ ์น์ ๊ฐ๊ฐ ๋
๋ฆฝ๋ FSD ์ ํ๋ฆฌ์ผ์ด์
์ด๋ฉฐ, ๋๋ฉ์ธ ๋ก์งยทUIยทHTTP ํต์ ยท์ธ์ฆ์ @bx/shared์์ ๊ณต์ ํฉ๋๋ค.
| ์ฃผ์ ๋๊ตฌ | ๋ฒ์ |
|---|---|
| React | 19.2.x |
| TypeScript | 6.0.x |
| Vite | 8.0.x |
| TanStack Query | 5.100.x |
| TanStack Router | 1.130.x |
| SVGR | vite-plugin-svgr 5.2.x |
| pnpm | 11.1.3 |
pnpm install์ฐ๊ฒฐํ ๋ฐฑ์๋๋ฅผ ๋ช
๋ น์ผ๋ก ์ ํํฉ๋๋ค. ์๋ฅผ ๋ค์ด PC ์ฑ์ ๋ก์ปฌ Nest(http://127.0.0.1:18081)์ ์ฐ๊ฒฐํ๋ ค๋ฉด:
pnpm dev:pc:nest์๊ฒฉ ๋ฐฑ์๋๋ :remote, ๋ก์ปฌ Nest๋ :nest, Mock์ :mock์ ์ฌ์ฉํฉ๋๋ค.
pnpm dev:pc:remote
pnpm dev:mobile:nest
pnpm dev:admin:mockMock ๋ชจ๋๋ API ์๋ฒ๋ฅผ ๋ณ๋ ํฐ๋ฏธ๋์์ ํจ๊ป ์คํํด์ผ ํฉ๋๋ค.
pnpm dev:server
pnpm dev:pc:mock๊ฐ๋ ํฌํธ
- ๐ป PC ์น: http://localhost:3000
- ๐ฑ ๋ชจ๋ฐ์ผ ์น: http://localhost:3001
- โ๏ธ ๊ด๋ฆฌ์ ํฌํ: http://localhost:3002
- ๐ก Mock API: http://127.0.0.1:3333
apps/storybook ํ workspace๊ฐ HubยทSharedยทPCยทAdminยทMobile์ ๋ค์ฏ config profile์ ๊ด๋ฆฌํฉ๋๋ค. ๊ฐ ๋์ Storybook์ ์๊ธฐ ์ฑ์ ์ ์ญ CSS์ source story๋ง ๋ก๋ํ๊ณ , Hub๋ Composition์ผ๋ก ๋ค ์นดํ๋ก๊ทธ๋ฅผ ํ ์ฌ์ด๋๋ฐ์์ ์ฐ๊ฒฐํฉ๋๋ค.
pnpm storybook # Hub์ ๋ค ๋์ Storybook ๋์ ์คํ
pnpm storybook:hub # Hub๋ง ์คํ
pnpm storybook:shared # Shared๋ง ์คํ
pnpm storybook:pc # PC๋ง ์คํ
pnpm storybook:admin # Admin๋ง ์คํ
pnpm storybook:mobile # Mobile๋ง ์คํ
pnpm build:storybook # ๋ค์ฏ Storybook ์ ์ ๋น๋| ์นดํ๋ก๊ทธ | ๋ก์ปฌ URL | ์ด๊ธฐ ์ ์ ๋ฒ์ |
|---|---|---|
| Hub | http://localhost:6005 | Composition ํ์ |
| Shared | http://localhost:6006 | CSS token, ๊ณตํต Button |
| PC | http://localhost:6007 | CSS token, PopoverPanel |
| Admin | http://localhost:6008 | CSS token, AdminFilterBar |
| Mobile | http://localhost:6009 | CSS token, AiSearchCard (390 ร 844 viewport) |
์ฌ์ฌ์ฉ ์ปดํฌ๋ํธ story๋ packages/shared์ ์๋ณธ ์ปดํฌ๋ํธ ์์, ์ฑ ์ ์ฉ story๋ ๊ฐ ์ฑ์ ์๋ณธ ์์ ๋ก๋๋ค. apps/storybook/stories์๋ token์ฒ๋ผ ์ฌ๋ฌ ์นดํ๋ก๊ทธ๊ฐ ํจ๊ป ์ฐ๋ ์ ์์ฉ story๋ง ๋ก๋๋ค.
ํ์ฌ ๋จ๊ณ๋ ์ ์ฅ์์ CSS ๋ณ์๋ฅผ token source๋ก ์ฌ์ฉํ๋ ๊ฒฝ๋ POC์ ๋๋ค. Figma ์ฐ๋, token JSON ์์ฑ, MSW, router/auth provider, interaction test๋ ์ฌ์ฉ ์ฌ๋ก๊ฐ ํ์ธ๋ ๋ค ํ์ฅํฉ๋๋ค.
๊ฐ๋ฐ ์ค์๋ ์์ค๋ฅผ ์์ ํ์ง ์๊ณ Vite mode์ ์ฑ๋ณ ๋ฌผ๋ฆฌ ํ๊ฒฝ ํ์ผ๋ก ์ฐ๊ฒฐ ๋์์ ์ ํํฉ๋๋ค. remote์ nest๋ ๋ธ๋ผ์ฐ์ ๊ฐ ๊ฐ์ origin์ Vite dev server๋ก ์์ฒญํ๊ณ , Vite proxy๊ฐ ์ ํํ ๋ฐฑ์๋ ์๋ฒ๋ก ์ ๋ฌํด CORS/ํฌ๋ก์ค๋๋ฉ์ธ ์ฟ ํค ๋ฌธ์ ๋ฅผ ํผํฉ๋๋ค.
| Mode | ํ๊ฒฝ ํ์ผ | ์ฑ API URL | Vite proxy target | ์คํ ์์ |
|---|---|---|---|---|
remote |
.env.remote |
/channel/backend/api/v1 |
http://192.168.110.217 |
pnpm dev:pc:remote |
nest |
.env.nest |
/channel/backend/api/v1 |
http://127.0.0.1:18081 |
pnpm dev:pc:nest |
mock |
.env.mock |
http://127.0.0.1:3333 |
์ฌ์ฉํ์ง ์์ | pnpm dev:server + pnpm dev:pc:mock |
production |
.env.production |
/channel/backend/api/v1 |
Nginx same-origin | pnpm build |
ํต์ฌ: Mock ์๋ฒ(mock/server.js)๋ Nest์ ๋์ผํ ๊ณ์ฝ(๊ณตํต envelope + JWT ์ธ์ฆ)์ ํ๋ด๋ ๋๋ค. ๋ฐ๋ผ์ ์ฑ์ ๋จ์ผ ์ฝ๋ํจ์ค๋ก ๋์ํ๋ฉฐ, mock์์ ๊ฒ์ฆํ ์ธ์ฆยทํต์ ๋ก์ง์ด Nest ์ฐ๋ ์ ๊ทธ๋๋ก ์ ์ง๋ฉ๋๋ค. (
pnpm dev:server๋ก ๊ตฌ๋, Node ๋ด์ฅ ๋ชจ๋๋ง ์ฌ์ฉํด ์์กด์ฑ ์์)
๊ฐ ์ฑ์ ์๋ ํ์ผ์ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ์ง๋๋ค.
apps/pc-web/.env.remote
apps/pc-web/.env.nest
apps/pc-web/.env.mock
# apps/mobile-web, apps/admin-portal์๋ ๊ฐ์ ์ธ ํ์ผ์ด ์์ต๋๋ค.- ํ๊ฒฝ ํ์ผ์ ๊ฐ ์ฑ ๋๋ ํ ๋ฆฌ์ ์์นํด์ผ ํฉ๋๋ค. ๋ฃจํธ
.env๋ Vite ์ฑ์ด ์ฝ์ง ์์ต๋๋ค. - ๊ฐ์ธ๋ณ ์ฃผ์ ๋ณ๊ฒฝ์ ์ถ์ ํ์ผ์ ์์ ํ์ง ๋ง๊ณ ๊ฐ์ ์ฑ์
.env.remote.local,.env.nest.local,.env.mock.local์ ๋ง๋ญ๋๋ค. ์ด ํ์ผ๋ค์ Git์์ ์ ์ธ๋ฉ๋๋ค. VITE_API_URL์ ์๋ ๊ฒฝ๋ก๋ก ๋๋ฉด ์ฑ์vite.config.tsproxy๋ฅผ ํ๊ณ , ์ ๋ URL๋ก ๋๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ํด๋น ์๋ฒ๋ก ์ง์ ์์ฒญํฉ๋๋ค.- entity API wrapper์๋
/system/menus/list๊ฐ์ ์๋ API ๊ฒฝ๋ก๋ง ๋๊ณ ํธ์คํธ ์ฃผ์๋ฅผ ๋ฃ์ง ์์ต๋๋ค. API_DOCS_URLS๋pnpm gen:api๊ฐ OpenAPI ๋ฌธ์๋ฅผ ๊ฐ์ ธ์ฌ ์ฃผ์์ด๋ฉฐ, ์ฑ์ ๋ฐํ์ ๋ฐฑ์๋ ์ ํ๊ณผ๋ ๋ณ๊ฐ์ ๋๋ค.- Mock ์๋ฒ๋
db.json์ ๊ทธ๋๋ก ์๋นํ๋ ๋ชจ๋ ์๋ต์ envelope๋ก ๊ฐ์ธ๊ณ ,/auth/*์๋ํฌ์ธํธ๋ Nest ํํ์ ๊ฐ์ง ํ ํฐ(๋จผ ๋ฏธ๋ ๋ง๋ฃ)์ ๋ฐ๊ธํฉ๋๋ค. (db.json๋ณ๊ฒฝ ์ mock ์๋ฒ ์ฌ์์ ํ์)
์ค์๋ฒ ์๋ต์ ๊ณตํต๋ถ(success/code/msg)์ ๋ฐ์ดํฐ๋ถ(payload)๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. httpService๊ฐ payload๋ฅผ ์๋ ์ธ๋ํํ์ฌ ๋ฐํํฉ๋๋ค.
| ์ฝ๋ | ์์ | ์ค๋ช |
|---|---|---|
-1001 |
REQUIRED_VALUE_MISSING | ํ์ ์ ๋ ฅ๊ฐ ๋๋ฝ |
-1002 |
INVALID_TOKEN | ์ ํจํ์ง ์์ ํ ํฐ โ ๋ก๊ทธ์์ |
-1003 |
UNAUTHORIZED_CLIENT | ์ธ์ฆ๋์ง ์์ ํด๋ผ์ด์ธํธ โ ๋ก๊ทธ์์ |
-1004 |
EXPIRED_TOKEN | ํ ํฐ ๋ง๋ฃ โ ์๋ ์ฌ๋ฐ๊ธ(refresh) |
-1005 |
ACCESS_DENIED | ๋ฆฌ์์ค ์ ๊ทผ ๊ถํ ์์ (๋ก๊ทธ์์ X) |
-2003/-2004 |
JSON_*_PARSING | JSON ์ง๋ ฌํ/์ญ์ง๋ ฌํ ์ค๋ฅ |
-4001/-4002 |
DB_*_ERROR | DB ์กฐํ/์ ์ฅ ์ค๋ฅ |
-4003 |
๋ฐฑ์๋ ์ ์ | Database access error (ํ์ฌ FE API_ERROR_CODE์๋ ๋ฏธ๋ฑ๋ก) |
-9999 |
SERVER_ERROR | ์๋ฒ ๋ด๋ถ ์ค๋ฅ |
Nest ๋ฐฑ์๋ ์ฐ๋ ์ JWT ๊ธฐ๋ฐ ์ธ์ฆ์ด ๋์ํฉ๋๋ค. ํ ํฐ ๋ถ์ฐฉยท๋ง๋ฃ ์ ์๋ ์ฌ๋ฐ๊ธยท์ธ์ฆ ์คํจ ์ฒ๋ฆฌ๋ @bx/shared์ ์บก์ํ๋์ด ์์ผ๋ฉฐ, ์ฑ์ main.tsx์์ ํ ๋ฒ๋ง ์ฃผ์
ํฉ๋๋ค.
// apps/[app]/src/main.tsx
httpService.init({
baseURL: API_URL,
timeout: API_CONFIG.TIMEOUT,
auth: createHttpAuthConfig(),
});- ๋น๋ฐ๋ฒํธ๋ฅผ
sha256()๋ก ํด์ฑ โPOST /auth/login({ usrId, usrPwd }) - ์๋ต
payload(์ฌ์ฉ์ ์ ๋ณด + accessToken + accessTokenExpiresAt)๋ฅผuseAuthStore.setAuth()๋ก ์ ์ฅ - refreshToken ๊ฐ์ ์๋ฒ๊ฐ HttpOnly Cookie๋ก ๊ด๋ฆฌํ๊ณ , FE๋ localStorage์ ์ ์ฅํ์ง ์์
- accessToken ๋ฐ accessTokenExpiresAt์ localStorage์ ๋ณด๊ด(ํญ ๊ฐ ๊ณต์ ยท์๋ก๊ณ ์นจ ์ ์ง)
- ์์ฒญ: ๋ชจ๋ ์์ฒญ์
Authorization: Bearer <accessToken>์๋ ๋ถ์ฐฉ - ์๋ต (
-1004๋๋ HTTP 401):POST /auth/refresh-token์ผ๋ก ์ฌ๋ฐ๊ธ ํ ์์์ฒญ ์๋ ์ฌ์๋- refreshToken์ request body๊ฐ ์๋๋ผ HttpOnly Cookie๋ก ์ ์ก(
withCredentials: true) - ๋์ ๋ค๋ฐ ์์ฒญ์ single-flight๋ก refresh 1ํ๋ง ํธ์ถ
- ์ฌ๋ฐ๊ธ ์คํจ โ ๋ก๊ทธ์์ +
/login๋ฆฌ๋ค์ด๋ ํธ
- refreshToken์ request body๊ฐ ์๋๋ผ HttpOnly Cookie๋ก ์ ์ก(
- ์๋ต (
-1002/-1003): ์ฌ๋ฐ๊ธ ๋ถ๊ฐ โ ์ฆ์ ๋ก๊ทธ์์ - ์๋ต (
-1005): ์ธ๊ฐ(๊ถํ) ์ค๋ฅ โ ๋ก๊ทธ์์ํ์ง ์๊ณ ์๋ฌ ๊ทธ๋๋ก ์ ๋ฌ
๋ณดํธ ๋ผ์ฐํธ((page)/_page)๋ beforeLoad์์ requireAuth๋ฅผ ๋จผ์ ์ํํฉ๋๋ค. ensureValidAuthSession()์ accessTokenExpiresAt์ด ์ ํจํ๋ฉด ๋ฐ๋ก ํต๊ณผํ๊ณ , ๋ง๋ฃ๋๊ฑฐ๋ accessToken์ด ์์ผ๋ฉด refresh ์ฟ ํค๋ก accessToken ์ฌ๋ฐ๊ธ์ ์๋ํฉ๋๋ค. ์ ํจํ ์ธ์
๊ณผ ์ฌ์ฉ์ ID๊ฐ ๋ชจ๋ ์์ด์ผ ๋ณดํธ ํ๋ฉด์ ์ง์
ํฉ๋๋ค.
์ธ์ฆ ํต๊ณผ ํ PCยทAdminยทMobile์ ์ฑ๊ณผ ์ฌ์ฉ์ ID๋ฅผ ์กฐํฉํ ๋ฉ๋ด ์บ์ scope๋ก ensureBaseInfoBootstrapped๋ฅผ ํธ์ถํ๊ณ , ๊ธฐ์ค์ ๋ณด ์ค๋น๊ฐ ๋๋ ๋ค ํ๋ฉด์ ๋ก๋ฉํฉ๋๋ค.
PCยทAdminยทMobile ๋ณดํธ ํ๋ฉด ์ต์ด ์ง์ ์ ์ฝ๋/๋ฉ๋ด ๊ธฐ์ค์ ๋ณด๋ฅผ ์ค๋นํฉ๋๋ค. ์คํ ์ค ์๋ฌ๊ฐ ๋๋ ํ๋ฉด ์ง์ ์ ๋ง์ง ์๊ณ , ๊ฐ๋ฅํ ๊ฒฝ์ฐ ๊ธฐ์กด localStorage ์บ์๋ฅผ ์ฌ์ฌ์ฉํฉ๋๋ค.
POST /system/reference-data/versions/latest๋ก ๊ธฐ์ค์ ๋ณด ๋ฒ์ ์ ์กฐํํฉ๋๋ค. (refType/versionNoโtype/version์ผ๋ก ์ ๊ทํ)- ํ์
๋ณ localStorage ์บ์๊ฐ
serverVersion + schemaVersion๊ธฐ์ค์ผ๋ก ์ต์ ์ธ์ง ํ์ธํฉ๋๋ค. - ์ต์ ์บ์๊ฐ ์์ผ๋ฉด API๋ฅผ ํธ์ถํ์ง ์๊ณ ์บ์๋ฅผ sessionStorage์ ์ฃผ์ ํฉ๋๋ค.
- ์ต์ ์บ์๊ฐ ์์ผ๋ฉด ํด๋น ํ์
๋ง ์ฌ์กฐํํฉ๋๋ค.
- ๋ฉ๋ด:
POST /system/menus/list - ์ฝ๋:
POST /system/common-codes/groups/listํ ๊ทธ๋ฃน๋ณPOST /system/common-codes/groups/{groupCd}/codes/list
- ๋ฉ๋ด:
- ์กฐํ ๊ฒฐ๊ณผ๋ฅผ localStorage์ ์ ์ฅํ๊ณ , ํ๋ฉด์์ ์ฐ๋
CONFIG.SESSION.CODE/CONFIG.SESSION.MENU_LIST์ ์ฃผ์ ํฉ๋๋ค.
| ๋ ์ด์ด | ์ญํ |
|---|---|
| React Query | ์ฑ ์คํ ์ธ์
์์ ensureQueryData ์ค๋ณต ์คํ ๋ฐฉ์ง (baseInfoQueryKeys.bootstrap(menuCacheScope)) |
| localStorage | ์๋ก๊ณ ์นจ/์ฌ์ ์ ํ์๋ ๊ธฐ์ค์ ๋ณด ์ฌ์ฌ์ฉ (base-info:CODE, base-info:MENU:<app>:<usrId>) |
| sessionStorage | ๊ธฐ์กด ํ๋ฉด ๋ก์ง์ด ์ฐธ์กฐํ๋ ๋ฐํ์ ์ธ์
๋ฐ์ดํฐ (CONFIG.SESSION.CODE, CONFIG.SESSION.MENU_LIST) |
๊ณตํต์ฝ๋ ์บ์๋ ๋ชจ๋ ์ฑ์ด ๋์ผํ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ ๋ก base-info:CODE ํ๋๋ฅผ ๊ณต์ ํฉ๋๋ค. ๋ฉ๋ด๋ ์ฑ๊ณผ ์ฌ์ฉ์๋ณ ๊ตฌ์ฑ์ด ๋ค๋ฅผ ์ ์์ผ๋ฏ๋ก pc:<usrId>, admin:<usrId>, mobile:<usrId> scope๋ก ๋ถ๋ฆฌํฉ๋๋ค.
๋ฐฐํฌ ์์ ์ ๊ณ ์ ๋๋ ์ฝ๋ ๋ชฉ๋ก์ packages/shared/src/shared/constants/local-codes.ts์ LOCAL_CODE_MAP์์ ๊ด๋ฆฌํฉ๋๋ค. ํ๋ฉด์ ์ฝ๋ ์ถ์ฒ๋ฅผ ๊ตฌ๋ถํ์ง ์๊ณ groupCd๋ง ์ ๋ฌํ๋ฉฐ, $codeUtils์ Select๋ ๋ค์ ์ฐ์ ์์๋ก ์ฝ๋๋ฅผ ์กฐํํฉ๋๋ค.
- sessionStorage์ ์ ์ฌ๋ ์๋ฒ ์ฝ๋ ๊ทธ๋ฃน
- ์๋ฒ์ ํด๋น ๊ทธ๋ฃน์ด ์์ผ๋ฉด
LOCAL_CODE_MAP - ์๋ฒ์ ๋ก์ปฌ์ ๋ชจ๋ ์์ผ๋ฉด ๋น ๋ฐฐ์ด
์๋ฒ ๊ทธ๋ฃน์ด ๋น ๋ฐฐ์ด๋ก ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ ์๋ฒ๊ฐ ์๋์ ์ผ๋ก ๋น ๋ชฉ๋ก์ ์ ๊ณตํ ๊ฒ์ผ๋ก ๋ณด๊ณ ๋ก์ปฌ ์ฝ๋๋ก ๋์ฒดํ์ง ์์ต๋๋ค. ๋ก์ปฌ ์ฝ๋๋ ๋ฒ๋ค์ ํฌํจ๋๋ ์ ์ ๋ฐ์ดํฐ์ด๋ฏ๋ก React Query๋ localStorage์ ๋ณ๋๋ก ์ ์ฅํ์ง ์์ต๋๋ค.
Select์ FormSelect๋ ๋ณ๋์ placeholder๋ฅผ ์ ๊ณตํ์ง ์๊ณ ์ฒซ ๋ฒ์งธ ๋ณด์กฐ ํญ๋ชฉ์ emptyOption์ผ๋ก ํต์ผํฉ๋๋ค.
emptyOption |
๋ ๋๋ง | ์ฉ๋ |
|---|---|---|
์๋ต ๋๋ ALL |
'' / ์ ์ฒด |
์กฐํ ์กฐ๊ฑด ๊ธฐ๋ณธ๊ฐ |
SELECT |
'' / ์ ํ |
์ ๋ ฅ ํผ ์ ํ ์๋ด |
NONE |
๋ ๋๋งํ์ง ์์ | ์ฝ๋ ๋ชฉ๋ก๋ง ๋ ธ์ถ |
{ value, label } |
์ง์ ํ ๊ฐ๊ณผ ๋ฌธ๊ตฌ | ์ ์ฒด ์ ํ ๋ฑ ํ๋ฉด๋ณ ํํ |
schemaVersion์ FE ๋ด๋ถ ์บ์ ๊ตฌ์กฐ ๋ฒ์ ์
๋๋ค. ์๋ฒ ๋ฒ์ ์ด ๊ทธ๋๋ก์ฌ๋ FE ์ ์ฅ ๊ตฌ์กฐ๊ฐ ๋ฐ๋๋ฉด schemaVersion์ ์ฌ๋ ค ๊ฐ์ ๋ก ์ฌ์กฐํํ๊ฒ ํฉ๋๋ค. ํ์ฌ CODE๋ ๊ทธ๋ฃน+children ๊ตฌ์กฐ๋ฅผ ๋ฐ์ํด 2, MENU๋ 1์
๋๋ค.
staleTime๊ณผ gcTime์ ๋ฌดํ๋๋ก ์ค์ ํ์ผ๋ฏ๋ก ๋์ผํ QueryClient์ menuCacheScope๊ฐ ์ ์ง๋๋ ์ฑ ์คํ ์ค์๋ ๋ถํธ์คํธ๋ฉ์ ํ ๋ฒ๋ง ์ํํฉ๋๋ค. ์๋ฒ ๋ฒ์ ์ด ์ฑ ์คํ ์ค ๋ณ๊ฒฝ๋๋๋ผ๋ ์ฆ์ ๊ฐ์งํ์ง ์์ผ๋ฉฐ, ๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ์ด๋ ์ฑ ์ฌ์คํ์ผ๋ก ์ QueryClient๊ฐ ์์ฑ๋ ๋ ๋ฒ์ ์ ๋ค์ ํ์ธํฉ๋๋ค.
Admin์ ์ฝ๋ยท๋ฉ๋ด ๋ฑ๋ก/์์ /์ญ์ ๋ ํด๋น ๊ด๋ฆฌ ํ๋ฉด์ ๋ชฉ๋ก Query๋ง ๊ฐฑ์ ํฉ๋๋ค. CONFIG.SESSION.CODE์ CONFIG.SESSION.MENU_LIST์ ์ ์ฌ๋ ๊ธฐ์ค์ ๋ณด๋ ๋ค์ ์๋ก๊ณ ์นจ์์ ์๋ฒ ๋ฒ์ ์ ํ์ธํ ๋ค ๊ฐฑ์ ๋ฉ๋๋ค.
๋ผ์ฐํธ ๊ฐ๋๊ฐ ํธ์ถํ๋ bootstrapBaseInfoSafe๋ ์ ๋ throwํ์ง ์์ต๋๋ค. ์คํจ๋ ๋ฐํ๊ฐ์ failed ๋ฐฐ์ด์ ๋ด๊ธฐ๊ณ , beforeLoad๋ ์ด ๋ฐฐ์ด์ ํ์ธํ์ง ์์ ์ฑ ํ๋ฉด ์ง์
์ ํ์ฉํฉ๋๋ค. ๋ฒ์ ๋๋ ๋ฐ์ดํฐ ์กฐํ๊ฐ ์คํจํ๋ฉด ์ฌ์ฉ ๊ฐ๋ฅํ ๊ธฐ์กด ์บ์๋ฅผ sessionStorage์ ๋ณต์ํฉ๋๋ค.
๋ฐ๋ผ์ "์ฒซ ์ ์ + ๊ธฐ์ค์ ๋ณด ์กฐํ ์คํจ"(์ฌ์ฌ์ฉํ localStorage ์บ์๊ฐ ์์ง ์๋ ์ํ)์์๋ CONFIG.SESSION.CODE๊ฐ ๋น์ด ์๋ ์ฑ๋ก ํ๋ฉด์ด ๋ ๋๋ฉ๋๋ค. ์ด๋ $codeUtils์ Select์ groupCd๋ ๋ก์ปฌ ์ฝ๋ ๊ทธ๋ฃน์ด ์์ผ๋ฉด ํด๋น ๋ชฉ๋ก์ ์ฌ์ฉํ๊ณ , ๋ก์ปฌ์๋ ์๋ ๊ทธ๋ฃน์ด๋ฉด ๋น ๋ชฉ๋ก์ ์ฌ์ฉํฉ๋๋ค. ์ธ์
์ ๊ตฌ๋
ํ์ง ์์ผ๋ฏ๋ก ์ดํ ์๋ฒ ์ฝ๋๊ฐ ์ฑ์์ ธ๋ ์๋ก๊ณ ์นจ ์ ๊น์ง ๋ค์ ๋ ๋๋ง๋์ง๋ ์์ต๋๋ค.
๊ธฐ์ค์ ๋ณด๋ฅผ ๋ชป ๋ฐ์๋ ํ๋ฉด์ ์ด๋ฆฌ๋ ์ชฝ์ ํํ ๊ฒฐ๊ณผ์
๋๋ค. ์ฝ๋ ๋๋ฝ์ ์ค๋ฅ๋ก ๋ค๋ค์ผ ํ๋ ์๋น์ค๋ผ๋ฉด beforeLoad์์ failed์ CODE๊ฐ ํฌํจ๋๋์ง ํ์ธํด ์๋ฌ ํ๋ฉด์ผ๋ก ๋ณด๋ด๋๋ก ๋ฐ๊พธ๋ฉด ๋ฉ๋๋ค.
| ๋ช ๋ น | ์ค๋ช |
|---|---|
pnpm dev |
์ ์ฒด ์ฑ ๋ณ๋ ฌ ์คํ (Turborepo) |
pnpm dev:pc / dev:mobile / dev:admin |
๊ฐ๋ณ ์ฑ ๊ตฌ๋ |
pnpm dev:<pc|mobile|admin>:remote |
์ ํ ์ฑ โ ์๊ฒฉ ๋ฐฑ์๋ (192.168.110.217) |
pnpm dev:<pc|mobile|admin>:nest |
์ ํ ์ฑ โ ๋ก์ปฌ Nest (127.0.0.1:18081) |
pnpm dev:<pc|mobile|admin>:mock |
์ ํ ์ฑ โ ๋ก์ปฌ Mock (127.0.0.1:3333) |
pnpm dev:server |
Mock API ์๋ฒ (mock/server.js, 3333) |
pnpm dev:all |
์ ์ฒด ์ฑ(๊ธฐ๋ณธ .env) + Mock API ์๋ฒ ๋์ ๊ตฌ๋ |
pnpm storybook |
Composition Hub์ SharedยทPCยทAdminยทMobile Storybook ๋์ ๊ตฌ๋ |
pnpm storybook:<hub|shared|pc|admin|mobile> |
Storybook profile ๊ฐ๋ณ ๊ตฌ๋ |
| ๋ช ๋ น | ์ค๋ช |
|---|---|
pnpm check |
์ ์ฒด ํ์
๊ฒ์ฌ (turbo check โ ๊ฐ ์ฑ tsc --noEmit) |
pnpm lint |
Biome ๋ฆฐํธ |
pnpm format |
Biome ์ผ๊ด ํฌ๋งทํ |
pnpm check:api-paths |
httpService ๊ฒฝ๋ก๊ฐ generated OpenAPI ๊ฒฝ๋ก์ ๋ง๋์ง ๊ฒ์ฌ |
pnpm check:css-tokens |
CSS Modules์ ์ปดํฌ๋ํธ ์์ค์ raw color ์ฌ์ฉ ์ฌ๋ถ ๊ฒ์ฌ |
pnpm test |
์ ์ฒด workspace ๋จ์ ํ ์คํธ (Vitest, Turborepo ์บ์ฑ) |
pnpm build |
ํ๋ก๋์ ํตํฉ ๋น๋ (Turborepo ์บ์ฑ) |
pnpm build:storybook |
๋ค์ฏ Storybook ์ ์ ๋น๋ |
๋จ์ ํ ์คํธ:
pc-web,mobile-web,admin-portal,@bx/shared์ Vitest ์ค์ ๊ณผtest์คํฌ๋ฆฝํธ๊ฐ ์์ผ๋ฉฐ, ๋ฃจํธpnpm test๊ฐ Turborepo๋ฅผ ํตํด ์ ์ฒด workspace ํ ์คํธ๋ฅผ ์คํํฉ๋๋ค. ํ ์คํธ ํ์ผ์ด ์์ง ์๋ workspace๋--passWithNoTests๋ก ์ ์ ์ข ๋ฃํฉ๋๋ค. ํ์ฌ GitHub Actions workflow์๋pnpm test๋จ๊ณ๊ฐ ํฌํจ๋์ด ์์ง ์์ต๋๋ค.
| ๋ช ๋ น | ์ค๋ช |
|---|---|
pnpm test:e2e |
์ ์ฒด E2E |
pnpm test:e2e:pc / :mobile / :admin |
๊ฐ๋ณ ์ฑ E2E |
| ๋ช ๋ น | ์ค๋ช |
|---|---|
pnpm gen:api |
์ฌ๋ฌ ๋ฐฑ์๋ OpenAPI ์คํ โ ์๋น์ค๋ณ TS ํ์
์์ฑ (scripts/gen-api.mjs โ packages/shared/src/shared/api/*.schema.d.ts). ์คํ ๋ชฉ๋ก์ API_DOCS_URLS ํ๊ฒฝ๋ณ์๋ก ๋ฎ์ด์ฐ๊ธฐ |
pnpm gen:readme |
๋ก์ปฌ landing ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ฉ README HTML ์์ฑ (๋ฐฐํฌ ์ CI๊ฐ ์๋ ์คํ) |
pnpm wbs:pull -- --dry-run |
GitHub Projects์ ์ ๊ท ํญ๋ชฉ์ WBS ๋ฐ์ ์ ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ |
pnpm wbs:pull |
GitHub Projects์ ์ ๊ท ํญ๋ชฉ์ docs/wbs.md์ ์ถ๊ฐ (docs/wbs-pull.js) |
pnpm wbs:sync |
docs/wbs.md โ GitHub Projects ๋๊ธฐํ (docs/wbs-sync.js) |
pnpm wbs:force-sync |
WBS ๊ฐ์ ์ฌ๋๊ธฐํ (docs/wbs-force-sync.js) |
์ด์ ์๋ฒ๋ Nginx๋ก ์๋นํ๋ฉฐ, ๊ฐ ์ฑ์ **๋ณ๋ context(ํ์ ๊ฒฝ๋ก)**๋ก ๋ถ๋ฆฌ๋ฉ๋๋ค. ๋น๋ ์ Vite base๊ฐ ์ฃผ์
๋์ด ์์
๊ฒฝ๋ก๊ฐ ํด๋น context์ ๋ง์ถฐ์ง๋๋ค.
| ์ฑ | Nginx context | ๋น๋ base (vite.config) |
|---|---|---|
| pc-web | /pc/ |
/pc/ |
| mobile-web | /mobile/ |
/mobile/ |
| admin-portal | /admin/ |
/admin/ |
| ์๋ด ํ์ด์ง(landing) | / (๋ฃจํธ) |
์์ โ ๋น๋ ๋ถ์ ์ ์ ํ์ผ |
dev ์๋ฒ์์๋
base๊ฐ/๋ก ์ ์ง๋ฉ๋๋ค(๋น๋ ์์๋ง context ๊ฒฝ๋ก ์ ์ฉ).
CI/CD (.github/workflows/ci.yml)
develop ๋ธ๋์น์ push๋๋ฉด self-hosted ๋ฌ๋์์ ์๋์ผ๋ก ๋ค์์ ์ํํฉ๋๋ค.
์ ์ฒด ์ฐ๊ฒฐ ๊ตฌ์กฐ, job ์คํ ์์, ํ์ฌ ์ด์์ ์ฃผ์์ ์ CI/CD ์คํ ํ๋ฆ์์ ํ์ธํ ์ ์์ต๋๋ค.
| ํญ๋ชฉ | ๊ฐ |
|---|---|
| Runner | self-hosted |
| Node.js | 22 |
| pnpm | 11.1.3 (packageManager์ ๋๊ธฐํ) |
| ๋ฐฐํฌ ํธ๋ฆฌ๊ฑฐ | develop ๋ธ๋์น push |
| API URL ์ฃผ์ | VITE_API_URL=/channel/backend/api/v1 |
| ๋ฐฐํฌ ๊ฒฐ๊ณผ ์๋ฆผ | Discord Webhook (DISCORD_WH GitHub Actions secret) |
pnpm install --frozen-lockfilepnpm gen:api๋ก ๋ฐฑ์๋ OpenAPI ์คํ ๊ธฐ์ค ํ์ ์ฌ์์ฑgit diff --exit-code -- packages/shared/src/shared/api๋ก generated ํ์ ์ปค๋ฐ ๋๋ฝ ์ฌ๋ถ ํ์ธpnpm check:api-paths๋ก FE API ํธ์ถ ๊ฒฝ๋ก์ generated OpenAPI ๊ฒฝ๋ก ์ ํฉ์ฑ ํ์ธpnpm build:debug(์ ์ฒด ์ฑ ๋น๋,VITE_API_URL์ฃผ์ ) โpnpm checkโpnpm lintpnpm gen:readme๋กlanding/assets/fe.readme.html์ฌ์์ฑ- ๊ฐ ์ฑ
dist/*์landing/*๋ฅผ Nginx ์๋น ํด๋๋ก ๋ณต์ฌ (๋ฐฐํฌ ์๋ฃ ํ Discord ์๋ฆผ)
CI๋ ๋น๋ ์ฑ๊ณต ์ฌ๋ถ๋ฟ ์๋๋ผ ๋ฐฑ์๋ ์คํ๊ณผ FE generated ํ์ ์ ๋๊ธฐํ ์ฌ๋ถ๋ ํ์ธํฉ๋๋ค.
pnpm gen:api๋scripts/gen-api.mjs์ ๊ธฐ๋ณธ ๋ด๋ถ๋ง Swagger URL ๋๋API_DOCS_URLSํ๊ฒฝ๋ณ์๋ก ์ง์ ํ URL์์ ์คํ์ ๋ฐ์packages/shared/src/shared/api/*.schema.d.ts๋ฅผ ์ฌ์์ฑํฉ๋๋ค.- ์ฌ์์ฑ ํ
packages/shared/src/shared/api์ diff๊ฐ ์์ผ๋ฉด, ๋ฐฑ์๋ ์คํ ๋ณ๊ฒฝ์ด FE ํ์ ํ์ผ์ ๋ฐ์๋์ง ์์ ์ํ์ด๋ฏ๋ก CI๋ฅผ ์คํจ์ํต๋๋ค. pnpm check:api-paths๋packages/shared/src/entities์httpService.get/post/...ํธ์ถ ๊ฒฝ๋ก๊ฐ generated OpenAPI ๊ฒฝ๋ก์ ์กด์ฌํ๋์ง ํ์ธํฉ๋๋ค. ์์ง ์คํ์ ์๋ legacy ๊ฒฝ๋ก๋scripts/check-api-paths.mjs์ allowlist์ ์ฌ์ ์ ํจ๊ป ๊ด๋ฆฌํฉ๋๋ค.- ๊ฐ๋ฐ์ ๋ก์ปฌ์์๋ Swagger URL์ ์ ๊ทผ ๊ฐ๋ฅํ๋ฉด PR ์ ์ ๊ฐ์ ๊ฒ์ฌ๋ฅผ ๋ฏธ๋ฆฌ ์ํํ ์ ์์ต๋๋ค:
pnpm gen:api && git diff --exit-code -- packages/shared/src/shared/api. - self-hosted CI ๋ฌ๋๋ Swagger URL์ ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํฉ๋๋ค. ์ ๊ทผ์ด ์ด๋ ต๋ค๋ฉด Swagger JSON snapshot์ repo์ ์ ์ฅํ๊ฑฐ๋, ๋ฐฑ์๋ ๋ฆด๋ฆฌ์ฆ ์ฐ์ถ๋ฌผ๋ก OpenAPI JSON์ ์ ๊ณต๋ฐ๋ ๋ฐฉ์์ผ๋ก ์ ํํฉ๋๋ค.
gen-api์คํ ์ค missing schema patch ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ๋ฉด ๋ฐฑ์๋ OpenAPI ๋ฌธ์๊ฐ ๋ถ์์ ํ๋ค๋ ์ ํธ์ ๋๋ค. ๋ก์ปฌ ๊ฐ๋ฐ์์๋ warning์ผ๋ก ๋ณผ ์ ์์ง๋ง, ๋ฐฐํฌ์ฉ CI์์๋ ๋ฐฑ์๋ ์คํ ๋ณด์ ๋๋ allowlist ์ ์ฑ ์ ๋จผ์ ๊ฒํ ํฉ๋๋ค.
| ๋์ | ์๋ฒ ๋ฐฐํฌ ๊ฒฝ๋ก |
|---|---|
| pc-web | /Users/channelunit/apps/bx-cf-fe/pc-web |
| mobile-web | /Users/channelunit/apps/bx-cf-fe/mobile-web |
| admin-portal | /Users/channelunit/apps/bx-cf-fe/admin-portal |
| landing | /Users/channelunit/apps/bx-cf-fe/landing |
์ฐธ๊ณ : ํ์ฌ develop push ์ ์ธ ์ฑ์ด ๋ชจ๋ ํจ๊ป ๋น๋ยท๋ฐฐํฌ๋ฉ๋๋ค. ์ด์์ฉ ์ฑ๋ณ ๋ ๋ฆฝ ๋ฐฐํฌ๊ฐ ํ์ํ๋ฉด ์ํฌํ๋ก์ฐ๋ฅผ ๋ถ๋ฆฌ(๋ธ๋์น/ํ๊ทธ/
pathsํํฐ ๋๋ turbo affected)ํด์ผ ํฉ๋๋ค.์ฃผ์: CI๋
--frozen-lockfile๋ก ์์กด์ฑ์ ์ค์นํฉ๋๋ค.package.json์ dependencies/devDependencies๋ฅผ ๋ณ๊ฒฝํ๋ค๋ฉด ๋ฐ๋์pnpm-lock.yaml๋ ํจ๊ป ๊ฐฑ์ ํด ์ปค๋ฐํด์ผ ํฉ๋๋ค. lockfile๋ง ๊ฐฑ์ ํ๋ ค๋ฉดpnpm install --lockfile-only๋ฅผ ์ฌ์ฉํฉ๋๋ค.
landing/index.html์ FEยทBE ์๋ฃ(์๊ฐ PDFยทREADMEยทWBSยท์ ์ฅ์)๋ฅผ ๋งํฌ๋ก ์๋ดํ๋ ๋จ์ผ ์ ์ ํ์ด์ง์
๋๋ค. ์ด๋ค ์ฑ์๋ ์ํ์ง ์์ผ๋ฏ๋ก public/(์ฑ ๊ณต์ publicDir)์ด ์๋ ๋ณ๋ landing/์ ๋๊ณ , Nginx ๋ฃจํธ context๋ก ์๋นํฉ๋๋ค. CI/CD์์ pnpm gen:readme ์คํ ํ landing/* ์ ์ฒด๋ฅผ landing ๋ฐฐํฌ ๊ฒฝ๋ก๋ก ๋ณต์ฌํฉ๋๋ค.
pnpm gen:readme๋ก ์์ฑํ README HTML์ ๋ฐ์คํฌํฑ์์ H2 ์ ๋ชฉ๋ง ๋ชจ์ ์ฐ์ธก ๊ณ ์ ๋ชฉ์ฐจ๋ฅผ ์ ๊ณตํ๊ณ , ๋ฌธ์ ์คํฌ๋กค์ ๋ฐ๋ผ ํ์ฌ ํญ๋ชฉ์ ํ์ํฉ๋๋ค. ๋ชฉ์ฐจ ๋งํฌ๋ URL hash์ ๋ถ๋๋ฌ์ด ์คํฌ๋กค์ ์ฌ์ฉํ๋ฉฐ, 1100px ์ดํ ํ๋ฉด์์๋ ๋ชฉ์ฐจ๋ฅผ ์จ๊ธฐ๊ณ ๋ณธ๋ฌธ์ ์ ์ฒด ํญ์ผ๋ก ํ์ํฉ๋๋ค.
README HTML์ CI๊ฐ ๋ฐฐํฌ ์ ์ ์๋ ์์ฑํฉ๋๋ค. ๋ก์ปฌ landing ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ ํ์ํ ๊ฒฝ์ฐ์๋ง
pnpm gen:readme๋ฅผ ์คํํฉ๋๋ค. ํ์ BE ์ ์ฅ์๊ฐ ๋ก์ปฌ์ ์์ผ๋ฉดbe.readme.html๋ ํจ๊ป ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค.
bx-cf-fe/
โโโ package.json # ๋ฃจํธ ์คํฌ๋ฆฝํธ & ์ํฌ์คํ์ด์ค
โโโ pnpm-workspace.yaml # ์ํฌ์คํ์ด์ค ์ ์ (apps/*, packages/*)
โโโ turbo.json # Turborepo ํ์ดํ๋ผ์ธ ์บ์ฑ
โโโ biome.json # Biome ๋ฆฐํฐ & ํฌ๋งทํฐ (a11y ๊ท์น ์ ์ธ)
โโโ tsconfig.json # ๊ณตํต TS ์ค์ (๊ฐ ์ฑ์ด extends)
โโโ db.json # Mock ๋ฐ์ดํฐ
โโโ mock/ # Mock API ์๋ฒ (Nest ๊ณ์ฝ ํ๋ด, ๋ฌด์์กด์ฑ)
โ โโโ server.js
โโโ public/ # ๊ณต์ฉ ์ ์ ์์ฐ (์ฑ ๊ฐ ๊ณต์ publicDir)
โโโ landing/ # ์๋ด ํ์ด์ง (Nginx ๋ฃจํธ context, ์ฑ ๋น์ข
์)
โ โโโ index.html
โ โโโ assets/ # ์๊ฐ PDFยทPPTX, gen:readme ์ฐ์ถ๋ฌผ(fe/be.readme.html), backup/(์ด์ ํ
๋ง ๋ฑ)
โโโ scripts/
โ โโโ gen-api.mjs # OpenAPI โ TS ํ์
์์ฑ
โ โโโ check-api-paths.mjs # FE API ํธ์ถ ๊ฒฝ๋ก โ OpenAPI ๊ฒฝ๋ก ์ ํฉ์ฑ ์ฒดํฌ
โ โโโ gen-readme-html.mjs # README โ landing HTML ์์ฑ
โ โโโ presentations/ # ์ ์ ๋ฑ ๋น๋ยท์์ ์คํฌ๋ฆฝํธ (Biome ๊ฒ์ฌ ์ ์ธ)
โโโ .github/workflows/ci.yml # develop push ์ ๋น๋ยท๊ฒ์ฆยทNginx ๋ฐฐํฌ
โโโ docs/ # ํ๋ก์ ํธ ๋ฌธ์ & ๋๊ตฌ
โ โโโ order.md / todo.md / wbs.md
โ โโโ form-components.md # ํผ ์ปดํฌ๋ํธ ์ฌ์ฉ ๊ฐ์ด๋
โ โโโ ppt-intro.md # ์ ์ ๋ฑ ๋ด๋ฌํฐ๋ธ ์๊ณ
โ โโโ wbs-sync.js # WBS โ GitHub Projects ๋๊ธฐํ ์คํฌ๋ฆฝํธ
โ โโโ wbs-pull.js # GitHub Projects โ WBS ์ ๊ท ํญ๋ชฉ ๊ฐ์ ธ์ค๊ธฐ
โ โโโ superpowers/ # ๊ธฐ๋ฅ๋ณ ์์
๊ณํ(plans)ยท์ค๊ณ(specs) ๊ธฐ๋ก
โ โโโ ppt-build/ # ์ ์ PPT ๋น๋ยท๋ ๋ ์ฐ์ถ๋ฌผ (Biome ๊ฒ์ฌ ์ ์ธ)
โ
โโโ packages/
โ โโโ shared/ # ๊ณต์ ํจํค์ง (@bx/shared)
โ โโโ src/
โ โโโ entities/ # ๋๋ฉ์ธ: account, alarm, auth, base-info, common-code, menu, product, user
โ โ โโโ <entity>/ # โ api/ (HTTP ํธ์ถ)
โ โ # โ model/ (ํ์
ยทhookยทqueriesยทstoreยทstorage)
โ โ # โ ui/ (๋๋ฉ์ธ ์ปดํฌ๋ํธ)
โ โโโ shared/ # ๊ณตํต: ui, hooks, model, lib, types, constants, ajax
โ โ โโโ ajax/ # http.service (envelopeยท์ธํฐ์
ํฐยทJWT)
โ โ โโโ constants/ # api, error-codes, siteConfig, storage-keys
โ โ โโโ ui/ # alert, dialog, drawer, modal, toast, button, input ...
โ โโโ index.ts # ๋ฐฐ๋ด (์ธ๋ถ๋ก ์ผ๊ด Export)
โ
โโโ apps/
โโโ pc-web/ # PC ์น (3000) โ FSD ์ฑ
โ โโโ src/
โ โโโ app/ # ์ ์ญ ํ๋ก๋ฐ์ด๋ (modal ๋ฑ)
โ โโโ routes/ # TanStack Router ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
โ โ โโโ (auth)/ # ๋ก๊ทธ์ธ
โ โ โโโ (page)/ # ๋ณดํธ ๋ผ์ฐํธ (requireAuth)
โ โ โโโ (modal)/ # ๋ชจ๋ฌ ๋ผ์ฐํธ
โ โโโ pages/ # ํ์ด์ง ์ปดํฌ๋ํธ
โ โโโ features/ # ๊ธฐ๋ฅ ๋จ์ (auth, dashboard ...)
โ โโโ widgets/ # ๋ ์ด์์ ์์ ฏ (sidebar ๋ฑ)
โ โโโ queryClient.ts # ์ฑ ๊ณต์ฉ TanStack QueryClient
โ โโโ shared/ # ์ฑ ๋ก์ปฌ ๊ณตํต (guards ๋ฑ)
โ
โโโ mobile-web/ # ๋ชจ๋ฐ์ผ ์น (3001) โ FSD ์ฑ (๊ตฌ์กฐ ๋์ผ, ํ์คํฌ๋ฆฐ ๋ชจ๋ฌ)
โ
โโโ admin-portal/ # ๊ด๋ฆฌ์ ํฌํ (3002) โ ์ด์ ๊ด๋ฆฌ ์ฑ
โโโ src/
โโโ routes/ # ๋ก๊ทธ์ธยท๋์๋ณด๋ยท์ฝ๋ยท๋ฉ๋ดยท์ฌ์ฉ์ยทํ๋กํ ๋ผ์ฐํธ
โโโ pages/ # ๊ด๋ฆฌ์ ์
๋ฌด ํ๋ฉด
โโโ features/ # ์ฝ๋ยท๋ฉ๋ดยท์ฌ์ฉ์ ๋ฑ๋ก/์์ ๊ธฐ๋ฅ
โโโ widgets/ # ๊ด๋ฆฌ์ ๋ ์ด์์ยท์ฌ์ด๋๋ฐ
โโโ shared/ # ๊ด๋ฆฌ์ ์ ์ฉ ํผยทํํฐยท๋๋ก์ดยท์คํ์ผ
๋ผ์ฐํธ ํธ๋ฆฌ(
routeTree.gen.ts)๋ TanStack Router ํ๋ฌ๊ทธ์ธ์ด dev/build ์ ์๋ ์์ฑํฉ๋๋ค(์ง์ ์์ ๊ธ์ง).
์ด ์ ์ฅ์๋ Feature-Sliced Design ๊ณต์ ํํ์ด์ง์ ๊ตฌ์กฐํ ์์น์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ฉฐ, ์์ธํ ๊ฐ๋ ์ FSD ํ๊ตญ์ด Overview์์ ํ์ธํ ์ ์์ต๋๋ค. FSD๋ ํ๋ก ํธ์๋ ์ฝ๋๋ฅผ ์ฑ ์ ๋ฒ์์ ๋ฐ๋ฅธ Layer, ๋น์ฆ๋์ค ๋๋ฉ์ธ์ ๋ฐ๋ฅธ Slice, ๊ธฐ์ ์ญํ ์ ๋ฐ๋ฅธ Segment๋ก ๋๋๋ ์ํคํ ์ฒ ๋ฐฉ๋ฒ๋ก ์ ๋๋ค.
BX-CF๋ ์ด๋ฅผ ๋ชจ๋ ธ๋ ํฌ์ ๋ง๊ฒ ์ ์ฉํฉ๋๋ค.
| FSD Layer | ์ด ์ ์ฅ์์์์ ์ญํ |
|---|---|
app |
์ฑ ์ง์ ์ , ์ ์ญ Provider์ ์ค์ |
pages / routes |
ํ๋ฉด๊ณผ URL ์ง์ ๋จ์ |
widgets |
์ฌ์ด๋๋ฐยท๋ ์ด์์์ฒ๋ผ ๋ ๋ฆฝ์ ์ธ ๋ํ UI ๊ตฌ์ฑ |
features |
๋ก๊ทธ์ธ, ๋ฑ๋กยท์์ ์ฒ๋ผ ์ฌ์ฉ์ ํ๋ ์ค์ฌ์ ๊ธฐ๋ฅ |
entities |
account, auth, menu, product ๋ฑ ๋๋ฉ์ธ๋ณ APIยทmodelยทUI |
shared |
ํน์ ๋๋ฉ์ธ์ ์ข ์๋์ง ์๋ UIยท์ ํธ๋ฆฌํฐยทํต์ ยท์์ |
- ์ฑ๋ณ ํ๋ฉดยท๊ธฐ๋ฅยท์คํ์ผ์
apps/*/src์ ๋๊ณ , ์ฌ๋ฌ ์ฑ์ด ๊ณต์ ํ๋ ๋๋ฉ์ธ๊ณผ ๊ธฐ๋ฐ ๊ธฐ๋ฅ์packages/shared/src์์ ๊ด๋ฆฌํฉ๋๋ค. - Slice ๋ด๋ถ๋
api,model,ui๋ฑ์ Segment๋ก ์ญํ ์ ๋ถ๋ฆฌํฉ๋๋ค. - ์ธ๋ถ ๋ชจ๋์ Slice์
index.tsPublic API๋ฅผ ํตํด ์ ๊ทผํ๊ณ , ์์ Layer์์ ํ์ Layer ๋ฐฉํฅ์ผ๋ก ์์กดํ๋๋ก ๊ตฌ์ฑํฉ๋๋ค.
ESLint/Prettier ๋์ Rust ๊ธฐ๋ฐ Biome์ผ๋ก ํ์งยท์คํ์ผ์ ๊ด๋ฆฌํฉ๋๋ค.
- ์ ๊ทผ์ฑ(a11y) ๊ท์น ์ ์ธ: ํ๋กํ ํ์ดํ/๋ง์ด๊ทธ๋ ์ด์
์๋๋ฅผ ์ํด biome.json์์
a11y๋ฅผ ๋นํ์ฑํํ์ต๋๋ค. - ํฌ๋งท ๊ท์น: ์ธ๋ดํธ Space 2 ยท ๊ฐํ LF ยท ์์๋ฐ์ดํ(
') ยท ์ธ๋ฏธ์ฝ๋ก ํญ์.
- ์ํฐํฐยทํ
ยทUI๋ ๋๋ ํ ๋ฆฌ์
index.ts๋ก ๋ฌถ์ด ๋ ธ์ถํฉ๋๋ค. - ์ํ ์ฐธ์กฐ ๊ธ์ง:
@bx/shared๋ด๋ถ ๋ชจ๋๋ผ๋ฆฌ๋ ๋ฐ๋์ ๋ก์ปฌ ์๋ ๊ฒฝ๋ก๋ก ์ํฌํธํฉ๋๋ค. (ํจํค์ง ๋ช ์นญ@bx/shared๋ก ์๊ธฐ ์์ ์ ํธ์ถํ๋ฉด ์ํ ์์กด์ฑ ๋ฐ์)
์ ํ์ผ/๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ธฐ ์ ์๋ ๊ฐ์ ๋๋ฉ์ธ์ ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๋จผ์ ํ์ธํ๊ณ , entities ํ์์ ๊ธฐ์กด ํจํด์ ์ฐ์ ํฉ๋๋ค.
| ์ข ๋ฅ | ์์น |
|---|---|
| API ํธ์ถ | api/*.api.ts |
| ํ์ | model/*.type.ts |
| TanStack Query | model/*.queries.ts |
| storage/cache | model/*.storage.ts |
| hook/store/bootstrap ๋ฑ ๋ชจ๋ธ ๋ก์ง | model/*.hook.ts, model/*.store.ts, model/*.bootstrap.ts ๋ฑ ๊ธฐ์กด ํจํด์ ๋ง์ถค |
TanStack Query key๋ xxxQueryKeys = { all, list, detail, ... } ๊ฐ์ฒด ํจํด์ ์ฌ์ฉํฉ๋๋ค. ๊ธฐ์กด ์ปจ๋ฒค์
๊ณผ ๋ค๋ฅด๊ฒ ๊ฐ ํ์๊ฐ ์์ผ๋ฉด ๊ตฌํ ์ ์ ์ด์ ๋ฅผ ๋จผ์ ์ค๋ช
ํฉ๋๋ค.
- ๋ชจ๋ API ํธ์ถ์
@bx/shared์httpService.get/post/put/patch/delete๋ฅผ ์ฌ์ฉํฉ๋๋ค. - baseURL์
httpService.init()์์ 1ํ ์ค์ ํ๋ฏ๋ก, ๊ฐ API ํจ์๋ ์๋ ๊ฒฝ๋ก๋ง ์ฌ์ฉํฉ๋๋ค. (์:httpService.get('/product/list')) execute()๊ฐ envelope์payload๋ฅผ ์ธ๋ํํ์ฌ ๋ฐํํ๊ณ ,success: false๋ApiError๋ก throwํฉ๋๋ค.- envelope ์
๋ฌด ์ค๋ฅ, HTTP ์ค๋ฅ, ๋คํธ์ํฌ ๋จ์ , timeout, ์์ฒญ ์ทจ์๋ ๋ชจ๋
ApiError๋ก ์ ๊ทํ๋ฉ๋๋ค. ํธ์ถ๋ถ์์๋code,status,kind,payload,url,message๋ฅผ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ฌ๋ฌ ์ฑ์์ ์ฌ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ณธ UI๋
packages/shared/src/shared/ui์ ๊ตฌํํฉ๋๋ค. - ์ฑ๋ณ ๋์์ด๋ ์คํ์ผ์ด ํ์ํ๋ฉด shared ๊ธฐ๋ณธ ์ปดํฌ๋ํธ๋ฅผ ์กฐํฉํ๊ฑฐ๋ ํ์ฅํ๊ณ , wrapper์ ์คํ์ผ์ ํด๋น ์ฑ ๋ด๋ถ์ ๋ก๋๋ค.
- ์ฑ์ ๋จผ์ ๊ตฌํํ ๊ธฐ๋ฅ์ด๋ผ๋ ์ฌ์ฌ์ฉ ๋ฒ์๊ฐ ๋์ด์ง๋ฉด shared UI๋ก ์น๊ฒฉํฉ๋๋ค.
- ์คํ์ผ์ CSS Modules(
*.module.css)๋ง ์ฌ์ฉํฉ๋๋ค. Tailwind CSS๋ ์ ๊ฑฐํ์ผ๋ฉฐ, utility class ๊ธฐ๋ฐ ์คํ์ผ์ ์๋ก ์ถ๊ฐํ์ง ์์ต๋๋ค. - ์ปดํฌ๋ํธ๋
import styles from './Component.module.css'ํํ๋ก ์คํ์ผ์ ๊ฐ์ ธ์ค๊ณ , ์ฑ๋ณ ์คํ์ผ์ ํด๋น ์ฑ ๋ด๋ถ์ ๋ก๋๋ค. - ๊ณตํต ์ปดํฌ๋ํธ ์คํ์ผ์
packages/shared/src/shared/ui๋๋packages/shared/src/shared/form์ ์ปดํฌ๋ํธ ์ CSS Module์ ๋ก๋๋ค. - ์์์ ๊ฐ๋ฅํ ํ ์ฑ/๊ณตํต ํ
๋ง ํ ํฐ CSS variable์ ์ฌ์ฉํฉ๋๋ค. raw hex/rgb ์์์
pnpm check:css-tokens๋ก ๊ฒ์ฌํฉ๋๋ค. - ์ ์ญ CSS๋ reset, theme token, ์ฑ shell์ฒ๋ผ ์ ์ญ์ด์ด์ผ ํ๋ ์ต์ ๋ฒ์์๋ง ์ฌ์ฉํฉ๋๋ค.
์ธ ์ฑ(pc-web, mobile-web, admin-portal)์ Vite ์ค์ ์๋ vite-plugin-svgr๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.
- SVG๋ฅผ React ์ปดํฌ๋ํธ๋ก ์ฌ์ฉํ ๋๋
import Icon from './icon.svg?react'์ฒ๋ผ?react์ ๋ฏธ์ฌ๋ฅผ ๋ถ์ ๋๋ค. ?react๊ฐ ์๋ SVG import๋ ๊ธฐ์กด Vite ๋์๋๋ก URL ๋ฌธ์์ด๋ก ์ทจ๊ธํฉ๋๋ค.- ์ฑ์์ ๊ณต์ ๋๋ ์ ์ ์ด๋ฏธ์ง๋
public/assets/images/**์ ๋๊ณ , ์ฑ ์ ์ฉ ์์ค SVG๋ ๊ฐ ์ฑ์src/assets์ ๋ก๋๋ค. - ๋จ์ ์์ด์ฝ ๋ฒํผ์ ๊ฐ๋ฅํ๋ฉด SVG ํ์ผ์ ์ง์ ์กฐ์ํ๊ธฐ๋ณด๋ค ๊ธฐ์กด shared UI์ ์์ด์ฝ ์์ฐ์ ์กฐํฉํฉ๋๋ค.
| ๋ชจ๋ | ๋ด์ฉ |
|---|---|
api.ts |
API_URL(env ์ฃผ์
), IS_MOCK_API, API_CONFIG(ํ์์์ยท์ฌ์๋) |
error-codes.ts |
API_ERROR_CODE ๋ฐ ์ธ์ฆ ์ฝ๋ ํ๋ณ ํฌํผ |
siteConfig.ts |
์ ์ญ CONFIG (๋ชจ๋ยท์ธ์
ยทํ์ด๋จธยทํ
๋งยทํ๋ ๋ฑ) |
storage-keys.ts |
STORAGE_KEYS (ํ ํฐยท์ฌ์ฉ์ยท์บ์ ํค) |
index.ts |
APP_CONFIG, ROUTES, BANK_OPTIONS |
- ์ฑ(apps/*)์์:
@bx/shared๋ฐฐ๋ด๋ก ์ผ๊ด ์ํฌํธ. (์:import { API_URL, STORAGE_KEYS } from '@bx/shared';) - ํจํค์ง ๋ด๋ถ์์: ์๋ ๊ฒฝ๋ก๋ก ๊ฐ๋ณ ์ํฌํธ. (์:
import { API_URL } from './api';)
@bx/shared์ Zustand ์คํ ์ด(useModalStore/useModal)๋ก ๋ชจ๋ฌ ์คํ์ ๊ด๋ฆฌํ๊ณ , Radix Dialog ๊ธฐ๋ฐ UI๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- PC: ํ๋ฉด ์ค์ ๋ค์ด์ผ๋ก๊ทธ (
Dialog) - ๋ชจ๋ฐ์ผ: ํ์คํฌ๋ฆฐ ๋ค์ด์ผ๋ก๊ทธ
- ๋ชจ๋ฌ ํ๋ฉด์ ๊ฐ ์ฑ์
routes/(modal)/<name>/index.tsx์ ๋๊ณ ,useModal().open({ path: '<name>' })์ผ๋ก ํธ์ถํฉ๋๋ค.
@bx/shared๋ Promise ๊ธฐ๋ฐ openAlertยทopenConfirm๊ณผ Radix Dialog ๊ธฐ๋ฐ <AlertHost />๋ฅผ ์ ๊ณตํฉ๋๋ค. PCยทMobileยทAdmin์ main.tsx์์ <AlertHost />๋ฅผ ๋ผ์ฐํฐ์ ๊ฐ์ ๋ ๋ฒจ์ ํ ๋ฒ ๋ง์ดํธํฉ๋๋ค.
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<Toaster />
<AlertHost />
</QueryClientProvider>import { openAlert, openConfirm } from '@bx/shared';
await openAlert({
title: '์๋ฆผ',
message: '์ ์ฅ๋์์ต๋๋ค.',
confirmText: 'ํ์ธ',
});
const confirmed = await openConfirm({
title: '์ญ์ ํ์ธ',
message: '์ ํํ ํญ๋ชฉ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?',
confirmText: '์ญ์ ',
cancelText: '์ ์ง',
});
if (!confirmed) return;openAlert๋ ํ์ธ ๋ฒํผ๋ง ํ์ํ๋ฉฐ, ๋ซํ ๋๊น์ง ๊ธฐ๋ค๋ฆด ์ ์์ต๋๋ค.openConfirm์ ๊ธฐ๋ณธ์ ์ผ๋กํ์ธยท์ทจ์๋ฒํผ์ ํ์ํ๊ณPromise<boolean>์ ๋ฐํํฉ๋๋ค.- Confirm์ ํ์ธ ๋ฒํผ์ ๋๋ฅธ ๊ฒฝ์ฐ์๋ง
true์ด๋ฉฐ, ์ทจ์ยทESCยท์ค๋ฒ๋ ์ด ํด๋ฆญ์false์ ๋๋ค. - ์ฌ๋ฌ ์์ฒญ์ queue์ ์์ฌ ์์๋๋ก ํ์๋ฉ๋๋ค.
createQueryClient()์ QueryCache์ MutationCache๊ฐ Query/Mutation ์คํจ๋ฅผ handleApiError()๋ก ์ ๋ฌํฉ๋๋ค. ๋ณ๋ ์ ์ฑ
์ด ์์ผ๋ฉด ์๋ฒ ๋ฉ์์ง๋ก ๊ณตํต alert๋ฅผ ํ์ํ๋ฉฐ, ๋์์ ๊ฐ์ ๋ฉ์์ง๊ฐ ์ฌ๋ฌ ๋ฒ ๋ฐ์ํ๋ฉด ํ๋๋ก ํฉ์นฉ๋๋ค.
// ํ๋ฉด์์ ์ง์ ์ค๋ฅ๋ฅผ ํ์
useQuery({
...queryOptions,
meta: { error: { silent: true } },
});
// ๊ณตํต alert ๋ฉ์์ง ๋ณ๊ฒฝ
useLogin({
meta: {
error: {
message: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํด์ฃผ์ธ์.',
},
},
});
// alert๋ฅผ ๋ซ์ ๋ค ํ์ฒ๋ฆฌ
useMutation({
...mutationOptions,
meta: { error: { onClose: () => navigate({ to: '/list' }) } },
});meta.error ์ต์
|
์ญํ |
|---|---|
silent |
๊ณตํต alert๋ฅผ ํ์ํ์ง ์์. boolean ๋๋ (error) => boolean ์ฌ์ฉ |
ignoreCodes |
์ง์ ํ ์๋ฒ ์๋ฌ ์ฝ๋๋ง ๊ณตํต ์ฒ๋ฆฌ์์ ์ ์ธ |
message |
alert ๋ฉ์์ง ๋ณ๊ฒฝ. ๋ฌธ์์ด ๋๋ (error) => string ์ฌ์ฉ |
onClose |
์ฌ์ฉ์๊ฐ ๊ณตํต alert๋ฅผ ๋ซ์ ๋ค ์คํ |
์ธ์ฆ ์ค๋ฅ๋ HTTP ์ธํฐ์
ํฐ๊ฐ ์ฌ๋ฐ๊ธยท๋ก๊ทธ์์์ ์ฒ๋ฆฌํ๊ณ , ์ทจ์๋ ์์ฒญ๊ณผ TanStack Router redirect๋ ๊ณตํต alert์์ ์ ์ธํฉ๋๋ค. ์
๋ฌดยท์ธ์ฆ ์ค๋ฅ๋ ์ฌ์๋ํ์ง ์์ผ๋ฉฐ networkยทtimeoutยทserver ์ค๋ฅ๋ง ํ ๋ฒ ์ฌ์๋ํฉ๋๋ค. React Query๋ฅผ ๊ฑฐ์น์ง ์๋ ์ง์ ํธ์ถ์ catch์์ handleApiError(error, policy)๋ฅผ ํธ์ถํฉ๋๋ค.
Toast๋ ํ์ธ์ด ํ์ ์๋ ์งง์ ์ฑ๊ณตยท์ํ ์๋ฆผ์ ์ฌ์ฉํ๊ณ , Alert๋ ์ฌ์ฉ์์ ํ์ธ์ด ํ์ํ ์ค๋ฅยท์๋ด, Confirm์ ์ญ์ ์ฒ๋ผ ์ฌ์ฉ์ ๊ฒฐ์ ์ด ํ์ํ ์์
์ ์ฌ์ฉํฉ๋๋ค. ์ ๊ท ์ฝ๋์์๋ window.alertยทwindow.confirm ๋์ ๊ณตํต API๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํ์ฌ Admin ์ญ์ ํ๋ฉด์ ๋จ์ ์๋ native confirm์ ์์ฐจ ๋ง์ด๊ทธ๋ ์ด์
๋์์
๋๋ค.
@bx/shared๊ฐ sonner๋ฅผ ์๊ฒ ๊ฐ์ผ Toaster ์ปดํฌ๋ํธ์ toast ํจ์๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ธ ์ฑ ๋ชจ๋ main.tsx์์ <Toaster />๋ฅผ ๋ผ์ฐํฐ์ ๊ฐ์ ๋ ๋ฒจ์ ํ ๋ฒ ๋ง์ดํธํ๋ฉฐ, ํธ์ถ์ ์ด๋์๋ toast(...)๋ก ํฉ๋๋ค.
// apps/[app]/src/main.tsx
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<Toaster />
</QueryClientProvider>import { toast } from '@bx/shared';
toast.success('์ ์ฅ๋์์ต๋๋ค.');
toast.error('์ ์ฅ์ ์คํจํ์ต๋๋ค.');- ๊ธฐ๋ณธ๊ฐ์
position="bottom-center"ยทcloseButtonํ์ฑ์ด๋ฉฐ, ํ์ ์<Toaster />์ sonner props๋ฅผ ๊ทธ๋๋ก ๋๊ฒจ ๋ฎ์ด์ธ ์ ์์ต๋๋ค. - ์์ด์ฝ์ lucide-react ๊ธฐ๋ฐ์ผ๋ก success/info/warning/error/loading์ด ๋ฏธ๋ฆฌ ์ง์ ๋์ด ์์ต๋๋ค.
- ์์์ ์ฑ๋ณ ํ
๋ง ํ ํฐ(
--surface-elevated,--foreground,--border)์ var()๋ก ์ฐธ์กฐํ๋ฏ๋ก, pc-web์.darkยทadmin์[data-admin-theme]ยทmobile-web์ light ๊ธฐ๋ณธ๊ฐ์ ๋ํผ ์์ ์์ด ๋ฐ๋ผ๊ฐ๋๋ค.
Type(Scope): Subject
| Type | ์ค๋ช |
|---|---|
| feat | ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ |
| fix | ๋ฒ๊ทธยท์๋ฌ ์์ |
| docs | ๋ฌธ์ ์์ (README ๋ฑ) |
| style | ํฌ๋งทยท์คํ์ผ (๊ธฐ๋ฅ ์ํฅ ์์) |
| refactor | ๊ตฌ์กฐ ๋ฆฌํฉํฐ๋ง |
| build | ๋น๋ ์ค์ (Vite, pnpm ๋ฑ) |
| chore | ํจํค์ง ์ ๋ฐ์ดํธยท์ก๋ฌด |
๊ธฐ์ ๋ฌธ์ ๋ฐ ์ํคํ ์ฒ ๊ฐ์ ์ ์์ ๊ฐ๋ฐ ๋ฆฌ๋์๊ฒ ์ ๋ฌํด ์ฃผ์ธ์.
{ "success": true, // ์ฑ๊ณต/์คํจ "code": "0", // ์ฑ๊ณต: "0", ์คํจ: ์์ ๋ฌธ์์ด "msg": "success", // ๋ฉ์์ง "payload": { } // ์ค์ ๋ฐ์ดํฐ (any) }