조민수 조민수 05-06
api use Query + axios 형태로 변경 예시 use BoardListQuery 추가
@cc5447c24586787063638cef240c8358a1d1d4b4
package-lock.json
--- package-lock.json
+++ package-lock.json
@@ -1,14 +1,15 @@
 {
-  "name": "react-app",
+  "name": "base-react",
   "version": "0.0.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
-      "name": "react-app",
+      "name": "base-react",
       "version": "0.0.0",
       "dependencies": {
         "@tanstack/react-query": "^5.99.0",
+        "axios": "^1.16.0",
         "react": "^19.2.4",
         "react-dom": "^19.2.4",
         "react-router-dom": "^7.14.1",
@@ -1338,6 +1339,23 @@
       "dev": true,
       "license": "Python-2.0"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
+      "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1401,6 +1419,19 @@
       },
       "engines": {
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/callsites": {
@@ -1480,6 +1511,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1554,6 +1597,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/detect-libc": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1564,12 +1616,71 @@
         "node": ">=8"
       }
     },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.5.338",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz",
       "integrity": "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg==",
       "dev": true,
       "license": "ISC"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
     },
     "node_modules/escalade": {
       "version": "3.2.0",
@@ -1868,6 +1979,42 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1883,6 +2030,15 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1891,6 +2047,43 @@
       "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/glob-parent": {
@@ -1919,6 +2112,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1927,6 +2132,45 @@
       "license": "MIT",
       "engines": {
         "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/hermes-estree": {
@@ -2410,6 +2654,36 @@
         "yallist": "^3.0.2"
       }
     },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -2605,6 +2879,15 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
package.json
--- package.json
+++ package.json
@@ -12,6 +12,7 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.99.0",
+    "axios": "^1.16.0",
     "react": "^19.2.4",
     "react-dom": "^19.2.4",
     "react-router-dom": "^7.14.1",
src/App.tsx
--- src/App.tsx
+++ src/App.tsx
@@ -2,6 +2,7 @@
 import { UserLayout } from './user/UserLayout';
 import { UserListPage } from './user/UserListPage';
 import {AdminLayout} from "./admin/layout/AdminLayout.tsx";
+import {AdminRoute} from "./admin/route/AdminRoute.tsx";
 
 type Skin = 'admin' | 'user';
 
@@ -53,7 +54,8 @@
       </div>
 
       {skin === 'admin' ? (
-        <AdminLayout children={undefined}>
+        <AdminLayout>
+          <AdminRoute />
         </AdminLayout>
       ) : (
         <UserLayout>
src/admin/component/menu/MenuListItem.tsx
--- src/admin/component/menu/MenuListItem.tsx
+++ src/admin/component/menu/MenuListItem.tsx
@@ -1,4 +1,6 @@
+import {Link} from "react-router-dom";
 import type {MenuItem} from "./MenuList.tsx";
+import {ADMIN_ROUTE_PREFIX} from "../../route/adminRouteMap.ts";
 
 type MenuItemProps = {
     menuItem: MenuItem,
@@ -13,18 +15,22 @@
                                  openedMenuNo,
                                  onClick,
                              }: MenuItemProps) => {
+
+
     return (
         <li className={`depth01 ${openedMenuNo === menuItem.no ? "on" : ""}`} onClick={() => onClick(menuItem.no)}>
             <button className="menu_title">{menuItem.name}</button>
             <ul className="depth02">
                 {menuItemList.map((item: MenuItem, index: number) => {
+                    const routePath = ADMIN_ROUTE_PREFIX + item.url;
+
                     return item.upperNo === menuItem.no ? (
                         <li key={index} value={item.url}>
-                            <a>{item.name}</a>
+                            <Link to={routePath}>{item.name}</Link>
                         </li>
                     ) : null;
                 })}
             </ul>
         </li>
     );
-}
(No newline at end of file)
+}
 
src/admin/feature/board/api/boardApi.ts (added)
+++ src/admin/feature/board/api/boardApi.ts
@@ -0,0 +1,7 @@
+import type {BoardSearchParams, BoardListItem} from "../model/board.types.ts";
+import {apiClient} from "../../../../api/apiClient.ts";
+import type {PageResponse} from "../../../../type/pageResponse.ts";
+
+export async function fetchBoardList(params: BoardSearchParams) {
+    return apiClient.get<PageResponse<BoardListItem>>('/cop/bbs/list.do', params);
+}
 
src/admin/feature/board/hook/useBoardList.ts (added)
+++ src/admin/feature/board/hook/useBoardList.ts
@@ -0,0 +1,13 @@
+import type {BoardSearchParams} from "../model/board.types.ts";
+
+import {keepPreviousData, useQuery} from "@tanstack/react-query";
+import {fetchBoardList} from "../api/boardApi.ts";
+
+export function useBoardListQuery(searchParams: BoardSearchParams) {
+
+    return useQuery({
+        queryKey: ['boardList', searchParams],
+        queryFn: () => fetchBoardList(searchParams),
+        placeholderData: keepPreviousData,
+    });
+}
 
src/admin/feature/board/model/board.types.ts (added)
+++ src/admin/feature/board/model/board.types.ts
@@ -0,0 +1,18 @@
+export interface BoardSearchParams {
+    searchCondition: string
+    searchSortOrder: string
+    searchKeyword: string
+    pageUnit: number,
+    pageIndex: number
+}
+
+export interface BoardListItem {
+    bbsId: string
+    bbsNm: string
+    menuNm: string
+    newCnt: number
+    totCnt: number
+    bbsTyCodeNm: string
+    frstRegisterPnttm: string
+    useAt: 'Y' | 'N'
+}(No newline at end of file)
 
src/admin/feature/board/page/BoardListPage.tsx (added)
+++ src/admin/feature/board/page/BoardListPage.tsx
@@ -0,0 +1,7 @@
+export const BoardListPage = () => {
+    return (
+        <div>
+            <h2>게시판 관리</h2>
+        </div>
+    );
+};
src/admin/hook/useMenuList.ts
--- src/admin/hook/useMenuList.ts
+++ src/admin/hook/useMenuList.ts
@@ -1,78 +1,47 @@
-import {useEffect, useState} from 'react';
-import {apiClient} from '../../api/apiClient';
-import type {MenuItem} from '../component/menu/MenuList';
+import {fetchMenuList} from "../../api/menuApi.ts";
+import {useQuery} from "@tanstack/react-query";
+import type {MenuItem, MenuItemResponse} from "../../type/menu.ts";
 
-type BackendMenuItem = {
-    menuNo?: number | string;
-    menuNm?: string;
-    url?: string;
-    upperMenuId?: number | string;
-};
-
-type MenuLeftResponse = {
-    head?: BackendMenuItem[];
-    menu?: BackendMenuItem[];
-};
-
-function toMenuItem(item: BackendMenuItem): MenuItem {
+function toMenuItem(item: MenuItemResponse): MenuItem {
     return {
         no: String(item.menuNo ?? ''),
         name: item.menuNm ?? '',
-        url: item.url ?? '#',
+        url: item.chkURL ?? '#',
         upperNo: String(item.upperMenuId ?? '0'),
     };
 }
 
 export const useMenuList = () => {
-    const [headMenuList, setHeadMenuList] = useState<MenuItem[]>([]);
-    const [menuList, setMenuList] = useState<MenuItem[]>([]);
-    const [isLoading, setIsLoading] = useState(true);
-    const [errorMessage, setErrorMessage] = useState<string | null>(null);
+    const query = useQuery({
+        queryKey: ['menuList'],
+        queryFn: fetchMenuList,
+        select: (data) => {
+            console.log(data);
+            const headMenuList =
+                (data.head ?? [])
+                    .map(toMenuItem)
+                    .filter((item: MenuItem) => item.no && item.name);
 
-    useEffect(() => {
+            const menuList =
+                (data.menu ?? [])
+                    .map(toMenuItem)
+                    .filter((item: MenuItem) => item.no && item.name);
 
-        let mounted = true;
+            return {
+                headMenuList,
+                menuList,
+            };
+        },
 
-        apiClient.get<MenuLeftResponse>('/sym/mms/menuLeft.do')
-            .then((data) => {
-                if (!mounted) {
-                    return;
-                }
+        staleTime: 1000 * 60 * 10,
+    });
 
-                const nextHeadMenuList = (data.head ?? []).map(toMenuItem).filter((item) => item.no && item.name);
-                const nextMenuList = (data.menu ?? []).map(toMenuItem).filter((item) => item.no && item.name);
-
-                setHeadMenuList(nextHeadMenuList.length > 0 ? nextHeadMenuList : []);
-                setMenuList(nextMenuList.length > 0 ? nextMenuList : []);
-
-                setErrorMessage(null);
-            })
-
-            .catch((error: Error) => {
-
-                if (!mounted) {
-                    return;
-                }
-
-                setErrorMessage(error ? error.message : '메뉴 조회에 실패했습니다.');
-            })
-
-            .finally(() => {
-                if (mounted) {
-                    setIsLoading(false);
-                }
-            });
-
-        return () => {
-            mounted = false;
-        };
-
-    }, []);
+    console.log(query);
 
     return {
-        headMenuList,
-        menuList,
-        isLoading,
-        errorMessage,
+        headMenuList: query.data?.headMenuList ?? [],
+        menuList: query.data?.menuList ?? [],
+        isLoading: query.isLoading,
+        errorMessage: query.error instanceof Error ? query.error.message : null,
     };
 };
(No newline at end of file)
src/admin/route/AdminRoute.tsx
--- src/admin/route/AdminRoute.tsx
+++ src/admin/route/AdminRoute.tsx
@@ -1,8 +1,17 @@
+import {Navigate, Route, Routes} from "react-router-dom";
+import {BoardListPage} from "../feature/board/page/BoardListPage.tsx";
+import {ADMIN_BBS_MASTER_ROUTE} from "./adminRouteMap.ts";
 
+const ReadyPage = () => {
+    return <div>Preparing menu.</div>;
+};
 
-export const adminRoute = () => {
+export const AdminRoute = () => {
     return (
-        <>
-        </>
+        <Routes>
+            <Route path="/" element={<Navigate to={ADMIN_BBS_MASTER_ROUTE} replace />} />
+            <Route path={ADMIN_BBS_MASTER_ROUTE} element={<BoardListPage />} />
+            <Route path="*" element={<ReadyPage />} />
+        </Routes>
     );
-}
+};
 
src/admin/route/adminRouteMap.ts (added)
+++ src/admin/route/adminRouteMap.ts
@@ -0,0 +1,22 @@
+export const ADMIN_ROUTE_PREFIX = '/admin';
+
+export const ADMIN_MENU_CREATE_TREE_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/mnu/mcm/EgovMenuCreatSelectJtree.do`;
+export const ADMIN_AUTHOR_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/ram/EgovAuthorList.do`;
+export const ADMIN_MAIN_ZONE_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/mainZoneList.do`;
+export const ADMIN_CONTENT_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/cnt/contentList.do`;
+export const ADMIN_MAIN_PAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/cmm/main/mainPage.do`;
+export const ADMIN_BBS_MASTER_ROUTE = `${ADMIN_ROUTE_PREFIX}/cop/bbs/SelectBBSMasterInfs.do`;
+export const ADMIN_LOGIN_GROUP_POLICY_ROUTE = `${ADMIN_ROUTE_PREFIX}/uat/uap/selectLoginGroupPolicyList.do`;
+export const ADMIN_MEMBER_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/umt/EgovMberManage.do`;
+export const ADMIN_POPUP_ZONE_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/popupZoneList.do`;
+export const ADMIN_MENU_CREATE_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/mnu/mcm/EgovMenuCreatManageSelect.do`;
+export const ADMIN_USER_MANAGE_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/umt/user/EgovUserManage.do`;
+export const ADMIN_COMMON_CODE_TREE_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/ccm/ccc/EgovCcmCmmnCodeTree.do`;
+export const ADMIN_AUTHOR_GROUP_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/rgm/EgovAuthorGroupList.do`;
+export const ADMIN_POPUP_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/pwm/egovPopupList.do`;
+export const ADMIN_WEB_LOG_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/log/clg/SelectWebLogList.do`;
+export const ADMIN_ROLE_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sec/rmt/EgovRoleList.do`;
+export const ADMIN_BANNER_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/uss/ion/bnr/selectBannerList.do`;
+export const ADMIN_USER_WEB_LOG_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/log/clg/NSelectWebLogList.do`;
+export const ADMIN_LOGIN_LOG_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/log/clg/SelectLoginLogList.do`;
+export const ADMIN_LOG_METHOD_LIST_ROUTE = `${ADMIN_ROUTE_PREFIX}/sym/log/clg/SelectLogMethodList.do`;
src/api/apiClient.ts
--- src/api/apiClient.ts
+++ src/api/apiClient.ts
@@ -1,57 +1,46 @@
+import axios, {type AxiosRequestConfig, type AxiosResponse} from 'axios';
+import type {ApiResponse} from "../type/apiResponse.ts";
+
 const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '';
 const ADMIN_LOGIN_URL = '/uat/uia/actionSecurityLogin.do';
 const ADMIN_ID = 'admin';
 const ADMIN_PASSWORD = '1';
 const MAX_RETRY_COUNT = 3;
 
-type ApiResponse<T> = {
-    success: boolean;
-    message: string;
-    data: T;
-}
-
-
 class ApiClient {
     private loginPromise: Promise<void> | null = null;
 
-    private toApiUrl(path: string) {
-        if (/^https?:\/\//i.test(path)) {
-            return path;
-        }
-
-        return `${API_BASE_URL}${path}`;
-    }
-
-    private createHeaders(headers?: HeadersInit) {
-        const result = new Headers(headers);
-
-        result.set('X-Requested-With', 'XMLHttpRequest');
-
-        return result;
-    }
+    private readonly client = axios.create({
+        baseURL: API_BASE_URL,
+        withCredentials: true,
+        headers: {
+            'X-Requested-With': 'XMLHttpRequest',
+        },
+        validateStatus: () => true,
+    });
 
     private async login() {
         if (!this.loginPromise) {
-            this.loginPromise = fetch(this.toApiUrl(ADMIN_LOGIN_URL), {
-                method: 'POST',
-                credentials: 'include',
-                headers: {
-                    'Content-Type': 'application/x-www-form-urlencoded',
-                    'X-Requested-With': 'XMLHttpRequest',
-                },
-                body: new URLSearchParams({
+            this.loginPromise = this.client.post<ApiResponse<unknown>>(
+                ADMIN_LOGIN_URL,
+                new URLSearchParams({
                     id: ADMIN_ID,
                     password: ADMIN_PASSWORD,
                 }),
-            }).then(async (response) => {
-                if (!response.ok) {
-                    throw new Error(`로그인 실패 : ${response.status}`);
+                {
+                    headers: {
+                        'Content-Type': 'application/x-www-form-urlencoded',
+                    },
+                },
+            ).then((response) => {
+                if (response.status < 200 || response.status >= 300) {
+                    throw new Error(`Login failed: ${response.status}`);
                 }
 
-                const contentType = response.headers.get('content-type') ?? '';
+                const contentType = String(response.headers['content-type'] ?? '');
 
-                if (contentType === 'application/json') {
-                    const result = (await response.json()) as ApiResponse<unknown>;
+                if (contentType.includes('application/json')) {
+                    const result = response.data;
 
                     if (!result.success) {
                         throw new Error(result.message);
@@ -61,72 +50,55 @@
                 this.loginPromise = null;
             });
         }
+
         return this.loginPromise;
     }
 
-    private async needsLogin(response: Response) {
-        if(response.status === 401 || response.status === 403) {
+    private needsLogin(response: AxiosResponse<unknown>) {
+        if (response.status === 401 || response.status === 403) {
             return true;
         }
 
-        const contentType=  response.headers.get("content-type") ?? '';
+        const contentType = String(response.headers['content-type'] ?? '');
 
-        if (contentType.includes('text/html')) {
-
-            const html = await response.clone().text();
-
+        if (contentType.includes('text/html') && typeof response.data === 'string') {
             return (
-                html.includes('/uat/uia/actionMain.do') ||
-                html.includes('actionSecurityLogin.do')
+                response.data.includes('/uat/uia/actionMain.do') ||
+                response.data.includes('actionSecurityLogin.do')
             );
         }
 
-        if (contentType.includes('application/json')) {
-
-            try {
-
-                const result = (await response.clone().json()) as ApiResponse<unknown>;
-
-                return (!result.success && /login|로그인/i.test(result.message ?? ''));
-
-            } catch {
-                return false;
-            }
+        if (contentType.includes('application/json') && this.isApiResponse(response.data)) {
+            return !response.data.success && /login|로그인/i.test(response.data.message ?? '');
         }
 
         return false;
     }
-    private async request(
-        path: string,
-        init: RequestInit = {},
-        retryCount = 0
-    ): Promise<Response> {
 
-        const response = await fetch(this.toApiUrl(path), {
-            ...init,
-            credentials: 'include',
-            headers: this.createHeaders(init.headers),
-            redirect: 'manual',
-        });
+    private async request<T>(
+        config: AxiosRequestConfig,
+        retryCount = 0,
+    ): Promise<AxiosResponse<ApiResponse<T>>> {
+        const response = await this.client.request<ApiResponse<T>>(config);
 
-        if (await this.needsLogin(response)) {
-
+        if (this.needsLogin(response)) {
             if (retryCount >= MAX_RETRY_COUNT) {
-                throw new Error('인증 재시도 횟수 초과');
+                throw new Error('Authentication retry count exceeded');
             }
 
             await this.login();
 
-            return this.request(path, init, retryCount + 1);
+            return this.request<T>(config, retryCount + 1);
         }
 
         return response;
     }
 
-    async get<T>(path: string): Promise<T> {
-
-        const response = await this.request(path, {
+    async get<T>(path: string, params?: AxiosRequestConfig['params']): Promise<T> {
+        const response = await this.request<T>({
+            url: path,
             method: 'GET',
+            params,
         });
 
         return this.parseJson<T>(response);
@@ -134,12 +106,12 @@
 
     async post<T>(
         path: string,
-        body?: unknown
+        body?: unknown,
     ): Promise<T> {
-
-        const response = await this.request(path, {
+        const response = await this.request<T>({
+            url: path,
             method: 'POST',
-            body: body ? JSON.stringify(body) : undefined,
+            data: body,
             headers: {
                 'Content-Type': 'application/json',
             },
@@ -148,21 +120,27 @@
         return this.parseJson<T>(response);
     }
 
-
-    private async parseJson<T>(response: Response): Promise<T> {
-
-        if (!response.ok) {
+    private parseJson<T>(response: AxiosResponse<ApiResponse<T>>): T {
+        if (response.status < 200 || response.status >= 300) {
             throw new Error(`API request failed: ${response.status}`);
         }
 
-        const result = (await response.json()) as ApiResponse<T>;
+        const result = response.data;
+
+        if (!this.isApiResponse<T>(result)) {
+            throw new Error('Invalid API response');
+        }
 
         if (!result.success) {
             throw new Error(result.message ?? 'API request failed');
         }
 
-        return result.data as T;
+        return result.data;
+    }
+
+    private isApiResponse<T>(data: unknown): data is ApiResponse<T> {
+        return typeof data === 'object' && data !== null && 'success' in data;
     }
 }
 
-export const apiClient = new ApiClient();
(No newline at end of file)
+export const apiClient = new ApiClient();
 
src/api/menuApi.ts (added)
+++ src/api/menuApi.ts
@@ -0,0 +1,6 @@
+import {apiClient} from "./apiClient.ts";
+import type {MenuLeftResponse} from "../type/menu.ts";
+
+export async function fetchMenuList(): Promise<MenuLeftResponse> {
+    return await apiClient.get<MenuLeftResponse>('/sym/mms/menuLeft.do');
+}(No newline at end of file)
src/main.tsx
--- src/main.tsx
+++ src/main.tsx
@@ -1,10 +1,15 @@
-import { StrictMode } from 'react';
 import { createRoot } from 'react-dom/client';
+import {QueryClientProvider, QueryClient} from "@tanstack/react-query";
+import {BrowserRouter} from "react-router-dom";
 import App from './App';
 import './styles/app.css';
 
+const queryClient = new QueryClient();
+
 createRoot(document.getElementById('root')!).render(
-  <StrictMode>
-    <App />
-  </StrictMode>,
+  <QueryClientProvider client={queryClient}>
+    <BrowserRouter>
+      <App />
+    </BrowserRouter>
+  </QueryClientProvider>,
 );
 
src/type/apiResponse.ts (added)
+++ src/type/apiResponse.ts
@@ -0,0 +1,5 @@
+export interface ApiResponse<T> {
+    success: boolean;
+    message: string;
+    data: T;
+}(No newline at end of file)
 
src/type/menu.ts (added)
+++ src/type/menu.ts
@@ -0,0 +1,18 @@
+export interface MenuItemResponse {
+    menuNo?: number | string;
+    menuNm?: string;
+    chkURL?: string;
+    upperMenuId?: number | string;
+}
+
+export interface MenuLeftResponse {
+    head?: MenuItemResponse[];
+    menu?: MenuItemResponse[];
+}
+
+export interface MenuItem {
+    no: string,
+    name: string,
+    url: string,
+    upperNo: string,
+}
 
src/type/pageResponse.ts (added)
+++ src/type/pageResponse.ts
@@ -0,0 +1,6 @@
+export interface PageResponse<T> {
+    list: T[]
+    totalCount: number
+    currentPage: number
+    recordPerPage: number
+}(No newline at end of file)
Add a comment
List