diff --git a/bench/benchmarker/world/owner.go b/bench/benchmarker/world/owner.go index 895d78e2..698688c0 100644 --- a/bench/benchmarker/world/owner.go +++ b/bench/benchmarker/world/owner.go @@ -3,6 +3,7 @@ package world import ( "fmt" "log/slog" + "math" "math/rand/v2" "sync/atomic" "time" @@ -102,7 +103,7 @@ func (p *Owner) Tick(ctx *Context) error { if err := p.ValidateSales(last.ServerCompletedAt, res); err != nil { return WrapCodeError(ErrorCodeSalesMismatched, err) } - if increase := res.Total/15000 - p.createChairTryCount; increase > 0 { + if increase := desiredChairNum(res.Total) - p.createChairTryCount; increase > 0 { ctx.ContestantLogger().Info("一定の売上が立ったためオーナーの椅子が増加します", slog.Int("id", int(p.ID)), slog.Int("increase", increase)) for range increase { p.createChairTryCount++ @@ -233,3 +234,11 @@ func (p *Owner) ValidateChairs(serverSide *GetOwnerChairsResponse) error { } return nil } + +func desiredChairNum(s int) int { + const ( + a = 15000 + r = 1.02 + ) + return int(math.Log((a-float64(s)*(1-r))/a) / math.Log(r)) +} diff --git a/bench/benchmarker/world/owner_test.go b/bench/benchmarker/world/owner_test.go new file mode 100644 index 00000000..415efcf6 --- /dev/null +++ b/bench/benchmarker/world/owner_test.go @@ -0,0 +1,14 @@ +package world + +import "testing" + +func TestDesiredChairNum(t *testing.T) { + n := 0 + for i := 0; i < 10000; i++ { + num := desiredChairNum(i * 100) + if num > n { + n = num + t.Logf("num: %d (original: %d), sales: %d", num, i*100/15000, i*100) + } + } +} diff --git a/frontend/app/components/icon/chair.tsx b/frontend/app/components/icon/chair.tsx index b4e36737..e6d6fa77 100644 --- a/frontend/app/components/icon/chair.tsx +++ b/frontend/app/components/icon/chair.tsx @@ -78,7 +78,9 @@ export const ChairIcon: FC<{ model: string } & ComponentProps<"svg">> = ({ ...props }) => { const chairType = useMemo(() => { - return ChairTypes[model ? model.charCodeAt(0) % ChairTypes.length : 0]; + return ChairTypes[ + model ? (model.charCodeAt(0) + model.length) % ChairTypes.length : 0 + ]; }, [model]); return ; }; diff --git a/frontend/app/components/modules/simulator-chair-status/simulator-chair-status.tsx b/frontend/app/components/modules/simulator-chair-status/simulator-chair-status.tsx index 2f95a501..c35eb4ab 100644 --- a/frontend/app/components/modules/simulator-chair-status/simulator-chair-status.tsx +++ b/frontend/app/components/modules/simulator-chair-status/simulator-chair-status.tsx @@ -10,8 +10,8 @@ const StatusList = { PICKUP: ["乗車待ち", colors.amber["500"]], CARRYING: ["賃走", colors.red["500"]], ARRIVED: ["到着", colors.emerald["500"]], - COMPLETED: ["完了", colors.emerald["500"]], -} as const; + COMPLETED: ["空車", colors.sky["500"]], +} as const satisfies Record; export const SimulatorChairRideStatus: FC< ComponentProps<"div"> & { diff --git a/frontend/app/components/modules/simulator-display/simulator-chair-display.tsx b/frontend/app/components/modules/simulator-display/simulator-chair-display.tsx index 06fc40b8..bee65f4e 100644 --- a/frontend/app/components/modules/simulator-display/simulator-chair-display.tsx +++ b/frontend/app/components/modules/simulator-display/simulator-chair-display.tsx @@ -99,37 +99,37 @@ const ChairProgress: FC<{ return rideStatus !== undefined ? getSimulatorStartCoordinate() : null; }, [rideStatus]); - const pickupProgress: number = useMemo(() => { + const progressToPickup: number = useMemo(() => { if (!rideStatus || !pickupLoc || !startLoc || !currentLoc) { return 0; } switch (rideStatus) { case "MATCHING": + case "COMPLETED": return 0; case "PICKUP": case "ARRIVED": case "CARRYING": - case "COMPLETED": return 100; default: return progress(startLoc, currentLoc, pickupLoc); } }, [rideStatus, pickupLoc, startLoc, currentLoc]); - const distanceProgress: number = useMemo(() => { + const progressToDestination: number = useMemo(() => { if (!rideStatus || !destLoc || !pickupLoc || !currentLoc) { return 0; } switch (rideStatus) { case "MATCHING": + case "COMPLETED": case "PICKUP": case "ENROUTE": return 0; case "ARRIVED": - case "COMPLETED": return 100; default: - return progress(destLoc, currentLoc, pickupLoc); + return progress(pickupLoc, currentLoc, destLoc); } }, [rideStatus, destLoc, pickupLoc, currentLoc]); @@ -140,13 +140,11 @@ const ChairProgress: FC<{
{rideStatus && - ["PICKUP", "CARRYING", "ARRIVED", "COMPLETED"].includes( - rideStatus, - ) && ( + ["PICKUP", "CARRYING", "ARRIVED"].includes(rideStatus) && ( )}
@@ -154,16 +152,17 @@ const ChairProgress: FC<{
- {rideStatus && ["MATCHING", "ENROUTE"].includes(rideStatus) && ( - - )} + {rideStatus && + ["MATCHING", "COMPLETED", "ENROUTE"].includes(rideStatus) && ( + + )}
diff --git a/frontend/app/contexts/client-context.tsx b/frontend/app/contexts/client-context.tsx index 9050dbc9..8b213123 100644 --- a/frontend/app/contexts/client-context.tsx +++ b/frontend/app/contexts/client-context.tsx @@ -13,7 +13,7 @@ import { } from "~/api/api-components"; import { isClientApiError } from "~/types"; import { getCookieValue } from "~/utils/get-cookie-value"; -import { getUserAccessToken, getUserId } from "~/utils/storage"; +import { getUserId } from "~/utils/storage"; function jsonFromSSEResponse(value: string) { const data = value.slice("data:".length).trim(); @@ -140,13 +140,11 @@ export const useNotification = (): type ClientContextProps = { data?: AppGetNotificationResponse["data"]; userId?: string | null; - accessToken?: string | null; }; const ClientContext = createContext({}); -export const UserProvider = ({ children }: { children: ReactNode }) => { - const [accessToken] = useState(() => getUserAccessToken()); +export const ClientProvider = ({ children }: { children: ReactNode }) => { const [userId] = useState(() => getUserId()); const navigate = useNavigate(); const data = useNotification(); @@ -160,7 +158,7 @@ export const UserProvider = ({ children }: { children: ReactNode }) => { }, [navigate]); return ( - + {children} ); diff --git a/frontend/app/contexts/simulator-context.tsx b/frontend/app/contexts/simulator-context.tsx index 45063871..0161d013 100644 --- a/frontend/app/contexts/simulator-context.tsx +++ b/frontend/app/contexts/simulator-context.tsx @@ -3,10 +3,14 @@ import { createContext, useContext, useEffect, + useMemo, useState, } from "react"; import type { Coordinate } from "~/api/api-schemas"; -import { getSimulateChair } from "~/utils/get-initial-data"; +import { + getSimulateChair, + getSimulateChairFromToken, +} from "~/utils/get-initial-data"; import { apiBaseURL } from "~/api/api-base-url"; import { @@ -15,11 +19,13 @@ import { } from "~/api/api-components"; import { SimulatorChair } from "~/types"; import { getSimulatorCurrentCoordinate } from "~/utils/storage"; +import { getCookieValue } from "~/utils/get-cookie-value"; type SimulatorContextProps = { chair?: SimulatorChair; data?: ChairGetNotificationResponse["data"]; setCoordinate?: (coordinate: Coordinate) => void; + setToken?: (token: string) => void; }; const SimulatorContext = createContext({}); @@ -29,8 +35,6 @@ function jsonFromSseResult(value: string) { return JSON.parse(data) as T; } -const simulateChair = getSimulateChair(); - const useNotification = (): ChairGetNotificationResponse["data"] => { const [isSse, setIsSse] = useState(false); const [notification, setNotification] = @@ -61,6 +65,9 @@ const useNotification = (): ChairGetNotificationResponse["data"] => { | undefined; setNotification(json); } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return; + } console.error(error); } }; @@ -134,6 +141,9 @@ const useNotification = (): ChairGetNotificationResponse["data"] => { }); timeoutId = setTimeout(() => void polling(), retryAfterMs); } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return; + } console.error(error); } }; @@ -149,8 +159,30 @@ const useNotification = (): ChairGetNotificationResponse["data"] => { return notification?.data; }; + + export const SimulatorProvider = ({ children }: { children: ReactNode }) => { + + const [token, setToken] = useState(); + + const simulateChair = useMemo(() => { + return token ? getSimulateChairFromToken(token) : getSimulateChair(); + }, [token]); + + useEffect(() => { + const token = getCookieValue(document.cookie, "chair_session"); + if (token) { + setToken(token); + return; + } + // TODO: tokenがなければUI上で選択させるようにする + if (simulateChair?.token) { + document.cookie = `chair_session=${simulateChair.token}; path=/`; + } + }, [simulateChair]); + const data = useNotification(); + const [coordinate, setCoordinate] = useState(() => { const coordinate = getSimulatorCurrentCoordinate(); return coordinate ?? { latitude: 0, longitude: 0 }; @@ -168,6 +200,7 @@ export const SimulatorProvider = ({ children }: { children: ReactNode }) => { data, chair: simulateChair ? { ...simulateChair, coordinate } : undefined, setCoordinate, + setToken }} > {children} diff --git a/frontend/app/globals.d.ts b/frontend/app/globals.d.ts index 8996cfc8..3ef9d6fa 100644 --- a/frontend/app/globals.d.ts +++ b/frontend/app/globals.d.ts @@ -3,19 +3,4 @@ */ declare const __API_BASE_URL__: string; -declare const __INITIAL_OWNER_DATA__: - | { - owners: { - id: string; - name: string; - token: string; - }[]; - targetSimulatorChair: { - id: string; - owner_id: string; - name: string; - model: string; - token: string; - }; - } - | undefined; +declare const __INITIAL_DATA__: object; diff --git a/frontend/app/routes/client._index/driving-state/arrived.tsx b/frontend/app/routes/client._index/driving-state/arrived.tsx index ea4a57c6..a39664cc 100644 --- a/frontend/app/routes/client._index/driving-state/arrived.tsx +++ b/frontend/app/routes/client._index/driving-state/arrived.tsx @@ -12,7 +12,7 @@ import { useClientContext } from "~/contexts/client-context"; import confetti from "canvas-confetti"; export const Arrived = ({ onEvaluated }: { onEvaluated: () => void }) => { - const { accessToken, data } = useClientContext(); + const { data } = useClientContext(); const [rating, setRating] = useState(0); const onClick: MouseEventHandler = useCallback( @@ -20,9 +20,6 @@ export const Arrived = ({ onEvaluated }: { onEvaluated: () => void }) => { e.preventDefault(); try { void fetchAppPostRideEvaluation({ - headers: { - Authorization: `Bearer ${accessToken}`, - }, pathParams: { rideId: data?.ride_id ?? "", }, @@ -35,7 +32,7 @@ export const Arrived = ({ onEvaluated }: { onEvaluated: () => void }) => { } onEvaluated(); }, - [onEvaluated, accessToken, data?.ride_id, rating], + [onEvaluated, data?.ride_id, rating], ); useEffect(() => { diff --git a/frontend/app/routes/client._index/route.tsx b/frontend/app/routes/client._index/route.tsx index 1f5a46ed..b3212836 100644 --- a/frontend/app/routes/client._index/route.tsx +++ b/frontend/app/routes/client._index/route.tsx @@ -89,26 +89,19 @@ export default function Index() { if (!currentLocation || !destLocation) { return; } - const abortController = new AbortController(); - fetchAppPostRidesEstimatedFare( - { - body: { - pickup_coordinate: currentLocation, - destination_coordinate: destLocation, - }, + fetchAppPostRidesEstimatedFare({ + body: { + pickup_coordinate: currentLocation, + destination_coordinate: destLocation, }, - abortController.signal, - ) + }) .then((res) => setEstimatePrice({ fare: res.fare, discount: res.discount }), ) - .catch((err) => { - console.error(err); + .catch((error) => { + console.error(error); setEstimatePrice(undefined); }); - return () => { - abortController.abort(); - }; }, [currentLocation, destLocation]); const handleRideRequest = useCallback(async () => { @@ -150,6 +143,9 @@ export default function Index() { }); setDisplayedChairs(chairs); } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return; + } console.error(error); } }; @@ -282,6 +278,7 @@ export default function Index() { statusModalRef.current?.close(); setCurrentLocation(destLocation); setDestLocation(undefined); + setEstimatePrice(undefined); }} /> )} diff --git a/frontend/app/routes/client.history/route.tsx b/frontend/app/routes/client.history/route.tsx index 878a9722..467a30b9 100644 --- a/frontend/app/routes/client.history/route.tsx +++ b/frontend/app/routes/client.history/route.tsx @@ -22,18 +22,14 @@ export default function Index() { const [rides, setRides] = useState([]); useEffect(() => { - const abortController = new AbortController(); void (async () => { try { - const res = await fetchAppGetRides({}, abortController.signal); + const res = await fetchAppGetRides({}); setRides(res.rides); } catch (error) { console.error(error); } })(); - return () => { - abortController.abort(); - }; }, []); return ( diff --git a/frontend/app/routes/client/route.tsx b/frontend/app/routes/client/route.tsx index 92d4f0c8..4be79069 100644 --- a/frontend/app/routes/client/route.tsx +++ b/frontend/app/routes/client/route.tsx @@ -1,14 +1,14 @@ import { Outlet } from "@remix-run/react"; import { FooterNavigation } from "~/components/modules/footer-navigation/footer-navigation"; import { MainFrame } from "~/components/primitives/frame/frame"; -import { UserProvider } from "../../contexts/client-context"; +import { ClientProvider } from "../../contexts/client-context"; export default function ClientLayout() { return ( - + - + ); diff --git a/frontend/app/routes/owner.sales/route.tsx b/frontend/app/routes/owner.sales/route.tsx index f44a1f36..6beef6f3 100644 --- a/frontend/app/routes/owner.sales/route.tsx +++ b/frontend/app/routes/owner.sales/route.tsx @@ -45,27 +45,20 @@ export default function Index() { const since = salesDate?.since; const until = salesDate?.until; if (!since || !until) return; - let abortController: AbortController | undefined; void (async () => { try { - abortController = new AbortController(); setSales( - await fetchOwnerGetSales( - { - queryParams: { - until: timestamp(until), - since: timestamp(since), - }, + await fetchOwnerGetSales({ + queryParams: { + until: timestamp(until), + since: timestamp(since), }, - abortController.signal, - ), + }), ); } catch (error) { console.error(error); } })(); - - return () => abortController?.abort(); }, [salesDate, setSales]); const chairModelMap = useMemo( diff --git a/frontend/app/utils/get-initial-data.ts b/frontend/app/utils/get-initial-data.ts index 10087ec6..62bc31dd 100644 --- a/frontend/app/utils/get-initial-data.ts +++ b/frontend/app/utils/get-initial-data.ts @@ -12,16 +12,41 @@ type InitialOwner = { token: string; }; -const initialOwnerData = __INITIAL_OWNER_DATA__; +type initialDataType = + | { + owners: { + id: string; + name: string; + token: string; + }[]; + simulatorChairs: { + id: string; + owner_id: string; + name: string; + model: string; + token: string; + }[]; + } + | undefined; + +const initialData = __INITIAL_DATA__ as initialDataType; export const getOwners = (): InitialOwner[] => { return ( - initialOwnerData?.owners?.map((owner) => ({ + initialData?.owners?.map((owner) => ({ ...owner, })) ?? [] ); }; -export const getSimulateChair = (): InitialChair | undefined => { - return initialOwnerData?.targetSimulatorChair; +export const getSimulateChair = (index?: number): InitialChair | undefined => { + return index + ? initialData?.simulatorChairs[index] + : initialData?.simulatorChairs[0]; +}; + +export const getSimulateChairFromToken = ( + token: string, +): InitialChair | undefined => { + return initialData?.simulatorChairs.find((c) => c.token === token); }; diff --git a/frontend/app/utils/storage.ts b/frontend/app/utils/storage.ts index 240211af..8e337412 100644 --- a/frontend/app/utils/storage.ts +++ b/frontend/app/utils/storage.ts @@ -64,11 +64,3 @@ export const setUserId = (id: string) => { export const getUserId = (): string | null => { return getStorage("user.id", sessionStorage); }; - -export const setUserAccessToken = (id: string) => { - return setStorage("user.accessToken", id, sessionStorage); -}; - -export const getUserAccessToken = (): string | null => { - return getStorage("user.accessToken", sessionStorage); -}; diff --git a/frontend/initial-owner-data.json b/frontend/initial-data.json similarity index 51% rename from frontend/initial-owner-data.json rename to frontend/initial-data.json index 4d02bb34..6f7ef303 100644 --- a/frontend/initial-owner-data.json +++ b/frontend/initial-data.json @@ -26,11 +26,27 @@ "token": "3585ccc78c507660a4802973d269849e" } ], - "targetSimulatorChair": { - "id": "01JDFEF7MGXXCJKW1MNJXPA77A", - "owner_id": "01JDFEDF008NTA922W12FS7800", - "name": "QC-L13-8361", - "model": "クエストチェア Lite", - "token": "3013d5ec84e1b230f913a17d71ef27c8" - } + "simulatorChairs": [ + { + "id": "01JDFEF7MGXXCJKW1MNJXPA77A", + "owner_id": "01JDFEDF008NTA922W12FS7800", + "name": "QC-L13-8361", + "model": "クエストチェア Lite", + "token": "3013d5ec84e1b230f913a17d71ef27c8" + }, + { + "id": "01JDFEXJM006PH4C1EVHJ31QMV", + "owner_id": "01JDFEDF008NTA922W12FS7800", + "name": "LC-48-2566", + "model": "Legacy Chair", + "token": "b63d4c58ded093013ea5a483e2184d91" + }, + { + "id": "01JDFGTKR0586QH1QGXRACGB80", + "owner_id": "01JDFEDF008NTA922W12FS7800", + "name": "IS-33-8740", + "model": "Infinity Seat", + "token": "091cd034ce1ac4a9dd9fb1b5ec52c3dc" + } + ] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b63e50cf..8d2e6c96 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,10 +20,8 @@ const DEFAULT_URL = `http://${DEFAULT_HOSTNAME}:${DEFAULT_PORT}`; type APIResponse = Record; -const intialOwnerData = existsSync("./initial-owner-data.json") - ? (JSON.parse( - readFileSync("./initial-owner-data.json").toString(), - ) as unknown) +const intialOwnerData = existsSync("./initial-data.json") + ? (JSON.parse(readFileSync("./initial-data.json").toString()) as unknown) : undefined; const getLoggedInURLForClient = async () => { @@ -253,7 +251,7 @@ export const config = { ], define: { [alternativeURLExpression]: `"${process.env["API_BASE_URL"] ?? "."}"`, - __INITIAL_OWNER_DATA__: intialOwnerData, + __INITIAL_DATA__: intialOwnerData, }, server: { proxy: { diff --git a/webapp/go/app_handlers.go b/webapp/go/app_handlers.go index 20882df0..ccacee3f 100644 --- a/webapp/go/app_handlers.go +++ b/webapp/go/app_handlers.go @@ -857,17 +857,14 @@ func appGetNearbyChairs(w http.ResponseWriter, r *http.Request) { continue } - ride := &Ride{} - if err := tx.Get( - ride, - `SELECT * FROM rides WHERE chair_id = ? ORDER BY created_at DESC LIMIT 1`, - chair.ID, - ); err != nil { - if !errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusInternalServerError, err) - return - } - } else { + rides := []*Ride{} + if err := tx.Select(&rides, `SELECT * FROM rides WHERE chair_id = ? ORDER BY created_at DESC`, chair.ID); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + skip := false + for _, ride := range rides { // 過去にライドが存在し、かつ、それが完了していない場合はスキップ status, err := getLatestRideStatus(tx, ride.ID) if err != nil { @@ -875,9 +872,13 @@ func appGetNearbyChairs(w http.ResponseWriter, r *http.Request) { return } if status != "COMPLETED" { - continue + skip = true + break } } + if skip { + continue + } // 最新の位置情報を取得 chairLocation := &ChairLocation{}