diff --git a/.env.example b/.env.example index c80b40a..8b6154c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # frontend VITE_API_URL= +VITE_REDIRECT_URL= # backend POSTGRES_USER= @@ -11,3 +12,5 @@ BETTER_AUTH_URL= BETTER_AUTH_SECRET= DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= diff --git a/.github/workflows/deploy-frontend.yaml b/.github/workflows/deploy-frontend.yaml index a1e02c8..293066d 100644 --- a/.github/workflows/deploy-frontend.yaml +++ b/.github/workflows/deploy-frontend.yaml @@ -24,6 +24,7 @@ jobs: - name: Build env: VITE_API_URL: ${{ secrets.VITE_API_URL }} + VITE_CLIENT_URL: ${{ secrets.VITE_CLIENT_URL }} VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }} run: nix develop --command bash -c "pnpm i && pnpm run build" # Deploy Frontend to Cloudflare Workers diff --git a/.github/workflows/preview-frontend.yaml b/.github/workflows/preview-frontend.yaml index 13b8e5e..84c2c80 100644 --- a/.github/workflows/preview-frontend.yaml +++ b/.github/workflows/preview-frontend.yaml @@ -26,6 +26,7 @@ jobs: id: build env: VITE_API_URL: ${{ secrets.VITE_API_URL }} + VITE_CLIENT_URL: ${{ secrets.VITE_CLIENT_URL }} VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }} run: nix develop --command bash -c "pnpm i && pnpm run frontend:build" # Deploy Preview to Cloudflare Workers diff --git a/assets/er.mmd b/assets/er.mmd index 9f9a9aa..882ff85 100644 --- a/assets/er.mmd +++ b/assets/er.mmd @@ -7,15 +7,10 @@ erDiagram text image created_at timestamp updated_at timestamp - } - UserProfile { - text user_id "PK FK" text display_name text avatar_url text bio - created_at timestamp - updated_at timestamp } Session { @@ -84,8 +79,6 @@ erDiagram User ||--|{ Session : "id<->user_id" User ||--|{ Account : "id<->user_id" - User ||--o| UserProfile : "id<->user_id" - User ||--o{ Group : "id<->created_by" Group ||--o{ GroupMembership : "id<->group_id" diff --git a/assets/er.png b/assets/er.png index b07ad4b..eea88dd 100644 Binary files a/assets/er.png and b/assets/er.png differ diff --git a/flake.lock b/flake.lock index 8af9108..58e912d 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764642553, - "narHash": "sha256-mvbFFzVBhVK1FjyPHZGMAKpNiqkr7k++xIwy+p/NQvA=", + "lastModified": 1767966113, + "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f720de59066162ee879adcc8c79e15c51fe6bfb4", + "rev": "5f02c91314c8ba4afe83b256b023756412218535", "type": "github" }, "original": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d34c85..262766e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,8 +145,8 @@ catalogs: specifier: ^17.2.3 version: 17.2.3 dotenv-caster: - specifier: ^2.1.23 - version: 2.1.23 + specifier: ^2.2.1 + version: 2.2.1 importers: @@ -205,7 +205,7 @@ importers: version: 17.2.3 dotenv-caster: specifier: catalog:setup - version: 2.1.23(dotenv@17.2.3) + version: 2.2.1(dotenv@17.2.3) drizzle-kit: specifier: catalog:backend version: 0.31.6 @@ -481,7 +481,7 @@ importers: version: 17.2.3 dotenv-caster: specifier: catalog:setup - version: 2.1.23(dotenv@17.2.3) + version: 2.2.1(dotenv@17.2.3) tsx: specifier: catalog:node version: 4.20.6 @@ -2346,8 +2346,8 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dotenv-caster@2.1.23: - resolution: {integrity: sha512-L276rkMu8t9BEqciqhE6JxtShJ2zBLhZ3drcK3+0WGNkJobpTiRJ9eFRmlMaz0Ewnl4m5BqD6ZaGFL6GQMtFaA==} + dotenv-caster@2.2.1: + resolution: {integrity: sha512-B4ESLIOIB7hL1KiGKMw7Y4R5NP/faRliC47fsR4/axZwWmaDeHdYamA6dRFJU5k2AzkyEWUjzIFHlGfctoRVNg==} peerDependencies: dotenv: ^17.2.3 @@ -5916,7 +5916,7 @@ snapshots: dlv@1.1.3: {} - dotenv-caster@2.1.23(dotenv@17.2.3): + dotenv-caster@2.2.1(dotenv@17.2.3): dependencies: dotenv: 17.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index edce2b7..ebe4608 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -56,7 +56,7 @@ catalogs: turbo: ^2.5.8 setup: dotenv: ^17.2.3 - dotenv-caster: ^2.1.23 + dotenv-caster: ^2.2.1 ignoredBuiltDependencies: - '@swc/core' diff --git a/products/backend/.env.example b/products/backend/.env.example index 037faab..8e61426 100644 --- a/products/backend/.env.example +++ b/products/backend/.env.example @@ -1,5 +1,8 @@ DATABASE_URL= +DATABASE_CREDENTIALS_SSL_REJECT_UNAUTHORIZED= BETTER_AUTH_URL= BETTER_AUTH_SECRET= DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= diff --git a/products/backend/README.md b/products/backend/README.md index f6ab4d9..495ad95 100644 --- a/products/backend/README.md +++ b/products/backend/README.md @@ -36,6 +36,10 @@ better-authが実行時に参照する設定ファイル。 pnpm run backend:better-auth:generate ``` +`auto-auth-schema.ts`が更新される。 + +このファイルを読み、適宜`src/db/auth-schema.ts`を手動で更新する。 + 3. `pnpm run backend:drizzle:generate`を実行する。 ```sh @@ -100,6 +104,21 @@ pnpm run backend:deploy ※ HyperdriveのIDが漏洩すると、他者にデータベースを操作される可能性があるため、デプロイ完了後は必ず元に戻してからステージング (`git add`) をすること。 +## drizzleのスキーマファイルの構造 + +```mermaid +flowchart LR + auth-schema["src/db/auth-schema.ts (better-auth関係のテーブル定義用)"] + pay-crew2["src/db/pay-crew2-schema.ts (pay-crew2関係のテーブル定義用)"] + relation["src/db/relation.ts (リレーション定義用)"] + schema["src/db/schema.ts (エクスポート用)"] + auth-schema --> relation + auth-schema --> schema + relation --> schema + pay-crew2 --> relation + pay-crew2 --> schema +``` + ## 構造 この構造は、レイヤードアーキテクチャから着想を得て定義している。 @@ -116,6 +135,7 @@ pnpm run backend:deploy - hono - @hono/zod-openapi - zod-openapi-share + - better-auth #### presentation/share @@ -128,6 +148,7 @@ presentation層で共通して使用するエンティティを定義する。 - 外部依存 - drizzle + - better-auth ### db @@ -146,6 +167,7 @@ openapi.jsonの生成を行う処理を定義している。 - hono - @hono/zod-openapi - zod-openapi-share + - better-auth ## 依存関係 diff --git a/products/backend/drizzle.config.ts b/products/backend/drizzle.config.ts index a2ea9b5..fb5f4f0 100644 --- a/products/backend/drizzle.config.ts +++ b/products/backend/drizzle.config.ts @@ -16,7 +16,7 @@ const databaseCredentialsSslRejectUnauthorized = caster.castBoolean( export default { dialect: 'postgresql', - schema: './src/db/schema.ts', + schema: ['./src/db/auth-schema.ts', './src/db/pay-crew2-schema.ts', './src/db/relation.ts'], out: './drizzle', dbCredentials: { url: databaseUrl, diff --git a/products/backend/src/db/auth-schema.ts b/products/backend/src/db/auth-schema.ts index 9949897..5f9fb35 100644 --- a/products/backend/src/db/auth-schema.ts +++ b/products/backend/src/db/auth-schema.ts @@ -1,36 +1,26 @@ import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core'; -export const user = pgTable('user', { - id: text('id').primaryKey(), - name: text('name').notNull(), - email: text('email').notNull().unique(), - emailVerified: boolean('email_verified').default(false).notNull(), - image: text('image'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at') - .defaultNow() - .$onUpdate(() => /* @__PURE__ */ new Date()) - .notNull(), -}); - -export const userProfile = pgTable( - 'user_profile', +export const user = pgTable( + 'user', { - userId: text('user_id') - .primaryKey() - .references(() => user.id, { onDelete: 'cascade' }), - displayName: text('display_name'), - avatarUrl: text('avatar_url'), - bio: text('bio'), + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: boolean('email_verified').default(false).notNull(), + image: text('image'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), + // 追加のプロフィール情報 + displayName: text('display_name'), + avatarUrl: text('avatar_url'), + bio: text('bio'), }, (table) => [ - // displayName に対する二次索引を作成 - index('user_profile_display_name_idx').on(table.displayName), + // display_name に対する二次索引を作成 + index('user_display_name_idx').on(table.displayName), ] ); diff --git a/products/backend/src/db/relation.ts b/products/backend/src/db/relation.ts index fee3630..1fb7a54 100644 --- a/products/backend/src/db/relation.ts +++ b/products/backend/src/db/relation.ts @@ -1,17 +1,11 @@ // drizzle import { relations } from 'drizzle-orm'; -import { account, session, user, userProfile } from './auth-schema'; -import { debt, group, groupMembership } from './pay-crew2-schema'; // tables -export { user, userProfile, session, account, verification } from './auth-schema'; -export { group, groupMembership, debt } from './pay-crew2-schema'; +import { account, session, user } from './auth-schema'; +import { debt, group, groupMembership } from './pay-crew2-schema'; // auth-schema -export const userRelations = relations(user, ({ one, many }) => ({ - userProfile: one(userProfile, { - fields: [user.id], - references: [userProfile.userId], - }), +export const userRelations = relations(user, ({ many }) => ({ sessions: many(session, { relationName: 'user__session_userId', }), @@ -35,13 +29,6 @@ export const userRelations = relations(user, ({ one, many }) => ({ }), })); -export const userProfileRelations = relations(userProfile, ({ one }) => ({ - user: one(user, { - fields: [userProfile.userId], - references: [user.id], - }), -})); - export const sessionRelations = relations(session, ({ one }) => ({ user: one(user, { fields: [session.userId], diff --git a/products/backend/src/db/schema.ts b/products/backend/src/db/schema.ts index 895b6a6..0a3fc8f 100644 --- a/products/backend/src/db/schema.ts +++ b/products/backend/src/db/schema.ts @@ -1 +1,3 @@ +export * from './auth-schema'; +export * from './pay-crew2-schema'; export * from './relation'; diff --git a/products/backend/src/index.ts b/products/backend/src/index.ts index 4fc0e9d..48635be 100644 --- a/products/backend/src/index.ts +++ b/products/backend/src/index.ts @@ -20,7 +20,7 @@ app.use( cors({ origin: ['https://pay-crew2.yukiosada.work', 'http://localhost:5173'], allowHeaders: ['Content-Type', 'Authorization', 'baggage', 'sentry-trace'], - allowMethods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS'], + allowMethods: ['POST', 'PUT', 'PATCH', 'GET', 'DELETE', 'OPTIONS'], exposeHeaders: ['Content-Length'], maxAge: 600, credentials: true, diff --git a/products/backend/src/presentation/factory/hono.ts b/products/backend/src/presentation/factory/hono.ts index 399a708..3d02f8d 100644 --- a/products/backend/src/presentation/factory/hono.ts +++ b/products/backend/src/presentation/factory/hono.ts @@ -14,7 +14,7 @@ const honoFactory = () => { if (!result.success) { console.error(result.error); throw new HTTPException(400, { - message: 'Zod Validation Error', + message: 'Bad Request', }); } }, diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 4d5ac4a..99f4209 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -21,8 +21,6 @@ import { registerGroupDebtResponseSchema, type RegisterGroupDebtResponseSchemaType, deleteGroupDebtRequestSchema, - deleteGroupDebtResponseSchema, - type DeleteGroupDebtResponseSchemaType, } from 'validator'; // error schema import { route } from '../share/error'; @@ -30,7 +28,7 @@ import { route } from '../share/error'; import { drizzle } from 'drizzle-orm/node-postgres'; import { and, eq, isNull } from 'drizzle-orm'; import { group, debt, groupMembership } from '../../db/pay-crew2-schema'; -import { user, userProfile } from '../../db/auth-schema'; +import { user } from '../../db/auth-schema'; const hono = honoFactory(); @@ -39,7 +37,7 @@ const createGroupSchema = route.createSchema( { path: '/api/group/create', method: 'post', - description: 'create a new group', + description: 'グループを新規作成するエンドポイント', security: [{ SessionCookie: [] }], request: { body: { @@ -62,55 +60,44 @@ const createGroupSchema = route.createSchema( }, }, }, - [400, 401, 500] as const + [401, 500] as const ); hono.openapi(createGroupSchema, async (c) => { - c.status(201); + const loginUser = c.get('user'); const body = c.req.valid('json'); - const user = c.get('user'); - //validation - if (!body || !body.name) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // set group name - const groupName = body.name; - - // create invited_id - let uuid1 = crypto.randomUUID(); - let uuid2 = crypto.randomUUID(); - // invite_id format: - - const invite_id = `${uuid1}-${uuid2}`; + // 現在時刻の取得 + const now = new Date(); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - //* insert group data *// + //* グループ情報を挿入 *// const result = await db .insert(group) .values({ id: crypto.randomUUID(), - name: groupName, - invite_id: invite_id, - createdBy: user.id, - createdAt: new Date(), - updatedAt: new Date(), + name: body.group_name, + invite_id: `${crypto.randomUUID()}-${crypto.randomUUID()}`, + createdBy: loginUser.id, + createdAt: now, + updatedAt: now, }) - .returning(); + .returning({ + id: group.id, + invite_id: group.invite_id, + }); - //* insert created_by user to groupMembership *// - await db - .insert(groupMembership) - .values({ - id: crypto.randomUUID(), - groupId: result[0].id, - userId: user.id, - joinedAt: new Date(), - }) - .returning(); + //* グループ作成者をgroupMembershipに挿入 *// + await db.insert(groupMembership).values({ + id: crypto.randomUUID(), + groupId: result[0].id, + userId: loginUser.id, + joinedAt: now, + }); - // return response + // レスポンス return c.json( { group_id: result[0].id, @@ -125,7 +112,7 @@ const joinGroupSchema = route.createSchema( { path: '/api/group/join', method: 'post', - description: 'join an existing group', + description: '招待IDを使ってグループに参加するエンドポイント', security: [{ SessionCookie: [] }], request: { body: { @@ -152,39 +139,62 @@ const joinGroupSchema = route.createSchema( ); hono.openapi(joinGroupSchema, async (c) => { - c.status(201); const body = c.req.valid('json'); const user = c.get('user'); - //validation - if (!body || !body.invite_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - const inviteId = body.invite_id; + // 現在時刻の取得 + const now = new Date(); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // find group by invite_id - const groupData = await db.select().from(group).where(eq(group.invite_id, inviteId)).limit(1); + // invite_id から group を取得 + const groupData = await db + .select({ + id: group.id, + }) + .from(group) + .where(eq(group.invite_id, body.invite_id)) + .limit(1); + + // invite_id が不正な場合はエラー if (groupData.length === 0) { - // validate invite_id - throw new HTTPException(401, { message: 'Invalid invite_id' }); + throw new HTTPException(400, { message: 'Bad Request' }); } - // add user to the group - const groupId = groupData[0].id; + // すでに参加している場合は即座に返スす + const existingMembership = await db + .select({ + groupMembershipId: groupMembership.id, + }) + .from(groupMembership) + .where(and(eq(groupMembership.groupId, groupData[0].id), eq(groupMembership.userId, user.id))) + .limit(1); + + if (existingMembership.length > 0) { + // レスポンス + return c.json( + { + group_id: groupData[0].id, + } satisfies JoinGroupResponseSchemaType, + 201 + ); + } + + // loginUser を invite_id の group に参加させる const result = await db .insert(groupMembership) .values({ id: crypto.randomUUID(), - groupId: groupId, + groupId: groupData[0].id, userId: user.id, - joinedAt: new Date(), + joinedAt: now, }) - .returning(); + .returning({ + groupId: groupMembership.groupId, + }); - // return response + // レスポンス return c.json( { group_id: result[0].groupId, @@ -211,8 +221,8 @@ const getGroupInfoSchema = route.createSchema( }, }, responses: { - 200: { - description: 'OK', + 201: { + description: 'Created', content: { 'application/json': { schema: getGroupInfoResponseSchema, @@ -225,106 +235,98 @@ const getGroupInfoSchema = route.createSchema( ); hono.openapi(getGroupInfoSchema, async (c) => { - c.status(201); const body = c.req.valid('json'); const loginUser = c.get('user'); - // NOTE: このvalidationは、groupのhistoryの取得のエンドポイントでも全く同じものを使うので、共通化を検討すること - // NOTE: --- 共通化開始 --- - // validation - if (!body || !body.group_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - const groupId = body.group_id; - - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // find me from the group (validation) + // NOTE: --- 共通化開始 --- + // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db - .select() + .select({ + groupMembershipId: groupMembership.id, + }) .from(groupMembership) - .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id))) + .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) .limit(1); + + // body.group_id のグループのメンバーでない場合はエラー if (me.length === 0) { - // validate membership - throw new HTTPException(401, { message: 'User is not a member of the group' }); + throw new HTTPException(400, { message: 'Bad Request' }); } // NOTE: --- 共通化終了 --- - //* get group info *// - const groupData = await db.select().from(group).where(eq(group.id, groupId)).limit(1); - if (groupData.length === 0) { - // validate group_id - throw new HTTPException(401, { message: 'Invalid group_id' }); - } + //* body.group_id のグループ情報を取得 *// + const groupData = await db + .select({ + name: group.name, + createdBy: group.createdBy, + }) + .from(group) + .where(eq(group.id, body.group_id)) + .limit(1); - //* get created_by member's name *// - const createdByUserId = groupData[0].createdBy; - let createdByUserName = ''; - // get user name from user table - const createdByUserInfo = await db.select().from(user).where(eq(user.id, createdByUserId)).limit(1); - if (createdByUserInfo.length === 0) { - throw new HTTPException(500, { message: 'The creator of the group was not found' }); - } - createdByUserName = createdByUserInfo[0].name; - // get display name from user_profile table - const createdByDisplayNameInfo = await db - .select() - .from(userProfile) - .where(eq(userProfile.userId, createdByUserId)) + //* body.group_id のグループ作成者情報を取得 *// + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const createdByUserNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, groupData[0].createdBy)) .limit(1); - if ( - createdByDisplayNameInfo.length > 0 && - typeof createdByDisplayNameInfo[0].displayName === 'string' && - createdByDisplayNameInfo[0].displayName.length > 0 - ) { - createdByUserName = createdByDisplayNameInfo[0].displayName; - } - //* get members *// - // get all member userIds from groupMembership table - const membersData = await db.select().from(groupMembership).where(eq(groupMembership.groupId, groupId)); - const memberUserIds = membersData.map((member) => member.userId); - // get user name or display name for each member + // NOTE: 共通化できそう (info.ts と group.ts で共通化) + // NOTE: --- 共通化開始 --- + //* body.group_id のメンバー情報を取得 *// + // グループメンバーのユーザーIDを取得 (group_membership table) + const memberUserIds = await db + .select({ + userId: groupMembership.userId, + }) + .from(groupMembership) + .where(eq(groupMembership.groupId, body.group_id)); + + // メンバー情報を格納する配列 const members: GetGroupInfoResponseMemberElementSchemaType[] = []; + for (const memberUserId of memberUserIds) { - // initialize user name - let userName = ''; - - // get user name from user table - const userNameInfo = await db.select().from(user).where(eq(user.id, memberUserId)).limit(1); - if (userNameInfo.length === 0) { - throw new HTTPException(500, { message: 'A group member was not found' }); - } - //set user name - userName = userNameInfo[0].name; - - // get display name from user_profile table - const displayNameInfo = await db.select().from(userProfile).where(eq(userProfile.userId, memberUserId)).limit(1); - if ( - displayNameInfo.length > 0 && - typeof displayNameInfo[0].displayName === 'string' && - displayNameInfo[0].displayName.length > 0 - ) { - userName = displayNameInfo[0].displayName; - } - - // push to members array + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const memberUserNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, memberUserId.userId)) + .limit(1); + + // メンバー情報を配列に追加 members.push({ - user_id: memberUserId, - user_name: userName, + user_id: memberUserId.userId, + user_name: + memberUserNameInfo[0].displayName !== null && memberUserNameInfo[0].displayName.length > 0 + ? memberUserNameInfo[0].displayName + : memberUserNameInfo[0].name, }); } + // NOTE: --- 共通化終了 --- // return response return c.json( { group_name: groupData[0].name, - created_by: createdByUserName, + created_by: + createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 + ? createdByUserNameInfo[0].displayName + : createdByUserNameInfo[0].name, members: members, } satisfies GetGroupInfoResponseSchemaType, - 200 + 201 ); }); @@ -346,8 +348,8 @@ const getGroupDebtHistorySchema = route.createSchema( }, }, responses: { - 200: { - description: 'OK', + 201: { + description: 'Created', content: { 'application/json': { schema: getGroupDebtHistoryResponseSchema, @@ -360,97 +362,91 @@ const getGroupDebtHistorySchema = route.createSchema( ); hono.openapi(getGroupDebtHistorySchema, async (c) => { - c.status(201); - const body = c.req.valid('json'); const loginUser = c.get('user'); + const body = c.req.valid('json'); - // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること - // NOTE: --- 共通化開始 --- - // validation - if (!body || !body.group_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - const groupId = body.group_id; - - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // find me from the group (validation) + // NOTE: --- 共通化開始 --- + // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db - .select() + .select({ + groupMembershipId: groupMembership.id, + }) .from(groupMembership) - .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id))) + .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) .limit(1); + + // body.group_id のグループのメンバーでない場合はエラー if (me.length === 0) { - // validate membership - throw new HTTPException(401, { message: 'User is not a member of the group' }); + throw new HTTPException(400, { message: 'Bad Request' }); } // NOTE: --- 共通化終了 --- - //* get each group debt info *// + //* グループの貸し借り履歴を取得 *// + // 貸し借り履歴を格納する配列 let debtData: GetGroupDebtHistoryResponseElementSchemaType[] = []; + + // body.group_id の貸し借り履歴を取得 (debt table) const rawDebtData = await db - .select() + .select({ + id: debt.id, + debtorId: debt.debtorId, + creditorId: debt.creditorId, + amount: debt.amount, + }) .from(debt) - .where(and(eq(debt.groupId, groupId), isNull(debt.deletedAt))); + .where(and(eq(debt.groupId, body.group_id), isNull(debt.deletedAt))); + for (const debtEntry of rawDebtData) { - // get debtor name from user table - const debtorInfo = await db.select().from(user).where(eq(user.id, debtEntry.debtorId)).limit(1); - if (debtorInfo.length === 0) { - throw new HTTPException(500, { message: 'A debtor user was not found' }); - } - let debtorName = debtorInfo[0].name; - // get display name from user_profile table - const debtorDisplayNameInfo = await db - .select() - .from(userProfile) - .where(eq(userProfile.userId, debtEntry.debtorId)) + // debt_name を取得 + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const DebtorNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, debtEntry.debtorId)) .limit(1); - if ( - debtorDisplayNameInfo.length > 0 && - typeof debtorDisplayNameInfo[0].displayName === 'string' && - debtorDisplayNameInfo[0].displayName.length > 0 - ) { - debtorName = debtorDisplayNameInfo[0].displayName; - } - - // get creditor name from user table - const creditorInfo = await db.select().from(user).where(eq(user.id, debtEntry.creditorId)).limit(1); - if (creditorInfo.length === 0) { - throw new HTTPException(500, { message: 'A creditor user was not found' }); - } - let creditorName = creditorInfo[0].name; - // get display name from user_profile table - const creditorDisplayNameInfo = await db - .select() - .from(userProfile) - .where(eq(userProfile.userId, debtEntry.creditorId)) + + // creditor_name を取得 + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const CreditorNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, debtEntry.creditorId)) .limit(1); - if ( - creditorDisplayNameInfo.length > 0 && - typeof creditorDisplayNameInfo[0].displayName === 'string' && - creditorDisplayNameInfo[0].displayName.length > 0 - ) { - creditorName = creditorDisplayNameInfo[0].displayName; - } - - // push to debtData array + + // 貸し借り履歴データを配列に追加 debtData.push({ debt_id: debtEntry.id, debtor_id: debtEntry.debtorId, - debtor_name: debtorName, + debtor_name: + DebtorNameInfo[0].displayName !== null && DebtorNameInfo[0].displayName.length > 0 + ? DebtorNameInfo[0].displayName + : DebtorNameInfo[0].name, creditor_id: debtEntry.creditorId, - creditor_name: creditorName, + creditor_name: + CreditorNameInfo[0].displayName !== null && CreditorNameInfo[0].displayName.length > 0 + ? CreditorNameInfo[0].displayName + : CreditorNameInfo[0].name, amount: debtEntry.amount, }); } - // return response + // レスポンス return c.json( { debts: debtData, } satisfies GetGroupDebtHistoryResponseSchemaType, - 200 + 201 ); }); @@ -486,49 +482,51 @@ const registerGroupDebtSchema = route.createSchema( ); hono.openapi(registerGroupDebtSchema, async (c) => { - c.status(201); - const body = c.req.valid('json'); const loginUser = c.get('user'); + const body = c.req.valid('json'); - // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること - // NOTE: --- 共通化開始 --- - // validation - if (!body || !body.group_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - const groupId = body.group_id; + // 現在時刻の取得 + const now = new Date(); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // find me from the group (validation) + // NOTE: --- 共通化開始 --- + // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db - .select() + .select({ + groupMembershipId: groupMembership.id, + }) .from(groupMembership) - .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id))) + .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) .limit(1); + + // body.group_id のグループのメンバーでない場合はエラー if (me.length === 0) { - // validate membership - throw new HTTPException(401, { message: 'User is not a member of the group' }); + throw new HTTPException(400, { message: 'Bad Request' }); } // NOTE: --- 共通化終了 --- - // register group debt entry + // body.group_id に貸し借り履歴を追加 (debt table) const result = await db .insert(debt) .values({ id: crypto.randomUUID(), - groupId: groupId, + groupId: body.group_id, creditorId: body.creditor_id, debtorId: body.debtor_id, amount: body.amount, - description: body.description ? body.description : null, - occurredAt: body.occurred_at ? new Date(body.occurred_at) : new Date(), - createdAt: new Date(), - updatedAt: new Date(), - deletedBy: null, + description: typeof body.description === 'undefined' ? null : body.description, + occurredAt: body.occurred_at ? new Date(body.occurred_at) : now, + createdAt: now, + updatedAt: now, }) - .returning(); + .returning({ + creditorId: debt.creditorId, + debtorId: debt.debtorId, + amount: debt.amount, + occurredAt: debt.occurredAt, + }); return c.json( { @@ -559,13 +557,8 @@ const deleteGroupDebtSchema = route.createSchema( }, }, responses: { - 200: { - description: 'Created', - content: { - 'application/json': { - schema: deleteGroupDebtResponseSchema, - }, - }, + 204: { + description: 'No Content', }, }, }, @@ -573,51 +566,43 @@ const deleteGroupDebtSchema = route.createSchema( ); hono.openapi(deleteGroupDebtSchema, async (c) => { - c.status(200); - const body = c.req.valid('json'); const loginUser = c.get('user'); + const body = c.req.valid('json'); - // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること - // NOTE: --- 共通化開始 --- - // validation - if (!body || !body.group_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - const groupId = body.group_id; + // 現在時刻の取得 + const now = new Date(); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // find me from the group (validation) + // NOTE: --- 共通化開始 --- + // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db - .select() + .select({ + groupMembershipId: groupMembership.id, + }) .from(groupMembership) - .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id))) + .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) .limit(1); + + // body.group_id のグループのメンバーでない場合はエラー if (me.length === 0) { - // validate membership - throw new HTTPException(401, { message: 'User is not a member of the group' }); + throw new HTTPException(400, { message: 'Bad Request' }); } // NOTE: --- 共通化終了 --- - const now = new Date(); - // delete group debt entry (soft delete) - const result = await db + //* body.debt_id の貸し借りの履歴の削除 (論理削除) *// + await db .update(debt) .set({ deletedBy: loginUser.id, deletedAt: now, updatedAt: now, }) - .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, groupId))) - .returning(); + .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, body.group_id), isNull(debt.deletedAt))); - return c.json( - { - debt_id: result[0].id, - } satisfies DeleteGroupDebtResponseSchemaType, - 200 - ); + // レスポンス + return c.body(null, 204); }); export default hono; diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 0634a47..6a7452c 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -1,5 +1,4 @@ // hono instance -import { HTTPException } from 'hono/http-exception'; import honoFactory from '../factory/hono'; // validator import { @@ -11,8 +10,6 @@ import { type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, type InfoAboutGroupsTheUserBelongsToResponseSchemaType, deleteInfoAboutUserRepaymentRequestSchema, - deleteInfoAboutUserRepaymentResponseSchema, - DeleteInfoAboutUserRepaymentResponseSchemaType, } from 'validator'; // error schema import { route } from '../share/error'; @@ -20,7 +17,9 @@ import { route } from '../share/error'; import { drizzle } from 'drizzle-orm/node-postgres'; import { and, or, eq, inArray, isNull } from 'drizzle-orm'; import { group, debt, groupMembership } from '../../db/pay-crew2-schema'; -import { user, userProfile } from '../../db/auth-schema'; +import { user } from '../../db/auth-schema'; +// types +import { TransactionType } from '../utils/types'; const hono = honoFactory(); @@ -29,7 +28,7 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( { path: '/api/info/group', method: 'get', - description: 'get info about groups the user belongs to', + description: 'ログインユーザーが参加しているグループ一覧を取得するエンドポイント', security: [{ SessionCookie: [] }], request: {}, responses: { @@ -49,11 +48,13 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => { const loginUser = c.get('user'); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - //* get group info that belongs to the user *// + // グループ情報格納用配列 let groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = []; + + //* ユーザが参加しているグループ情報を取得 (group table) *// const groupsData = await db .select({ id: group.id, @@ -72,50 +73,68 @@ hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => { ); for (const groupData of groupsData) { - //* get members *// - // get all member userIds from groupMembership table - const membersData = await db.select().from(groupMembership).where(eq(groupMembership.groupId, groupData.id)); - const memberUserIds = membersData.map((member) => member.userId); - // get user name or display name for each member + //* body.group_id のグループ作成者情報を取得 *// + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const createdByUserNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, groupData.createdBy)) + .limit(1); + + // NOTE: --- 共通化開始 --- + //* groupData.id のメンバー情報を取得 *// + // グループメンバーのユーザーIDを取得 (group_membership table) + const memberUserIds = await db + .select({ + userId: groupMembership.userId, + }) + .from(groupMembership) + .where(eq(groupMembership.groupId, groupData.id)); + + // メンバー情報を格納する配列 const members: InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] = []; + for (const memberUserId of memberUserIds) { - // initialize user name - let userName = ''; - - // get user name from user table - const userNameInfo = await db.select().from(user).where(eq(user.id, memberUserId)).limit(1); - if (userNameInfo.length === 0) { - throw new HTTPException(500, { message: 'A group member was not found' }); - } - //set user name - userName = userNameInfo[0].name; - - // get display name from user_profile table - const displayNameInfo = await db.select().from(userProfile).where(eq(userProfile.userId, memberUserId)).limit(1); - if ( - displayNameInfo.length > 0 && - typeof displayNameInfo[0].displayName === 'string' && - displayNameInfo[0].displayName.length > 0 - ) { - userName = displayNameInfo[0].displayName; - } - - // push to members array + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const userNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, memberUserId.userId)) + .limit(1); + + // メンバー情報を配列に追加 members.push({ - user_id: memberUserId, - user_name: userName, + user_id: memberUserId.userId, + user_name: + userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 + ? userNameInfo[0].displayName + : userNameInfo[0].name, }); } - // push to groupInfo array + // NOTE: --- 共通化終了 --- + + // グループ情報を配列に追加 groupInfo.push({ group_id: groupData.id, group_name: groupData.name, - created_by: groupData.createdBy, + created_by_id: groupData.createdBy, + created_by_name: + createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 + ? createdByUserNameInfo[0].displayName + : createdByUserNameInfo[0].name, members: members, }); } - // return response + // レスポンス return c.json( { groups: groupInfo, @@ -129,7 +148,7 @@ const infoAboutUserTransactionsSchema = route.createSchema( { path: '/api/info/transaction', method: 'get', - description: 'get info about user transactions', + description: 'ログインユーザーの貸し借りの履歴を取得するエンドポイント', security: [{ SessionCookie: [] }], request: {}, responses: { @@ -146,39 +165,40 @@ const infoAboutUserTransactionsSchema = route.createSchema( [401, 500] as const ); -type TransactionType = { - user_id: string; // 取引相手のユーザID - lent_amount: number; // 貸した金額 - borrowed_amount: number; // 借りた金額 -}; - hono.openapi(infoAboutUserTransactionsSchema, async (c) => { const loginUser = c.get('user'); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - // loginUserが貸している取引履歴を取得 + //* loginUserが貸している取引履歴を取得 *// // マイナス n 円 const creditorTransactions = await db - .select() + .select({ + debtorId: debt.debtorId, + amount: debt.amount, + }) .from(debt) .where(and(eq(debt.creditorId, loginUser.id), isNull(debt.deletedAt))); - // loginUserが借りている取引履歴を取得 + //* loginUserが借りている取引履歴を取得 *// // プラス n 円 const debtorTransactions = await db - .select() + .select({ + creditorId: debt.creditorId, + amount: debt.amount, + }) .from(debt) .where(and(eq(debt.debtorId, loginUser.id), isNull(debt.deletedAt))); - // 取引相手ごとに集計 + //* 取引相手ごとに集計 *// + // 集計結果を格納するMap const transactions: Map = new Map(); // 貸している取引を集計 - for (const transaction of creditorTransactions) { - const userId = transaction.debtorId; - const amount = transaction.amount; + for (const creditorTransaction of creditorTransactions) { + const userId = creditorTransaction.debtorId; + const amount = creditorTransaction.amount; if (!transactions.has(userId)) { transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); } @@ -187,9 +207,9 @@ hono.openapi(infoAboutUserTransactionsSchema, async (c) => { } // 借りている取引を集計 - for (const transaction of debtorTransactions) { - const userId = transaction.creditorId; - const amount = transaction.amount; + for (const debtorTransaction of debtorTransactions) { + const userId = debtorTransaction.creditorId; + const amount = debtorTransaction.amount; if (!transactions.has(userId)) { transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); } @@ -197,38 +217,33 @@ hono.openapi(infoAboutUserTransactionsSchema, async (c) => { existing.borrowed_amount += amount; } - // netAmount の計算 + // 集計結果を格納する配列 const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = []; + + // 貸し借りの合算 for (const transaction of transactions.values()) { - // calculate net amount: netAmount = borrowed_amount - lent_amount + // 合算結果: netAmount = borrowed_amount - lent_amount const netAmount = transaction.borrowed_amount - transaction.lent_amount; - let userName = ''; - // get user name from user table - const userInfo = await db.select().from(user).where(eq(user.id, transaction.user_id)).limit(1); - if (userInfo.length === 0) { - throw new HTTPException(500, { message: 'A transaction counterparty was not found' }); - } - userName = userInfo[0].name; - // get display name from user_profile table - const displayNameInfo = await db - .select() - .from(userProfile) - .where(eq(userProfile.userId, transaction.user_id)) + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const userNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, transaction.user_id)) .limit(1); - if ( - displayNameInfo.length > 0 && - typeof displayNameInfo[0].displayName === 'string' && - displayNameInfo[0].displayName.length > 0 - ) { - userName = displayNameInfo[0].displayName; - } - // push to aggregatedTransactions array if netAmount is not zero + // netAmount が 0 でない場合のみ配列に追加 if (netAmount !== 0) { aggregatedTransactions.push({ counterparty_id: transaction.user_id, - counterparty_name: userName, + counterparty_name: + userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 + ? userNameInfo[0].displayName + : userNameInfo[0].name, amount: netAmount, }); } @@ -248,7 +263,7 @@ const infoUserRepaymentSchema = route.createSchema( { path: '/api/info/transaction', method: 'delete', - description: 'do user repayment', + description: 'ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント', security: [{ SessionCookie: [] }], request: { body: { @@ -261,34 +276,24 @@ const infoUserRepaymentSchema = route.createSchema( }, }, responses: { - 200: { - description: 'Created', - content: { - 'application/json': { - schema: deleteInfoAboutUserRepaymentResponseSchema, - }, - }, + 204: { + description: 'No Content', }, }, }, - [400, 401, 500] as const + [401, 500] as const ); hono.openapi(infoUserRepaymentSchema, async (c) => { - const body = c.req.valid('json'); const loginUser = c.get('user'); + const body = c.req.valid('json'); - // validation - if (!body || !body.counterparty_id) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); const now = new Date(); // loginUser <-> counterparty_id 間の取引履歴を削除(deletedAt, deletedBy をセット) - const creditorTransactions = await db + await db .update(debt) .set({ deletedBy: loginUser.id, @@ -300,16 +305,10 @@ hono.openapi(infoUserRepaymentSchema, async (c) => { and(eq(debt.creditorId, loginUser.id), eq(debt.debtorId, body.counterparty_id), isNull(debt.deletedAt)), and(eq(debt.debtorId, loginUser.id), eq(debt.creditorId, body.counterparty_id), isNull(debt.deletedAt)) ) - ) - .returning(); + ); - // return response - return c.json( - { - counterparty_id: body.counterparty_id, - } satisfies DeleteInfoAboutUserRepaymentResponseSchemaType, - 200 - ); + // レスポンス + return c.body(null, 204); }); export default hono; diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index 5fa3170..912d9ae 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -1,20 +1,17 @@ // hono instance -import { HTTPException } from 'hono/http-exception'; import honoFactory from '../factory/hono'; // validator import { getUserProfileResponseSchema, type GetUserProfileResponseSchemaType, updateUserProfileRequestSchema, - updateUserProfileResponseSchema, - type UpdateUserProfileResponseSchemaType, } from 'validator'; // error schema import { route } from '../share/error'; // drizzle import { drizzle } from 'drizzle-orm/node-postgres'; import { eq } from 'drizzle-orm'; -import { userProfile } from '../../db/auth-schema'; +import { user } from '../../db/auth-schema'; const hono = honoFactory(); @@ -23,7 +20,7 @@ const getUserProfile = route.createSchema( { path: '/api/profile', method: 'get', - description: 'get user profile', + description: 'ログインユーザーのプロフィール情報を取得するエンドポイント', security: [{ SessionCookie: [] }], request: {}, responses: { @@ -37,39 +34,28 @@ const getUserProfile = route.createSchema( }, }, }, - [400, 401, 500] as const + [401, 500] as const ); hono.openapi(getUserProfile, async (c) => { - const user = c.get('user'); + const loginUser = c.get('user'); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - //* get user profile *// - const userProfileData = await db - .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio }) - .from(userProfile) - .where(eq(userProfile.userId, user.id)) + //* ユーザ情報を取得 (user table) *// + const userData = await db + .select({ display_name: user.displayName, avatar_url: user.avatarUrl, bio: user.bio }) + .from(user) + .where(eq(user.id, loginUser.id)) .limit(1); - if (userProfileData.length === 0) { - return c.json( - { - display_name: '', - avatar_url: '', - bio: '', - } satisfies GetUserProfileResponseSchemaType, - 200 - ); - } - - // return response + // レスポンス return c.json( { - display_name: userProfileData[0].display_name === null ? '' : userProfileData[0].display_name, - avatar_url: userProfileData[0].avatar_url === null ? '' : userProfileData[0].avatar_url, - bio: userProfileData[0].bio === null ? '' : userProfileData[0].bio, + display_name: userData[0].display_name === null ? '' : userData[0].display_name, + avatar_url: userData[0].avatar_url === null ? '' : userData[0].avatar_url, + bio: userData[0].bio === null ? '' : userData[0].bio, } satisfies GetUserProfileResponseSchemaType, 200 ); @@ -79,8 +65,8 @@ hono.openapi(getUserProfile, async (c) => { const updateUserProfile = route.createSchema( { path: '/api/profile', - method: 'put', - description: 'update user profile', + method: 'patch', + description: 'ログインユーザーのプロフィール情報を更新するエンドポイント', security: [{ SessionCookie: [] }], request: { body: { @@ -93,78 +79,33 @@ const updateUserProfile = route.createSchema( }, }, responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: updateUserProfileResponseSchema, - }, - }, + 204: { + description: 'No Content', }, }, }, - [400, 401, 500] as const + [401, 500] as const ); hono.openapi(updateUserProfile, async (c) => { - const user = c.get('user'); + const loginUser = c.get('user'); const body = c.req.valid('json'); - // connect to db + // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); - //* check user profile exists *// - const isUserProfileData = await db - .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio }) - .from(userProfile) - .where(eq(userProfile.userId, user.id)) - .limit(1); - - if (isUserProfileData.length === 0) { - //* insert empty profile *// - const result = await db.insert(userProfile).values({ - userId: user.id, + //* ユーザ情報を更新 (user table) *// + await db + .update(user) + .set({ displayName: typeof body.display_name === 'undefined' ? null : body.display_name, avatarUrl: typeof body.avatar_url === 'undefined' ? null : body.avatar_url, bio: typeof body.bio === 'undefined' ? null : body.bio, - }); + }) + .where(eq(user.id, loginUser.id)); - console.log('Update Result:', result); - } else { - //* update user profile *// - const result = await db - .update(userProfile) - .set({ - displayName: typeof body.display_name === 'undefined' ? null : body.display_name, - avatarUrl: typeof body.avatar_url === 'undefined' ? null : body.avatar_url, - bio: typeof body.bio === 'undefined' ? null : body.bio, - }) - .where(eq(userProfile.userId, user.id)); - - console.log('Update Result:', result); - } - - //* get updated user profile *// - const userProfileData = await db - .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio }) - .from(userProfile) - .where(eq(userProfile.userId, user.id)) - .limit(1); - - // validation - if (userProfileData.length === 0) { - throw new HTTPException(500, { message: 'Failed to retrieve updated user profile' }); - } - - // return response - return c.json( - { - display_name: userProfileData[0].display_name === null ? '' : userProfileData[0].display_name, - avatar_url: userProfileData[0].avatar_url === null ? '' : userProfileData[0].avatar_url, - bio: userProfileData[0].bio === null ? '' : userProfileData[0].bio, - } satisfies UpdateUserProfileResponseSchemaType, - 200 - ); + // レスポンス + return c.body(null, 204); }); export default hono; diff --git a/products/backend/src/presentation/utils/types.ts b/products/backend/src/presentation/utils/types.ts new file mode 100644 index 0000000..b853b82 --- /dev/null +++ b/products/backend/src/presentation/utils/types.ts @@ -0,0 +1,5 @@ +export type TransactionType = { + user_id: string; // 取引相手のユーザID + lent_amount: number; // 貸した金額 + borrowed_amount: number; // 借りた金額 +}; diff --git a/products/frontend/.env.example b/products/frontend/.env.example index 59a578e..0419fb2 100644 --- a/products/frontend/.env.example +++ b/products/frontend/.env.example @@ -1,2 +1,3 @@ VITE_API_URL= +VITE_CLIENT_URL= VITE_REDIRECT_URL= diff --git a/products/frontend/openapi.json b/products/frontend/openapi.json index 0789491..f8b95d0 100644 --- a/products/frontend/openapi.json +++ b/products/frontend/openapi.json @@ -1,10 +1,5 @@ { "openapi": "3.1.0", - "security": [ - { - "SessionCookie": [] - } - ], "info": { "title": "Pay Crew2 API", "version": "0.0.1", @@ -24,7 +19,7 @@ "paths": { "/api/info/group": { "get": { - "description": "get info about groups the user belongs to", + "description": "ログインユーザーが参加しているグループ一覧を取得するエンドポイント", "security": [ { "SessionCookie": [] @@ -51,7 +46,11 @@ "type": "string", "minLength": 1 }, - "created_by": { + "created_by_id": { + "type": "string", + "minLength": 1 + }, + "created_by_name": { "type": "string", "minLength": 1 }, @@ -79,7 +78,8 @@ "required": [ "group_id", "group_name", - "created_by", + "created_by_id", + "created_by_name", "members" ] } @@ -873,7 +873,7 @@ }, "/api/info/transaction": { "get": { - "description": "get info about user transactions", + "description": "ログインユーザーの貸し借りの履歴を取得するエンドポイント", "security": [ { "SessionCookie": [] @@ -1699,7 +1699,7 @@ } }, "delete": { - "description": "do user repayment", + "description": "ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント", "security": [ { "SessionCookie": [] @@ -1725,27 +1725,11 @@ } }, "responses": { - "200": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "counterparty_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "counterparty_id" - ] - } - } - } + "204": { + "description": "No Content" }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2132,8 +2116,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -2519,9 +2503,64 @@ } } } + } + } + } + }, + "/api/group/create": { + "post": { + "description": "グループを新規作成するエンドポイント", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "group_name" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "format": "uuid" + }, + "invite_id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "group_id", + "invite_id" + ] + } + } + } }, - "500": { - "description": "Internal Server Error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -2907,64 +2946,9 @@ } } } - } - } - } - }, - "/api/group/create": { - "post": { - "description": "create a new group", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "name" - ] - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "format": "uuid" - }, - "invite_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "group_id", - "invite_id" - ] - } - } - } }, - "400": { - "description": "Bad Request", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -3350,9 +3334,59 @@ } } } + } + } + } + }, + "/api/group/join": { + "post": { + "description": "招待IDを使ってグループに参加するエンドポイント", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "invite_id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "invite_id" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "group_id" + ] + } + } + } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -3739,8 +3773,8 @@ } } }, - "500": { - "description": "Internal Server Error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4126,59 +4160,9 @@ } } } - } - } - } - }, - "/api/group/join": { - "post": { - "description": "join an existing group", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "invite_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "invite_id" - ] - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "group_id" - ] - } - } - } }, - "400": { - "description": "Bad Request", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -4564,12 +4548,88 @@ } } } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { + } + } + } + }, + "/api/group/info": { + "post": { + "description": "get group information", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "group_id" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_name": { + "type": "string", + "format": "uuid" + }, + "created_by": { + "type": "string", + "minLength": 1 + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "minLength": 1 + }, + "user_name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "user_id", + "user_name" + ] + } + } + }, + "required": [ + "group_name", + "created_by", + "members" + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "type": "object", "properties": { "status": { @@ -4953,8 +5013,8 @@ } } }, - "500": { - "description": "Internal Server Error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5340,85 +5400,9 @@ } } } - } - } - } - }, - "/api/group/info": { - "post": { - "description": "get group information", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "group_id" - ] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_name": { - "type": "string", - "format": "uuid" - }, - "created_by": { - "type": "string", - "minLength": 1 - }, - "members": { - "type": "array", - "items": { - "type": "object", - "properties": { - "user_id": { - "type": "string", - "minLength": 1 - }, - "user_name": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "user_id", - "user_name" - ] - } - } - }, - "required": [ - "group_name", - "created_by", - "members" - ] - } - } - } }, - "400": { - "description": "Bad Request", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -5804,9 +5788,95 @@ } } } + } + } + } + }, + "/api/group/debt/history": { + "post": { + "description": "get group debt history", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "group_id" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "debts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "debt_id": { + "type": "string", + "minLength": 1 + }, + "debtor_id": { + "type": "string", + "minLength": 1 + }, + "debtor_name": { + "type": "string", + "minLength": 1 + }, + "creditor_id": { + "type": "string", + "minLength": 1 + }, + "creditor_name": { + "type": "string", + "minLength": 1 + }, + "amount": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "debt_id", + "debtor_id", + "debtor_name", + "creditor_id", + "creditor_name", + "amount" + ] + } + } + }, + "required": [ + "debts" + ] + } + } + } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -6193,1732 +6263,8 @@ } } }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "anyOf": [ - { - "type": "number", - "enum": [ - 100 - ] - }, - { - "type": "number", - "enum": [ - 102 - ] - }, - { - "type": "number", - "enum": [ - 103 - ] - }, - { - "type": "number", - "enum": [ - 200 - ] - }, - { - "type": "number", - "enum": [ - 201 - ] - }, - { - "type": "number", - "enum": [ - 202 - ] - }, - { - "type": "number", - "enum": [ - 203 - ] - }, - { - "type": "number", - "enum": [ - 206 - ] - }, - { - "type": "number", - "enum": [ - 207 - ] - }, - { - "type": "number", - "enum": [ - 208 - ] - }, - { - "type": "number", - "enum": [ - 226 - ] - }, - { - "type": "number", - "enum": [ - 300 - ] - }, - { - "type": "number", - "enum": [ - 301 - ] - }, - { - "type": "number", - "enum": [ - 302 - ] - }, - { - "type": "number", - "enum": [ - 303 - ] - }, - { - "type": "number", - "enum": [ - 305 - ] - }, - { - "type": "number", - "enum": [ - 306 - ] - }, - { - "type": "number", - "enum": [ - 307 - ] - }, - { - "type": "number", - "enum": [ - 308 - ] - }, - { - "type": "number", - "enum": [ - 400 - ] - }, - { - "type": "number", - "enum": [ - 401 - ] - }, - { - "type": "number", - "enum": [ - 402 - ] - }, - { - "type": "number", - "enum": [ - 403 - ] - }, - { - "type": "number", - "enum": [ - 404 - ] - }, - { - "type": "number", - "enum": [ - 405 - ] - }, - { - "type": "number", - "enum": [ - 406 - ] - }, - { - "type": "number", - "enum": [ - 407 - ] - }, - { - "type": "number", - "enum": [ - 408 - ] - }, - { - "type": "number", - "enum": [ - 409 - ] - }, - { - "type": "number", - "enum": [ - 410 - ] - }, - { - "type": "number", - "enum": [ - 411 - ] - }, - { - "type": "number", - "enum": [ - 412 - ] - }, - { - "type": "number", - "enum": [ - 413 - ] - }, - { - "type": "number", - "enum": [ - 414 - ] - }, - { - "type": "number", - "enum": [ - 415 - ] - }, - { - "type": "number", - "enum": [ - 416 - ] - }, - { - "type": "number", - "enum": [ - 417 - ] - }, - { - "type": "number", - "enum": [ - 418 - ] - }, - { - "type": "number", - "enum": [ - 421 - ] - }, - { - "type": "number", - "enum": [ - 422 - ] - }, - { - "type": "number", - "enum": [ - 423 - ] - }, - { - "type": "number", - "enum": [ - 424 - ] - }, - { - "type": "number", - "enum": [ - 425 - ] - }, - { - "type": "number", - "enum": [ - 426 - ] - }, - { - "type": "number", - "enum": [ - 428 - ] - }, - { - "type": "number", - "enum": [ - 429 - ] - }, - { - "type": "number", - "enum": [ - 431 - ] - }, - { - "type": "number", - "enum": [ - 451 - ] - }, - { - "type": "number", - "enum": [ - 500 - ] - }, - { - "type": "number", - "enum": [ - 501 - ] - }, - { - "type": "number", - "enum": [ - 502 - ] - }, - { - "type": "number", - "enum": [ - 503 - ] - }, - { - "type": "number", - "enum": [ - 504 - ] - }, - { - "type": "number", - "enum": [ - 505 - ] - }, - { - "type": "number", - "enum": [ - 506 - ] - }, - { - "type": "number", - "enum": [ - 507 - ] - }, - { - "type": "number", - "enum": [ - 508 - ] - }, - { - "type": "number", - "enum": [ - 510 - ] - }, - { - "type": "number", - "enum": [ - 511 - ] - }, - { - "type": "number", - "enum": [ - -1 - ] - } - ], - "example": 400, - "description": "HTTPステータスコード" - }, - "message": { - "type": "string", - "minLength": 1, - "example": "Bad Request", - "description": "エラーメッセージ" - } - }, - "required": [ - "status", - "message" - ] - } - } - } - } - } - } - }, - "/api/group/debt/history": { - "post": { - "description": "get group debt history", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "group_id" - ] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "debts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "debt_id": { - "type": "string", - "minLength": 1 - }, - "debtor_id": { - "type": "string", - "minLength": 1 - }, - "debtor_name": { - "type": "string", - "minLength": 1 - }, - "creditor_id": { - "type": "string", - "minLength": 1 - }, - "creditor_name": { - "type": "string", - "minLength": 1 - }, - "amount": { - "type": "number", - "minimum": 0 - } - }, - "required": [ - "debt_id", - "debtor_id", - "debtor_name", - "creditor_id", - "creditor_name", - "amount" - ] - } - } - }, - "required": [ - "debts" - ] - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "anyOf": [ - { - "type": "number", - "enum": [ - 100 - ] - }, - { - "type": "number", - "enum": [ - 102 - ] - }, - { - "type": "number", - "enum": [ - 103 - ] - }, - { - "type": "number", - "enum": [ - 200 - ] - }, - { - "type": "number", - "enum": [ - 201 - ] - }, - { - "type": "number", - "enum": [ - 202 - ] - }, - { - "type": "number", - "enum": [ - 203 - ] - }, - { - "type": "number", - "enum": [ - 206 - ] - }, - { - "type": "number", - "enum": [ - 207 - ] - }, - { - "type": "number", - "enum": [ - 208 - ] - }, - { - "type": "number", - "enum": [ - 226 - ] - }, - { - "type": "number", - "enum": [ - 300 - ] - }, - { - "type": "number", - "enum": [ - 301 - ] - }, - { - "type": "number", - "enum": [ - 302 - ] - }, - { - "type": "number", - "enum": [ - 303 - ] - }, - { - "type": "number", - "enum": [ - 305 - ] - }, - { - "type": "number", - "enum": [ - 306 - ] - }, - { - "type": "number", - "enum": [ - 307 - ] - }, - { - "type": "number", - "enum": [ - 308 - ] - }, - { - "type": "number", - "enum": [ - 400 - ] - }, - { - "type": "number", - "enum": [ - 401 - ] - }, - { - "type": "number", - "enum": [ - 402 - ] - }, - { - "type": "number", - "enum": [ - 403 - ] - }, - { - "type": "number", - "enum": [ - 404 - ] - }, - { - "type": "number", - "enum": [ - 405 - ] - }, - { - "type": "number", - "enum": [ - 406 - ] - }, - { - "type": "number", - "enum": [ - 407 - ] - }, - { - "type": "number", - "enum": [ - 408 - ] - }, - { - "type": "number", - "enum": [ - 409 - ] - }, - { - "type": "number", - "enum": [ - 410 - ] - }, - { - "type": "number", - "enum": [ - 411 - ] - }, - { - "type": "number", - "enum": [ - 412 - ] - }, - { - "type": "number", - "enum": [ - 413 - ] - }, - { - "type": "number", - "enum": [ - 414 - ] - }, - { - "type": "number", - "enum": [ - 415 - ] - }, - { - "type": "number", - "enum": [ - 416 - ] - }, - { - "type": "number", - "enum": [ - 417 - ] - }, - { - "type": "number", - "enum": [ - 418 - ] - }, - { - "type": "number", - "enum": [ - 421 - ] - }, - { - "type": "number", - "enum": [ - 422 - ] - }, - { - "type": "number", - "enum": [ - 423 - ] - }, - { - "type": "number", - "enum": [ - 424 - ] - }, - { - "type": "number", - "enum": [ - 425 - ] - }, - { - "type": "number", - "enum": [ - 426 - ] - }, - { - "type": "number", - "enum": [ - 428 - ] - }, - { - "type": "number", - "enum": [ - 429 - ] - }, - { - "type": "number", - "enum": [ - 431 - ] - }, - { - "type": "number", - "enum": [ - 451 - ] - }, - { - "type": "number", - "enum": [ - 500 - ] - }, - { - "type": "number", - "enum": [ - 501 - ] - }, - { - "type": "number", - "enum": [ - 502 - ] - }, - { - "type": "number", - "enum": [ - 503 - ] - }, - { - "type": "number", - "enum": [ - 504 - ] - }, - { - "type": "number", - "enum": [ - 505 - ] - }, - { - "type": "number", - "enum": [ - 506 - ] - }, - { - "type": "number", - "enum": [ - 507 - ] - }, - { - "type": "number", - "enum": [ - 508 - ] - }, - { - "type": "number", - "enum": [ - 510 - ] - }, - { - "type": "number", - "enum": [ - 511 - ] - }, - { - "type": "number", - "enum": [ - -1 - ] - } - ], - "example": 400, - "description": "HTTPステータスコード" - }, - "message": { - "type": "string", - "minLength": 1, - "example": "Bad Request", - "description": "エラーメッセージ" - } - }, - "required": [ - "status", - "message" - ] - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "anyOf": [ - { - "type": "number", - "enum": [ - 100 - ] - }, - { - "type": "number", - "enum": [ - 102 - ] - }, - { - "type": "number", - "enum": [ - 103 - ] - }, - { - "type": "number", - "enum": [ - 200 - ] - }, - { - "type": "number", - "enum": [ - 201 - ] - }, - { - "type": "number", - "enum": [ - 202 - ] - }, - { - "type": "number", - "enum": [ - 203 - ] - }, - { - "type": "number", - "enum": [ - 206 - ] - }, - { - "type": "number", - "enum": [ - 207 - ] - }, - { - "type": "number", - "enum": [ - 208 - ] - }, - { - "type": "number", - "enum": [ - 226 - ] - }, - { - "type": "number", - "enum": [ - 300 - ] - }, - { - "type": "number", - "enum": [ - 301 - ] - }, - { - "type": "number", - "enum": [ - 302 - ] - }, - { - "type": "number", - "enum": [ - 303 - ] - }, - { - "type": "number", - "enum": [ - 305 - ] - }, - { - "type": "number", - "enum": [ - 306 - ] - }, - { - "type": "number", - "enum": [ - 307 - ] - }, - { - "type": "number", - "enum": [ - 308 - ] - }, - { - "type": "number", - "enum": [ - 400 - ] - }, - { - "type": "number", - "enum": [ - 401 - ] - }, - { - "type": "number", - "enum": [ - 402 - ] - }, - { - "type": "number", - "enum": [ - 403 - ] - }, - { - "type": "number", - "enum": [ - 404 - ] - }, - { - "type": "number", - "enum": [ - 405 - ] - }, - { - "type": "number", - "enum": [ - 406 - ] - }, - { - "type": "number", - "enum": [ - 407 - ] - }, - { - "type": "number", - "enum": [ - 408 - ] - }, - { - "type": "number", - "enum": [ - 409 - ] - }, - { - "type": "number", - "enum": [ - 410 - ] - }, - { - "type": "number", - "enum": [ - 411 - ] - }, - { - "type": "number", - "enum": [ - 412 - ] - }, - { - "type": "number", - "enum": [ - 413 - ] - }, - { - "type": "number", - "enum": [ - 414 - ] - }, - { - "type": "number", - "enum": [ - 415 - ] - }, - { - "type": "number", - "enum": [ - 416 - ] - }, - { - "type": "number", - "enum": [ - 417 - ] - }, - { - "type": "number", - "enum": [ - 418 - ] - }, - { - "type": "number", - "enum": [ - 421 - ] - }, - { - "type": "number", - "enum": [ - 422 - ] - }, - { - "type": "number", - "enum": [ - 423 - ] - }, - { - "type": "number", - "enum": [ - 424 - ] - }, - { - "type": "number", - "enum": [ - 425 - ] - }, - { - "type": "number", - "enum": [ - 426 - ] - }, - { - "type": "number", - "enum": [ - 428 - ] - }, - { - "type": "number", - "enum": [ - 429 - ] - }, - { - "type": "number", - "enum": [ - 431 - ] - }, - { - "type": "number", - "enum": [ - 451 - ] - }, - { - "type": "number", - "enum": [ - 500 - ] - }, - { - "type": "number", - "enum": [ - 501 - ] - }, - { - "type": "number", - "enum": [ - 502 - ] - }, - { - "type": "number", - "enum": [ - 503 - ] - }, - { - "type": "number", - "enum": [ - 504 - ] - }, - { - "type": "number", - "enum": [ - 505 - ] - }, - { - "type": "number", - "enum": [ - 506 - ] - }, - { - "type": "number", - "enum": [ - 507 - ] - }, - { - "type": "number", - "enum": [ - 508 - ] - }, - { - "type": "number", - "enum": [ - 510 - ] - }, - { - "type": "number", - "enum": [ - 511 - ] - }, - { - "type": "number", - "enum": [ - -1 - ] - } - ], - "example": 400, - "description": "HTTPステータスコード" - }, - "message": { - "type": "string", - "minLength": 1, - "example": "Bad Request", - "description": "エラーメッセージ" - } - }, - "required": [ - "status", - "message" - ] - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "anyOf": [ - { - "type": "number", - "enum": [ - 100 - ] - }, - { - "type": "number", - "enum": [ - 102 - ] - }, - { - "type": "number", - "enum": [ - 103 - ] - }, - { - "type": "number", - "enum": [ - 200 - ] - }, - { - "type": "number", - "enum": [ - 201 - ] - }, - { - "type": "number", - "enum": [ - 202 - ] - }, - { - "type": "number", - "enum": [ - 203 - ] - }, - { - "type": "number", - "enum": [ - 206 - ] - }, - { - "type": "number", - "enum": [ - 207 - ] - }, - { - "type": "number", - "enum": [ - 208 - ] - }, - { - "type": "number", - "enum": [ - 226 - ] - }, - { - "type": "number", - "enum": [ - 300 - ] - }, - { - "type": "number", - "enum": [ - 301 - ] - }, - { - "type": "number", - "enum": [ - 302 - ] - }, - { - "type": "number", - "enum": [ - 303 - ] - }, - { - "type": "number", - "enum": [ - 305 - ] - }, - { - "type": "number", - "enum": [ - 306 - ] - }, - { - "type": "number", - "enum": [ - 307 - ] - }, - { - "type": "number", - "enum": [ - 308 - ] - }, - { - "type": "number", - "enum": [ - 400 - ] - }, - { - "type": "number", - "enum": [ - 401 - ] - }, - { - "type": "number", - "enum": [ - 402 - ] - }, - { - "type": "number", - "enum": [ - 403 - ] - }, - { - "type": "number", - "enum": [ - 404 - ] - }, - { - "type": "number", - "enum": [ - 405 - ] - }, - { - "type": "number", - "enum": [ - 406 - ] - }, - { - "type": "number", - "enum": [ - 407 - ] - }, - { - "type": "number", - "enum": [ - 408 - ] - }, - { - "type": "number", - "enum": [ - 409 - ] - }, - { - "type": "number", - "enum": [ - 410 - ] - }, - { - "type": "number", - "enum": [ - 411 - ] - }, - { - "type": "number", - "enum": [ - 412 - ] - }, - { - "type": "number", - "enum": [ - 413 - ] - }, - { - "type": "number", - "enum": [ - 414 - ] - }, - { - "type": "number", - "enum": [ - 415 - ] - }, - { - "type": "number", - "enum": [ - 416 - ] - }, - { - "type": "number", - "enum": [ - 417 - ] - }, - { - "type": "number", - "enum": [ - 418 - ] - }, - { - "type": "number", - "enum": [ - 421 - ] - }, - { - "type": "number", - "enum": [ - 422 - ] - }, - { - "type": "number", - "enum": [ - 423 - ] - }, - { - "type": "number", - "enum": [ - 424 - ] - }, - { - "type": "number", - "enum": [ - 425 - ] - }, - { - "type": "number", - "enum": [ - 426 - ] - }, - { - "type": "number", - "enum": [ - 428 - ] - }, - { - "type": "number", - "enum": [ - 429 - ] - }, - { - "type": "number", - "enum": [ - 431 - ] - }, - { - "type": "number", - "enum": [ - 451 - ] - }, - { - "type": "number", - "enum": [ - 500 - ] - }, - { - "type": "number", - "enum": [ - 501 - ] - }, - { - "type": "number", - "enum": [ - 502 - ] - }, - { - "type": "number", - "enum": [ - 503 - ] - }, - { - "type": "number", - "enum": [ - 504 - ] - }, - { - "type": "number", - "enum": [ - 505 - ] - }, - { - "type": "number", - "enum": [ - 506 - ] - }, - { - "type": "number", - "enum": [ - 507 - ] - }, - { - "type": "number", - "enum": [ - 508 - ] - }, - { - "type": "number", - "enum": [ - 510 - ] - }, - { - "type": "number", - "enum": [ - 511 - ] - }, - { - "type": "number", - "enum": [ - -1 - ] - } - ], - "example": 400, - "description": "HTTPステータスコード" - }, - "message": { - "type": "string", - "minLength": 1, - "example": "Bad Request", - "description": "エラーメッセージ" - } - }, - "required": [ - "status", - "message" - ] - } - } - } - } - } - } - }, - "/api/group/debt/register": { - "post": { - "description": "register group debt entry", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "minLength": 1 - }, - "creditor_id": { - "type": "string", - "minLength": 1 - }, - "debtor_id": { - "type": "string", - "minLength": 1 - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string" - }, - "occurred_at": { - "type": "string" - } - }, - "required": [ - "group_id", - "creditor_id", - "debtor_id", - "amount" - ] - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "creditorId": { - "type": "string", - "minLength": 1 - }, - "debtorId": { - "type": "string", - "minLength": 1 - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "occurredAt": { - "type": "string", - "format": "date" - } - }, - "required": [ - "creditorId", - "debtorId", - "amount", - "occurredAt" - ] - } - } - } - }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8305,8 +6651,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -8692,9 +7038,95 @@ } } } + } + } + } + }, + "/api/group/debt/register": { + "post": { + "description": "register group debt entry", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "minLength": 1 + }, + "creditor_id": { + "type": "string", + "minLength": 1 + }, + "debtor_id": { + "type": "string", + "minLength": 1 + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string" + }, + "occurred_at": { + "type": "string" + } + }, + "required": [ + "group_id", + "creditor_id", + "debtor_id", + "amount" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "creditorId": { + "type": "string", + "minLength": 1 + }, + "debtorId": { + "type": "string", + "minLength": 1 + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "occurredAt": { + "type": "string", + "format": "date" + } + }, + "required": [ + "creditorId", + "debtorId", + "amount", + "occurredAt" + ] + } + } + } }, - "500": { - "description": "Internal Server Error", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -9049,95 +7481,40 @@ "enum": [ 510 ] - }, - { - "type": "number", - "enum": [ - 511 - ] - }, - { - "type": "number", - "enum": [ - -1 - ] - } - ], - "example": 400, - "description": "HTTPステータスコード" - }, - "message": { - "type": "string", - "minLength": 1, - "example": "Bad Request", - "description": "エラーメッセージ" - } - }, - "required": [ - "status", - "message" - ] - } - } - } - } - } - } - }, - "/api/group/debt/delete": { - "delete": { - "description": "delete group debt entry", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "group_id": { - "type": "string", - "minLength": 1 - }, - "debt_id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "group_id", - "debt_id" - ] - } - } - } - }, - "responses": { - "200": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "debt_id": { + }, + { + "type": "number", + "enum": [ + 511 + ] + }, + { + "type": "number", + "enum": [ + -1 + ] + } + ], + "example": 400, + "description": "HTTPステータスコード" + }, + "message": { "type": "string", - "minLength": 1 + "minLength": 1, + "example": "Bad Request", + "description": "エラーメッセージ" } }, "required": [ - "debt_id" + "status", + "message" ] } } } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -9524,8 +7901,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -9911,9 +8288,48 @@ } } } + } + } + } + }, + "/api/group/debt/delete": { + "delete": { + "description": "delete group debt entry", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_id": { + "type": "string", + "minLength": 1 + }, + "debt_id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "group_id", + "debt_id" + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" }, - "500": { - "description": "Internal Server Error", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -10299,49 +8715,9 @@ } } } - } - } - } - }, - "/api/profile": { - "get": { - "description": "get user profile", - "security": [ - { - "SessionCookie": [] - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "display_name": { - "type": "string", - "minLength": 1 - }, - "avatar_url": { - "type": "string", - "format": "uri" - }, - "bio": { - "type": "string" - } - }, - "required": [ - "display_name", - "avatar_url", - "bio" - ] - } - } - } }, - "400": { - "description": "Bad Request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -10728,8 +9104,8 @@ } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -11115,9 +9491,49 @@ } } } + } + } + } + }, + "/api/profile": { + "get": { + "description": "ログインユーザーのプロフィール情報を取得するエンドポイント", + "security": [ + { + "SessionCookie": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "minLength": 1 + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "bio": { + "type": "string" + } + }, + "required": [ + "display_name", + "avatar_url", + "bio" + ] + } + } + } }, - "500": { - "description": "Internal Server Error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -11503,69 +9919,9 @@ } } } - } - } - }, - "put": { - "description": "update user profile", - "security": [ - { - "SessionCookie": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "display_name": { - "type": "string" - }, - "avatar_url": { - "type": "string", - "format": "uri" - }, - "bio": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "display_name": { - "type": "string", - "minLength": 1 - }, - "avatar_url": { - "type": "string", - "format": "uri" - }, - "bio": { - "type": "string" - } - }, - "required": [ - "display_name", - "avatar_url", - "bio" - ] - } - } - } }, - "400": { - "description": "Bad Request", + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { @@ -11951,6 +10307,41 @@ } } } + } + } + }, + "patch": { + "description": "ログインユーザーのプロフィール情報を更新するエンドポイント", + "security": [ + { + "SessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "bio": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" }, "401": { "description": "Unauthorized", diff --git a/products/frontend/src/api/openapi.d.ts b/products/frontend/src/api/openapi.d.ts index d65d222..8993b57 100644 --- a/products/frontend/src/api/openapi.d.ts +++ b/products/frontend/src/api/openapi.d.ts @@ -4,85 +4,6 @@ */ export interface paths { - "/api/session": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description sample endpoint to get session info */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - id: string; - email: string; - }; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/info/group": { parameters: { query?: never; @@ -90,7 +11,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description get info about groups the user belongs to */ + /** @description ログインユーザーが参加しているグループ一覧を取得するエンドポイント */ get: { parameters: { query?: never; @@ -111,7 +32,8 @@ export interface paths { /** Format: uuid */ group_id: string; group_name: string; - created_by: string; + created_by_id: string; + created_by_name: string; members: { user_id: string; user_name: string; @@ -177,7 +99,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description get info about user transactions */ + /** @description ログインユーザーの貸し借りの履歴を取得するエンドポイント */ get: { parameters: { query?: never; @@ -246,7 +168,7 @@ export interface paths { }; put?: never; post?: never; - /** @description do user repayment */ + /** @description ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント */ delete: { parameters: { query?: never; @@ -262,36 +184,12 @@ export interface paths { }; }; responses: { - /** @description Created */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - counterparty_id: string; - }; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; + content?: never; }; /** @description Unauthorized */ 401: { @@ -349,7 +247,7 @@ export interface paths { }; get?: never; put?: never; - /** @description create a new group */ + /** @description グループを新規作成するエンドポイント */ post: { parameters: { query?: never; @@ -360,7 +258,7 @@ export interface paths { requestBody: { content: { "application/json": { - name: string; + group_name: string; }; }; }; @@ -378,26 +276,6 @@ export interface paths { }; }; }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; - }; /** @description Unauthorized */ 401: { headers: { @@ -455,7 +333,7 @@ export interface paths { }; get?: never; put?: never; - /** @description join an existing group */ + /** @description 招待IDを使ってグループに参加するエンドポイント */ post: { parameters: { query?: never; @@ -576,8 +454,8 @@ export interface paths { }; }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; @@ -686,8 +564,8 @@ export interface paths { }; }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; @@ -912,16 +790,12 @@ export interface paths { }; }; responses: { - /** @description Created */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - debt_id: string; - }; - }; + content?: never; }; /** @description Bad Request */ 400: { @@ -997,7 +871,7 @@ export interface paths { path?: never; cookie?: never; }; - /** @description get user profile */ + /** @description ログインユーザーのプロフィール情報を取得するエンドポイント */ get: { parameters: { query?: never; @@ -1021,26 +895,6 @@ export interface paths { }; }; }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; - }; /** @description Unauthorized */ 401: { headers: { @@ -1083,8 +937,13 @@ export interface paths { }; }; }; - /** @description update user profile */ - put: { + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** @description ログインユーザーのプロフィール情報を更新するエンドポイント */ + patch: { parameters: { query?: never; header?: never; @@ -1102,39 +961,12 @@ export interface paths { }; }; responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - display_name: string; - /** Format: uri */ - avatar_url: string; - bio: string; - }; - }; - }; - /** @description Bad Request */ - 400: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - /** - * @description HTTPステータスコード - * @example 400 - */ - status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1; - /** - * @description エラーメッセージ - * @example Bad Request - */ - message: string; - }; - }; + content?: never; }; /** @description Unauthorized */ 401: { @@ -1178,11 +1010,6 @@ export interface paths { }; }; }; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; trace?: never; }; } diff --git a/products/frontend/src/routes/GenerateGroup/index.tsx b/products/frontend/src/routes/GenerateGroup/index.tsx index ddb1360..8da7cc9 100644 --- a/products/frontend/src/routes/GenerateGroup/index.tsx +++ b/products/frontend/src/routes/GenerateGroup/index.tsx @@ -1,6 +1,5 @@ import { useState, type FC } from 'react'; // @tanstack/react-query -// import { useQueryClient } from '@tanstack/react-query'; import { $api } from '../../api/fetchClient'; // react-hook-form import { ErrorMessage } from '@hookform/error-message'; @@ -12,24 +11,28 @@ import { type CreateGroupResponseSchemaType, createGroupRequestSchema, } from 'validator'; +// react-router +import { Link } from 'react-router'; const GenerateGroup: FC = () => { - // const queryClient = useQueryClient(); - const [isResultError, setIsResultError] = useState(false); + // コピー状態管理 + const [copyStatus, setCopyStatus] = useState<'idle' | 'copying' | 'success' | 'error'>('idle'); + const [inviteUrl, setInviteUrl] = useState(null); + + // グループ作成処理 const [result, setResult] = useState(null); const { mutate, isSuccess, isPending, isError, error } = $api.useMutation('post', '/api/group/create', { onSuccess: (data) => { - if (!data?.group_id || !data?.invite_id) { - setIsResultError(true); - } else { - setResult({ - group_id: data?.group_id, - invite_id: data?.invite_id, - }); - } + setResult({ + group_id: data?.group_id, + invite_id: data?.invite_id, + }); + setInviteUrl(`${import.meta.env.VITE_CLIENT_URL}/invite/${data?.invite_id}`); + setCopyStatus('idle'); }, }); + // react-hook-formの設定 const { register, handleSubmit, @@ -37,22 +40,35 @@ const GenerateGroup: FC = () => { } = useForm({ resolver: zodResolver(createGroupRequestSchema), defaultValues: { - name: '', + group_name: '', }, }); + // グループ作成ハンドラ const onSubmit: SubmitHandler = async (formData) => { mutate({ body: formData, credentials: 'include' }); }; + // 招待URLハンドラ + const inviteUrlHandler = async (url: string) => { + try { + setCopyStatus('copying'); + await navigator.clipboard.writeText(url); + setCopyStatus('success'); + } catch (e) { + console.error(e); + setCopyStatus('error'); + } + }; + return ( <>

