--- src/admin/hook/useMenuList.ts
+++ src/admin/hook/useMenuList.ts
... | ... | @@ -1,84 +1,78 @@ |
| 1 |
-import { useEffect, useState } from 'react';
|
|
| 2 |
-import { adminAuthJson } from '../../api/adminAuth';
|
|
| 3 |
-import type { MenuItem } from '../component/menu/MenuList';
|
|
| 1 |
+import {useEffect, useState} from 'react';
|
|
| 2 |
+import {apiClient} from '../../api/apiClient';
|
|
| 3 |
+import type {MenuItem} from '../component/menu/MenuList';
|
|
| 4 | 4 |
|
| 5 | 5 |
type BackendMenuItem = {
|
| 6 |
- menuNo?: number | string; |
|
| 7 |
- menuNm?: string; |
|
| 8 |
- url?: string; |
|
| 9 |
- upperMenuId?: number | string; |
|
| 6 |
+ menuNo?: number | string; |
|
| 7 |
+ menuNm?: string; |
|
| 8 |
+ url?: string; |
|
| 9 |
+ upperMenuId?: number | string; |
|
| 10 | 10 |
}; |
| 11 | 11 |
|
| 12 | 12 |
type MenuLeftResponse = {
|
| 13 |
- head?: BackendMenuItem[]; |
|
| 14 |
- menu?: BackendMenuItem[]; |
|
| 13 |
+ head?: BackendMenuItem[]; |
|
| 14 |
+ menu?: BackendMenuItem[]; |
|
| 15 | 15 |
}; |
| 16 | 16 |
|
| 17 |
-const fallbackHeadMenuList: MenuItem[] = [ |
|
| 18 |
- { name: '테스트 메뉴1', no: '1', url: '#', upperNo: '0' },
|
|
| 19 |
- { name: '테스트 메뉴2', no: '2', url: '#', upperNo: '0' },
|
|
| 20 |
- { name: '테스트 메뉴3', no: '3', url: '#', upperNo: '0' },
|
|
| 21 |
-]; |
|
| 22 |
- |
|
| 23 |
-const fallbackMenuList: MenuItem[] = [ |
|
| 24 |
- { name: '테스트 1', no: '6', url: '#', upperNo: '1' },
|
|
| 25 |
- { name: '테스트 2', no: '7', url: '#', upperNo: '1' },
|
|
| 26 |
- { name: '테스트 3', no: '8', url: '#', upperNo: '2' },
|
|
| 27 |
-]; |
|
| 28 |
- |
|
| 29 | 17 |
function toMenuItem(item: BackendMenuItem): MenuItem {
|
| 30 |
- return {
|
|
| 31 |
- no: String(item.menuNo ?? ''), |
|
| 32 |
- name: item.menuNm ?? '', |
|
| 33 |
- url: item.url ?? '#', |
|
| 34 |
- upperNo: String(item.upperMenuId ?? '0'), |
|
| 35 |
- }; |
|
| 18 |
+ return {
|
|
| 19 |
+ no: String(item.menuNo ?? ''), |
|
| 20 |
+ name: item.menuNm ?? '', |
|
| 21 |
+ url: item.url ?? '#', |
|
| 22 |
+ upperNo: String(item.upperMenuId ?? '0'), |
|
| 23 |
+ }; |
|
| 36 | 24 |
} |
| 37 | 25 |
|
| 38 | 26 |
export const useMenuList = () => {
|
| 39 |
- const [headMenuList, setHeadMenuList] = useState<MenuItem[]>(fallbackHeadMenuList); |
|
| 40 |
- const [menuList, setMenuList] = useState<MenuItem[]>(fallbackMenuList); |
|
| 41 |
- const [isLoading, setIsLoading] = useState(true); |
|
| 42 |
- const [errorMessage, setErrorMessage] = useState<string | null>(null); |
|
| 27 |
+ const [headMenuList, setHeadMenuList] = useState<MenuItem[]>([]); |
|
| 28 |
+ const [menuList, setMenuList] = useState<MenuItem[]>([]); |
|
| 29 |
+ const [isLoading, setIsLoading] = useState(true); |
|
| 30 |
+ const [errorMessage, setErrorMessage] = useState<string | null>(null); |
|
| 43 | 31 |
|
| 44 |
- useEffect(() => {
|
|
| 45 |
- let mounted = true; |
|
| 32 |
+ useEffect(() => {
|
|
| 46 | 33 |
|
| 47 |
- adminAuthJson<MenuLeftResponse>('/sym/mms/menuLeft.do')
|
|
| 48 |
- .then((data) => {
|
|
| 49 |
- if (!mounted) {
|
|
| 50 |
- return; |
|
| 51 |
- } |
|
| 34 |
+ let mounted = true; |
|
| 52 | 35 |
|
| 53 |
- const nextHeadMenuList = (data.head ?? []).map(toMenuItem).filter((item) => item.no && item.name); |
|
| 54 |
- const nextMenuList = (data.menu ?? []).map(toMenuItem).filter((item) => item.no && item.name); |
|
| 36 |
+ apiClient.get<MenuLeftResponse>('/sym/mms/menuLeft.do')
|
|
| 37 |
+ .then((data) => {
|
|
| 38 |
+ if (!mounted) {
|
|
| 39 |
+ return; |
|
| 40 |
+ } |
|
| 55 | 41 |
|
| 56 |
- setHeadMenuList(nextHeadMenuList.length > 0 ? nextHeadMenuList : fallbackHeadMenuList); |
|
| 57 |
- setMenuList(nextMenuList.length > 0 ? nextMenuList : fallbackMenuList); |
|
| 58 |
- setErrorMessage(null); |
|
| 59 |
- }) |
|
| 60 |
- .catch((error: unknown) => {
|
|
| 61 |
- if (!mounted) {
|
|
| 62 |
- return; |
|
| 63 |
- } |
|
| 42 |
+ const nextHeadMenuList = (data.head ?? []).map(toMenuItem).filter((item) => item.no && item.name); |
|
| 43 |
+ const nextMenuList = (data.menu ?? []).map(toMenuItem).filter((item) => item.no && item.name); |
|
| 64 | 44 |
|
| 65 |
- setErrorMessage(error instanceof Error ? error.message : '메뉴 조회에 실패했습니다.'); |
|
| 66 |
- }) |
|
| 67 |
- .finally(() => {
|
|
| 68 |
- if (mounted) {
|
|
| 69 |
- setIsLoading(false); |
|
| 70 |
- } |
|
| 71 |
- }); |
|
| 45 |
+ setHeadMenuList(nextHeadMenuList.length > 0 ? nextHeadMenuList : []); |
|
| 46 |
+ setMenuList(nextMenuList.length > 0 ? nextMenuList : []); |
|
| 72 | 47 |
|
| 73 |
- return () => {
|
|
| 74 |
- mounted = false; |
|
| 48 |
+ setErrorMessage(null); |
|
| 49 |
+ }) |
|
| 50 |
+ |
|
| 51 |
+ .catch((error: Error) => {
|
|
| 52 |
+ |
|
| 53 |
+ if (!mounted) {
|
|
| 54 |
+ return; |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ setErrorMessage(error ? error.message : '메뉴 조회에 실패했습니다.'); |
|
| 58 |
+ }) |
|
| 59 |
+ |
|
| 60 |
+ .finally(() => {
|
|
| 61 |
+ if (mounted) {
|
|
| 62 |
+ setIsLoading(false); |
|
| 63 |
+ } |
|
| 64 |
+ }); |
|
| 65 |
+ |
|
| 66 |
+ return () => {
|
|
| 67 |
+ mounted = false; |
|
| 68 |
+ }; |
|
| 69 |
+ |
|
| 70 |
+ }, []); |
|
| 71 |
+ |
|
| 72 |
+ return {
|
|
| 73 |
+ headMenuList, |
|
| 74 |
+ menuList, |
|
| 75 |
+ isLoading, |
|
| 76 |
+ errorMessage, |
|
| 75 | 77 |
}; |
| 76 |
- }, []); |
|
| 77 |
- |
|
| 78 |
- return {
|
|
| 79 |
- headMenuList, |
|
| 80 |
- menuList, |
|
| 81 |
- isLoading, |
|
| 82 |
- errorMessage, |
|
| 83 |
- }; |
|
| 84 |
-}; |
|
| 78 |
+};(No newline at end of file) |
--- src/api/adminAuth.ts
... | ... | @@ -1,123 +0,0 @@ |
| 1 | -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ''; | |
| 2 | -const ADMIN_LOGIN_URL = '/uat/uia/actionSecurityLogin.do'; | |
| 3 | -const ADMIN_ID = 'admin'; | |
| 4 | -const ADMIN_PASSWORD = '1'; | |
| 5 | - | |
| 6 | -type ApiEnvelope<T> = { | |
| 7 | - success?: boolean; | |
| 8 | - message?: string; | |
| 9 | - data?: T; | |
| 10 | -}; | |
| 11 | - | |
| 12 | -let loginPromise: Promise<void> | null = null; | |
| 13 | - | |
| 14 | -function toApiUrl(path: string) { | |
| 15 | - if (/^https?:\/\//i.test(path)) { | |
| 16 | - return path; | |
| 17 | - } | |
| 18 | - | |
| 19 | - return `${API_BASE_URL}${path}`; | |
| 20 | -} | |
| 21 | - | |
| 22 | -function createHeaders(headers?: HeadersInit) { | |
| 23 | - return new Headers(headers); | |
| 24 | -} | |
| 25 | - | |
| 26 | -async function adminLogin() { | |
| 27 | - if (!loginPromise) { | |
| 28 | - loginPromise = fetch(toApiUrl(ADMIN_LOGIN_URL), { | |
| 29 | - method: 'POST', | |
| 30 | - credentials: 'include', | |
| 31 | - headers: { | |
| 32 | - 'Content-Type': 'application/x-www-form-urlencoded', | |
| 33 | - 'X-Requested-With': 'XMLHttpRequest', | |
| 34 | - }, | |
| 35 | - body: new URLSearchParams({ | |
| 36 | - id: ADMIN_ID, | |
| 37 | - password: ADMIN_PASSWORD, | |
| 38 | - }), | |
| 39 | - }) | |
| 40 | - .then(async (response) => { | |
| 41 | - if (!response.ok) { | |
| 42 | - throw new Error(`Admin login failed: ${response.status}`); | |
| 43 | - } | |
| 44 | - | |
| 45 | - const contentType = response.headers.get('content-type') ?? ''; | |
| 46 | - if (contentType.includes('application/json')) { | |
| 47 | - const result = (await response.json()) as ApiEnvelope<unknown>; | |
| 48 | - if (result.success === false) { | |
| 49 | - throw new Error(result.message ?? 'Admin login failed'); | |
| 50 | - } | |
| 51 | - } | |
| 52 | - }) | |
| 53 | - .finally(() => { | |
| 54 | - loginPromise = null; | |
| 55 | - }); | |
| 56 | - } | |
| 57 | - | |
| 58 | - return loginPromise; | |
| 59 | -} | |
| 60 | - | |
| 61 | -async function needsAdminLogin(response: Response) { | |
| 62 | - if (response.type === 'opaqueredirect' || response.status === 0) { | |
| 63 | - return true; | |
| 64 | - } | |
| 65 | - | |
| 66 | - if (response.status >= 300 && response.status < 400) { | |
| 67 | - return true; | |
| 68 | - } | |
| 69 | - | |
| 70 | - if (response.status === 401 || response.status === 403) { | |
| 71 | - return true; | |
| 72 | - } | |
| 73 | - | |
| 74 | - const contentType = response.headers.get('content-type') ?? ''; | |
| 75 | - | |
| 76 | - if (contentType.includes('text/html')) { | |
| 77 | - const html = await response.clone().text(); | |
| 78 | - return html.includes('/uat/uia/actionMain.do') || html.includes('actionSecurityLogin.do'); | |
| 79 | - } | |
| 80 | - | |
| 81 | - if (contentType.includes('application/json')) { | |
| 82 | - try { | |
| 83 | - const result = (await response.clone().json()) as ApiEnvelope<unknown>; | |
| 84 | - const message = result.message ?? ''; | |
| 85 | - return result.success === false && /login|로그인|/.test(message); | |
| 86 | - } catch { | |
| 87 | - return false; | |
| 88 | - } | |
| 89 | - } | |
| 90 | - | |
| 91 | - return false; | |
| 92 | -} | |
| 93 | - | |
| 94 | -export async function adminAuthFetch(path: string, init: RequestInit = {}, retry = true): Promise<Response> { | |
| 95 | - const response = await fetch(toApiUrl(path), { | |
| 96 | - ...init, | |
| 97 | - credentials: 'include', | |
| 98 | - headers: createHeaders(init.headers), | |
| 99 | - redirect: 'manual', | |
| 100 | - }); | |
| 101 | - | |
| 102 | - if (retry && (await needsAdminLogin(response))) { | |
| 103 | - await adminLogin(); | |
| 104 | - return adminAuthFetch(path, init, false); | |
| 105 | - } | |
| 106 | - | |
| 107 | - return response; | |
| 108 | -} | |
| 109 | - | |
| 110 | -export async function adminAuthJson<T>(path: string, init?: RequestInit): Promise<T> { | |
| 111 | - const response = await adminAuthFetch(path, init); | |
| 112 | - | |
| 113 | - if (!response.ok) { | |
| 114 | - throw new Error(`API request failed: ${response.status}`); | |
| 115 | - } | |
| 116 | - | |
| 117 | - const result = (await response.json()) as ApiEnvelope<T>; | |
| 118 | - if (result.success === false) { | |
| 119 | - throw new Error(result.message ?? 'API request failed'); | |
| 120 | - } | |
| 121 | - | |
| 122 | - return result.data as T; | |
| 123 | -} |
+++ src/api/apiClient.ts
... | ... | @@ -0,0 +1,168 @@ |
| 1 | +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ''; | |
| 2 | +const ADMIN_LOGIN_URL = '/uat/uia/actionSecurityLogin.do'; | |
| 3 | +const ADMIN_ID = 'admin'; | |
| 4 | +const ADMIN_PASSWORD = '1'; | |
| 5 | +const MAX_RETRY_COUNT = 3; | |
| 6 | + | |
| 7 | +type ApiResponse<T> = { | |
| 8 | + success: boolean; | |
| 9 | + message: string; | |
| 10 | + data: T; | |
| 11 | +} | |
| 12 | + | |
| 13 | + | |
| 14 | +class ApiClient { | |
| 15 | + private loginPromise: Promise<void> | null = null; | |
| 16 | + | |
| 17 | + private toApiUrl(path: string) { | |
| 18 | + if (/^https?:\/\//i.test(path)) { | |
| 19 | + return path; | |
| 20 | + } | |
| 21 | + | |
| 22 | + return `${API_BASE_URL}${path}`; | |
| 23 | + } | |
| 24 | + | |
| 25 | + private createHeaders(headers?: HeadersInit) { | |
| 26 | + const result = new Headers(headers); | |
| 27 | + | |
| 28 | + result.set('X-Requested-With', 'XMLHttpRequest'); | |
| 29 | + | |
| 30 | + return result; | |
| 31 | + } | |
| 32 | + | |
| 33 | + private async login() { | |
| 34 | + if (!this.loginPromise) { | |
| 35 | + this.loginPromise = fetch(this.toApiUrl(ADMIN_LOGIN_URL), { | |
| 36 | + method: 'POST', | |
| 37 | + credentials: 'include', | |
| 38 | + headers: { | |
| 39 | + 'Content-Type': 'application/x-www-form-urlencoded', | |
| 40 | + 'X-Requested-With': 'XMLHttpRequest', | |
| 41 | + }, | |
| 42 | + body: new URLSearchParams({ | |
| 43 | + id: ADMIN_ID, | |
| 44 | + password: ADMIN_PASSWORD, | |
| 45 | + }), | |
| 46 | + }).then(async (response) => { | |
| 47 | + if (!response.ok) { | |
| 48 | + throw new Error(`로그인 실패 : ${response.status}`); | |
| 49 | + } | |
| 50 | + | |
| 51 | + const contentType = response.headers.get('content-type') ?? ''; | |
| 52 | + | |
| 53 | + if (contentType === 'application/json') { | |
| 54 | + const result = (await response.json()) as ApiResponse<unknown>; | |
| 55 | + | |
| 56 | + if (!result.success) { | |
| 57 | + throw new Error(result.message); | |
| 58 | + } | |
| 59 | + } | |
| 60 | + }).finally(() => { | |
| 61 | + this.loginPromise = null; | |
| 62 | + }); | |
| 63 | + } | |
| 64 | + return this.loginPromise; | |
| 65 | + } | |
| 66 | + | |
| 67 | + private async needsLogin(response: Response) { | |
| 68 | + if(response.status === 401 || response.status === 403) { | |
| 69 | + return true; | |
| 70 | + } | |
| 71 | + | |
| 72 | + const contentType= response.headers.get("content-type") ?? ''; | |
| 73 | + | |
| 74 | + if (contentType.includes('text/html')) { | |
| 75 | + | |
| 76 | + const html = await response.clone().text(); | |
| 77 | + | |
| 78 | + return ( | |
| 79 | + html.includes('/uat/uia/actionMain.do') || | |
| 80 | + html.includes('actionSecurityLogin.do') | |
| 81 | + ); | |
| 82 | + } | |
| 83 | + | |
| 84 | + if (contentType.includes('application/json')) { | |
| 85 | + | |
| 86 | + try { | |
| 87 | + | |
| 88 | + const result = (await response.clone().json()) as ApiResponse<unknown>; | |
| 89 | + | |
| 90 | + return (!result.success && /login|로그인/i.test(result.message ?? '')); | |
| 91 | + | |
| 92 | + } catch { | |
| 93 | + return false; | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + return false; | |
| 98 | + } | |
| 99 | + private async request( | |
| 100 | + path: string, | |
| 101 | + init: RequestInit = {}, | |
| 102 | + retryCount = 0 | |
| 103 | + ): Promise<Response> { | |
| 104 | + | |
| 105 | + const response = await fetch(this.toApiUrl(path), { | |
| 106 | + ...init, | |
| 107 | + credentials: 'include', | |
| 108 | + headers: this.createHeaders(init.headers), | |
| 109 | + redirect: 'manual', | |
| 110 | + }); | |
| 111 | + | |
| 112 | + if (await this.needsLogin(response)) { | |
| 113 | + | |
| 114 | + if (retryCount >= MAX_RETRY_COUNT) { | |
| 115 | + throw new Error('인증 재시도 횟수 초과'); | |
| 116 | + } | |
| 117 | + | |
| 118 | + await this.login(); | |
| 119 | + | |
| 120 | + return this.request(path, init, retryCount + 1); | |
| 121 | + } | |
| 122 | + | |
| 123 | + return response; | |
| 124 | + } | |
| 125 | + | |
| 126 | + async get<T>(path: string): Promise<T> { | |
| 127 | + | |
| 128 | + const response = await this.request(path, { | |
| 129 | + method: 'GET', | |
| 130 | + }); | |
| 131 | + | |
| 132 | + return this.parseJson<T>(response); | |
| 133 | + } | |
| 134 | + | |
| 135 | + async post<T>( | |
| 136 | + path: string, | |
| 137 | + body?: unknown | |
| 138 | + ): Promise<T> { | |
| 139 | + | |
| 140 | + const response = await this.request(path, { | |
| 141 | + method: 'POST', | |
| 142 | + body: body ? JSON.stringify(body) : undefined, | |
| 143 | + headers: { | |
| 144 | + 'Content-Type': 'application/json', | |
| 145 | + }, | |
| 146 | + }); | |
| 147 | + | |
| 148 | + return this.parseJson<T>(response); | |
| 149 | + } | |
| 150 | + | |
| 151 | + | |
| 152 | + private async parseJson<T>(response: Response): Promise<T> { | |
| 153 | + | |
| 154 | + if (!response.ok) { | |
| 155 | + throw new Error(`API request failed: ${response.status}`); | |
| 156 | + } | |
| 157 | + | |
| 158 | + const result = (await response.json()) as ApiResponse<T>; | |
| 159 | + | |
| 160 | + if (!result.success) { | |
| 161 | + throw new Error(result.message ?? 'API request failed'); | |
| 162 | + } | |
| 163 | + | |
| 164 | + return result.data as T; | |
| 165 | + } | |
| 166 | +} | |
| 167 | + | |
| 168 | +export const apiClient = new ApiClient();(No newline at end of file) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?