Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5a7fa06
サーバー全部消す
ef81sp Jun 1, 2025
2a01c25
テストも消す
ef81sp Jun 1, 2025
33564a1
消す
ef81sp Jun 1, 2025
d462ab8
ドキュメント整備
ef81sp Jun 4, 2025
ad666e1
wip
ef81sp Jun 12, 2025
0ba8213
wip
ef81sp Jun 12, 2025
b364803
wip
ef81sp Jun 12, 2025
511d36c
wip
ef81sp Jun 12, 2025
da3dca8
wip
ef81sp Jun 12, 2025
ecea9e0
wip
ef81sp Jun 12, 2025
b2c1886
wip
ef81sp Jun 12, 2025
6843966
wip
ef81sp Jun 12, 2025
9b80f44
退室処理
ef81sp Jun 14, 2025
f490e2d
wip
ef81sp Jun 14, 2025
5fb3624
wip
ef81sp Jun 14, 2025
0c843b4
トークン出さない
ef81sp Jun 14, 2025
febfad2
wip
ef81sp Jun 14, 2025
d427d73
再入場
ef81sp Jun 14, 2025
c8ba372
wip
ef81sp Jun 14, 2025
4983a10
kv_debug
ef81sp Jun 14, 2025
4e5617a
wip
ef81sp Jun 14, 2025
4e88d16
WebSocket消す
ef81sp Jun 16, 2025
3fee09d
webSocket接続
ef81sp Jun 16, 2025
57bcbfe
wip
ef81sp Jun 16, 2025
57d6680
回答送信
ef81sp Jun 16, 2025
bcb4eb5
wip
ef81sp Jun 16, 2025
fd09a48
回答開く
ef81sp Jun 16, 2025
0391e4f
isAudience
ef81sp Jun 16, 2025
0164c22
wip
ef81sp Jun 16, 2025
ba8d3e9
wip
ef81sp Jun 16, 2025
c7de6eb
回答消去
ef81sp Jun 16, 2025
770821d
wip
ef81sp Jun 16, 2025
6f7f2ee
test
ef81sp Jun 17, 2025
306fb7c
a
ef81sp Jun 17, 2025
ddd234e
fmt
ef81sp Jun 17, 2025
1681640
競合をすべて現在のブランチの内容で解消(mainの内容は破棄)
ef81sp Jun 17, 2025
5bbdb1f
タブを閉じたら退室
ef81sp Jun 17, 2025
3cf9ce2
fmt
ef81sp Jun 17, 2025
fd5c0af
不使用ファイル・コードを整理
ef81sp Jun 17, 2025
1dee7dd
a
ef81sp Jun 17, 2025
e432cbc
切断時退室
ef81sp Jun 17, 2025
880c868
切断時退室
ef81sp Jun 17, 2025
b677f6d
fmt
ef81sp Jun 17, 2025
9e76858
del kvcleanup
ef81sp Jun 17, 2025
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
8 changes: 8 additions & 0 deletions .github/ignore.copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 参考になるドキュメント

設計.md を見て

## ルール

- 余計なコメントを書かないで。複雑な処理の説明とかはいいけど、コードの内容を説明するコメントは不要。
- コメントは日本語で書いてください。
2 changes: 0 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,3 @@ jobs:
project: "ppap"
entrypoint: "backend/server.ts"
root: "."


2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?

test
1 change: 0 additions & 1 deletion Procfile

This file was deleted.

6 changes: 6 additions & 0 deletions backend/clear_kv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// KVストアを完全にクリアするユーティリティ
export async function clearKvAll(kv: Deno.Kv) {
for await (const entry of kv.list({ prefix: [] })) {
await kv.delete(entry.key)
}
}
250 changes: 250 additions & 0 deletions backend/handlers/roomHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { Room, UserTokenInfo } from "../type.ts"
import { CreateRoomRequest, CreateRoomResponse, RoomForClient } from "../type.ts"
import { CreateRoomRequestSchema } from "../validate.ts"
import { createRoom, createUserToken, leaveRoom, roomKey, userTokenKey } from "../kv.ts"

export function toRoomForClient(room: Room, userToken: string): RoomForClient {
return {
id: room.id,
participants: room.participants.map((p, i) => ({
name: p.name,
userNumber: i,
isMe: p.token === userToken,
answer: p.answer,
isAudience: p.isAudience ?? false,
})),
config: room.config,
createdAt: room.createdAt,
updatedAt: room.updatedAt,
}
}