グループの作成

- - - + + +
+ {copyStatus === 'success' &&

コピーしました

} + {copyStatus === 'error' &&

コピーに失敗しました

} + + )} + グループページへ移動 - ) : null} + )} ); }; diff --git a/products/frontend/src/routes/GroupDetail/index.tsx b/products/frontend/src/routes/GroupDetail/index.tsx index 3b39400..d329ea7 100644 --- a/products/frontend/src/routes/GroupDetail/index.tsx +++ b/products/frontend/src/routes/GroupDetail/index.tsx @@ -1,13 +1,12 @@ -import { useEffect, useState, type FC } from 'react'; +import { useEffect, useMemo, useState, type FC } from 'react'; import { useParams } from 'react-router'; // @tanstack/react-query -// import { useQueryClient } from '@tanstack/react-query'; import { $api } from '../../api/fetchClient'; import { - registerGroupDebtRequestSchema, type GetGroupDebtHistoryResponseSchemaType, type GetGroupInfoResponseSchemaType, - type RegisterGroupDebtRequestSchemaType, + RegisterGroupDebtFormSchema, + type RegisterGroupDebtFormSchemaType, } from 'validator'; // react-hook-form import { ErrorMessage } from '@hookform/error-message'; @@ -15,54 +14,44 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm, type SubmitHandler } from 'react-hook-form'; const GroupDetail: FC = () => { + // URLパラメータからgroupIdを取得 const { groupId } = useParams<{ groupId: string }>(); + + // groupIdが存在しない場合の処理 if (!groupId) return

