diff --git a/products/backend/src/db/pay-crew2-schema.ts b/products/backend/src/db/pay-crew2-schema.ts index 01f7ae0..7c09e0a 100644 --- a/products/backend/src/db/pay-crew2-schema.ts +++ b/products/backend/src/db/pay-crew2-schema.ts @@ -1,5 +1,5 @@ // drizzle -import { pgTable, text, timestamp, index, uuid, uniqueIndex, integer } from 'drizzle-orm/pg-core'; +import { pgTable, text, timestamp, index, uuid, uniqueIndex, integer, date } from 'drizzle-orm/pg-core'; //auth-schema import { user } from './auth-schema'; @@ -8,7 +8,7 @@ export const group = pgTable( { id: uuid('id').primaryKey(), name: text('name').notNull(), - invite_id: text('invite_id').notNull().unique(), + inviteId: text('invite_id').notNull().unique(), createdBy: text('created_by') .references(() => user.id, { onDelete: 'set null' }) .notNull(), @@ -58,7 +58,7 @@ export const debt = pgTable( .references(() => user.id, { onDelete: 'cascade' }), amount: integer('amount').notNull(), description: text('description'), - occurredAt: timestamp('occurred_at').defaultNow().notNull(), + occurredAt: date('occurred_at').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .defaultNow() diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 99f4209..fd4aa45 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -18,8 +18,6 @@ import { type GetGroupDebtHistoryResponseSchemaType, GetGroupInfoResponseMemberElementSchemaType, registerGroupDebtRequestSchema, - registerGroupDebtResponseSchema, - type RegisterGroupDebtResponseSchemaType, deleteGroupDebtRequestSchema, } from 'validator'; // error schema @@ -67,9 +65,6 @@ hono.openapi(createGroupSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // 現在時刻の取得 - const now = new Date(); - // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); @@ -79,14 +74,12 @@ hono.openapi(createGroupSchema, async (c) => { .values({ id: crypto.randomUUID(), name: body.group_name, - invite_id: `${crypto.randomUUID()}-${crypto.randomUUID()}`, + inviteId: `${crypto.randomUUID()}-${crypto.randomUUID()}`, createdBy: loginUser.id, - createdAt: now, - updatedAt: now, }) .returning({ id: group.id, - invite_id: group.invite_id, + inviteId: group.inviteId, }); //* グループ作成者をgroupMembershipに挿入 *// @@ -94,14 +87,13 @@ hono.openapi(createGroupSchema, async (c) => { id: crypto.randomUUID(), groupId: result[0].id, userId: loginUser.id, - joinedAt: now, }); // レスポンス return c.json( { group_id: result[0].id, - invite_id: result[0].invite_id, + invite_id: result[0].inviteId, } satisfies CreateGroupResponseSchemaType, 201 ); @@ -142,9 +134,6 @@ hono.openapi(joinGroupSchema, async (c) => { const body = c.req.valid('json'); const user = c.get('user'); - // 現在時刻の取得 - const now = new Date(); - // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); @@ -154,7 +143,7 @@ hono.openapi(joinGroupSchema, async (c) => { id: group.id, }) .from(group) - .where(eq(group.invite_id, body.invite_id)) + .where(eq(group.inviteId, body.invite_id)) .limit(1); // invite_id が不正な場合はエラー @@ -188,7 +177,6 @@ hono.openapi(joinGroupSchema, async (c) => { id: crypto.randomUUID(), groupId: groupData[0].id, userId: user.id, - joinedAt: now, }) .returning({ groupId: groupMembership.groupId, @@ -261,6 +249,7 @@ hono.openapi(getGroupInfoSchema, async (c) => { const groupData = await db .select({ name: group.name, + inviteId: group.inviteId, createdBy: group.createdBy, }) .from(group) @@ -320,7 +309,9 @@ hono.openapi(getGroupInfoSchema, async (c) => { return c.json( { group_name: groupData[0].name, - created_by: + invite_id: groupData[0].inviteId, + created_by_id: groupData[0].createdBy, + created_by_name: createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 ? createdByUserNameInfo[0].displayName : createdByUserNameInfo[0].name, @@ -395,6 +386,8 @@ hono.openapi(getGroupDebtHistorySchema, async (c) => { debtorId: debt.debtorId, creditorId: debt.creditorId, amount: debt.amount, + description: debt.description, + occurredAt: debt.occurredAt, }) .from(debt) .where(and(eq(debt.groupId, body.group_id), isNull(debt.deletedAt))); @@ -438,6 +431,8 @@ hono.openapi(getGroupDebtHistorySchema, async (c) => { ? CreditorNameInfo[0].displayName : CreditorNameInfo[0].name, amount: debtEntry.amount, + description: debtEntry.description === null ? '' : debtEntry.description, + occurred_at: debtEntry.occurredAt, }); } @@ -468,13 +463,8 @@ const registerGroupDebtSchema = route.createSchema( }, }, responses: { - 201: { - description: 'Created', - content: { - 'application/json': { - schema: registerGroupDebtResponseSchema, - }, - }, + 204: { + description: 'No Content', }, }, }, @@ -485,9 +475,6 @@ hono.openapi(registerGroupDebtSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // 現在時刻の取得 - const now = new Date(); - // データベース接続 const db = drizzle({ connection: c.env.HYPERDRIVE }); @@ -508,35 +495,17 @@ hono.openapi(registerGroupDebtSchema, async (c) => { // NOTE: --- 共通化終了 --- // body.group_id に貸し借り履歴を追加 (debt table) - const result = await db - .insert(debt) - .values({ - id: crypto.randomUUID(), - groupId: body.group_id, - creditorId: body.creditor_id, - debtorId: body.debtor_id, - amount: body.amount, - description: typeof body.description === 'undefined' ? null : body.description, - occurredAt: body.occurred_at ? new Date(body.occurred_at) : now, - createdAt: now, - updatedAt: now, - }) - .returning({ - creditorId: debt.creditorId, - debtorId: debt.debtorId, - amount: debt.amount, - occurredAt: debt.occurredAt, - }); + await db.insert(debt).values({ + id: crypto.randomUUID(), + groupId: body.group_id, + creditorId: body.creditor_id, + debtorId: body.debtor_id, + amount: body.amount, + description: typeof body.description === 'undefined' ? null : body.description, + occurredAt: body.occurred_at, + }); - return c.json( - { - creditorId: result[0].creditorId, - debtorId: result[0].debtorId, - amount: result[0].amount, - occurredAt: result[0].occurredAt, - } satisfies RegisterGroupDebtResponseSchemaType, - 201 - ); + return c.body(null, 204); }); // TODO: 貸し借りの履歴の削除エンドポイントの登録 @@ -597,7 +566,6 @@ hono.openapi(deleteGroupDebtSchema, async (c) => { .set({ deletedBy: loginUser.id, deletedAt: now, - updatedAt: now, }) .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, body.group_id), isNull(debt.deletedAt))); diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 6a7452c..0417912 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -298,7 +298,6 @@ hono.openapi(infoUserRepaymentSchema, async (c) => { .set({ deletedBy: loginUser.id, deletedAt: now, - updatedAt: now, }) .where( or( diff --git a/products/frontend/openapi.json b/products/frontend/openapi.json index f8b95d0..f4883c1 100644 --- a/products/frontend/openapi.json +++ b/products/frontend/openapi.json @@ -4591,7 +4591,15 @@ "type": "string", "format": "uuid" }, - "created_by": { + "invite_id": { + "type": "string", + "minLength": 1 + }, + "created_by_id": { + "type": "string", + "minLength": 1 + }, + "created_by_name": { "type": "string", "minLength": 1 }, @@ -4618,7 +4626,9 @@ }, "required": [ "group_name", - "created_by", + "invite_id", + "created_by_id", + "created_by_name", "members" ] } @@ -5855,6 +5865,14 @@ "amount": { "type": "number", "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 0 + }, + "occurred_at": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } }, "required": [ @@ -5863,7 +5881,9 @@ "debtor_name", "creditor_id", "creditor_name", - "amount" + "amount", + "description", + "occurred_at" ] } } @@ -7077,53 +7097,24 @@ "type": "string" }, "occurred_at": { - "type": "string" + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } }, "required": [ "group_id", "creditor_id", "debtor_id", - "amount" + "amount", + "occurred_at" ] } } } }, "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" - ] - } - } - } + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", diff --git a/products/frontend/src/api/openapi.d.ts b/products/frontend/src/api/openapi.d.ts index 8993b57..50ae9b4 100644 --- a/products/frontend/src/api/openapi.d.ts +++ b/products/frontend/src/api/openapi.d.ts @@ -463,7 +463,9 @@ export interface paths { "application/json": { /** Format: uuid */ group_name: string; - created_by: string; + invite_id: string; + created_by_id: string; + created_by_name: string; members: { user_id: string; user_name: string; @@ -578,6 +580,8 @@ export interface paths { creditor_id: string; creditor_name: string; amount: number; + description: string; + occurred_at: string; }[]; }; }; @@ -675,25 +679,17 @@ export interface paths { debtor_id: string; amount: number; description?: string; - occurred_at?: string; + occurred_at: string; }; }; }; responses: { - /** @description Created */ - 201: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - creditorId: string; - debtorId: string; - amount: number; - /** Format: date */ - occurredAt: string; - }; - }; + content?: never; }; /** @description Bad Request */ 400: { diff --git a/products/frontend/src/routes/GenerateGroup/index.tsx b/products/frontend/src/routes/GenerateGroup/index.tsx index 8da7cc9..d3af381 100644 --- a/products/frontend/src/routes/GenerateGroup/index.tsx +++ b/products/frontend/src/routes/GenerateGroup/index.tsx @@ -80,7 +80,7 @@ const GenerateGroup: FC = () => { : isError ? `グループの作成に失敗しました: ${error.message}` : isSuccess - ? 'グループの作成しました' + ? 'グループの作成に成功しました' : null}

