Skip to content

Commit e2f5ae0

Browse files
committed
編集可能
1 parent ae0f0cd commit e2f5ae0

File tree

11 files changed

+421
-17
lines changed

11 files changed

+421
-17
lines changed

common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type {
1414
RelationshipID,
1515
Relationship,
1616
CourseID,
17+
InterestSubjectID,
1718
Slot,
1819
Course,
1920
Enrollment,

common/zod/schemas.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,6 @@ export const IntroLongSchema = z
3232
// .min(2, { message: "自己紹介文は2文字以上です" })
3333
.max(225, { message: "自己紹介文は225文字以下です" });
3434

35-
export const InterestSubjectSchema = z.object({
36-
id: z.number(),
37-
name: z.string(),
38-
group: z.string(),
39-
});
40-
41-
export const InterestSchema = z.object({
42-
userId: UserIDSchema,
43-
subjectId: z.number(),
44-
});
45-
4635
export const UserSchema = z.object({
4736
id: UserIDSchema,
4837
guid: GUIDSchema,
@@ -76,6 +65,8 @@ export const RelationshipSchema = z.object({
7665

7766
export const CourseIDSchema = z.string();
7867

68+
export const InterestSubjectIDSchema = z.number();
69+
7970
export const DaySchema = z.enum([
8071
"mon",
8172
"tue",
@@ -108,6 +99,17 @@ export const EnrollmentSchema = z.object({
10899
courseId: CourseIDSchema,
109100
});
110101

102+
export const InterestSubjectSchema = z.object({
103+
id: InterestSubjectIDSchema,
104+
name: z.string(),
105+
group: z.string(),
106+
});
107+
108+
export const InterestSchema = z.object({
109+
userId: UserIDSchema,
110+
subjectId: InterestSubjectIDSchema,
111+
});
112+
111113
export const UserWithCoursesAndSubjectsSchema = UserSchema.extend({
112114
courses: CourseSchema.array(),
113115
interestSubjects: InterestSubjectSchema.array(),

common/zod/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
InitSharedRoomSchema,
1616
InitUserSchema,
1717
InterestSchema,
18+
InterestSubjectIDSchema,
1819
InterestSubjectSchema,
1920
IntroLongSchema,
2021
IntroShortSchema,
@@ -57,6 +58,7 @@ export type Step1User = z.infer<typeof Step1UserSchema>;
5758
export type RelationshipID = z.infer<typeof RelationshipIDSchema>;
5859
export type Relationship = z.infer<typeof RelationshipSchema>;
5960
export type CourseID = z.infer<typeof CourseIDSchema>;
61+
export type InterestSubjectID = z.infer<typeof InterestSubjectIDSchema>;
6062
export type Slot = z.infer<typeof SlotSchema>;
6163
export type Course = z.infer<typeof CourseSchema>;
6264
export type Enrollment = z.infer<typeof EnrollmentSchema>;

server/src/database/interest.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,33 @@ export async function remove(userId: UserID, subjectId: number) {
3737
},
3838
});
3939
}
40+
41+
export async function updateMultipleWithTransaction(
42+
userId: UserID,
43+
subjectIds: number[],
44+
) {
45+
return await prisma.$transaction(async (prisma) => {
46+
await prisma.interest.deleteMany({
47+
where: {
48+
userId,
49+
},
50+
});
51+
52+
await prisma.interest.createMany({
53+
data: subjectIds.map((subjectId) => ({
54+
userId,
55+
subjectId,
56+
})),
57+
});
58+
});
59+
}
60+
61+
export async function search(query: string): Promise<InterestSubject[]> {
62+
return await prisma.interestSubject.findMany({
63+
where: {
64+
name: {
65+
contains: query,
66+
},
67+
},
68+
});
69+
}

server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import coursesRoutes from "./router/courses";
99
import matchesRoutes from "./router/matches";
1010
import pictureRoutes from "./router/picture";
1111
import requestsRoutes from "./router/requests";
12+
import subjectsRoutes from "./router/subjects";
1213
import usersRoutes from "./router/users";
1314

1415
const app = express();
@@ -52,6 +53,7 @@ app.get("/", (_, res) => {
5253
app.use("/picture", pictureRoutes);
5354
app.use("/users", usersRoutes);
5455
app.use("/courses", coursesRoutes);
56+
app.use("/subjects", subjectsRoutes);
5557
app.use("/requests", requestsRoutes);
5658
app.use("/matches", matchesRoutes);
5759
app.use("/chat", chatRoutes);

server/src/router/subjects.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import express, { type Request, type Response } from "express";
2+
import * as interest from "../database/interest";
3+
import { safeGetUserId } from "../firebase/auth/db";
4+
5+
const router = express.Router();
6+
7+
router.get("/userId/:userId", async (req: Request, res: Response) => {
8+
const userId = Number.parseInt(req.params.userId);
9+
if (Number.isNaN(userId)) {
10+
return res.status(400).json({ error: "Invalid userId" });
11+
}
12+
try {
13+
const subjects = await interest.of(userId);
14+
res.status(200).json(subjects);
15+
} catch (error) {
16+
console.error("Error fetching subjects by userId:", error);
17+
res.status(500).json({ error: "Failed to fetch subjects by userId" });
18+
}
19+
});
20+
21+
router.get("/mine", async (req: Request, res: Response) => {
22+
const userId = await safeGetUserId(req);
23+
if (!userId.ok) return res.status(401).send("auth error");
24+
try {
25+
const subjects = await interest.of(userId.value);
26+
res.status(200).json(subjects);
27+
} catch (error) {
28+
console.error("Error fetching subjects:", error);
29+
res.status(500).json({ error: "Failed to fetch subjects" });
30+
}
31+
});
32+
33+
router.patch("/mine", async (req: Request, res: Response) => {
34+
const userId = await safeGetUserId(req);
35+
if (!userId.ok) return res.status(401).send("auth error");
36+
const { subjectId } = req.body;
37+
try {
38+
const newSubject = await interest.get(subjectId);
39+
if (!newSubject) {
40+
return res.status(404).json({ error: "Subject not found" });
41+
}
42+
} catch (err) {
43+
console.error("Error fetching subject:", err);
44+
res.status(500).json({ error: "Failed to fetch subject" });
45+
}
46+
try {
47+
const updatedSubjects = await interest.add(userId.value, subjectId);
48+
res.status(200).json(updatedSubjects);
49+
} catch (error) {
50+
console.error("Error updating subjects:", error);
51+
res.status(500).json({ error: "Failed to update subjects" });
52+
}
53+
});
54+
55+
router.delete("/mine", async (req: Request, res: Response) => {
56+
const userId = await safeGetUserId(req);
57+
if (!userId.ok) return res.status(401).send("auth error");
58+
const { subjectId } = req.body;
59+
try {
60+
const newSubject = await interest.get(subjectId);
61+
if (!newSubject) {
62+
return res.status(404).json({ error: "Subject not found" });
63+
}
64+
} catch (err) {
65+
console.error("Error fetching subject:", err);
66+
res.status(500).json({ error: "Failed to fetch subject" });
67+
}
68+
try {
69+
const updatedSubjects = await interest.remove(userId.value, subjectId);
70+
res.status(200).json(updatedSubjects);
71+
} catch (error) {
72+
console.error("Error deleting subjects:", error);
73+
res.status(500).json({ error: "Failed to delete subjects" });
74+
}
75+
});
76+
77+
router.put("/mine", async (req: Request, res: Response) => {
78+
const userId = await safeGetUserId(req);
79+
if (!userId.ok) return res.status(401).send("auth error");
80+
const { subjectIds } = req.body;
81+
if (!Array.isArray(subjectIds)) {
82+
return res.status(400).json({ error: "subjectIds must be an array" });
83+
}
84+
try {
85+
const newSubjects = await Promise.all(
86+
subjectIds.map((id) => interest.get(id)),
87+
);
88+
if (newSubjects.some((s) => !s)) {
89+
return res.status(404).json({ error: "Subject not found" });
90+
}
91+
} catch (err) {
92+
console.error("Error fetching subjects:", err);
93+
res.status(500).json({ error: "Failed to fetch subjects" });
94+
}
95+
try {
96+
const updatedSubjects = await interest.updateMultipleWithTransaction(
97+
userId.value,
98+
subjectIds,
99+
);
100+
res.status(200).json(updatedSubjects);
101+
} catch (error) {
102+
console.error("Error updating subjects:", error);
103+
res.status(500).json({ error: "Failed to update subjects" });
104+
}
105+
});
106+
107+
router.get("/search/:query", async (req: Request, res: Response) => {
108+
// TODO: token との兼ね合いで、クエリパラメータでなく一旦パスパラメータとしている
109+
const q = req.params.query;
110+
if (typeof q !== "string") {
111+
return res.status(400).json({ error: "Invalid query" });
112+
}
113+
try {
114+
const subjects = await interest.search(q);
115+
res.status(200).json(subjects);
116+
} catch (error) {
117+
console.error("Error searching subjects:", error);
118+
res.status(500).json({ error: "Failed to search subjects" });
119+
}
120+
});
121+
122+
export default router;

web/api/internal/endpoints.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,65 @@ export const coursesDayPeriod = (day: Day, period: number) => {
236236
return `${API_ENDPOINT}/courses/day-period?day=${day}&period=${period}`;
237237
};
238238

239+
/**
240+
* [v] 実装済み
241+
* GET -> get subjects the user is interested in.
242+
* - statuses:
243+
* - 200: ok.
244+
* - body: InterestSubject[]
245+
* - 401: unauthorized.
246+
* - 500: internal error.
247+
*/
248+
export const subjectsUserId = (userId: UserID) => {
249+
return `${API_ENDPOINT}/subjects/userId/${userId}`;
250+
};
251+
252+
/**
253+
* [v] 実装済み
254+
* GET -> get my subjects.
255+
* - statuses:
256+
* - 200: ok.
257+
* - body: InterestSubject[]
258+
* - 401: unauthorized.
259+
* - 500: internal error.
260+
* PATCH -> update my subjects.
261+
* - request body: SubjectId
262+
* - statuses:
263+
* - 200: ok.
264+
* - body: InterestSubject[]
265+
* - 401: unauthorized.
266+
* - 500: internal error.
267+
* DELETE -> delete my subjects.
268+
* - request body: SubjectId
269+
* - statuses:
270+
* - 200: ok.
271+
* - body: InterestSubject[]
272+
* - 401: unauthorized.
273+
* - 500: internal error.
274+
* PUT → replace my subjects.
275+
* - request body: SubjectId[]
276+
* - statuses:
277+
* - 200: ok.
278+
* - body: InterestSubject[]
279+
* - 401: unauthorized.
280+
* - 500: internal error.
281+
*/
282+
export const subjectsMine = `${API_ENDPOINT}/subjects/mine`;
283+
284+
/**
285+
* [] 実装済み
286+
* GET -> search subjects.
287+
* - statuses:
288+
* - 200: ok.
289+
* - body: InterestSubject[]
290+
* - 401: unauthorized.
291+
* - 500: internal error.
292+
**/
293+
export const subjectsSearch = (query: string) => {
294+
// TODO: token との兼ね合いで、クエリパラメータでなく一旦パスパラメータとしている
295+
return `${API_ENDPOINT}/subjects/search/${query}`;
296+
};
297+
239298
/**
240299
* [v] 実装済み
241300
* PUT -> create request.
@@ -400,6 +459,9 @@ export default {
400459
cancelRequest,
401460
coursesUserId,
402461
coursesDayPeriod,
462+
subjectsUserId,
463+
subjectsMine,
464+
subjectsSearch,
403465
roomOverview,
404466
dmTo,
405467
dmWith,

web/api/subject.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type {
2+
InterestSubject,
3+
InterestSubjectID,
4+
UserID,
5+
} from "common/types.ts";
6+
import { InterestSubjectSchema } from "common/zod/schemas.ts";
7+
import { z } from "zod";
8+
import { credFetch } from "../firebase/auth/lib.ts";
9+
import { type Hook, useCustomizedSWR } from "../hooks/useCustomizedSWR.ts";
10+
import endpoints from "./internal/endpoints.ts";
11+
12+
// 自身の興味分野を取得する
13+
export function useMyInterests(): Hook<InterestSubject[]> {
14+
return useCustomizedSWR(
15+
"interests::mine",
16+
getMySubjects,
17+
z.array(InterestSubjectSchema),
18+
);
19+
}
20+
21+
async function getMySubjects(): Promise<InterestSubject[]> {
22+
const res = await credFetch("GET", endpoints.subjectsMine);
23+
return res.json();
24+
}
25+
26+
// 自身の興味分野を更新する
27+
export async function update(
28+
newSubjectIds: InterestSubjectID[],
29+
): Promise<void> {
30+
const url = endpoints.subjectsMine;
31+
await credFetch("PUT", url, { subjectIds: newSubjectIds });
32+
}
33+
34+
// 指定した userId のユーザの興味分野を取得
35+
export async function get(id: UserID): Promise<InterestSubject[] | null> {
36+
const res = await credFetch("GET", endpoints.subjectsUserId(id));
37+
return await res.json();
38+
}
39+
40+
// キーワードで興味分野を検索
41+
export function useSearch(q: string): Hook<InterestSubject[]> {
42+
return useCustomizedSWR(
43+
`interests::search::${q}`,
44+
() => search(q),
45+
z.array(InterestSubjectSchema),
46+
);
47+
}
48+
49+
export async function search(q: string): Promise<InterestSubject[]> {
50+
const res = await credFetch("GET", endpoints.subjectsSearch(q));
51+
return await res.json();
52+
}

0 commit comments

Comments
 (0)