Group ID is not provided.

; - const [isGroupInfoResultError, setIsGroupInfoResultError] = useState(false); + // グループ情報の取得 const [groupInfoResult, setGroupInfoResult] = useState(null); const groupInfoMutation = $api.useMutation('post', `/api/group/info`, { onSuccess: (data) => { - if (!data?.group_name || !data?.created_by || !data?.members) { - setIsGroupInfoResultError(true); - } else { - // convert dat to GetGroupInfoResponseSchemaType - const groupInfo: GetGroupInfoResponseSchemaType = { - group_name: data.group_name, - created_by: data.created_by, - members: data.members, - }; - // set group info result - setGroupInfoResult(groupInfo); - } + // グループ情報をセット + setGroupInfoResult(data); }, }); - const [isDebtHistoryResultError, setIsDebtHistoryResultError] = useState(false); + // グループの貸し借り履歴の取得 const [debtHistoryResult, setDebtHistoryResult] = useState(null); const debtHistoryMutation = $api.useMutation('post', `/api/group/debt/history`, { onSuccess: (data) => { - if (!data?.debts) { - setIsDebtHistoryResultError(true); - } else { - // convert dat to GetGroupDebtHistoryResponseSchemaType - const debtHistory: GetGroupDebtHistoryResponseSchemaType = { - debts: data.debts, - }; - // set group info result - setDebtHistoryResult(debtHistory); - } + // グループの貸し借り履歴をセット + setDebtHistoryResult(data); }, }); + // コンポーネントマウント時にグループ情報と貸し借り履歴を取得 useEffect(() => { groupInfoMutation.mutate({ body: { group_id: groupId }, credentials: 'include' }); debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' }); }, []); - // register debt form - const RegisterGroupDebtFormSchema = registerGroupDebtRequestSchema.omit({ group_id: true }); - type RegisterGroupDebtFormSchemaType = Omit; + // 貸し借り登録処理 + const debtRegisterMutation = $api.useMutation('post', `/api/group/debt/register`, { + onSuccess: () => { + debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' }); + }, + }); + //NOTE: こいつは、どうにかする予定 const toDatetimeLocal = (d: Date) => { const pad = (n: number) => String(n).padStart(2, '0'); const yyyy = d.getFullYear(); @@ -73,15 +62,12 @@ const GroupDetail: FC = () => { return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; // "YYYY-MM-DDTHH:mm" }; - const debtRegisterMutation = $api.useMutation('post', `/api/group/debt/register`, { - onSuccess: () => { - debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' }); - }, - }); - + // react-hook-formの設定 const { register, handleSubmit, + watch, + setValue, formState: { errors }, } = useForm({ resolver: zodResolver(RegisterGroupDebtFormSchema), @@ -94,15 +80,33 @@ const GroupDetail: FC = () => { }, }); + // 同じ人を借り手/貸し手に選べないようにするためにwatch + const debtorId = watch('debtor_id'); + const creditorId = watch('creditor_id'); + + // メンバー一覧の取得 + // membersのnullを除外して + + +
- - + +
+
+
+
@@ -183,28 +213,26 @@ const GroupDetail: FC = () => { ? `グループの作成に失敗しました: ${debtRegisterMutation.error.message}` : debtRegisterMutation.isSuccess ? 'グループの作成しました' - : ''} + : null}