diff --git a/products/frontend/src/routes/GroupDetail/index.tsx b/products/frontend/src/routes/GroupDetail/index.tsx index d329ea7..b735348 100644 --- a/products/frontend/src/routes/GroupDetail/index.tsx +++ b/products/frontend/src/routes/GroupDetail/index.tsx @@ -16,16 +16,32 @@ 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 [copyStatus, setCopyStatus] = useState<'idle' | 'copying' | 'success' | 'error'>('idle'); + const [inviteUrl, setInviteUrl] = useState(null); + // 招待URLハンドラ + const inviteUrlHandler = async (url: string) => { + try { + setCopyStatus('copying'); + await navigator.clipboard.writeText(url); + setCopyStatus('success'); + } catch (e) { + console.error(e); + setCopyStatus('error'); + } + }; + // グループ情報の取得 const [groupInfoResult, setGroupInfoResult] = useState(null); const groupInfoMutation = $api.useMutation('post', `/api/group/info`, { onSuccess: (data) => { // グループ情報をセット setGroupInfoResult(data); + // 招待URLをセット + setInviteUrl(`${import.meta.env.VITE_CLIENT_URL}/invite/${data.invite_id}`); }, }); @@ -51,18 +67,12 @@ const GroupDetail: FC = () => { }, }); - //NOTE: こいつは、どうにかする予定 - const toDatetimeLocal = (d: Date) => { + // react-hook-formの設定 + const toDateInputValue = (d: Date) => { const pad = (n: number) => String(n).padStart(2, '0'); - const yyyy = d.getFullYear(); - const mm = pad(d.getMonth() + 1); - const dd = pad(d.getDate()); - const hh = pad(d.getHours()); - const mi = pad(d.getMinutes()); - return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; // "YYYY-MM-DDTHH:mm" + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; }; - // react-hook-formの設定 const { register, handleSubmit, @@ -76,7 +86,7 @@ const GroupDetail: FC = () => { debtor_id: '', amount: 0, description: '', - occurred_at: toDatetimeLocal(new Date()), + occurred_at: toDateInputValue(new Date()), }, }); @@ -101,12 +111,6 @@ const GroupDetail: FC = () => { // 貸し借り登録ハンドラ const onSubmit: SubmitHandler = (formData) => { - // occurred_atをISO文字列に変換(未入力の場合はundefined) - const occurred_at = - typeof formData.occurred_at === 'string' && formData.occurred_at !== '' - ? new Date(formData.occurred_at).toISOString() - : undefined; - debtRegisterMutation.mutate({ body: { group_id: groupId, @@ -114,7 +118,7 @@ const GroupDetail: FC = () => { creditor_id: formData.creditor_id, amount: formData.amount, description: formData.description, - occurred_at: occurred_at, + occurred_at: formData.occurred_at, }, credentials: 'include', }); @@ -126,7 +130,6 @@ const GroupDetail: FC = () => { debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' }); }, }); - // 貸し借り削除ハンドラ const deleteGroupDebtHandler = (debtId: string) => { deleteGroupDebtMutation.mutate({ body: { group_id: groupId, debt_id: debtId }, credentials: 'include' }); @@ -140,15 +143,21 @@ const GroupDetail: FC = () => { {groupInfoMutation.isSuccess && groupInfoResult && ( <>

