From 5088db430be8891ba1a081a12e4f7869fe121f40 Mon Sep 17 00:00:00 2001 From: oreo Date: Wed, 27 Nov 2024 03:34:53 +0900 Subject: [PATCH 1/5] feat: interface MEE007, 009, 011 --- .../interface/src/api/meeting/apiMee007.ts | 60 +++++++++++++++++++ .../interface/src/api/meeting/apiMee009.ts | 57 ++++++++++++++++++ .../interface/src/api/meeting/apiMee011.ts | 52 ++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 packages/interface/src/api/meeting/apiMee007.ts create mode 100644 packages/interface/src/api/meeting/apiMee009.ts create mode 100644 packages/interface/src/api/meeting/apiMee011.ts diff --git a/packages/interface/src/api/meeting/apiMee007.ts b/packages/interface/src/api/meeting/apiMee007.ts new file mode 100644 index 000000000..46f793080 --- /dev/null +++ b/packages/interface/src/api/meeting/apiMee007.ts @@ -0,0 +1,60 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; + +/** + * @version v0.1 + * @description 해당 meeting의 모든 meetingAgenda의 정보를 가져옵니다. + */ + +const url = (meetingId: number) => + `/executive/meetings/meeting/${meetingId}/agendas`; +const method = "GET"; + +const requestParam = z.object({ + meetingId: z.coerce.number().int().min(1), +}); + +const requestQuery = z.object({}); + +const requestBody = z.object({}); + +const responseBodyMap = { + [HttpStatusCode.Ok]: z.object({ + agendas: z + .object({ + agendaId: z.coerce.number().int().min(1), + agendaEnumId: z.coerce.number().int().min(1), + title: z.coerce.string(), + description: z.coerce.string(), + }) + .array(), + }), +}; + +const responseErrorMap = {}; + +const apiMee007 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiMee007RequestParam = z.infer; +type ApiMee007RequestQuery = z.infer; +type ApiMee007RequestBody = z.infer; +type ApiMee007ResponseCreated = z.infer< + (typeof apiMee007.responseBodyMap)[200] +>; + +export default apiMee007; + +export type { + ApiMee007RequestParam, + ApiMee007RequestQuery, + ApiMee007RequestBody, + ApiMee007ResponseCreated, +}; diff --git a/packages/interface/src/api/meeting/apiMee009.ts b/packages/interface/src/api/meeting/apiMee009.ts new file mode 100644 index 000000000..a16c36645 --- /dev/null +++ b/packages/interface/src/api/meeting/apiMee009.ts @@ -0,0 +1,57 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; + +/** + * @version v0.1 + * @description 전체 agendaList에 대한 순서 변경을 진행합니다. + */ + +const url = (meetingId: number) => + `/executive/meetings/meeting/${meetingId}/agendas`; +const method = "PATCH"; + +const requestParam = z.object({ + meetingId: z.coerce.number().int().min(1), +}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + agendaIdList: z + .object({ + agendaId: z.coerce.number().int().min(1), + }) + .array(), +}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({}), +}; + +const responseErrorMap = {}; + +const apiMee009 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiMee009RequestParam = z.infer; +type ApiMee009RequestQuery = z.infer; +type ApiMee009RequestBody = z.infer; +type ApiMee009ResponseCreated = z.infer< + (typeof apiMee009.responseBodyMap)[201] +>; + +export default apiMee009; + +export type { + ApiMee009RequestParam, + ApiMee009RequestQuery, + ApiMee009RequestBody, + ApiMee009ResponseCreated, +}; diff --git a/packages/interface/src/api/meeting/apiMee011.ts b/packages/interface/src/api/meeting/apiMee011.ts new file mode 100644 index 000000000..ae8d3cfb0 --- /dev/null +++ b/packages/interface/src/api/meeting/apiMee011.ts @@ -0,0 +1,52 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; + +/** + * @version v0.1 + * @description 다른 회의에서 이미 존재하는 안건을 불러와서 현재 회의와의 새로운 Mapping을 추가합니다. + */ + +const url = (meetingId: number, agendaId: number) => + `/executive/meetings/meeting/${meetingId}/agendas/agenda/${agendaId}`; +const method = "POST"; + +const requestParam = z.object({ + meetingId: z.coerce.number().int().min(1), + agendaId: z.coerce.number().int().min(1), +}); + +const requestQuery = z.object({}); + +const requestBody = z.object({}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({}), +}; + +const responseErrorMap = {}; + +const apiMee011 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiMee011RequestParam = z.infer; +type ApiMee011RequestQuery = z.infer; +type ApiMee011RequestBody = z.infer; +type ApiMee011ResponseCreated = z.infer< + (typeof apiMee011.responseBodyMap)[201] +>; + +export default apiMee011; + +export type { + ApiMee011RequestParam, + ApiMee011RequestQuery, + ApiMee011RequestBody, + ApiMee011ResponseCreated, +}; From c27d2c23b0c24b893ba8be73ee11788e44cd264b Mon Sep 17 00:00:00 2001 From: oreo Date: Wed, 27 Nov 2024 03:43:20 +0900 Subject: [PATCH 2/5] fix: bringing param, body more comfortable in MEE006, 008, 010 --- .../meeting/agenda/agenda.controller.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/api/src/feature/meeting/agenda/agenda.controller.ts b/packages/api/src/feature/meeting/agenda/agenda.controller.ts index 7126ae5a4..084a973f9 100644 --- a/packages/api/src/feature/meeting/agenda/agenda.controller.ts +++ b/packages/api/src/feature/meeting/agenda/agenda.controller.ts @@ -40,15 +40,15 @@ export default class AgendaController { @UsePipes(new ZodPipe(apiMee006)) async postExecutiveMeetingAgenda( @GetExecutive() user: GetExecutive, - @Param() { meetingId }: ApiMee006RequestParam, - @Body() { meetingEnumId, description, title }: ApiMee006RequestBody, + @Param() param: ApiMee006RequestParam, + @Body() body: ApiMee006RequestBody, ): Promise { const result = await this.meetingService.postExecutiveMeetingAgenda( user.executiveId, - meetingId, - meetingEnumId, - description, - title, + param.meetingId, + body.meetingEnumId, + body.description, + body.title, ); return result; } @@ -58,15 +58,15 @@ export default class AgendaController { @UsePipes(new ZodPipe(apiMee008)) async patchExecutiveMeetingAgenda( @GetExecutive() user: GetExecutive, - @Param() { agendaId }: ApiMee008RequestParam, - @Body() { agendaEnumId, description, title }: ApiMee008RequestBody, + @Param() param: ApiMee008RequestParam, + @Body() body: ApiMee008RequestBody, ): Promise { const result = await this.meetingService.patchExecutiveMeetingAgenda( user.executiveId, - agendaId, - agendaEnumId, - description, - title, + param.agendaId, + body.agendaEnumId, + body.description, + body.title, ); return result; } @@ -76,12 +76,12 @@ export default class AgendaController { @UsePipes(new ZodPipe(apiMee010)) async deleteExecutiveMeetingAgenda( @GetExecutive() user: GetExecutive, - @Param() { meetingId, agendaId }: ApiMee010RequestParam, + @Param() param: ApiMee010RequestParam, ): Promise { const result = await this.meetingService.deleteExecutiveMeetingAgenda( user.executiveId, - meetingId, - agendaId, + param.meetingId, + param.agendaId, ); return result; } From 132260ace2fcaa57ecd5f997114711c69fd5465a Mon Sep 17 00:00:00 2001 From: oreo Date: Wed, 27 Nov 2024 07:40:27 +0900 Subject: [PATCH 3/5] feat: api MEE 007, 009, 011 --- .../meeting/agenda/agenda.controller.ts | 55 ++++++++ .../feature/meeting/agenda/agenda.service.ts | 64 +++++++++ .../src/feature/meeting/meeting.repository.ts | 129 ++++++++++++++++++ .../interface/src/api/meeting/apiMee009.ts | 6 +- 4 files changed, 249 insertions(+), 5 deletions(-) diff --git a/packages/api/src/feature/meeting/agenda/agenda.controller.ts b/packages/api/src/feature/meeting/agenda/agenda.controller.ts index 084a973f9..61fb56c25 100644 --- a/packages/api/src/feature/meeting/agenda/agenda.controller.ts +++ b/packages/api/src/feature/meeting/agenda/agenda.controller.ts @@ -14,17 +14,33 @@ import apiMee006, { ApiMee006ResponseCreated, } from "@sparcs-clubs/interface/api/meeting/apiMee006"; +import apiMee007, { + ApiMee007RequestParam, + ApiMee007ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee007"; + import apiMee008, { ApiMee008RequestBody, ApiMee008RequestParam, ApiMee008ResponseOk, } from "@sparcs-clubs/interface/api/meeting/apiMee008"; +import apiMee009, { + ApiMee009RequestBody, + ApiMee009RequestParam, + ApiMee009ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee009"; + import apiMee010, { ApiMee010RequestParam, ApiMee010ResponseOk, } from "@sparcs-clubs/interface/api/meeting/apiMee010"; +import apiMee011, { + ApiMee011RequestParam, + ApiMee011ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee011"; + import { ZodPipe } from "@sparcs-clubs/api/common/pipe/zod-pipe"; import { Executive } from "@sparcs-clubs/api/common/util/decorators/method-decorator"; import { GetExecutive } from "@sparcs-clubs/api/common/util/decorators/param-decorator"; @@ -53,6 +69,15 @@ export default class AgendaController { return result; } + @Post("/executive/meetings/meeting/:meetingId/agendas") + @UsePipes(new ZodPipe(apiMee007)) + async getExecutiveMeetingAgendas( + @Param() param: ApiMee007RequestParam, + ): Promise { + const result = await this.meetingService.getMeetingAgendas(param); + return result; + } + @Executive() @Patch("/executive/meetings/meeting/:meetingId/agendas/agenda/:agendaId") @UsePipes(new ZodPipe(apiMee008)) @@ -71,6 +96,22 @@ export default class AgendaController { return result; } + @Executive() + @Patch("/executive/meetings/meeting/:meetingId/agendas") + @UsePipes(new ZodPipe(apiMee009)) + async patchExecutiveMeetingAgendasOrder( + @GetExecutive() user: GetExecutive, + @Param() param: ApiMee009RequestParam, + @Body() body: ApiMee009RequestBody, + ): Promise { + const result = await this.meetingService.patchExecutiveMeetingAgendasOrder( + user.executiveId, + param, + body, + ); + return result; + } + @Executive() @Delete("/executive/meetings/meeting/:meetingId/agendas/agenda/:agendaId") @UsePipes(new ZodPipe(apiMee010)) @@ -85,4 +126,18 @@ export default class AgendaController { ); return result; } + + @Executive() + @Delete("/executive/meetings/meeting/:meetingId/agendas/agenda/:agendaId") + @UsePipes(new ZodPipe(apiMee011)) + async postExecutiveMeetingAgendaImport( + @GetExecutive() user: GetExecutive, + @Param() param: ApiMee011RequestParam, + ): Promise { + const result = await this.meetingService.postExecutiveMeetingAgendaImport( + user.executiveId, + param, + ); + return result; + } } diff --git a/packages/api/src/feature/meeting/agenda/agenda.service.ts b/packages/api/src/feature/meeting/agenda/agenda.service.ts index b577f8593..532f3532c 100644 --- a/packages/api/src/feature/meeting/agenda/agenda.service.ts +++ b/packages/api/src/feature/meeting/agenda/agenda.service.ts @@ -7,10 +7,26 @@ import { import { ApiMee006ResponseCreated } from "@sparcs-clubs/interface/api/meeting/apiMee006"; +import { + ApiMee007RequestParam, + ApiMee007ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee007"; + import { ApiMee008ResponseOk } from "@sparcs-clubs/interface/api/meeting/apiMee008"; +import { + ApiMee009RequestBody, + ApiMee009RequestParam, + ApiMee009ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee009"; + import { ApiMee010ResponseOk } from "@sparcs-clubs/interface/api/meeting/apiMee010"; +import { + ApiMee011RequestParam, + ApiMee011ResponseCreated, +} from "@sparcs-clubs/interface/api/meeting/apiMee011"; + import UserPublicService from "@sparcs-clubs/api/feature/user/service/user.public.service"; import { MeetingRepository } from "../meeting.repository"; @@ -47,6 +63,13 @@ export class AgendaService { return {}; } + async getMeetingAgendas( + param: ApiMee007RequestParam, + ): Promise { + const res = await this.meetingRepository.getMeetingAgendas(param.meetingId); + return res; + } + async patchExecutiveMeetingAgenda( executiveId: number, agendaId: number, @@ -72,6 +95,27 @@ export class AgendaService { return {}; } + async patchExecutiveMeetingAgendasOrder( + executiveId: number, + param: ApiMee009RequestParam, + body: ApiMee009RequestBody, + ): Promise { + const user = await this.userPublicService.getExecutiveById({ + id: executiveId, + }); + + if (!user) { + throw new HttpException("Executive not found", HttpStatus.NOT_FOUND); + } + + await this.meetingRepository.updateMeetingAgendasOrder( + param.meetingId, + body.agendaIdList, + ); + + return {}; + } + async deleteExecutiveMeetingAgenda( executiveId: number, meetingId: number, @@ -92,4 +136,24 @@ export class AgendaService { return {}; } + + async postExecutiveMeetingAgendaImport( + executiveId: number, + param: ApiMee011RequestParam, + ): Promise { + const user = await this.userPublicService.getExecutiveById({ + id: executiveId, + }); + + if (!user) { + throw new HttpException("Executive not found", HttpStatus.NOT_FOUND); + } + + await this.meetingRepository.addMeetingAgendaMapping( + param.meetingId, + param.agendaId, + ); + + return {}; + } } diff --git a/packages/api/src/feature/meeting/meeting.repository.ts b/packages/api/src/feature/meeting/meeting.repository.ts index 580e4a62f..691b9edf5 100644 --- a/packages/api/src/feature/meeting/meeting.repository.ts +++ b/packages/api/src/feature/meeting/meeting.repository.ts @@ -383,6 +383,40 @@ export class MeetingRepository { return isInsertAgendaAndMappingSuccess; } + async getMeetingAgendas(meetingId: number) { + const rawAgendas = await this.db + .select({ + agendaId: MeetingAgenda.id, + agendaEnumId: MeetingAgenda.MeetingAgendaEnum, + title: MeetingAgenda.title, + description: MeetingAgenda.description, + }) + .from(MeetingMapping) + .innerJoin( + MeetingAgenda, + eq(MeetingMapping.meetingAgendaId, MeetingAgenda.id), + ) + .where(eq(MeetingMapping.meetingId, meetingId)); + + if (rawAgendas.length === 0) { + logger.debug( + `[MeetingRepository] No agendas found for meeting ID: ${meetingId}`, + ); + return null; + } + + const response = { + agendas: rawAgendas.map(agenda => ({ + agendaId: agenda.agendaId, + agendaEnumId: agenda.agendaEnumId, + title: agenda.title, + description: agenda.description, + })), + }; + + return response; + } + async updateMeetingAgenda( agendaId: number, agendaEnumId: number, @@ -486,4 +520,99 @@ export class MeetingRepository { return meetingAgendaMappingDeleteResult; } + + async updateMeetingAgendasOrder(meetingId: number, agendaIdList: number[]) { + if (agendaIdList.length === 0) { + logger.debug( + `[MeetingRepository] No agenda IDs provided for meeting ID: ${meetingId}`, + ); + return null; // No need to update if the list is empty + } + + await this.db.transaction(async trx => { + const updatePromises = agendaIdList.map((agendaId, index) => + // Create a promise for each agenda update + trx + .update(MeetingMapping) + .set({ meetingAgendaPosition: index }) // Set the new position based on the index + .where( + and( + eq(MeetingMapping.meetingAgendaId, agendaId), + eq(MeetingMapping.meetingId, meetingId), + ), + ) + .then(result => { + // Log a warning if no row was updated + if (result[0].affectedRows === 0) { + logger.warn( + `[MeetingRepository] No MeetingMapping found for meeting ID: ${meetingId} and agenda ID: ${agendaId}`, + ); + } + }), + ); + + // Wait for all updates to complete concurrently + await Promise.all(updatePromises); + }); + + logger.info( + `[MeetingRepository] Successfully updated agenda positions for meeting ID: ${meetingId}`, + ); + return { success: true }; + } + + async addMeetingAgendaMapping(meetingId: number, agendaId: number) { + // Check if a mapping already exists to avoid duplicates + const existingMapping = await this.db + .select() + .from(MeetingMapping) + .where( + and( + eq(MeetingMapping.meetingId, meetingId), + eq(MeetingMapping.meetingAgendaId, agendaId), + ), + ) + .execute(); + + if (existingMapping.length > 0) { + logger.debug( + `[MeetingRepository] Mapping already exists for meetingId: ${meetingId} and agendaId: ${agendaId}`, + ); + return { success: false, message: "Mapping already exists" }; + } + + // Get the current maximum meetingAgendaPosition for the given meetingId + const [maxPositionResult] = await this.db + .select({ + maxPosition: sql`MAX(${MeetingMapping.meetingAgendaPosition})`, + }) + .from(MeetingMapping) + .where(eq(MeetingMapping.meetingId, meetingId)) + .execute(); + + const newPosition = (maxPositionResult?.maxPosition || 0) + 1; + + // Insert the new mapping into the MeetingMapping table + const result = await this.db + .insert(MeetingMapping) + .values({ + meetingId, + meetingAgendaId: agendaId, + meetingAgendaPosition: newPosition, // Set position to the next available value + meetingAgendaEntityType: 1, // Entity type is fixed to 1 + }) + .execute(); + + // Check if insertion was successful + if (result[0].affectedRows === 1) { + logger.info( + `[MeetingRepository] Successfully added mapping for meetingId: ${meetingId} and agendaId: ${agendaId} with position: ${newPosition}`, + ); + return { success: true }; + } + logger.error( + `[MeetingRepository] Failed to add mapping for meetingId: ${meetingId} and agendaId: ${agendaId}`, + ); + return { success: false, message: "Failed to add mapping" }; + } } diff --git a/packages/interface/src/api/meeting/apiMee009.ts b/packages/interface/src/api/meeting/apiMee009.ts index a16c36645..dd23d1c94 100644 --- a/packages/interface/src/api/meeting/apiMee009.ts +++ b/packages/interface/src/api/meeting/apiMee009.ts @@ -17,11 +17,7 @@ const requestParam = z.object({ const requestQuery = z.object({}); const requestBody = z.object({ - agendaIdList: z - .object({ - agendaId: z.coerce.number().int().min(1), - }) - .array(), + agendaIdList: z.number().array().min(1), }); const responseBodyMap = { From 2d08ee402d0760e4ce21fffcf9f0a35062e1daad Mon Sep 17 00:00:00 2001 From: oreo Date: Sat, 4 Jan 2025 18:57:37 +0900 Subject: [PATCH 4/5] fix: meeting agenda API --- .../src/feature/meeting/agenda/agenda.controller.ts | 6 +++--- .../api/src/feature/meeting/agenda/agenda.service.ts | 12 +++++++----- .../api/src/feature/meeting/meeting.repository.ts | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/api/src/feature/meeting/agenda/agenda.controller.ts b/packages/api/src/feature/meeting/agenda/agenda.controller.ts index 61fb56c25..1242b243d 100644 --- a/packages/api/src/feature/meeting/agenda/agenda.controller.ts +++ b/packages/api/src/feature/meeting/agenda/agenda.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, + Get, Param, Patch, Post, @@ -69,7 +70,7 @@ export default class AgendaController { return result; } - @Post("/executive/meetings/meeting/:meetingId/agendas") + @Get("/executive/meetings/meeting/:meetingId/agendas") @UsePipes(new ZodPipe(apiMee007)) async getExecutiveMeetingAgendas( @Param() param: ApiMee007RequestParam, @@ -121,8 +122,7 @@ export default class AgendaController { ): Promise { const result = await this.meetingService.deleteExecutiveMeetingAgenda( user.executiveId, - param.meetingId, - param.agendaId, + param, ); return result; } diff --git a/packages/api/src/feature/meeting/agenda/agenda.service.ts b/packages/api/src/feature/meeting/agenda/agenda.service.ts index 532f3532c..7ecfe5347 100644 --- a/packages/api/src/feature/meeting/agenda/agenda.service.ts +++ b/packages/api/src/feature/meeting/agenda/agenda.service.ts @@ -20,7 +20,10 @@ import { ApiMee009ResponseCreated, } from "@sparcs-clubs/interface/api/meeting/apiMee009"; -import { ApiMee010ResponseOk } from "@sparcs-clubs/interface/api/meeting/apiMee010"; +import { + ApiMee010RequestParam, + ApiMee010ResponseOk, +} from "@sparcs-clubs/interface/api/meeting/apiMee010"; import { ApiMee011RequestParam, @@ -118,8 +121,7 @@ export class AgendaService { async deleteExecutiveMeetingAgenda( executiveId: number, - meetingId: number, - agendaId: number, + param: ApiMee010RequestParam, ): Promise { const user = await this.userPublicService.getExecutiveById({ id: executiveId, @@ -130,8 +132,8 @@ export class AgendaService { } await this.meetingRepository.deleteMeetingAgendaMapping( - meetingId, - agendaId, + param.meetingId, + param.agendaId, ); return {}; diff --git a/packages/api/src/feature/meeting/meeting.repository.ts b/packages/api/src/feature/meeting/meeting.repository.ts index 691b9edf5..c3678a463 100644 --- a/packages/api/src/feature/meeting/meeting.repository.ts +++ b/packages/api/src/feature/meeting/meeting.repository.ts @@ -539,6 +539,7 @@ export class MeetingRepository { and( eq(MeetingMapping.meetingAgendaId, agendaId), eq(MeetingMapping.meetingId, meetingId), + isNull(MeetingMapping.deletedAt), // Filter out soft-deleted records ), ) .then(result => { From 0c081156308f4a5b2553eab6e732baef5b4717be Mon Sep 17 00:00:00 2001 From: oreo Date: Sat, 4 Jan 2025 18:58:10 +0900 Subject: [PATCH 5/5] feat: meeting agenda entity type enum --- .../src/common/enum/meetingAgendaEntityType.enum.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/interface/src/common/enum/meetingAgendaEntityType.enum.ts diff --git a/packages/interface/src/common/enum/meetingAgendaEntityType.enum.ts b/packages/interface/src/common/enum/meetingAgendaEntityType.enum.ts new file mode 100644 index 000000000..57bb84b2e --- /dev/null +++ b/packages/interface/src/common/enum/meetingAgendaEntityType.enum.ts @@ -0,0 +1,13 @@ +/** + * @description + * Agenda에 연결될 수 있는 Entity 분류를 위한 enum + * Content: Mapping하는 요소가 회의록 등의 글인 경우 + * Vote: Mapping하는 요소가 회의에서 진행된 투표인 경우 + * Agenda: 그냥 Agenda까지만 나타내기 위한 Mapping인 경우 + */ + +export enum MeetingAgendaEntityType { + Content = 1, + Vote, + Agenda, +}