- ) : isGroupInfoResultError ? ( -

グループ情報の取得に失敗しました。再度お試しください。

- ) : null} + )}

グループの貸し借りの履歴

- {debtHistoryMutation.isSuccess && !isDebtHistoryResultError && debtHistoryResult ? ( + {debtHistoryMutation.isPending &&

貸し借りの履歴を取得中...

} + {debtHistoryMutation.isError &&

貸し借りの履歴の取得に失敗しました。再度お試しください。

} + {debtHistoryMutation.isSuccess && debtHistoryResult && (
    {debtHistoryResult.debts.map((debt, index) => (
  • - {debt.debtor_name} さんが {debt.creditor_name} さんに {debt.amount} 円を負っています。 + {debt.debtor_name} さんが {debt.creditor_name} さんに {debt.amount} 円を借りています。
  • ))}
- ) : isDebtHistoryResultError ? ( -

グループの履歴の取得に失敗しました。再度お試しください。

- ) : null} + )} ); }; diff --git a/products/frontend/src/routes/Invite/index.tsx b/products/frontend/src/routes/Invite/index.tsx index 6f84ad1..9b2e232 100644 --- a/products/frontend/src/routes/Invite/index.tsx +++ b/products/frontend/src/routes/Invite/index.tsx @@ -1,49 +1,42 @@ import { useState, type FC } from 'react'; import { Link, useParams } from 'react-router'; // @tanstack/react-query -// import { useQueryClient } from '@tanstack/react-query'; import { $api } from '../../api/fetchClient'; const Invite: FC = () => { + // URLパラメータからinviteIdを取得 const { inviteId } = useParams<{ inviteId: string }>(); + + // inviteIdが存在しない場合の処理 if (!inviteId) return

