diff --git a/@types/environment.d.ts b/@types/environment.d.ts index b91f1df..f6e17a2 100644 --- a/@types/environment.d.ts +++ b/@types/environment.d.ts @@ -5,5 +5,6 @@ declare namespace NodeJS { PASSPORT_KAKAO_CLIENT_ID: string; PASSPORT_KAKAO_CLIENT_SECRET: string; EXPRESS_SESSION_SECRET: string; + EC2IP: string; } } diff --git a/config/tsoa.json b/config/tsoa.json index 0bcf66a..5f7ae35 100644 --- a/config/tsoa.json +++ b/config/tsoa.json @@ -8,6 +8,10 @@ "validate": true, "spec": { "servers": [ + { + "url": "http://18.208.62.86:3000", + "description": "Production Server" + }, { "url": "http://localhost:3000", "description": "local server" diff --git a/package.json b/package.json index 3aee67f..89da04a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "@tsoa/runtime": "^6.6.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dotenv": "^16.4.7", "express": "^5.0.0-0", "express-session": "^1.18.1", diff --git a/src/app.ts b/src/app.ts index 7a4024f..b6d4691 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,7 +14,7 @@ import passport from 'passport'; import session from 'express-session'; import {PrismaSessionStore} from '@quixo3/prisma-session-store'; import cookieParser from 'cookie-parser'; -import {sessionAuthMiddleware} from './auth.config.js'; +// import {sessionAuthMiddleware} from './auth.config.js'; import {prisma} from './db.config.js'; import {RegisterRoutes} from './routers/tsoaRoutes.js'; import {authRouter} from './routers/auth.router.js'; @@ -24,7 +24,13 @@ dotenv.config(); const app = express(); const port = process.env.PORT; -app.use(cors()); +app.use( + // cors({ + // origin: ['http://localhost:3000', 'http://18.208.62.86:3000'], // 프론트엔드 주소 + // credentials: true, + // }), + cors(), +); app.use(express.static('public')); app.use(express.json()); app.use(express.urlencoded({extended: false})); @@ -61,6 +67,9 @@ app.use( saveUninitialized: false, cookie: { maxAge: 7 * 24 * 60 * 60 * 1000, // 일주일 + // sameSite: 'lax', + // secure: false, + // httpOnly: true, }, store: new PrismaSessionStore(prisma, { checkPeriod: 2 * 60 * 1000, @@ -88,7 +97,7 @@ app.use(passport.session()); app.use('/oauth2', authRouter); // 인증 미들웨어 -app.use(sessionAuthMiddleware); +// app.use(sessionAuthMiddleware); // 로그인 후 RegisterRoutes(app); @@ -127,6 +136,6 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => { app.use(errorHandler); -app.listen(port, () => { +app.listen(Number(port), '0.0.0.0', () => { console.log(`Example app listening on port ${port}`); }); diff --git a/src/auth.config.ts b/src/auth.config.ts index 831a857..0b77f90 100644 --- a/src/auth.config.ts +++ b/src/auth.config.ts @@ -9,6 +9,7 @@ import {UserModel} from './models/user.model.js'; import {SocialProfile} from './models/auth.model.js'; import {Request, Response, NextFunction} from 'express'; import {ServerError, AuthError, SessionError} from './error.js'; +// import {publishUserId} from './mqtt-client.js'; dotenv.config(); const updateOrCreateSocialAccount = async ( @@ -50,7 +51,7 @@ export const kakaoStrategy = new KakaoStrategy( { clientID: process.env.PASSPORT_KAKAO_CLIENT_ID!, clientSecret: process.env.PASSPORT_KAKAO_CLIENT_SECRET!, // Optional in Kakao - callbackURL: 'http://localhost:3000/oauth2/callback/kakao', + callbackURL: `http://${process.env.EC2IP}:3000/oauth2/callback/kakao`, }, async (accessToken, refreshToken, profile, cb) => { try { @@ -84,7 +85,7 @@ const verifyUser = async ( // SocialAccount 데이터 추가 또는 업데이트 const {id, email, name} = user; await updateOrCreateSocialAccount(id, profile, provider); - + // publishUserId(user.id); return {id, email, name}; } @@ -107,6 +108,8 @@ const verifyUser = async ( ], }); + // publishUserId(createdUser.id); + // SocialAccount 데이터 추가 const {id, email, name} = createdUser; await updateOrCreateSocialAccount(id, profile, provider); diff --git a/src/controllers/tsoa.focus-target.controller.ts b/src/controllers/tsoa.focus-target.controller.ts index c7b20b8..21dfd89 100644 --- a/src/controllers/tsoa.focus-target.controller.ts +++ b/src/controllers/tsoa.focus-target.controller.ts @@ -13,6 +13,7 @@ import { import {Request as ExpressRequest} from 'express'; import {StatusCodes} from 'http-status-codes'; import { + DailyStatisticsResponse, FocusTargetListResponse, FocusTargetResponse, } from '../models/focus-target.model.js'; @@ -22,6 +23,7 @@ import { TsoaSuccessResponse, } from '../models/tsoa-response.js'; import { + DailyStatisticsGet, FocusTargetListGet, FocusTargetUpdateDisable, FocusTargetUpdateEnable, @@ -142,11 +144,10 @@ export class FocusTargetController extends Controller { ], }, }) - public async GetFocusTargetList( - @Request() req: ExpressRequest, - ): Promise> { + public async GetFocusTargetList() // @Request() req: ExpressRequest, + : Promise> { try { - const userId = BigInt(req.user!.id); + const userId = BigInt(11); //BigInt(req.user!.id); const focusTargetList = await FocusTargetListGet(userId); return new TsoaSuccessResponse(focusTargetList); } catch (error) { @@ -163,4 +164,59 @@ export class FocusTargetController extends Controller { // throw error; // } // } + + /** + * 집중 시간 일간 통계를 조회하는 API입니다. + * + * 0: 토, 1: 일, 2: 월, 3: 화, 4: 수, 5: 목, 6: 금 + * + * @summary 일간 통계 조회 API + * @returns 일간 통계 조회 결과를 반환합니다. + */ + @Get('/statistics/daily') + @Tags('Focus-Target-Controller') + @SuccessResponse(StatusCodes.OK, '일간 통계 조회 성공 응답') + @Example({ + resultType: 'SUCCESS', + error: null, + success: { + dailyTotalTime: { + '0': 60, + '1': 60, + '2': 60, + '3': 60, + '4': 60, + '5': 60, + '6': 60, + }, + today: { + disabledTarget: [ + { + target: '책/교재', + targetId: '1', + startTime: '2025-01-17T03:50:25', + endTime: '2025-01-17T04:50:25', + }, + ], + enabledTarget: [ + { + target: '책/교재', + targetId: '1', + startTime: '2025-01-17T03:50:25', + endTime: '2025-01-17T04:50:25', + }, + ], + }, + }, + }) + public async GetDailyStatistics() // @Request() req: ExpressRequest, + : Promise> { + try { + const userId = BigInt(11); //BigInt(req.user!.id); + const dailyStatistics = await DailyStatisticsGet(userId); + return new TsoaSuccessResponse(dailyStatistics); + } catch (error) { + throw error; + } + } } diff --git a/src/controllers/tsoa.group.controller.ts b/src/controllers/tsoa.group.controller.ts index 0b0055d..b9234d2 100644 --- a/src/controllers/tsoa.group.controller.ts +++ b/src/controllers/tsoa.group.controller.ts @@ -2,7 +2,7 @@ import { Controller, Example, Post, - Request, + // Request, Route, SuccessResponse, Response, @@ -17,7 +17,7 @@ import { ITsoaSuccessResponse, TsoaSuccessResponse, } from '../models/tsoa-response.js'; -import {Request as ExpressRequest} from 'express'; +// import {Request as ExpressRequest} from 'express'; import { BodyToGroup, GroupListResponse, @@ -68,11 +68,11 @@ export class GroupController extends Controller { }, }) public async handleGroupAdd( - @Request() req: ExpressRequest, + // @Request() req: ExpressRequest, @Body() body: BodyToGroup, ): Promise> { try { - const hostId = BigInt(req.user!.id); + const hostId = BigInt(11); //BigInt(req.user!.id); const group = await groupCreate(hostId, bodyToGroup(body)); return new TsoaSuccessResponse(group); } catch (error) { @@ -114,7 +114,7 @@ export class GroupController extends Controller { }, }) public async handleGroupGet( - @Request() req: ExpressRequest, + // @Request() req: ExpressRequest, @Path('groupId') groupIdParam: string, ): Promise> { try { @@ -143,11 +143,10 @@ export class GroupController extends Controller { groups: [{id: '1', name: 'string', memberCount: 1}], }, }) - public async handleGroupListGet( - @Request() req: ExpressRequest, - ): Promise> { + public async handleGroupListGet() // @Request() req: ExpressRequest, + : Promise> { try { - const userId = BigInt(req.user!.id); + const userId = BigInt(11); //BigInt(req.user!.id); const group = await groupListGet(userId); return new TsoaSuccessResponse(group); } catch (error) { @@ -200,11 +199,11 @@ export class GroupController extends Controller { }, }) public async handledGroupJoin( - @Request() req: ExpressRequest, + // @Request() req: ExpressRequest, @Path('groupId') groupIdParam: string, ): Promise> { try { - const userId = BigInt(req.user!.id); + const userId = BigInt(11); //BigInt(req.user!.id); const groupId = BigInt(groupIdParam); const groupUser = await groupJoin(groupId, userId); return new TsoaSuccessResponse(groupUser); diff --git a/src/controllers/tsoa.schedule.controller.ts b/src/controllers/tsoa.schedule.controller.ts index 8af090e..bf72a3e 100644 --- a/src/controllers/tsoa.schedule.controller.ts +++ b/src/controllers/tsoa.schedule.controller.ts @@ -102,11 +102,11 @@ export class ScheduleController extends Controller { }, }) public async handleScheduleAdd( - @Request() req: ExpressRequest, + // @Request() req: ExpressRequest, @Body() body: BodyToSchedule, ): Promise> { try { - const userId = BigInt(req.user!.id); + const userId = BigInt(11); //BigInt(req.user!.id); const schedule = await scheduleCreate(userId, bodyToSchedule(body)); return new TsoaSuccessResponse(schedule); } catch (error) { @@ -195,11 +195,10 @@ export class ScheduleController extends Controller { }, }, }) - public async handleWeekScheduleGet( - @Request() req: ExpressRequest, - ): Promise> { + public async handleWeekScheduleGet() // @Request() req: ExpressRequest, + : Promise> { try { - const userId = BigInt(req.user!.id); + const userId = BigInt(11); //BigInt(req.user!.id); const weeklySchedule = await weeklyScheduleGet(userId); console.log('최종 주간 일정:', weeklySchedule); diff --git a/src/dtos/focus-target.dto.ts b/src/dtos/focus-target.dto.ts index fa9ab54..7663a3d 100644 --- a/src/dtos/focus-target.dto.ts +++ b/src/dtos/focus-target.dto.ts @@ -1,4 +1,5 @@ import { + DailyStatisticsResponse, FocusTargetListResponse, FocusTargetResponse, } from 'src/models/focus-target.model.js'; @@ -29,3 +30,26 @@ export const responseFromFocusTargetList = ({ })), }; }; + +export const responseFromDailyStatistics = ({ + dailyTotalTime, + today, +}: DailyStatisticsResponse): DailyStatisticsResponse => { + return { + dailyTotalTime: dailyTotalTime, + today: { + disabledTarget: today.disabledTarget.map(target => ({ + targetId: target.targetId, + startTime: target.startTime, + endTime: target.endTime, + target: target.target, + })), + enabledTarget: today.enabledTarget.map(target => ({ + targetId: target.targetId, + startTime: target.startTime, + endTime: target.endTime, + target: target.target, + })), + }, + }; +}; diff --git a/src/models/focus-target.model.ts b/src/models/focus-target.model.ts index e3a323c..fc08ada 100644 --- a/src/models/focus-target.model.ts +++ b/src/models/focus-target.model.ts @@ -7,7 +7,7 @@ export interface FocusTargetResponse { export interface FocusTargetState { targetId: string; - userId: string; + groupId: string[]; timestamp: Date; isInitial: Date; } @@ -20,3 +20,29 @@ export interface FocusTargetListResponse { status: number; }[]; } + +export interface DailyStatisticsResponse { + dailyTotalTime: { + '0': number; + '1': number; + '2': number; + '3': number; + '4': number; + '5': number; + '6': number; + }; + today: { + disabledTarget: { + target: string; + targetId: string; + startTime: Date; + endTime: Date; + }[]; + enabledTarget: { + target: string; + targetId: string; + startTime: Date; + endTime: Date; + }[]; + }; +} diff --git a/src/mqtt-client.ts b/src/mqtt-client.ts index c7bef22..c9b2df9 100644 --- a/src/mqtt-client.ts +++ b/src/mqtt-client.ts @@ -1,22 +1,129 @@ import mqtt from 'mqtt'; +import {saveFocusTargetState} from './repositories/focus-target.repository.js'; +import {prisma} from './db.config.js'; +type TimerInfo = { + startedAt: Date; + interval: NodeJS.Timeout; +}; +const timers = new Map(); // userId → 타이머 ID + +console.log('MQTT 연결 시도 중...'); export const mqttClient = mqtt.connect('mqtt://localhost:1883', { + // localhost:1883 // MOTT 브로커에 연결 username: '', password: '', clientId: `client_${Math.random().toString(16).substr(2, 8)}`, }); +// MQTT 연결 mqttClient.on('connect', () => { // 연결 성공 시 console.log('mqtt 연결 성공'); - mqttClient.subscribe('state/+', err => { + mqttClient.subscribe('state/user+', err => { // topic 구독 if (err) console.error('mqtt 구독 실패', err); else console.log('그룹 topic 구독 완료'); }); + + // MQTT 브로커로부터 메시지 수신 시 + mqttClient.on('message', async (topic, message) => { + try { + const payload = JSON.parse(message.toString()); // message - 실제 데이터 메시지 (buffer 형태) + console.log('수신 데이터:', payload); + const userId = topic.match(/^state\/user(\d+)$/); + if (userId === null) { + throw new Error('userId 파싱 실패'); + } + + console.log('payload:', payload); + + const prev = timers.get(userId[1]); + + if (!prev) { + // 타이머가 아직 없는 경우 → 최초 메시지 + timers.set(userId[1], { + startedAt: payload.timestamp, + interval: setInterval(() => {}, 1000), // 향후 확장 가능 + }); + console.log( + `타이머 시작: [user${userId}] ${payload.timestamp.toISOString()}`, + ); + return; + } + + let state = { + ...payload, + }; + const startedAt = timers.get(userId[1]); + if (payload.isInitial !== null && startedAt) { + state = { + ...payload, + isInitial: startedAt, + }; + } else { + state = { + ...payload, + isInitial: startedAt, + }; + } + console.log(`[user${userId}] 상태 저장 완료: ${state}`); + + await saveFocusTargetState(BigInt(userId[1]), state); // DB에 저장 + // sendStateToClients(state); // WebSocket을 통해 연결된 모든 client에게 해당 상태를 브로드캐스트 + timers.set(userId[1], { + startedAt: payload.timestamp, + interval: prev.interval, + }); + } catch (err) { + console.error('메시지 처리 실패', err); + } + }); }); +export async function publishGroupIdList(userId: bigint) { + const groupIdList = await prisma.userGroup.findMany({ + where: {userId: userId}, + select: {groupId: true}, + }); + const payload = { + groupIdList: groupIdList.map(groupId => groupId.toString()), + }; + console.log('groupIdList in mqtt:', payload); + if (mqttClient.connected) { + mqttClient.publish('init/caps1', JSON.stringify(payload), err => { + if (err) { + console.error('MQTT publish 실패:', err); + } else { + console.log('MQTT groupIdList 전송 완료:', payload); + } + }); + } else { + mqttClient.once('connect', () => { + mqttClient.publish('user/connect', JSON.stringify(payload), err => { + if (err) { + console.error('MQTT publish 실패:', err); + } else { + console.log('MQTT groupIdList 전송 완료:', payload); + } + }); + }); + } +} + mqttClient.on('error', error => { console.error('mqtt 에러', error); }); + +mqttClient.on('close', () => { + console.error('MQTT 연결이 닫혔습니다'); +}); + +mqttClient.on('offline', () => { + console.error('MQTT 브로커가 오프라인입니다'); +}); + +mqttClient.on('reconnect', () => { + console.log('MQTT 재연결 시도 중...'); +}); diff --git a/src/mqtt-server.ts b/src/mqtt-server.ts index 0c64525..cdbd93d 100644 --- a/src/mqtt-server.ts +++ b/src/mqtt-server.ts @@ -1,10 +1,12 @@ import {mqttClient} from './mqtt-client.js'; -import {saveFocusTargetState} from './repositories/focus-target.repository.js'; +// import {saveFocusTargetState} from './repositories/focus-target.repository.js'; // import {sendStateToClients} from './websocket-server.js'; mqttClient.on('message', async (topic, message) => { // MQTT 브로커로부터 메시지 수신 시 콜백 실행 try { + console.log('topic:', topic); + console.log('message:', message); const payload = JSON.parse(message.toString()); // message - 실제 데이터 메시지 (buffer 형태) const state = { targetId: payload.targetId, @@ -13,7 +15,7 @@ mqttClient.on('message', async (topic, message) => { isInitial: payload.isInitial, }; console.log('수신 데이터:', state); - await saveFocusTargetState(state); // DB에 저장 + // await saveFocusTargetState(state); // DB에 저장 // sendStateToClients(state); // WebSocket을 통해 연결된 모든 client에게 해당 상태를 브로드캐스트 } catch (err) { console.error('메시지 처리 실패', err); diff --git a/src/repositories/focus-target.repository.ts b/src/repositories/focus-target.repository.ts index 472cf04..62fc7e9 100644 --- a/src/repositories/focus-target.repository.ts +++ b/src/repositories/focus-target.repository.ts @@ -1,5 +1,6 @@ import {prisma} from 'src/db.config.js'; import { + DailyStatisticsResponse, FocusTargetListResponse, FocusTargetResponse, FocusTargetState, @@ -102,6 +103,7 @@ export const getFocusTargetList = async ( }; export async function saveFocusTargetState( + userId: bigint, data: FocusTargetState, ): Promise { const targetMap: Record = { @@ -114,7 +116,7 @@ export async function saveFocusTargetState( const checkFocusTargetState = await prisma.focusTarget.findFirst({ where: { target: targetMap[data.targetId], - userId: BigInt(data.userId), + userId: BigInt(userId), }, }); @@ -122,72 +124,227 @@ export async function saveFocusTargetState( return null; } - while (data.isInitial > data.timestamp) { - if (data.isInitial) { - if (checkFocusTargetState.status === 1) { - await prisma.enabledFocusTargetTimeTable.create({ - data: { - measurementStartAt: data.isInitial, - measurementEndAt: data.timestamp, - focusTargetId: checkFocusTargetState.id, - }, - }); - } else { - await prisma.disabledFocusTargetTimeTable.create({ - data: { - measurementStartAt: data.isInitial, - measurementEndAt: data.timestamp, - focusTargetId: checkFocusTargetState.id, - }, - }); - } - } else { - const latestOffline = await prisma.offlineTimeTable.findFirst({ - orderBy: { - measurementEndAt: 'desc', + if (checkFocusTargetState.status === 1) { + await prisma.enabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: new Date(data.isInitial), + measurementEndAt: new Date(data.timestamp), + focusTargetId: checkFocusTargetState.id, + }, + }); + } else { + await prisma.disabledFocusTargetTimeTable.create({ + data: { + measurementStartAt: new Date(data.isInitial), + measurementEndAt: new Date(data.timestamp), + focusTargetId: checkFocusTargetState.id, + }, + }); + } + + //if (data.isInitial) { + // if (checkFocusTargetState.status === 1) { + // await prisma.enabledFocusTargetTimeTable.create({ + // data: { + // measurementStartAt: new Date(data.isInitial), + // measurementEndAt: new Date(data.timestamp), + // focusTargetId: checkFocusTargetState.id, + // }, + // }); + // } else { + // await prisma.disabledFocusTargetTimeTable.create({ + // data: { + // measurementStartAt: new Date(data.isInitial), + // measurementEndAt: new Date(data.timestamp), + // focusTargetId: checkFocusTargetState.id, + // }, + // }); + // } + //} else { + // const latestOffline = await prisma.offlineTimeTable.findFirst({ + // orderBy: { + // measurementEndAt: 'desc', + // }, + // }); + // const latestEmpty = await prisma.emptyTimeTable.findFirst({ + // orderBy: { + // measurementEndAt: 'desc', + // }, + // }); + // const latestEnableTarget = + // await prisma.enabledFocusTargetTimeTable.findFirst({ + // orderBy: { + // measurementEndAt: 'desc', + // }, + // }); + // const latestDisableTarget = + // await prisma.disabledFocusTargetTimeTable.findFirst({ + // orderBy: { + // measurementEndAt: 'desc', + // }, + // }); + // const t1 = latestOffline?.measurementEndAt; + // const t2 = latestEmpty?.measurementEndAt; + // const t3 = latestEnableTarget?.measurementEndAt; + // const t4 = latestDisableTarget?.measurementEndAt; + // const latestTimes = [t1, t2, t3, t4] + // .filter(Boolean) + // .reduce((a, b) => (a! > b! ? a : b)); + // if (checkFocusTargetState.status === 1) { + // await prisma.enabledFocusTargetTimeTable.create({ + // data: { + // measurementStartAt: new Date(latestTimes!), + // measurementEndAt: new Date(data.timestamp), + // focusTargetId: checkFocusTargetState.id, + // }, + // }); + // } else { + // await prisma.disabledFocusTargetTimeTable.create({ + // data: { + // measurementStartAt: new Date(latestTimes!), + // measurementEndAt: new Date(data.timestamp), + // focusTargetId: checkFocusTargetState.id, + // }, + // }); + // } + // } +} + +export async function getDailyStatistics( + userId: bigint, +): Promise { + const todayDate = new Date(); + const day = todayDate.getDay(); // 0(일) ~ 6(토) + const startDate = new Date(todayDate); + startDate.setDate(todayDate.getDate() - day); // 0(일) + startDate.setHours(0, 0, 0, 0); + //onsole.log(startDate.toISOString()); + const endDate = new Date(startDate); // 0(일) + endDate.setDate(startDate.getDate() + 6); + endDate.setHours(23, 59, 59, 999); + //console.log(endDate.toISOString()); + + const dailyStatistics: DailyStatisticsResponse['dailyTotalTime'] = { + '0': 0, + '1': 0, + '2': 0, + '3': 0, + '4': 0, + '5': 0, + '6': 0, + }; + + const enabledFocusTargets = await prisma.enabledFocusTargetTimeTable.findMany( + { + where: { + focusTarget: {userId}, + measurementStartAt: { + gte: startDate, + lt: endDate, }, - }); - const latestEmpty = await prisma.emptyTimeTable.findFirst({ - orderBy: { - measurementEndAt: 'desc', + }, + }, + ); + + for (const target of enabledFocusTargets) { + const start = target.measurementStartAt; + const end = target.measurementEndAt; + const minutes = (end.getTime() - start.getTime()) / 60000; + const day = getKSTDay(start); // 한국 시간 기준 요일로 보정 + dailyStatistics[day] += minutes; + } + + const disabledFocusTargets = + await prisma.disabledFocusTargetTimeTable.findMany({ + where: { + focusTarget: {userId}, + measurementStartAt: { + gte: startDate, + lt: endDate, }, - }); - const latestEnableTarget = - await prisma.enabledFocusTargetTimeTable.findFirst({ - orderBy: { - measurementEndAt: 'desc', - }, - }); - const latestDisableTarget = - await prisma.disabledFocusTargetTimeTable.findFirst({ - orderBy: { - measurementEndAt: 'desc', - }, - }); - const t1 = latestOffline?.measurementEndAt; - const t2 = latestEmpty?.measurementEndAt; - const t3 = latestEnableTarget?.measurementEndAt; - const t4 = latestDisableTarget?.measurementEndAt; - const latestTimes = [t1, t2, t3, t4] - .filter(Boolean) - .reduce((a, b) => (a! > b! ? a : b)); - if (checkFocusTargetState.status === 1) { - await prisma.disabledFocusTargetTimeTable.create({ - data: { - measurementStartAt: latestTimes!, - measurementEndAt: data.timestamp, - focusTargetId: checkFocusTargetState.id, - }, - }); - } else { - await prisma.disabledFocusTargetTimeTable.create({ - data: { - measurementStartAt: latestTimes!, - measurementEndAt: data.timestamp, - focusTargetId: checkFocusTargetState.id, - }, - }); - } - } + }, + }); + + for (const target of disabledFocusTargets) { + const start = target.measurementStartAt; + const end = target.measurementEndAt; + const minutes = (end.getTime() - start.getTime()) / 60000; + const day = getKSTDay(start); // 한국 시간 기준 요일로 보정 + dailyStatistics[day] += minutes; } + const todayStart = new Date(todayDate); + todayStart.setHours(0, 0, 0, 0); + + const todayEnd = new Date(todayDate); + todayEnd.setHours(23, 59, 59, 999); + + const enabledFocusTargetList = + await prisma.enabledFocusTargetTimeTable.findMany({ + where: { + focusTarget: {userId}, + measurementStartAt: { + gte: todayStart, + lt: todayEnd, + }, + }, + include: { + focusTarget: { + select: {target: true}, + }, + }, + }); + + const formattedEnabledFocusTargetList = enabledFocusTargetList.map( + enabledFocusTarget => ({ + targetId: enabledFocusTarget.focusTargetId!.toString(), + target: enabledFocusTarget.focusTarget!.target, + startTime: enabledFocusTarget.measurementStartAt, + endTime: enabledFocusTarget.measurementEndAt, + }), + ); + + const disabledFocusTargetList = + await prisma.disabledFocusTargetTimeTable.findMany({ + where: { + focusTarget: {userId}, + measurementStartAt: { + gte: todayStart, + lt: todayEnd, + }, + }, + include: { + focusTarget: { + select: {target: true}, + }, + }, + }); + + const formattedDisabledFocusTargetList = disabledFocusTargetList.map( + disabledFocusTarget => ({ + targetId: disabledFocusTarget.focusTargetId!.toString(), + target: disabledFocusTarget.focusTarget!.target, + startTime: disabledFocusTarget.measurementStartAt, + endTime: disabledFocusTarget.measurementEndAt, + }), + ); + + const formattedDailyStatistics = { + dailyTotalTime: dailyStatistics, + today: { + disabledTarget: formattedDisabledFocusTargetList, + enabledTarget: formattedEnabledFocusTargetList, + }, + }; + + return formattedDailyStatistics; +} + +function getKSTDay( + date: Date, +): keyof DailyStatisticsResponse['dailyTotalTime'] { + const KST_OFFSET = 9 * 60 * 60 * 1000; + const localDate = new Date(date.getTime() + KST_OFFSET); + return localDate + .getDay() + .toString() as keyof DailyStatisticsResponse['dailyTotalTime']; } diff --git a/src/repositories/schedule.repository.ts b/src/repositories/schedule.repository.ts index eeb3738..d948c6b 100644 --- a/src/repositories/schedule.repository.ts +++ b/src/repositories/schedule.repository.ts @@ -9,6 +9,7 @@ export const createSchedule = async ( data: BodyToSchedule, userId: bigint, ): Promise => { + console.log(data); const checkOverlapping = await prisma.schedule.findFirst({ where: { userId: userId, diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts index 736902f..d4bac31 100644 --- a/src/routers/auth.router.ts +++ b/src/routers/auth.router.ts @@ -46,6 +46,7 @@ authRouter.get( passport.authenticate('kakao', {failureRedirect: '/'}), (req, res) => { // 로그인 성공 시 처리 - res.redirect('/user/mypage'); // 온보딩 페이지로 리디렉션(예정) + // res.redirect('/user/mypage'); // 온보딩 페이지로 리디렉션(예정) + res.redirect('http://localhost:3000/onboarding'); }, ); diff --git a/src/routers/tsoaRoutes.ts b/src/routers/tsoaRoutes.ts index f2cd25c..d2fb5af 100644 --- a/src/routers/tsoaRoutes.ts +++ b/src/routers/tsoaRoutes.ts @@ -195,6 +195,25 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DailyStatisticsResponse": { + "dataType": "refObject", + "properties": { + "dailyTotalTime": {"dataType":"nestedObjectLiteral","nestedProperties":{"0":{"dataType":"double","required":true},"1":{"dataType":"double","required":true},"2":{"dataType":"double","required":true},"3":{"dataType":"double","required":true},"4":{"dataType":"double","required":true},"5":{"dataType":"double","required":true},"6":{"dataType":"double","required":true}},"required":true}, + "today": {"dataType":"nestedObjectLiteral","nestedProperties":{"enabledTarget":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endTime":{"dataType":"datetime","required":true},"startTime":{"dataType":"datetime","required":true},"targetId":{"dataType":"string","required":true},"target":{"dataType":"string","required":true}}},"required":true},"disabledTarget":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"endTime":{"dataType":"datetime","required":true},"startTime":{"dataType":"datetime","required":true},"targetId":{"dataType":"string","required":true},"target":{"dataType":"string","required":true}}},"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ITsoaSuccessResponse_DailyStatisticsResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"string","required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"DailyStatisticsResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa }; const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); @@ -213,7 +232,6 @@ export function RegisterRoutes(app: Router) { const argsScheduleController_handleScheduleAdd: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"ref":"BodyToSchedule"}, }; app.post('/schedule', @@ -244,7 +262,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsScheduleController_handleWeekScheduleGet: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, }; app.get('/schedule/weekly', ...(fetchMiddlewares(ScheduleController)), @@ -305,7 +322,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsGroupController_handleGroupAdd: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"ref":"BodyToGroup"}, }; app.post('/group', @@ -336,7 +352,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsGroupController_handleGroupGet: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, groupIdParam: {"in":"path","name":"groupId","required":true,"dataType":"string"}, }; app.get('/group/:groupId', @@ -367,7 +382,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsGroupController_handleGroupListGet: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, }; app.get('/group', ...(fetchMiddlewares(GroupController)), @@ -397,7 +411,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsGroupController_handledGroupJoin: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, groupIdParam: {"in":"path","name":"groupId","required":true,"dataType":"string"}, }; app.post('/group/:groupId/user', @@ -490,7 +503,6 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsFocusTargetController_GetFocusTargetList: Record = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, }; app.get('/focusTarget', ...(fetchMiddlewares(FocusTargetController)), @@ -519,6 +531,35 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsFocusTargetController_GetDailyStatistics: Record = { + }; + app.get('/focusTarget/statistics/daily', + ...(fetchMiddlewares(FocusTargetController)), + ...(fetchMiddlewares(FocusTargetController.prototype.GetDailyStatistics)), + + async function FocusTargetController_GetDailyStatistics(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsFocusTargetController_GetDailyStatistics, request, response }); + + const controller = new FocusTargetController(); + + await templateService.apiHandler({ + methodName: 'GetDailyStatistics', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/services/focus-target.service.ts b/src/services/focus-target.service.ts index aadad81..cbcf473 100644 --- a/src/services/focus-target.service.ts +++ b/src/services/focus-target.service.ts @@ -4,14 +4,17 @@ import { } from 'src/dtos/focus-target.dto.js'; import {FocusTargetEnableError} from 'src/error.js'; import { + DailyStatisticsResponse, FocusTargetListResponse, FocusTargetResponse, } from 'src/models/focus-target.model.js'; import { allowFocusTarget, + getDailyStatistics, getFocusTargetList, notAllowFocusTarget, } from 'src/repositories/focus-target.repository.js'; +import {responseFromDailyStatistics} from '../dtos/focus-target.dto.js'; export const FocusTargetUpdateEnable = async ( focusTargetId: bigint, @@ -43,3 +46,10 @@ export const FocusTargetListGet = async ( const focusTargetList = await getFocusTargetList(userId); return responseFromFocusTargetList(focusTargetList); }; + +export const DailyStatisticsGet = async ( + userId: bigint, +): Promise => { + const dailyStatistics = await getDailyStatistics(userId); + return responseFromDailyStatistics(dailyStatistics); +}; diff --git a/swagger/swagger.json b/swagger/swagger.json index d2ce3f4..38f839e 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -792,6 +792,147 @@ ], "type": "object", "additionalProperties": false + }, + "DailyStatisticsResponse": { + "properties": { + "dailyTotalTime": { + "properties": { + "0": { + "type": "number", + "format": "double" + }, + "1": { + "type": "number", + "format": "double" + }, + "2": { + "type": "number", + "format": "double" + }, + "3": { + "type": "number", + "format": "double" + }, + "4": { + "type": "number", + "format": "double" + }, + "5": { + "type": "number", + "format": "double" + }, + "6": { + "type": "number", + "format": "double" + } + }, + "required": [ + "6", + "5", + "4", + "3", + "2", + "1", + "0" + ], + "type": "object" + }, + "today": { + "properties": { + "enabledTarget": { + "items": { + "properties": { + "endTime": { + "type": "string", + "format": "date-time" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "targetId": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "endTime", + "startTime", + "targetId", + "target" + ], + "type": "object" + }, + "type": "array" + }, + "disabledTarget": { + "items": { + "properties": { + "endTime": { + "type": "string", + "format": "date-time" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "targetId": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "endTime", + "startTime", + "targetId", + "target" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "enabledTarget", + "disabledTarget" + ], + "type": "object" + } + }, + "required": [ + "dailyTotalTime", + "today" + ], + "type": "object", + "additionalProperties": false + }, + "ITsoaSuccessResponse_DailyStatisticsResponse_": { + "properties": { + "resultType": { + "type": "string" + }, + "error": { + "type": "number", + "enum": [ + null + ], + "nullable": true + }, + "success": { + "$ref": "#/components/schemas/DailyStatisticsResponse" + } + }, + "required": [ + "resultType", + "error", + "success" + ], + "type": "object", + "additionalProperties": false } }, "securitySchemes": { @@ -1555,9 +1696,74 @@ "security": [], "parameters": [] } + }, + "/focusTarget/statistics/daily": { + "get": { + "operationId": "GetDailyStatistics", + "responses": { + "200": { + "description": "일간 통계 조회 성공 응답", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ITsoaSuccessResponse_DailyStatisticsResponse_" + }, + "examples": { + "Example 1": { + "value": { + "resultType": "SUCCESS", + "error": null, + "success": { + "dailyTotalTime": { + "0": 60, + "1": 60, + "2": 60, + "3": 60, + "4": 60, + "5": 60, + "6": 60 + }, + "today": { + "disabledTarget": [ + { + "target": "책/교재", + "targetId": "1", + "startTime": "2025-01-17T03:50:25", + "endTime": "2025-01-17T04:50:25" + } + ], + "enabledTarget": [ + { + "target": "책/교재", + "targetId": "1", + "startTime": "2025-01-17T03:50:25", + "endTime": "2025-01-17T04:50:25" + } + ] + } + } + } + } + } + } + } + } + }, + "description": "집중 시간 일간 통계를 조회하는 API입니다.\n\n0: 토, 1: 일, 2: 월, 3: 화, 4: 수, 5: 목, 6: 금", + "summary": "일간 통계 조회 API", + "tags": [ + "Focus-Target-Controller" + ], + "security": [], + "parameters": [] + } } }, "servers": [ + { + "url": "http://18.208.62.86:3000", + "description": "Production Server" + }, { "url": "http://localhost:3000", "description": "local server" diff --git a/yarn.lock b/yarn.lock index d3fe8ce..4e428f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1632,6 +1632,16 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +date-fns-tz@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz#647dc56d38ac33a3e37b65e9d5c4cda5af5e58e6" + integrity sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ== + +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"