Skip to content

Commit 35a1a18

Browse files
authored
興味分野編集 (#571)
1 parent ae0f0cd commit 35a1a18

File tree

11 files changed

+542
-17
lines changed

11 files changed

+542
-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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ export async function get(id: number): Promise<InterestSubject | null> {
99
return await prisma.interestSubject.findUnique({ where: { id } });
1010
}
1111

12+
export async function create(name: string): Promise<InterestSubject> {
13+
const existingTag = await prisma.interestSubject.findMany({
14+
where: { name },
15+
});
16+
if (existingTag.length > 0) {
17+
throw new Error("同名のタグがすでに存在します");
18+
}
19+
return await prisma.interestSubject.create({
20+
data: {
21+
name,
22+
group: "", // TODO: 運用次第
23+
},
24+
});
25+
}
26+
1227
export async function of(userId: UserID): Promise<InterestSubject[]> {
1328
return await prisma.interest
1429
.findMany({
@@ -37,3 +52,23 @@ export async function remove(userId: UserID, subjectId: number) {
3752
},
3853
});
3954
}
55+
56+
export async function updateMultipleWithTransaction(
57+
userId: UserID,
58+
subjectIds: number[],
59+
) {
60+
return await prisma.$transaction(async (prisma) => {
61+
await prisma.interest.deleteMany({
62+
where: {
63+
userId,
64+
},
65+
});
66+
67+
await prisma.interest.createMany({
68+
data: subjectIds.map((subjectId) => ({
69+
userId,
70+
subjectId,
71+
})),
72+
});
73+
});
74+
}

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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.post("/", async (req: Request, res: Response) => {
34+
const { name } = req.body;
35+
if (typeof name !== "string") {
36+
return res.status(400).json({ error: "name must be a string" });
37+
}
38+
try {
39+
const newSubject = await interest.create(name);
40+
res.status(201).json(newSubject);
41+
} catch (error) {
42+
console.error("Error creating subject:", error);
43+
res.status(500).json({ error: "Failed to create subject" });
44+
}
45+
});
46+
47+
router.patch("/mine", async (req: Request, res: Response) => {
48+
const userId = await safeGetUserId(req);
49+
if (!userId.ok) return res.status(401).send("auth error");
50+
const { subjectId } = req.body;
51+
try {
52+
const newSubject = await interest.get(subjectId);
53+
if (!newSubject) {
54+
return res.status(404).json({ error: "Subject not found" });
55+
}
56+
} catch (err) {
57+
console.error("Error fetching subject:", err);
58+
res.status(500).json({ error: "Failed to fetch subject" });
59+
}
60+
try {
61+
const updatedSubjects = await interest.add(userId.value, subjectId);
62+
res.status(200).json(updatedSubjects);
63+
} catch (error) {
64+
console.error("Error updating subjects:", error);
65+
res.status(500).json({ error: "Failed to update subjects" });
66+
}
67+
});
68+
69+
router.delete("/mine", async (req: Request, res: Response) => {
70+
const userId = await safeGetUserId(req);
71+
if (!userId.ok) return res.status(401).send("auth error");
72+
const { subjectId } = req.body;
73+
try {
74+
const newSubject = await interest.get(subjectId);
75+
if (!newSubject) {
76+
return res.status(404).json({ error: "Subject not found" });
77+
}
78+
} catch (err) {
79+
console.error("Error fetching subject:", err);
80+
res.status(500).json({ error: "Failed to fetch subject" });
81+
}
82+
try {
83+
const updatedSubjects = await interest.remove(userId.value, subjectId);
84+
res.status(200).json(updatedSubjects);
85+
} catch (error) {
86+
console.error("Error deleting subjects:", error);
87+
res.status(500).json({ error: "Failed to delete subjects" });
88+
}
89+
});
90+
91+
router.put("/mine", async (req: Request, res: Response) => {
92+
const userId = await safeGetUserId(req);
93+
if (!userId.ok) return res.status(401).send("auth error");
94+
const { subjectIds } = req.body;
95+
if (!Array.isArray(subjectIds)) {
96+
return res.status(400).json({ error: "subjectIds must be an array" });
97+
}
98+
try {
99+
const newSubjects = await Promise.all(
100+
subjectIds.map((id) => interest.get(id)),
101+
);
102+
if (newSubjects.some((s) => !s)) {
103+
return res.status(404).json({ error: "Subject not found" });
104+
}
105+
} catch (err) {
106+
console.error("Error fetching subjects:", err);
107+
res.status(500).json({ error: "Failed to fetch subjects" });
108+
}
109+
try {
110+
const updatedSubjects = await interest.updateMultipleWithTransaction(
111+
userId.value,
112+
subjectIds,
113+
);
114+
res.status(200).json(updatedSubjects);
115+
} catch (error) {
116+
console.error("Error updating subjects:", error);
117+
res.status(500).json({ error: "Failed to update subjects" });
118+
}
119+
});
120+
121+
router.get("/all", async (req: Request, res: Response) => {
122+
try {
123+
const subjects = await interest.all();
124+
res.status(200).json(subjects);
125+
} catch (error) {
126+
console.error("Error fetching subjects:", error);
127+
res.status(500).json({ error: "Failed to fetch subjects" });
128+
}
129+
});
130+
131+
export default router;

web/api/internal/endpoints.ts

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

239+
/**
240+
* [v] 実装済み
241+
* POST -> create a new subject.
242+
* - statuses:
243+
* - 200: ok.
244+
* - body: InterestSubject
245+
* - 401: unauthorized.
246+
* - 500: internal error.
247+
*/
248+
export const subjects = `${API_ENDPOINT}/subjects`;
249+
250+
/**
251+
* [v] 実装済み
252+
* GET -> get subjects the user is interested in.
253+
* - statuses:
254+
* - 200: ok.
255+
* - body: InterestSubject[]
256+
* - 401: unauthorized.
257+
* - 500: internal error.
258+
*/
259+
export const subjectsUserId = (userId: UserID) => {
260+
return `${API_ENDPOINT}/subjects/userId/${userId}`;
261+
};
262+
263+
/**
264+
* [v] 実装済み
265+
* GET -> get my subjects.
266+
* - statuses:
267+
* - 200: ok.
268+
* - body: InterestSubject[]
269+
* - 401: unauthorized.
270+
* - 500: internal error.
271+
* PATCH -> update my subjects.
272+
* - request body: SubjectId
273+
* - statuses:
274+
* - 200: ok.
275+
* - body: InterestSubject[]
276+
* - 401: unauthorized.
277+
* - 500: internal error.
278+
* DELETE -> delete my subjects.
279+
* - request body: SubjectId
280+
* - statuses:
281+
* - 200: ok.
282+
* - body: InterestSubject[]
283+
* - 401: unauthorized.
284+
* - 500: internal error.
285+
* PUT → replace my subjects.
286+
* - request body: SubjectId[]
287+
* - statuses:
288+
* - 200: ok.
289+
* - body: InterestSubject[]
290+
* - 401: unauthorized.
291+
* - 500: internal error.
292+
*/
293+
export const subjectsMine = `${API_ENDPOINT}/subjects/mine`;
294+
295+
/**
296+
* [v] 実装済み
297+
* GET -> get all subjects.
298+
* - statuses:
299+
* - 200: ok.
300+
* - body: InterestSubject[]
301+
* - 401: unauthorized.
302+
* - 500: internal error.
303+
*/
304+
export const subjectsAll = `${API_ENDPOINT}/subjects/all`;
305+
239306
/**
240307
* [v] 実装済み
241308
* PUT -> create request.
@@ -400,6 +467,10 @@ export default {
400467
cancelRequest,
401468
coursesUserId,
402469
coursesDayPeriod,
470+
subjects,
471+
subjectsUserId,
472+
subjectsMine,
473+
subjectsAll,
403474
roomOverview,
404475
dmTo,
405476
dmWith,

web/api/subject.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
const InterestSubjectListSchema = z.array(InterestSubjectSchema);
13+
14+
// 興味分野を作成する
15+
export async function create(name: string) {
16+
return await credFetch("POST", endpoints.subjects, { name });
17+
}
18+
19+
// 自身の興味分野を取得する
20+
export function useMyInterests(): Hook<InterestSubject[]> {
21+
return useCustomizedSWR(
22+
"interests::mine",
23+
getMySubjects,
24+
InterestSubjectListSchema,
25+
);
26+
}
27+
28+
async function getMySubjects(): Promise<InterestSubject[]> {
29+
const res = await credFetch("GET", endpoints.subjectsMine);
30+
return res.json();
31+
}
32+
33+
// 自身の興味分野を更新する
34+
export async function update(newSubjectIds: InterestSubjectID[]) {
35+
const url = endpoints.subjectsMine;
36+
return await credFetch("PUT", url, { subjectIds: newSubjectIds });
37+
}
38+
39+
// 指定した userId のユーザの興味分野を取得
40+
export async function get(id: UserID): Promise<InterestSubject[] | null> {
41+
const res = await credFetch("GET", endpoints.subjectsUserId(id));
42+
return await res.json();
43+
}
44+
45+
// すべての興味分野を取得
46+
export async function getAll(): Promise<InterestSubject[]> {
47+
const res = await credFetch("GET", endpoints.subjectsAll);
48+
return await res.json();
49+
}

0 commit comments

Comments
 (0)