Invite ID is not provided.

; - const [isResultError, setIsResultError] = useState(false); + // グループ参加処理 const [result, setResult] = useState(null); const { mutate, isSuccess, isPending, isError, error } = $api.useMutation('post', `/api/group/join`, { onSuccess: (data) => { - if (!data?.group_id) { - setIsResultError(true); - } else { - setResult(data.group_id); - } + // グループIDをセット + setResult(data.group_id); }, }); - const inviteClickHandler = (inviteId: string) => { + // グループ参加ハンドラ + const inviteHandler = (inviteId: string) => { mutate({ body: { invite_id: inviteId }, credentials: 'include' }); }; return ( <>

招待

-

招待ID: {inviteId}

- + {isPending &&

参加処理中...

} - {result &&

参加したグループID: {result}

} {isError &&

エラーが発生しました: {error.message}

} - {isSuccess && !isResultError && result ? ( + {isSuccess && ( <>

グループ参加結果

グループID: {result} に参加しました。

グループページへ移動 - ) : isResultError ? ( - <> -

グループ参加結果

-

グループの参加に失敗しました。再度お試しください。

- - ) : null} + )} ); }; diff --git a/products/frontend/src/routes/Profile/index.tsx b/products/frontend/src/routes/Profile/index.tsx index a2fdca3..9f50fd7 100644 --- a/products/frontend/src/routes/Profile/index.tsx +++ b/products/frontend/src/routes/Profile/index.tsx @@ -1,41 +1,22 @@ -import { useEffect, useState, type FC } from 'react'; +import { useEffect, type FC } from 'react'; // @tanstack/react-query -// import { useQueryClient } from '@tanstack/react-query'; import { $api } from '../../api/fetchClient'; -import { - updateUserProfileRequestSchema, - type UpdateUserProfileRequestSchemaType, - type UpdateUserProfileResponseSchemaType, -} from 'validator'; +import { updateUserProfileRequestSchema, type UpdateUserProfileRequestSchemaType } from 'validator'; // react-hook-form import { ErrorMessage } from '@hookform/error-message'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm, type SubmitHandler } from 'react-hook-form'; const Profile: FC = () => { + // ユーサープロフィール情報の取得 const userProfileQuery = $api.useQuery('get', '/api/profile', { credentials: 'include', }); - const [isUserProfileResultError, setIsUserProfileResultError] = useState(false); - const [userProfileResult, setUserProfileResult] = useState(null); - const userProfileMutation = $api.useMutation('put', `/api/profile`, { - onSuccess: (data) => { - if (!data) { - setIsUserProfileResultError(true); - } else { - // convert dat to UpdateUserProfileResponseSchemaType - const userProfile: UpdateUserProfileResponseSchemaType = { - display_name: data.display_name, - avatar_url: data.avatar_url, - bio: data.bio, - }; - // set group info result - setUserProfileResult(userProfile); - } - }, - }); + // ユーザープロフィール情報の更新 + const userProfileMutation = $api.useMutation('patch', `/api/profile`); + // react-hook-formの設定 const { register, handleSubmit, @@ -50,6 +31,7 @@ const Profile: FC = () => { } satisfies UpdateUserProfileRequestSchemaType, }); + // ユーザープロフィール情報が取得できたらフォームに現在のユーザープロフィール情報をセット useEffect(() => { if (userProfileQuery.data) { reset({ @@ -60,6 +42,7 @@ const Profile: FC = () => { } }, [userProfileQuery.data, reset]); + // プロフィール更新ハンドラ const onSubmit: SubmitHandler = (formData) => { userProfileMutation.mutate({ body: { ...formData }, credentials: 'include' }); }; @@ -67,11 +50,9 @@ const Profile: FC = () => { return ( <>

プロフィールの編集

- {userProfileQuery.isLoading ? ( -

Loading...

- ) : userProfileQuery.isError ? ( -

Error: {userProfileQuery.error.message}

- ) : ( + {userProfileQuery.isLoading &&

Loading...

} + {userProfileQuery.isError &&

Error: {userProfileQuery.error.message}

} + {userProfileQuery.isSuccess && ( <>
@@ -85,6 +66,7 @@ const Profile: FC = () => { />
+
{ />
+