Skip to content

Commit e20dbe5

Browse files
aster-voidnakaterm
andauthored
プロフィール編集の修正・興味分野編集の追加 (#527)
# PRの概要 プロフィール編集の修正・興味分野編集の追加 close #569 ## 具体的な変更内容 プロフィール編集に React Hook Form を導入した。またレイアウトを修正した。 興味分野編集の機能を追加した。 https://github.com/user-attachments/assets/76fe6c4d-d60b-4c4c-9034-f5c0e94ca41d ## 影響範囲 ## 動作要件 ## 補足 ## レビューリクエストを出す前にチェック! - [ ] 改めてセルフレビューしたか - [ ] 手動での動作検証を行ったか - [ ] server の機能追加ならば、テストを書いたか - 理由: 書いた | server の機能追加ではない - [ ] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 書いた | 間違った使い方は存在しない - [ ] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 --> --------- Co-authored-by: naka-12 <[email protected]>
1 parent 5cd9499 commit e20dbe5

File tree

19 files changed

+746
-497
lines changed

19 files changed

+746
-497
lines changed

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
33
"formatter": {
44
"indentStyle": "space",
5-
"indentWidth": 2
5+
"indentWidth": 2,
6+
"lineWidth": 80
67
},
78
"linter": {
89
"rules": {

bun.lockb

-52.6 KB
Binary file not shown.

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>;

flake.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node";
3737
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine";
3838
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt";
39+
40+
export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib
3941
'';
4042
};
4143
in

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
"license": "ISC",
1313
"devDependencies": {
1414
"@biomejs/biome": "^1.9.1",
15+
"husky": "^9.1.4",
16+
"lint-staged": "^15.2.10"
17+
},
18+
"dependencies": {
19+
"@hookform/resolvers": "^3.9.1",
20+
"react-hook-form": "^7.53.2",
1521
"@types/bun": "^1.1.10",
1622
"cspell": "^8.14.4",
1723
"typescript": "^5.6.2",
18-
"lefthook": "^1.8.2"
19-
},
20-
"dependencies": {
24+
"lefthook": "^1.8.2",
2125
"zod": "^3.23.8"
2226
},
2327
"trustedDependencies": ["@biomejs/biome", "lefthook"]

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;

0 commit comments

Comments
 (0)