diff --git a/common/types.ts b/common/types.ts index c89a5b5e..dc705e6c 100644 --- a/common/types.ts +++ b/common/types.ts @@ -7,6 +7,7 @@ export type { Gender, RelationshipStatus, InterestSubject, + Interest, User, InitUser, UpdateUser, @@ -17,6 +18,7 @@ export type { Course, Enrollment, Day, + UserWithCoursesAndSubjects, MessageID, ShareRoomID, Message, diff --git a/common/zod/schemas.ts b/common/zod/schemas.ts index 1c114305..52411ac6 100644 --- a/common/zod/schemas.ts +++ b/common/zod/schemas.ts @@ -38,6 +38,11 @@ export const InterestSubjectSchema = z.object({ group: z.string(), }); +export const InterestSchema = z.object({ + userId: UserIDSchema, + subjectId: z.number(), +}); + export const UserSchema = z.object({ id: UserIDSchema, guid: GUIDSchema, @@ -103,6 +108,11 @@ export const EnrollmentSchema = z.object({ courseId: CourseIDSchema, }); +export const UserWithCoursesAndSubjectsSchema = UserSchema.extend({ + courses: CourseSchema.array(), + interestSubjects: InterestSubjectSchema.array(), +}); + export const MessageIDSchema = z.number(); // TODO! Add __internal_prevent_cast_MessageID: PhantomData export const ShareRoomIDSchema = z.number(); diff --git a/common/zod/types.ts b/common/zod/types.ts index c0f1a44b..68714005 100644 --- a/common/zod/types.ts +++ b/common/zod/types.ts @@ -14,6 +14,7 @@ import type { InitRoomSchema, InitSharedRoomSchema, InitUserSchema, + InterestSchema, InterestSubjectSchema, IntroLongSchema, IntroShortSchema, @@ -37,6 +38,7 @@ import type { UpdateUserSchema, UserIDSchema, UserSchema, + UserWithCoursesAndSubjectsSchema, } from "./schemas"; export type UserID = z.infer; @@ -47,6 +49,7 @@ export type PictureUrl = z.infer; export type Gender = z.infer; export type RelationshipStatus = z.infer; export type InterestSubject = z.infer; +export type Interest = z.infer; export type User = z.infer; export type InitUser = z.infer; export type UpdateUser = z.infer; @@ -59,6 +62,9 @@ export type Course = z.infer; export type Enrollment = z.infer; export type Day = z.infer; export type Period = z.infer; +export type UserWithCoursesAndSubjects = z.infer< + typeof UserWithCoursesAndSubjectsSchema +>; export type MessageID = z.infer; export type ShareRoomID = z.infer; export type Message = z.infer; diff --git a/server/src/database/requests.ts b/server/src/database/requests.ts index e1e2efaa..0b829093 100644 --- a/server/src/database/requests.ts +++ b/server/src/database/requests.ts @@ -1,5 +1,9 @@ import { Err, Ok, type Result } from "common/lib/result"; -import type { Relationship, User, UserID } from "common/types"; +import type { + Relationship, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { prisma } from "./client"; // マッチリクエストの送信 @@ -114,7 +118,7 @@ export async function cancelRequest( //ユーザーへのリクエストを探す 俺をリクエストしているのは誰だ export async function getPendingRequestsToUser( userId: UserID, -): Promise> { +): Promise> { try { const found = await prisma.user.findMany({ where: { @@ -125,8 +129,37 @@ export async function getPendingRequestsToUser( }, }, }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, }); - return Ok(found); + return Ok( + found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }), + ); } catch (e) { return Err(e); } @@ -135,7 +168,7 @@ export async function getPendingRequestsToUser( //ユーザーがリクエストしている人を探す 俺がリクエストしているのは誰だ export async function getPendingRequestsFromUser( userId: UserID, -): Promise> { +): Promise> { try { const found = await prisma.user.findMany({ where: { @@ -146,15 +179,46 @@ export async function getPendingRequestsFromUser( }, }, }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, }); - return Ok(found); + return Ok( + found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }), + ); } catch (e) { return Err(e); } } //マッチした人の取得 -export async function getMatchedUser(userId: UserID): Promise> { +export async function getMatchedUser( + userId: UserID, +): Promise> { try { const found = await prisma.user.findMany({ where: { @@ -177,8 +241,37 @@ export async function getMatchedUser(userId: UserID): Promise> { }, ], }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, }); - return Ok(found); + return Ok( + found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }), + ); } catch (e) { return Err(e); } diff --git a/server/src/database/users.ts b/server/src/database/users.ts index 8ba7b9a1..3d008bd3 100644 --- a/server/src/database/users.ts +++ b/server/src/database/users.ts @@ -1,5 +1,13 @@ import { Err, Ok, type Result } from "common/lib/result"; -import type { GUID, UpdateUser, User, UserID } from "common/types"; +import type { + Course, + GUID, + InterestSubject, + UpdateUser, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { prisma } from "./client"; // ユーザーの作成 @@ -17,15 +25,42 @@ export async function createUser( } // ユーザーの取得 -export async function getUser(guid: GUID): Promise> { +export async function getUser( + guid: GUID, +): Promise> { try { const user = await prisma.user.findUnique({ where: { guid: guid, }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, }); if (!user) return Err(404); - return Ok(user); + return Ok({ + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }); } catch (e) { return Err(e); } @@ -50,14 +85,42 @@ export async function getUserIDByGUID(guid: GUID): Promise> { .catch((err) => Err(err)); } -export async function getUserByID(id: UserID): Promise> { +export async function getUserByID( + id: UserID, +): Promise> { try { const user = await prisma.user.findUnique({ where: { id, }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, + }); + if (!user) return Err(404); + return Ok({ + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), }); - return user === null ? Err(404) : Ok(user); } catch (e) { return Err(e); } @@ -100,10 +163,42 @@ export async function deleteUser(userId: UserID): Promise> { } // ユーザーの全取得 -export async function getAllUsers(): Promise> { +export async function getAllUsers(): Promise< + Result<(User & { courses: Course[]; interestSubjects: InterestSubject[] })[]> +> { try { - const users = await prisma.user.findMany(); - return Ok(users); + const users = await prisma.user.findMany({ + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, + }); + return Ok( + users.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }), + ); } catch (e) { return Err(e); } diff --git a/server/src/functions/engines/recommendation.ts b/server/src/functions/engines/recommendation.ts index 76850d01..cac0bcd4 100644 --- a/server/src/functions/engines/recommendation.ts +++ b/server/src/functions/engines/recommendation.ts @@ -1,6 +1,6 @@ import { recommend as sql } from "@prisma/client/sql"; import { Err, Ok, type Result } from "common/lib/result"; -import type { User, UserID } from "common/types"; +import type { UserID, UserWithCoursesAndSubjects } from "common/types"; import { prisma } from "../../database/client"; import { getUserByID } from "../../database/users"; @@ -8,7 +8,14 @@ export async function recommendedTo( user: UserID, limit: number, offset: number, -): Promise>> { +): Promise< + Result< + Array<{ + u: UserWithCoursesAndSubjects; + count: number; + }> + > +> { try { const result = await prisma.$queryRawTyped(sql(user, limit, offset)); return Promise.all( diff --git a/server/src/functions/user.ts b/server/src/functions/user.ts index 6a1a7644..228edc6e 100644 --- a/server/src/functions/user.ts +++ b/server/src/functions/user.ts @@ -1,5 +1,9 @@ -import { Result } from "common/lib/result"; -import type { GUID, User, UserID } from "common/types"; +import type { + GUID, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { getMatchedUser } from "../database/requests"; import * as db from "../database/users"; import * as http from "./share/http"; @@ -13,7 +17,9 @@ export async function getAllUsers(): Promise> { return http.ok(users.value); } -export async function getUser(guid: GUID): Promise> { +export async function getUser( + guid: GUID, +): Promise> { const user = await db.getUser(guid); if (!user.ok) { if (user.error === 404) return http.notFound(); @@ -42,7 +48,9 @@ export async function userExists(guid: GUID): Promise> { return http.internalError("db error"); } -export async function getMatched(user: UserID): Promise> { +export async function getMatched( + user: UserID, +): Promise> { const matchedUsers = await getMatchedUser(user); if (!matchedUsers.ok) return http.internalError(); diff --git a/server/src/router/users.ts b/server/src/router/users.ts index 5be8fcf8..87c6586e 100644 --- a/server/src/router/users.ts +++ b/server/src/router/users.ts @@ -1,4 +1,8 @@ -import type { GUID, UpdateUser } from "common/types"; +import type { + GUID, + UpdateUser, + UserWithCoursesAndSubjects, +} from "common/types"; import type { User } from "common/types"; import { GUIDSchema, @@ -106,7 +110,7 @@ router.get("/guid/:guid", async (req: Request, res: Response) => { if (!user.ok) { return res.status(404).json({ error: "User not found" }); } - const json: User = user.value; + const json: UserWithCoursesAndSubjects = user.value; res.status(200).json(json); }); diff --git a/web/api/internal/endpoints.ts b/web/api/internal/endpoints.ts index 2ebc350a..e087c87f 100644 --- a/web/api/internal/endpoints.ts +++ b/web/api/internal/endpoints.ts @@ -12,7 +12,7 @@ export type UserID = number; * GET -> get user's info. TODO: filter return info by user's options and open level. * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 400: not found. * - 500: internal error. **/ @@ -43,7 +43,7 @@ export const users = `${origin}/users`; * GET -> get top N users recommended to me. * - statuses: * - 200: good. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: auth error. * - 500: internal error **/ @@ -62,7 +62,7 @@ export const recommendedUsers = `${origin}/users/recommended`; * - request body: Omit * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 500: internal error. * * [v] 実装済み @@ -79,7 +79,7 @@ export const me = `${origin}/users/me`; * GET -> list all matched users. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ @@ -90,7 +90,7 @@ export const matchedUsers = `${origin}/users/matched`; * GET -> list all users that sent request to you. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ @@ -101,7 +101,7 @@ export const pendingRequestsToMe = `${origin}/users/pending/to-me`; * GET -> list all users that you sent request. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ @@ -112,7 +112,7 @@ export const pendingRequestsFromMe = `${origin}/users/pending/from-me`; * GET -> get user's info. TODO: filter return info by user's options and open level. * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 400: not found. * - 500: internal error. **/ @@ -126,6 +126,7 @@ export const userByGUID = (guid: GUID) => { * GET -> check if the user exists. * - statuses: * - 200: yes, user exists. + * - body: UserWithCoursesAndSubjects * - 404: no, user doesn't exist. * - 500: internal error. **/ diff --git a/web/api/user.ts b/web/api/user.ts index add3efcb..0f535341 100644 --- a/web/api/user.ts +++ b/web/api/user.ts @@ -1,6 +1,15 @@ -import type { GUID, UpdateUser, User, UserID } from "common/types.ts"; +import type { + GUID, + UpdateUser, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types.ts"; import { parseUser } from "common/zod/methods.ts"; -import { UserIDSchema, UserSchema } from "common/zod/schemas.ts"; +import { + UserIDSchema, + UserWithCoursesAndSubjectsSchema, +} from "common/zod/schemas.ts"; import { z } from "zod"; import { credFetch } from "~/firebase/auth/lib.ts"; import { type Hook, useCustomizedSWR } from "~/hooks/useCustomizedSWR.ts"; @@ -8,22 +17,22 @@ import { useAuthorizedData } from "~/hooks/useData.ts"; import endpoints from "./internal/endpoints.ts"; import type { Hook as UseHook } from "./share/types.ts"; -const UserListSchema = z.array(UserSchema); +const UserListSchema = z.array(UserWithCoursesAndSubjectsSchema); -export function useAll(): Hook { +export function useAll(): Hook { return useCustomizedSWR("users::all", all, UserListSchema); } -export function useRecommended(): UseHook { +export function useRecommended(): UseHook { const url = endpoints.recommendedUsers; - return useAuthorizedData(url); + return useAuthorizedData(url); } -export function useMatched(): Hook { +export function useMatched(): Hook { return useCustomizedSWR("users::matched", matched, UserListSchema); } -export function usePendingToMe(): Hook { +export function usePendingToMe(): Hook { return useCustomizedSWR("users::pending::to-me", pendingToMe, UserListSchema); } -export function usePendingFromMe(): Hook { +export function usePendingFromMe(): Hook { return useCustomizedSWR( "users::pending::from-me", pendingFromMe, @@ -31,30 +40,34 @@ export function usePendingFromMe(): Hook { ); } -async function all(): Promise { +async function all(): Promise { const res = await credFetch("GET", endpoints.users); return res.json(); } -async function matched(): Promise { +async function matched(): Promise { const res = await credFetch("GET", endpoints.matchedUsers); return res.json(); } -async function pendingToMe(): Promise { +async function pendingToMe(): Promise { const res = await credFetch("GET", endpoints.pendingRequestsToMe); return await res.json(); } -async function pendingFromMe(): Promise { +async function pendingFromMe(): Promise { const res = await credFetch("GET", endpoints.pendingRequestsFromMe); return await res.json(); } // 自身のユーザー情報を取得する -export function useAboutMe(): Hook { - return useCustomizedSWR("users::aboutMe", aboutMe, UserSchema); +export function useAboutMe(): Hook { + return useCustomizedSWR( + "users::aboutMe", + aboutMe, + UserWithCoursesAndSubjectsSchema, + ); } -async function aboutMe(): Promise { +async function aboutMe(): Promise { const res = await credFetch("GET", endpoints.me); return res.json(); } diff --git a/web/app/home/page.tsx b/web/app/home/page.tsx index aedd1545..f489da5f 100644 --- a/web/app/home/page.tsx +++ b/web/app/home/page.tsx @@ -1,7 +1,7 @@ "use client"; import CloseIcon from "@mui/icons-material/Close"; -import type { User } from "common/types"; +import type { UserWithCoursesAndSubjects } from "common/types"; import { motion, useAnimation } from "framer-motion"; import { useCallback, useEffect, useState } from "react"; import { MdThumbUp } from "react-icons/md"; @@ -21,9 +21,9 @@ export default function Home() { } = useMyID(); const [_, rerender] = useState({}); - const [recommended, setRecommended] = useState>( - () => new Queue([]), - ); + const [recommended, setRecommended] = useState< + Queue + >(() => new Queue([])); useEffect(() => { if (data) setRecommended(new Queue(data)); }, [data]); diff --git a/web/components/Card.tsx b/web/components/Card.tsx index 47c889c7..2ba27e57 100644 --- a/web/components/Card.tsx +++ b/web/components/Card.tsx @@ -1,12 +1,12 @@ import ThreeSixtyIcon from "@mui/icons-material/ThreeSixty"; import { Chip } from "@mui/material"; -import type { User, UserID } from "common/types"; +import type { UserID, UserWithCoursesAndSubjects } from "common/types"; import { useState } from "react"; import NonEditableCoursesTable from "./course/NonEditableCoursesTable"; import UserAvatar from "./human/avatar"; interface CardProps { - displayedUser: User; + displayedUser: UserWithCoursesAndSubjects; comparisonUserId?: UserID; onFlip?: (isBack: boolean) => void; } @@ -97,6 +97,12 @@ const CardFront = ({ displayedUser }: CardProps) => { {displayedUser.intro}

+

TODO: これはサンプルです

+
    + {displayedUser.interestSubjects.map((subject) => ( +
  • {subject.name}
  • + ))} +
diff --git a/web/components/DraggableCard.tsx b/web/components/DraggableCard.tsx index 130d5a5e..d6ae570d 100644 --- a/web/components/DraggableCard.tsx +++ b/web/components/DraggableCard.tsx @@ -1,6 +1,6 @@ import CloseIcon from "@mui/icons-material/Close"; import { Box, Typography } from "@mui/material"; -import type { User, UserID } from "common/types"; +import type { UserID, UserWithCoursesAndSubjects } from "common/types"; import { motion, useMotionValue, useMotionValueEvent } from "framer-motion"; import { useCallback, useState } from "react"; import { MdThumbUp } from "react-icons/md"; @@ -9,7 +9,7 @@ import { Card } from "./Card"; const SWIPE_THRESHOLD = 30; interface DraggableCardProps { - displayedUser: User; + displayedUser: UserWithCoursesAndSubjects; comparisonUserId?: UserID; onSwipeRight: () => void; onSwipeLeft: () => void; diff --git a/web/components/common/modal/ModalProvider.tsx b/web/components/common/modal/ModalProvider.tsx index 788369f1..37fe8836 100644 --- a/web/components/common/modal/ModalProvider.tsx +++ b/web/components/common/modal/ModalProvider.tsx @@ -1,4 +1,4 @@ -import type { User } from "common/types"; +import type { UserWithCoursesAndSubjects } from "common/types"; import { type ReactNode, createContext, useContext, useState } from "react"; import { useMyID } from "~/api/user"; import { Card } from "../../Card"; @@ -6,7 +6,7 @@ import { Card } from "../../Card"; const ModalContext = createContext(undefined); type ModalContextProps = { - openModal: (user: User) => void; + openModal: (user: UserWithCoursesAndSubjects) => void; closeModal: () => void; }; @@ -16,12 +16,13 @@ type ModalProviderProps = { export const ModalProvider = ({ children }: ModalProviderProps) => { const [open, setOpen] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUser, setSelectedUser] = + useState(null); const { state: { data: myId }, } = useMyID(); - const openModal = (user: User) => { + const openModal = (user: UserWithCoursesAndSubjects) => { setSelectedUser(user); setOpen(true); };