Repository: isucon/isucon11-qualify (language: go)

Issue:
[frontend]エンドポイント叩く関数の作成
https://github.com/isucon/isucon11-qualify/pull/54 ←これが入ったらやる
もうmainに入りそうだから #65 #66 #67 #68 はこれが終わるまでpendingで


Discussion / hints:

Fix patch:
diff --git a/webapp/frontend/package-lock.json b/webapp/frontend/package-lock.json
index 81e30ab5f..cd1b2a0d0 100644
--- a/webapp/frontend/package-lock.json
+++ b/webapp/frontend/package-lock.json
@@ -759,6 +759,14 @@
         }
       }
     },
+    "axios": {
+      "version": "0.21.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+      "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1444,6 +1452,11 @@
       "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
       "dev": true
     },
+    "follow-redirects": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
+      "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
+    },
     "fs-extra": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
diff --git a/webapp/frontend/package.json b/webapp/frontend/package.json
index 9545c4af5..d91d04937 100644
--- a/webapp/frontend/package.json
+++ b/webapp/frontend/package.json
@@ -1,7 +1,7 @@
 {
   "version": "0.0.0",
   "scripts": {
-    "dev": "vite",
+    "dev": "vite --port 3333",
     "build": "tsc && vite build",
     "serve": "vite preview",
     "lint": "eslint . --ext .js,.ts,.jsx,.tsx",
@@ -9,6 +9,7 @@
     "fmt:check": "prettier --check ."
   },
   "dependencies": {
+    "axios": "^0.21.1",
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
     "react-icons": "^4.2.0",
diff --git a/webapp/frontend/src/components/PageHeader/Controls.tsx b/webapp/frontend/src/components/PageHeader/Controls.tsx
index 289c1ba47..4f8ab973a 100644
--- a/webapp/frontend/src/components/PageHeader/Controls.tsx
+++ b/webapp/frontend/src/components/PageHeader/Controls.tsx
@@ -32,7 +32,7 @@ const Controls = () => {
       <ControlItem>
         <div className="flex items-center cursor-pointer" onClick={toggleModal}>
           <MdAccountCircle />
-          <div className="ml-1">{me.jiaUserID}</div>
+          <div className="ml-1">{me.jia_user_id}</div>
         </div>
       </ControlItem>
       <UserControlModal isOpen={isOpenModal} toggle={toggleModal} />
diff --git a/webapp/frontend/src/context/state.tsx b/webapp/frontend/src/context/state.tsx
index f75f4fae0..c885c16df 100644
--- a/webapp/frontend/src/context/state.tsx
+++ b/webapp/frontend/src/context/state.tsx
@@ -1,8 +1,6 @@
 import { createContext, Dispatch, useContext, useReducer } from 'react'
+import { User } from '../lib/apis'
 
-interface User {
-  jiaUserID: string
-}
 interface State {
   me: User | null
 }
diff --git a/webapp/frontend/src/lib/apis.ts b/webapp/frontend/src/lib/apis.ts
new file mode 100644
index 000000000..a9196c4ab
--- /dev/null
+++ b/webapp/frontend/src/lib/apis.ts
@@ -0,0 +1,168 @@
+import axios from 'axios'
+
+class Apis {
+  async postAuth(jwt: string) {
+    await axios.post<void>(
+      '/api/auth',
+      {},
+      {
+        headers: { Authorization: `Bearer ${jwt}` }
+      }
+    )
+  }
+
+  async postSignout() {
+    await axios.post<void>('/api/signout')
+  }
+
+  async getUserMe() {
+    const { data } = await axios.get<User>('/api/user/me')
+    return data
+  }
+
+  async getCatalog(catalogId: string) {
+    const { data } = await axios.get<Catalog>(`/api/catalog/${catalogId}`)
+    return data
+  }
+
+  async getIsus() {
+    const { data } = await axios.get<Isu[]>(`/api/isu`)
+    return data
+  }
+
+  async postIsu(isu: Isu) {
+    await axios.post<void>(`/api/isu`, isu)
+  }
+
+  async getIsuSearch(option?: IsuSearchRequest) {
+    const { data } = await axios.get<Isu[]>(`/api/isu/search`, {
+      params: option
+    })
+    return data
+  }
+
+  async getIsu(jiaIsuUuid: string) {
+    const { data } = await axios.get<Isu>(`/api/isu/${jiaIsuUuid}`)
+    return data
+  }
+
+  async putIsu(jiaIsuUuid: string, putIsuRequest: PutIsuRequest) {
+    const { data } = await axios.put<Isu>(
+      `/api/isu/${jiaIsuUuid}`,
+      putIsuRequest
+    )
+    return data
+  }
+
+  async deleteIsu(jiaIsuUuid: string) {
+    await axios.delete<Isu>(`/api/isu/${jiaIsuUuid}`)
+  }
+
+  async putIsuIcon(jiaIsuUuid: string, file: File) {
+    const data = new FormData()
+    data.append('image', file, file.name)
+    await axios.put<void>(`/api/isu/${jiaIsuUuid}/icon`, data, {
+      headers: { 'content-type': 'multipart/form-data' }
+    })
+  }
+
+  async getIsuGraphs(jiaIsuUuid: string) {
+    const { data } = await axios.get<Graph[]>(`/api/isu/${jiaIsuUuid}/graph`)
+    return data
+  }
+
+  async getConditions() {
+    const { data } = await axios.get<Condition[]>(`/api/condition`)
+    return data
+  }
+
+  async getIsuConditions(jiaIsuUuid: string) {
+    const { data } = await axios.get<Condition[]>(
+      `/api/condition/${jiaIsuUuid}`
+    )
+    return data
+  }
+}
+
+const apis = new Apis()
+export default apis
+
+export interface User {
+  jia_user_id: string
+}
+
+export interface Isu {
+  jia_isu_uuid: string
+  name: string
+  jia_catalog_id: string
+  character: string
+}
+
+export interface Catalog {
+  jia_catalog_id: string
+  name: string
+  limit_weight: number
+  weight: number
+  size: string
+  maker: string
+  tags: string
+}
+
+export interface IsuLog {
+  jia_isu_uuid: string
+  timestamp: string
+  is_sitting: boolean
+  condition: string
+  message: string
+  created_at: string
+}
+
+export interface GraphData {
+  score: number
+  sitting: number
+  detail: { [key: string]: number }
+}
+
+export interface Graph {
+  jia_isu_uuid: string
+  start_at: string
+  end_at: string
+  data: GraphData | null
+  created_at: string
+  updated_at: string
+}
+
+export interface IsuSearchRequest {
+  name?: string
+  charactor?: string
+  catalog_name?: string
+  min_limit_weight?: number
+  max_limit_weight?: number
+  catalog_tags?: string
+  page?: string
+}
+
+export interface PutIsuRequest {
+  name: string
+}
+
+export interface Condition {
+  jia_isu_uuid: string
+  isu_name: string
+  timestamp: string
+  is_sitting: boolean
+  condition: string
+  condition_level: string
+  message: string
+}
+
+// TODO: 作問の開発用、消す
+export const debugGetJWT = async (
+  req: { user: string; password: string } = {
+    user: 'isucon',
+    password: 'isucon'
+  }
+) => {
+  const { data } = await axios.post<string>('/standalone/auth', req)
+  return data
+}
diff --git a/webapp/frontend/src/pages/Auth.tsx b/webapp/frontend/src/pages/Auth.tsx
index d43169d11..ae0508255 100644
--- a/webapp/frontend/src/pages/Auth.tsx
+++ b/webapp/frontend/src/pages/Auth.tsx
@@ -1,7 +1,20 @@
+import { useDispatchContext } from '../context/state'
+import apis, { debugGetJWT } from '../lib/apis'
+
 const Auth = () => {
+  const dispatch = useDispatchContext()
+
+  // TODO: 外部APIのログインページにリダイレクト
+  const click = async () => {
+    const jwt = await debugGetJWT()
+    await apis.postAuth(jwt)
+
+    const me = await apis.getUserMe()
+    dispatch({ type: 'login', user: me })
+  }
   return (
     <div>
-      <button>ISU協会でログイン</button>
+      <button onClick={click}>ISU協会でログイン</button>
     </div>
   )
 }
diff --git a/webapp/frontend/vite.config.ts b/webapp/frontend/vite.config.ts
index 41ac4e353..24c659c1a 100644
--- a/webapp/frontend/vite.config.ts
+++ b/webapp/frontend/vite.config.ts
@@ -12,6 +12,19 @@ export default defineConfig({
       '/@': srcPath
     }
   },
+  server: {
+    proxy: {
+      '/api': {
+        target: 'http://localhost:3000',
+        changeOrigin: true
+      },
+      '/standalone': {
+        target: 'http://localhost:5000',
+        changeOrigin: true,
+        rewrite: path => path.replace(/^\/standalone/, 'api')
+      }
+    }
+  },
   plugins: [reactRefresh(), WindiCSS()],
   esbuild: {
     jsxInject: `import React from 'react'`


Test patch: