Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type {
RelationshipID,
Relationship,
CourseID,
InterestSubjectID,
Slot,
Course,
Enrollment,
Expand Down
24 changes: 13 additions & 11 deletions common/zod/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ export const IntroLongSchema = z
// .min(2, { message: "自己紹介文は2文字以上です" })
.max(225, { message: "自己紹介文は225文字以下です" });

export const InterestSubjectSchema = z.object({
id: z.number(),
name: z.string(),
group: z.string(),
});

export const InterestSchema = z.object({
userId: UserIDSchema,
subjectId: z.number(),
});

export const UserSchema = z.object({
id: UserIDSchema,
guid: GUIDSchema,
Expand Down Expand Up @@ -76,6 +65,8 @@ export const RelationshipSchema = z.object({

export const CourseIDSchema = z.string();

export const InterestSubjectIDSchema = z.number();

export const DaySchema = z.enum([
"mon",
"tue",
Expand Down Expand Up @@ -108,6 +99,17 @@ export const EnrollmentSchema = z.object({
courseId: CourseIDSchema,
});

export const InterestSubjectSchema = z.object({
id: InterestSubjectIDSchema,
name: z.string(),
group: z.string(),
});

export const InterestSchema = z.object({
userId: UserIDSchema,
subjectId: InterestSubjectIDSchema,
});

export const UserWithCoursesAndSubjectsSchema = UserSchema.extend({
courses: CourseSchema.array(),
interestSubjects: InterestSubjectSchema.array(),
Expand Down
2 changes: 2 additions & 0 deletions common/zod/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
InitSharedRoomSchema,
InitUserSchema,
InterestSchema,
InterestSubjectIDSchema,
InterestSubjectSchema,
IntroLongSchema,
IntroShortSchema,
Expand Down Expand Up @@ -57,6 +58,7 @@ export type Step1User = z.infer<typeof Step1UserSchema>;
export type RelationshipID = z.infer<typeof RelationshipIDSchema>;
export type Relationship = z.infer<typeof RelationshipSchema>;
export type CourseID = z.infer<typeof CourseIDSchema>;
export type InterestSubjectID = z.infer<typeof InterestSubjectIDSchema>;
export type Slot = z.infer<typeof SlotSchema>;
export type Course = z.infer<typeof CourseSchema>;
export type Enrollment = z.infer<typeof EnrollmentSchema>;
Expand Down
35 changes: 35 additions & 0 deletions server/src/database/interest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ export async function get(id: number): Promise<InterestSubject | null> {
return await prisma.interestSubject.findUnique({ where: { id } });
}

export async function create(name: string): Promise<InterestSubject> {
const existingTag = await prisma.interestSubject.findMany({
where: { name },
});
if (existingTag.length > 0) {
throw new Error("同名のタグがすでに存在します");
}
return await prisma.interestSubject.create({
data: {
name,
group: "", // TODO: 運用次第
},
});
}

export async function of(userId: UserID): Promise<InterestSubject[]> {
return await prisma.interest
.findMany({
Expand Down Expand Up @@ -37,3 +52,23 @@ export async function remove(userId: UserID, subjectId: number) {
},
});
}

export async function updateMultipleWithTransaction(
userId: UserID,
subjectIds: number[],
) {
return await prisma.$transaction(async (prisma) => {
await prisma.interest.deleteMany({
where: {
userId,
},
});

await prisma.interest.createMany({
data: subjectIds.map((subjectId) => ({
userId,
subjectId,
})),
});
});
}
2 changes: 2 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import coursesRoutes from "./router/courses";
import matchesRoutes from "./router/matches";
import pictureRoutes from "./router/picture";
import requestsRoutes from "./router/requests";
import subjectsRoutes from "./router/subjects";
import usersRoutes from "./router/users";

const app = express();
Expand Down Expand Up @@ -52,6 +53,7 @@ app.get("/", (_, res) => {
app.use("/picture", pictureRoutes);
app.use("/users", usersRoutes);
app.use("/courses", coursesRoutes);
app.use("/subjects", subjectsRoutes);
app.use("/requests", requestsRoutes);
app.use("/matches", matchesRoutes);
app.use("/chat", chatRoutes);
Expand Down
131 changes: 131 additions & 0 deletions server/src/router/subjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import express, { type Request, type Response } from "express";
import * as interest from "../database/interest";
import { safeGetUserId } from "../firebase/auth/db";

const router = express.Router();

router.get("/userId/:userId", async (req: Request, res: Response) => {
const userId = Number.parseInt(req.params.userId);
if (Number.isNaN(userId)) {
return res.status(400).json({ error: "Invalid userId" });
}
try {
const subjects = await interest.of(userId);
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects by userId:", error);
res.status(500).json({ error: "Failed to fetch subjects by userId" });
}
});

router.get("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
try {
const subjects = await interest.of(userId.value);
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects:", error);
res.status(500).json({ error: "Failed to fetch subjects" });
}
});

router.post("/", async (req: Request, res: Response) => {
const { name } = req.body;
if (typeof name !== "string") {
return res.status(400).json({ error: "name must be a string" });
}
try {
const newSubject = await interest.create(name);
res.status(201).json(newSubject);
} catch (error) {
console.error("Error creating subject:", error);
res.status(500).json({ error: "Failed to create subject" });
}
});

router.patch("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectId } = req.body;
try {
const newSubject = await interest.get(subjectId);
if (!newSubject) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subject:", err);
res.status(500).json({ error: "Failed to fetch subject" });
}
try {
const updatedSubjects = await interest.add(userId.value, subjectId);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error updating subjects:", error);
res.status(500).json({ error: "Failed to update subjects" });
}
});

router.delete("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectId } = req.body;
try {
const newSubject = await interest.get(subjectId);
if (!newSubject) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subject:", err);
res.status(500).json({ error: "Failed to fetch subject" });
}
try {
const updatedSubjects = await interest.remove(userId.value, subjectId);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error deleting subjects:", error);
res.status(500).json({ error: "Failed to delete subjects" });
}
});

router.put("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectIds } = req.body;
if (!Array.isArray(subjectIds)) {
return res.status(400).json({ error: "subjectIds must be an array" });
}
try {
const newSubjects = await Promise.all(
subjectIds.map((id) => interest.get(id)),
);
if (newSubjects.some((s) => !s)) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subjects:", err);
res.status(500).json({ error: "Failed to fetch subjects" });
}
try {
const updatedSubjects = await interest.updateMultipleWithTransaction(
userId.value,
subjectIds,
);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error updating subjects:", error);
res.status(500).json({ error: "Failed to update subjects" });
}
});

router.get("/all", async (req: Request, res: Response) => {
try {
const subjects = await interest.all();
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects:", error);
res.status(500).json({ error: "Failed to fetch subjects" });
}
});

export default router;
71 changes: 71 additions & 0 deletions web/api/internal/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,73 @@ export const coursesDayPeriod = (day: Day, period: number) => {
return `${API_ENDPOINT}/courses/day-period?day=${day}&period=${period}`;
};

/**
* [v] 実装済み
* POST -> create a new subject.
* - statuses:
* - 200: ok.
* - body: InterestSubject
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjects = `${API_ENDPOINT}/subjects`;

/**
* [v] 実装済み
* GET -> get subjects the user is interested in.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsUserId = (userId: UserID) => {
return `${API_ENDPOINT}/subjects/userId/${userId}`;
};

/**
* [v] 実装済み
* GET -> get my subjects.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* PATCH -> update my subjects.
* - request body: SubjectId
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* DELETE -> delete my subjects.
* - request body: SubjectId
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* PUT → replace my subjects.
* - request body: SubjectId[]
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsMine = `${API_ENDPOINT}/subjects/mine`;

/**
* [v] 実装済み
* GET -> get all subjects.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsAll = `${API_ENDPOINT}/subjects/all`;

/**
* [v] 実装済み
* PUT -> create request.
Expand Down Expand Up @@ -400,6 +467,10 @@ export default {
cancelRequest,
coursesUserId,
coursesDayPeriod,
subjects,
subjectsUserId,
subjectsMine,
subjectsAll,
roomOverview,
dmTo,
dmWith,
Expand Down
49 changes: 49 additions & 0 deletions web/api/subject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type {
InterestSubject,
InterestSubjectID,
UserID,
} from "common/types.ts";
import { InterestSubjectSchema } from "common/zod/schemas.ts";
import { z } from "zod";
import { credFetch } from "../firebase/auth/lib.ts";
import { type Hook, useCustomizedSWR } from "../hooks/useCustomizedSWR.ts";
import endpoints from "./internal/endpoints.ts";

const InterestSubjectListSchema = z.array(InterestSubjectSchema);

// 興味分野を作成する
export async function create(name: string) {
return await credFetch("POST", endpoints.subjects, { name });
}

// 自身の興味分野を取得する
export function useMyInterests(): Hook<InterestSubject[]> {
return useCustomizedSWR(
"interests::mine",
getMySubjects,
InterestSubjectListSchema,
);
}

async function getMySubjects(): Promise<InterestSubject[]> {
const res = await credFetch("GET", endpoints.subjectsMine);
return res.json();
}

// 自身の興味分野を更新する
export async function update(newSubjectIds: InterestSubjectID[]) {
const url = endpoints.subjectsMine;
return await credFetch("PUT", url, { subjectIds: newSubjectIds });
}

// 指定した userId のユーザの興味分野を取得
export async function get(id: UserID): Promise<InterestSubject[] | null> {
const res = await credFetch("GET", endpoints.subjectsUserId(id));
return await res.json();
}

// すべての興味分野を取得
export async function getAll(): Promise<InterestSubject[]> {
const res = await credFetch("GET", endpoints.subjectsAll);
return await res.json();
}
Loading
Loading