export async function handleCreateRoom(
req: Request,
kv: Deno.Kv,
): Promise<Response> {
let body: CreateRoomRequest
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
status: 400,
})
}
const parse = CreateRoomRequestSchema.safeParse(body)
if (!parse.success) {
return new Response(JSON.stringify({ error: "Validation error" }), {
status: 400,
})
}
const { userName } = parse.data
const roomId = crypto.randomUUID()
const userToken = crypto.randomUUID()
const now = Date.now()
const room: Room = {
id: roomId,
participants: [
{ token: userToken, name: userName, answer: "", isAudience: false },
],
config: { allowSpectators: true, maxParticipants: 50 },
createdAt: now,
updatedAt: now,
}
const userTokenInfo: UserTokenInfo = {
token: userToken,
currentRoomId: roomId,
name: userName,
isSpectator: false,
lastAccessedAt: now,
}
try {
await createRoom(kv, room)
await createUserToken(kv, userTokenInfo)
const userNumber = 0
const res = {
roomId,
userToken,
userNumber,
room: toRoomForClient(room, userToken),
}
return new Response(JSON.stringify(res), {
status: 201,
headers: { "content-type": "application/json" },
})
} catch (_e) {
return new Response(JSON.stringify({ error: "Server error" }), {
status: 500,
})
}
}

export async function handleJoinRoom(
req: Request,
roomId: string,
kv: Deno.Kv,
): Promise<Response> {
let body: { userName: string; userToken?: string }
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
status: 400,
})
}
if (
!body.userName ||
typeof body.userName !== "string" ||
body.userName.length > 24
) {
return new Response(JSON.stringify({ error: "Validation error" }), {
status: 400,
})
}
const roomRes = await kv.get<Room>(roomKey(roomId))
const room = roomRes.value
if (!room) {
return new Response(JSON.stringify({ error: "Room not found" }), {
status: 404,
})
}
let userToken = body.userToken
if (!userToken) {
userToken = crypto.randomUUID()
}
const userTokenInfoRes = await kv.get<UserTokenInfo>(userTokenKey(userToken))
let userTokenInfo = userTokenInfoRes.value
if (!userTokenInfo) {
userTokenInfo = {
token: userToken,
currentRoomId: roomId,
name: body.userName,
isSpectator: false,
lastAccessedAt: Date.now(),
}
await kv.atomic().set(userTokenKey(userToken), userTokenInfo).commit()
} else {
userTokenInfo = {
...userTokenInfo,
currentRoomId: roomId,
name: body.userName,
lastAccessedAt: Date.now(),
}
await kv.atomic().set(userTokenKey(userToken), userTokenInfo).commit()
}
if (!room.participants.some((p) => p.token === userToken)) {
room.participants.push({
token: userToken,
name: body.userName,
answer: "",
isAudience: false,
})
room.updatedAt = Date.now()
await kv.atomic().set(roomKey(roomId), room).commit()
}
const userNumber = room.participants.findIndex((p) => p.token === userToken)
return new Response(
JSON.stringify({
userToken,
userNumber,
room: toRoomForClient(room, userToken),
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
)
}

export async function handleLeaveRoom(
req: Request,
roomId: string,
kv: Deno.Kv,
): Promise<Response> {
let body: { userToken: string }
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
status: 400,
})
}
if (!body.userToken || typeof body.userToken !== "string") {
return new Response(JSON.stringify({ error: "Validation error" }), {
status: 400,
})
}
// leaveRoomの新しい呼び出し方に対応
const result = await leaveRoom(kv, { roomId, userToken: body.userToken })
if (!result.ok) {
return new Response(JSON.stringify({ error: result.error }), {
status: 500,
})
}
return new Response(
JSON.stringify({ message: "Successfully left the room" }),
{
status: 200,
headers: { "content-type": "application/json" },
},
)
}

export async function handleRejoinRoom(
req: Request,
roomId: string,
kv: Deno.Kv,
): Promise<Response> {
let body: { userToken: string }
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
status: 400,
})
}
const userToken = body.userToken
if (!userToken || typeof userToken !== "string") {
return new Response(JSON.stringify({ error: "Validation error" }), {
status: 400,
})
}
const userTokenInfoRes = await kv.get<UserTokenInfo>(userTokenKey(userToken))
const userTokenInfo = userTokenInfoRes.value
if (!userTokenInfo) {
return new Response(
JSON.stringify({ error: "User not found or invalid token" }),
{
status: 404,
},
)
}
const roomRes = await kv.get<Room>(roomKey(roomId))
const room = roomRes.value
if (!room) {
return new Response(JSON.stringify({ error: "Room not found" }), {
status: 404,
})
}
const participant = room.participants.find((p) => p.token === userToken)
if (!participant) {
return new Response(
JSON.stringify({ error: "User not joined in this room" }),
{
status: 404,
},
)
}
const userNumber = room.participants.findIndex((p) => p.token === userToken)
return new Response(
JSON.stringify({
userToken,
userNumber,
room: toRoomForClient(room, userToken),
userName: participant.name,
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
)
}
Loading