@@ -8,13 +8,14 @@ import {
88 createMentorRequestSchema ,
99 createPreRegistrationRequestSchema ,
1010 historyBulkCheckOutRequestSchema ,
11+ participantSearchQuerySchema ,
1112 participantsListQuerySchema ,
1213 scanRequestSchema ,
1314 updateMentorRequestSchema ,
1415} from '@tecnova/shared/schemas' ;
1516import { and , count , eq } from 'drizzle-orm' ;
1617import { drizzle } from 'drizzle-orm/d1' ;
17- import { Hono } from 'hono' ;
18+ import { type Context , Hono } from 'hono' ;
1819import { cors } from 'hono/cors' ;
1920import type { MiddlewareHandler } from 'hono/types' ;
2021import type { ContentfulStatusCode } from 'hono/utils/http-status' ;
@@ -39,7 +40,9 @@ import {
3940 recordBulkCheckOut ,
4041 recordCheckIn ,
4142 recordCheckOut ,
43+ searchActiveParticipantsByNickname ,
4244} from './lib/checkin' ;
45+ import { createParticipantDriveFolder } from './lib/drive-folder' ;
4346import {
4447 createPreRegistration ,
4548 deletePreRegistration ,
@@ -52,6 +55,8 @@ type Bindings = {
5255 DB : D1Database ;
5356 GOOGLE_SERVICE_ACCOUNT_KEY : string ;
5457 GOOGLE_SHEETS_ID : string ;
58+ GAS_DRIVE_WEBHOOK_URL ?: string ;
59+ GAS_DRIVE_WEBHOOK_SECRET ?: string ;
5560 GOOGLE_OAUTH_CLIENT_ID : string ;
5661 GOOGLE_OAUTH_CLIENT_SECRET : string ;
5762 BETTER_AUTH_SECRET : string ;
@@ -362,6 +367,36 @@ const internalError = (e: unknown) => ({
362367 message : e instanceof Error ? e . message : String ( e ) ,
363368} ) ;
364369
370+ const queueDriveFolderCreation = (
371+ c : Context < { Bindings : Bindings ; Variables : Variables } > ,
372+ participantId : string ,
373+ nickname : string ,
374+ ) => {
375+ const url = c . env . GAS_DRIVE_WEBHOOK_URL ?. trim ( ) ;
376+ const secret = c . env . GAS_DRIVE_WEBHOOK_SECRET ?. trim ( ) ;
377+ if ( ! url && ! secret ) return ;
378+ if ( ! url || ! secret ) {
379+ console . warn ( 'GAS Drive webhook is partially configured; skipping folder creation' ) ;
380+ return ;
381+ }
382+
383+ const promise = createParticipantDriveFolder ( { url, secret, participantId, nickname } )
384+ . then ( ( folder ) => {
385+ console . log (
386+ `Drive folder ready for participant ${ participantId } : ${ folder . folderName } (${ folder . folderId } ) reused=${ folder . reused } ` ,
387+ ) ;
388+ } )
389+ . catch ( ( e ) => {
390+ console . error ( `Drive folder creation failed for participant ${ participantId } :` , e ) ;
391+ } ) ;
392+
393+ try {
394+ c . executionCtx . waitUntil ( promise ) ;
395+ } catch {
396+ void promise ;
397+ }
398+ } ;
399+
365400const serializeScanResult = ( result : Awaited < ReturnType < typeof processScanValue > > ) => {
366401 if ( result . action === 'check_in' ) {
367402 return {
@@ -433,6 +468,7 @@ app.post('/checkin/activate', async (c) => {
433468 spreadsheetId : c . env . GOOGLE_SHEETS_ID ,
434469 preRegistrationId : parsed . data . preRegistrationId ,
435470 } ) ;
471+ queueDriveFolderCreation ( c , result . participantId , result . nickname ) ;
436472 return c . json ( {
437473 participantId : result . participantId ,
438474 nickname : result . nickname ,
@@ -536,6 +572,23 @@ app.post('/checkin/history/check-out-bulk', async (c) => {
536572 }
537573} ) ;
538574
575+ // マニュアル入力画面のニックネーム検索。`:participantId` ルートより前に
576+ // 登録する必要があるので注意(Hono は登録順マッチ)。
577+ app . get ( '/checkin/participants/search' , async ( c ) => {
578+ const parsed = participantSearchQuerySchema . safeParse ( { q : c . req . query ( 'q' ) } ) ;
579+ if ( ! parsed . success ) {
580+ return c . json ( invalidQueryError , 400 ) ;
581+ }
582+
583+ const db = createDb ( c . env ) ;
584+ try {
585+ const items = await searchActiveParticipantsByNickname ( db , parsed . data . q ) ;
586+ return c . json ( { participants : items } ) ;
587+ } catch ( e ) {
588+ return c . json ( internalError ( e ) , 500 ) ;
589+ }
590+ } ) ;
591+
539592// 受付専用の参加者プロフィール。QR/手入力後はまずここを表示し、
540593// 現在の状態に応じた操作だけをフロントに出す。
541594app . get ( '/checkin/participants/:participantId' , async ( c ) => {
0 commit comments