Skip to content

Commit

Permalink
リアクション機能をAPIに実装
Browse files Browse the repository at this point in the history
  • Loading branch information
MurakawaTakuya committed Jan 12, 2025
1 parent 5825d1d commit a79ca61
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 23 deletions.
21 changes: 19 additions & 2 deletions Documents/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ API is provided by Firebase Cloud Functions. Database is provided by Firestore.
- URL: /user/:userId
- Method: DELETE

## Goals
## Goal
### Create Goal
- URL: /goal
- Method: POST
Expand Down Expand Up @@ -141,7 +141,7 @@ API is provided by Firebase Cloud Functions. Database is provided by Firestore.
- URL: /goal/:goalId
- Method: DELETE

## Posts
## Post
### Create Post
- URL: /post
- Method: POST
Expand Down Expand Up @@ -258,3 +258,20 @@ Use Create Post API to update post.
]
}
```

## Reactions
- URL: /reaction/:goalId
- Method: PUT
- Request
- Headers
- Content-Type: application/json
- Body
- userId: string
- reaction: string
- Example
```json
{
"userId": "IK0Zc2hoUYaYjXoqzmCl",
"reactionType": "clap"
}
```
25 changes: 25 additions & 0 deletions Documents/Database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
データベースには`user``goal`の2つのコレクションがあります。`user`コレクションにはユーザーの情報が、`goal`コレクションにはユーザーが設定した目標と結果の情報が保存されます。

# userコレクション
user (コレクション)
├── userId (ドキュメント)
│ ├── fcmToken: string (初期値: "")
│ ├── name: string
│ └── streak: number (初期値: 0, 目標完了時に更新, streakがリセットされる場合はAPI側で0を返す)
├── 他のドキュメント...


# goalコレクション
goal (コレクション)
├── goalId (ドキュメント)
│ ├── userId: string
│ ├── deadline: Timestamp (例: 2024年12月18日 23:00:00 UTC+9)
│ ├── text: string (""は禁止)
│ ├── post: map | null (初期値: null)
│ │ ├── storedId: string (storageに保存されたファイルのID)
│ │ ├── submittedAt: Timestamp (目標完了時間)
│ │ └── text: string (""もOK)
│ └── reaction?: map (初期値: undefined)
│ ├── userId: string (リアクションの種類)
│ ├── 他のUserId...
├── 他のドキュメント...
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ firebase emulators:start
||Document in this project or related PR|Firebase/Google Cloud - Official Documents|
|-|-|-|
|API|[API Document](./Documents/API.md)|[Firebase Cloud Functions](https://firebase.google.com/docs/functions)|
|Database||[Firestore](https://firebase.google.com/docs/firestore)|
|Database|[Database Document](./Documents/Database.md)|[Firestore](https://firebase.google.com/docs/firestore)|
|Storage||[Cloud Storage for Firebase](https://firebase.google.com/docs/storage)|
|Authentication||[Firebase Authentication](https://firebase.google.com/docs/auth)|
|Receive Notification|[Cloud Messagingを実装 #49](https://github.com/MurakawaTakuya/todo-real/pull/49)|[Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/)|
Expand Down
2 changes: 2 additions & 0 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ admin.initializeApp({
import goalRouter from "./routers/goalRouter";
import notificationRouter from "./routers/notificationRouter";
import postRouter from "./routers/postRouter";
import reactionRouter from "./routers/reactionRouter";
import resultRouter from "./routers/resultRouter";
import userRouer from "./routers/userRouter";

Expand Down Expand Up @@ -100,6 +101,7 @@ app.use("/goal", goalRouter);
app.use("/post", postRouter);
app.use("/result", resultRouter);
app.use("/notification", notificationRouter);
app.use("/reaction", reactionRouter);

const region = "asia-northeast1";

Expand Down
12 changes: 7 additions & 5 deletions functions/src/routers/goalRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ router.get("/", async (req: Request, res: Response) => {
userId: data.userId,
text: data.text,
post: data.post,
reaction: data.reaction ?? null,
};
});

Expand All @@ -37,10 +38,6 @@ router.get("/", async (req: Request, res: Response) => {
router.get("/:userId", async (req: Request, res: Response) => {
const userId = req.params.userId;

if (!userId) {
return res.status(400).json({ message: "User ID is required" });
}

try {
const goalSnapshot = await db
.collection("goal")
Expand All @@ -59,6 +56,7 @@ router.get("/:userId", async (req: Request, res: Response) => {
deadline: new Date(data.deadline._seconds * 1000),
text: data.text,
post: data.post,
reaction: data.reaction ?? null,
};
});

Expand Down Expand Up @@ -115,6 +113,10 @@ router.put("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;
const { userId, deadline, text }: Partial<Goal> = req.body;

if (!goalId) {
return res.status(400).json({ message: "goalId is required" });
}

if (!userId && !deadline && !text) {
return res.status(400).json({
message: "At least one of userId, deadline, or text is required",
Expand Down Expand Up @@ -151,7 +153,7 @@ router.delete("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;

if (!goalId) {
return res.status(400).json({ message: "Goal ID is required" });
return res.status(400).json({ message: "goalId is required" });
}

const goalRef = db.collection("goal").doc(goalId);
Expand Down
8 changes: 2 additions & 6 deletions functions/src/routers/postRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ router.get("/", async (req: Request, res: Response) => {
router.get("/:userId", async (req: Request, res: Response) => {
const userId = req.params.userId;

if (!userId) {
return res.status(400).json({ message: "User ID is required" });
}

try {
const goalSnapshot = await db
.collection("goal")
Expand Down Expand Up @@ -139,7 +135,7 @@ router.delete("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;

if (!goalId) {
return res.status(400).json({ message: "Goal ID is required" });
return res.status(400).json({ message: "goalId is required" });
}

const goalRef = db.collection("goal").doc(goalId);
Expand Down Expand Up @@ -167,7 +163,7 @@ router.delete("/:goalId", async (req: Request, res: Response) => {
post: null,
});

return res.json({ message: "Post deleted successfully" });
return res.json({ message: "Post deleted successfully", goalId });
} catch (error) {
logger.error(error);
return res.status(500).json({ message: "Error deleting post" });
Expand Down
61 changes: 61 additions & 0 deletions functions/src/routers/reactionRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import express, { Request, Response } from "express";
import admin from "firebase-admin";
import { logger } from "firebase-functions";
import { Reaction, reactionTypeMap } from "../types";

const router = express.Router();
const db = admin.firestore();

// PUT: リアクションを更新
router.put("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;
const { userId, reactionType }: Partial<Reaction> = req.body;

if (!userId || !goalId) {
return res.status(400).json({
message: "userID and goalId are required",
});
}

try {
const goalRef = db.collection("goal").doc(goalId);
const goalDoc = await goalRef.get();

if (!goalDoc.exists) {
return res.status(404).json({ message: "Goal not found" });
}

const goalData = goalDoc.data();

const currentReactions = goalData?.post?.reaction || {};
if (!reactionType) {
// リアクション削除
delete currentReactions[userId];
} else {
// リアクション追加
const validReactionTypes = [
...reactionTypeMap.success,
...reactionTypeMap.failed,
];
if (!validReactionTypes.includes(reactionType)) {
return res.status(400).json({ message: "Invalid reactionType" });
}

currentReactions[userId] = reactionType;
}

await goalRef.update({
post: {
...goalData?.post,
reaction: currentReactions,
},
});

return res.json({ message: "Reaction updated successfully", goalId });
} catch (error) {
logger.error(error);
return res.status(500).json({ message: "Error updating Reaction" });
}
});

export default router;
1 change: 1 addition & 0 deletions functions/src/routers/resultRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const processGoals = async (
storedId: post.storedId,
submittedAt: post.submittedAt.toDate(),
},
reaction: data.reaction || null,
userData,
});
}
Expand Down
10 changes: 5 additions & 5 deletions functions/src/routers/userRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ router.get("/", async (req: Request, res: Response) => {
router.get("/id/:userId", async (req: Request, res: Response) => {
const userId = req.params.userId;

if (!userId) {
return res.status(400).json({ message: "User ID is required" });
}

try {
const userDoc = await getUserFromId(userId);

Expand Down Expand Up @@ -154,6 +150,10 @@ router.put("/:userId", async (req: Request, res: Response) => {
const userId = req.params.userId;
const { name, fcmToken }: Partial<User> = req.body;

if (!userId) {
return res.status(400).json({ message: "userId is required" });
}

if (!name && fcmToken === undefined) {
return res.status(400).json({
message: "At least one of name, streak, or fcmToken is required",
Expand Down Expand Up @@ -183,7 +183,7 @@ router.delete("/:userId", async (req: Request, res: Response) => {
const userId = req.params.userId;

if (!userId) {
return res.status(400).json({ message: "User ID is required" });
return res.status(400).json({ message: "userId is required" });
}

const userRef = db.collection("user").doc(userId);
Expand Down
6 changes: 3 additions & 3 deletions functions/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const db = admin.firestore();
// 完了した目標をカウント(postがnullでないものをカウント)
export const countCompletedGoals = async (userId: string): Promise<number> => {
if (!userId) {
throw new Error("User ID is required");
throw new Error("goalId is required");
}

try {
Expand All @@ -27,7 +27,7 @@ export const countCompletedGoals = async (userId: string): Promise<number> => {
// 失敗した目標をカウント(今より前の`deadline`を持ち、postがnullの目標の数をカウント)
export const countFailedGoals = async (userId: string): Promise<number> => {
if (!userId) {
throw new Error("User ID is required");
throw new Error("goalId is required");
}

try {
Expand Down Expand Up @@ -55,7 +55,7 @@ export const hasCompletedGoalWithinRange = async (
endDate: Date
): Promise<boolean> => {
if (!userId) {
throw new Error("User ID is required");
throw new Error("goalId is required");
}

try {
Expand Down
2 changes: 1 addition & 1 deletion functions/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const createTasksOnGoalCreate = onDocumentCreated(

try {
const goalData = event.data.data();
const marginTime = 5;
const marginTime = 10;
// 期限のmarginTime分前にタスクを設定
const deadline = new Date(
goalData.deadline.toDate().getTime() - marginTime * 60 * 1000
Expand Down
16 changes: 16 additions & 0 deletions functions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Goal {
deadline: Date;
text: string;
post: Post | null;
reaction: Reaction | null;
}

export interface GoalWithId extends Goal {
Expand All @@ -32,3 +33,18 @@ export interface Post {
export interface PostWithGoalId extends Post {
goalId: string; // Postの所属するGoalId
}

export interface Reaction {
userId: string;
reactionType: ReactionType["success"] | ReactionType["failed"];
}

export interface ReactionType {
success: "laugh" | "surprised" | "clap";
failed: "sad" | "angry" | "muscle";
}

export const reactionTypeMap = {
success: ["laugh", "surprised", "clap"] as const,
failed: ["sad", "angry", "muscle"] as const,
};

0 comments on commit a79ca61

Please sign in to comment.