グループ名: {groupInfoResult.group_name}

-

作成者: {groupInfoResult.created_by}

+

作成者: {groupInfoResult.created_by_name}

+ {inviteUrl && ( +
+ + + {copyStatus === 'success' &&

コピーしました

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

コピーに失敗しました

} +
+ )}

メンバー一覧

    {groupInfoResult.members.map((member) => - member ? ( -
  • - {member.user_name} (ID: {member.user_id}) -
  • - ) : null + member ?
  • {member.user_name}
  • : null )}

貸し借りの登録

@@ -199,7 +208,7 @@ const GroupDetail: FC = () => {
- +
@@ -208,11 +217,11 @@ const GroupDetail: FC = () => {

{debtRegisterMutation.isPending - ? 'グループの作成中...' + ? '貸し借りの登録中...' : debtRegisterMutation.isError - ? `グループの作成に失敗しました: ${debtRegisterMutation.error.message}` + ? `貸し借りの登録に失敗しました: ${debtRegisterMutation.error.message}` : debtRegisterMutation.isSuccess - ? 'グループの作成しました' + ? '貸し借りの登録に成功しました' : null}

@@ -225,7 +234,8 @@ const GroupDetail: FC = () => {
    {debtHistoryResult.debts.map((debt, index) => (
  • - {debt.debtor_name} さんが {debt.creditor_name} さんに {debt.amount} 円を借りています。 + {debt.debtor_name} さんが {debt.creditor_name} さんに {debt.amount} 円を借りています。 (詳細:{' '} + {debt.description}、発生日時: {debt.occurred_at} diff --git a/products/frontend/src/routes/Root/index.tsx b/products/frontend/src/routes/Root/index.tsx index 1c5d1fc..387fedd 100644 --- a/products/frontend/src/routes/Root/index.tsx +++ b/products/frontend/src/routes/Root/index.tsx @@ -75,9 +75,7 @@ const Root: FC = () => {
      {group.members.map((member) => ( -
    • - {member.user_name} (ID: {member.user_id}) -
    • +
    • {member.user_name}
    • ))}
  • diff --git a/products/validator/src/index.ts b/products/validator/src/index.ts index 17ee75f..46a82e1 100644 --- a/products/validator/src/index.ts +++ b/products/validator/src/index.ts @@ -52,7 +52,6 @@ export { type GetGroupDebtHistoryResponseElementSchemaType, } from './response/group'; export { getGroupDebtHistoryResponseSchema, type GetGroupDebtHistoryResponseSchemaType } from './response/group'; -export { registerGroupDebtResponseSchema, type RegisterGroupDebtResponseSchemaType } from './response/group'; // error response schema export { errorResponseSchema, type ErrorResponseSchemaType } from './response/error'; diff --git a/products/validator/src/request/group.ts b/products/validator/src/request/group.ts index abd9236..f9c7c41 100644 --- a/products/validator/src/request/group.ts +++ b/products/validator/src/request/group.ts @@ -30,7 +30,7 @@ export const registerGroupDebtRequestSchema = z.object({ debtor_id: z.string().min(1), amount: z.number().min(0), description: z.string().optional(), - occurred_at: z.string().optional(), + occurred_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), }); export type RegisterGroupDebtRequestSchemaType = z.infer; diff --git a/products/validator/src/response/group.ts b/products/validator/src/response/group.ts index 11fe17d..c747e7d 100644 --- a/products/validator/src/response/group.ts +++ b/products/validator/src/response/group.ts @@ -20,7 +20,9 @@ export const getGroupInfoResponseMemberElementSchema = z.object({ export const getGroupInfoResponseSchema = z.object({ group_name: z.uuid(), - created_by: z.string().min(1), + invite_id: z.string().min(1), + created_by_id: z.string().min(1), + created_by_name: z.string().min(1), members: z.array(getGroupInfoResponseMemberElementSchema), }); @@ -34,6 +36,8 @@ export const getGroupDebtHistoryResponseElementSchema = z.object({ creditor_id: z.string().min(1), creditor_name: z.string().min(1), amount: z.number().min(0), + description: z.string().min(0), + occurred_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), }); export const getGroupDebtHistoryResponseSchema = z.object({ @@ -42,12 +46,3 @@ export const getGroupDebtHistoryResponseSchema = z.object({ export type GetGroupDebtHistoryResponseElementSchemaType = z.infer; export type GetGroupDebtHistoryResponseSchemaType = z.infer; - -export const registerGroupDebtResponseSchema = z.object({ - creditorId: z.string().min(1), - debtorId: z.string().min(1), - amount: z.number().min(0), - occurredAt: z.date(), -}); - -export type RegisterGroupDebtResponseSchemaType = z.infer;