diff --git a/packages/api/src/feature/organization/controller/organization.controller.ts b/packages/api/src/feature/organization/controller/organization.controller.ts index c403d4f..1e8288e 100644 --- a/packages/api/src/feature/organization/controller/organization.controller.ts +++ b/packages/api/src/feature/organization/controller/organization.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Param, Post, UsePipes } from "@nestjs/common"; +import { + Body, + Controller, + Get, + Param, + Post, + Put, + UsePipes, +} from "@nestjs/common"; import { ZodPipe } from "@sparcs-students/api/common/pipes/zod-pipe"; import { @@ -9,7 +17,23 @@ import { ApiOrg002RequestUrl, apiOrg002, ApiOrg002RequestBody, - ApiOrg002ResponseOK, + ApiOrg002ResponseCreated, + ApiOrg003RequestUrl, + apiOrg003, + ApiOrg003RequestBody, + ApiOrg003ResponseCreated, + ApiOrg004RequestUrl, + apiOrg004, + ApiOrg004ResponseOK, + ApiOrg004RequestBody, + ApiOrg005RequestBody, + ApiOrg005ResponseCreated, + ApiOrg005RequestUrl, + apiOrg005, + ApiOrg006RequestBody, + ApiOrg006ResponseCreated, + apiOrg006, + ApiOrg006RequestUrl, } from "@sparcs-students/interface/api/organization/index"; import { OrganizationService } from "../service/organization.service"; @@ -30,8 +54,45 @@ export class OrganizationController { @UsePipes(new ZodPipe(apiOrg002)) async postOrganization( @Body() body: ApiOrg002RequestBody, - ): Promise { + ): Promise { const res = await this.organizationService.postOrganization(body); return res; } + + @Post(ApiOrg003RequestUrl) + @UsePipes(new ZodPipe(apiOrg003)) + async postOrganizationPresident( + @Body() body: ApiOrg003RequestBody, + ): Promise { + const res = await this.organizationService.postOrganizationPresident(body); + return res; + } + + @Put(ApiOrg004RequestUrl) + @UsePipes(new ZodPipe(apiOrg004)) + async putOrganizationPresidentRetire( + @Body() body: ApiOrg004RequestBody, + ): Promise { + const res = + await this.organizationService.putOrganizationPresidentRetire(body); + return res; + } + + @Post(ApiOrg005RequestUrl) + @UsePipes(new ZodPipe(apiOrg005)) + async postOrganizationMember( + @Body() body: ApiOrg005RequestBody, + ): Promise { + const res = await this.organizationService.postOrganizationMember(body); + return res; + } + + @Post(ApiOrg006RequestUrl) + @UsePipes(new ZodPipe(apiOrg006)) + async postOrganizationManager( + @Body() body: ApiOrg006RequestBody, + ): Promise { + const res = await this.organizationService.postOrganizationManager(body); + return res; + } } diff --git a/packages/api/src/feature/organization/organization.module.ts b/packages/api/src/feature/organization/organization.module.ts index 4cccd4b..be16f92 100644 --- a/packages/api/src/feature/organization/organization.module.ts +++ b/packages/api/src/feature/organization/organization.module.ts @@ -2,13 +2,14 @@ import { Module } from "@nestjs/common"; import { DrizzleModule } from "src/drizzle/drizzle.module"; import { SemesterModule } from "src/feature/semester/semester.module"; +import { UserModule } from "src/feature/user/user.module"; import { OrganizationService } from "./service/organization.service"; import { OrganizationController } from "./controller/organization.controller"; import { OrganizationPublicService } from "./service/organization.public.service"; import { OrganizationRepository } from "./repository/organization.repository"; @Module({ - imports: [DrizzleModule, SemesterModule], + imports: [DrizzleModule, SemesterModule, UserModule], controllers: [OrganizationController], providers: [ OrganizationService, diff --git a/packages/api/src/feature/organization/repository/organization.repository.ts b/packages/api/src/feature/organization/repository/organization.repository.ts index d6ac11f..28e9d11 100644 --- a/packages/api/src/feature/organization/repository/organization.repository.ts +++ b/packages/api/src/feature/organization/repository/organization.repository.ts @@ -1,6 +1,14 @@ import { Injectable, Inject } from "@nestjs/common"; -import { ApiOrg002RequestBody } from "@sparcs-students/interface/api/organization/index"; -import { and, or, lte, gte, eq, isNull } from "drizzle-orm"; +import logger from "@sparcs-students/api/common/util/logger"; + +import { + ApiOrg002RequestBody, + ApiOrg003RequestBody, + ApiOrg005RequestBody, + ApiOrg006RequestBody, +} from "@sparcs-students/interface/api/organization/index"; + +import { and, or, lte, gte, eq, isNull, desc } from "drizzle-orm"; import { MySql2Database } from "drizzle-orm/mysql2"; import { DrizzleAsyncProvider } from "src/drizzle/drizzle.provider"; @@ -17,6 +25,9 @@ import { UserStudentT, TeamT, Team, + OrganizationMember, + OrganizationManager, + OrganizationMemberT, } from "src/drizzle/schema"; export type OrganizationWithPresidentT = { @@ -154,4 +165,186 @@ export class OrganizationRepository { const res = await this.ckOrganizationBeforeCreate(body); return res; } + + async ckOrganizationPresidentBeforeCreate( + body: Omit, + ): Promise { + const select = await this.db + .select() + .from(OrganizationPresident) + .where( + and( + eq(OrganizationPresident.organizationId, body.organizationId), + eq( + OrganizationPresident.organizationPresidentTypeEnumId, + body.organizationPresidentTypeE, + ), + + isNull(OrganizationPresident.endTerm), + ), + ) + .limit(1) + .orderBy(desc(OrganizationPresident.createdAt)); + if (select.length === 0) { + return 0; + } + return select[0].id; + } + + async updateOrganizationPresidentRetire( + organizationPresidentId: number, + endTerm: Date, + ): Promise { + await this.db + .update(OrganizationPresident) + .set({ endTerm }) + .where(eq(OrganizationPresident.id, organizationPresidentId)) + .execute(); + + const resSelect = await this.db + .select() + .from(OrganizationPresident) + .where(eq(OrganizationPresident.id, organizationPresidentId)); + if (resSelect.length === 0 || resSelect[0].id !== organizationPresidentId) { + return 0; + } + return resSelect[0].id; + } + + async createOrganizationPresident( + body: Omit, + ): Promise { + await this.db + .insert(OrganizationPresident) + .values({ + organizationId: body.organizationId, + userId: body.userId, + organizationPresidentTypeEnumId: body.organizationPresidentTypeE, + phoneNumber: body.phoneNumber, + startTerm: body.startTerm, + endTerm: body.endTerm ? body.endTerm : null, + }) + .execute(); + + const res = await this.ckOrganizationPresidentBeforeCreate(body); + return res; + } + + async ckOrganizationPresidentAlready(userId: number): Promise { + // TODO: 지금은 공직일 때만 체크하는 로직이 없는데, 언젠가는 추가해야 함 + const select = await this.db + .select() + .from(OrganizationPresident) + .where( + and( + eq(OrganizationPresident.userId, userId), + isNull(OrganizationPresident.endTerm), + ), + ) + .limit(1); + if (select.length === 0) { + // TODO: 해당 president가 공직이 아닐 경우 그냥 0을 리턴하는 로직을 추가해야 함. + return 0; + } + return select[0].id; + } + + async selectOrganizationPresidentById( + organizationPresidentId: number, + ): Promise { + const res = this.db + .select() + .from(OrganizationPresident) + .where(and(eq(OrganizationPresident.id, organizationPresidentId))); + return res; + } + + async ckOrganizationMemberBeforeCreate( + body: ApiOrg005RequestBody, + ): Promise { + const res = await this.db + .select() + .from(OrganizationMember) + .where( + and( + eq(OrganizationMember.organizationId, body.organizationId), + eq(OrganizationMember.userId, body.userId), + isNull(OrganizationMember.endTerm), + ), + ) + .orderBy(desc(OrganizationMember.createdAt)) + .limit(1); + logger.info(res); + if (res.length === 0) { + return 0; + } + return res[0].id; + } + + async createOrganizationMember(body: ApiOrg005RequestBody): Promise { + await this.db + .insert(OrganizationMember) + .values({ + organizationId: body.organizationId, + userId: body.userId, + startTerm: body.startTerm, + endTerm: body.endTerm ? body.endTerm : null, + }) + .execute(); + + const res = await this.ckOrganizationMemberBeforeCreate(body); + return res; + } + + async selectOrganizationMemberByUserIdAndOrganizationId( + userId: number, + organizationId: number, + ): Promise { + const res = await this.db + .select() + .from(OrganizationMember) + .where( + and( + eq(OrganizationMember.userId, userId), + eq(OrganizationMember.organizationId, organizationId), + isNull(OrganizationMember.endTerm), + ), + ); + return res; + } + + async ckOrganizationManagerBeforeCreate( + body: ApiOrg006RequestBody, + ): Promise { + const res = await this.db + .select() + .from(OrganizationManager) + .where( + and( + eq(OrganizationManager.organizationId, body.organizationId), + eq(OrganizationManager.userId, body.userId), + eq(OrganizationManager.semesterId, body.semesterId), + ), + ) + .orderBy(desc(OrganizationManager.createdAt)) + .limit(1); + if (res.length === 0) { + return 0; + } + return res[0].id; + } + + async createOrganizationManager(body: ApiOrg006RequestBody): Promise { + await this.db + .insert(OrganizationManager) + .values({ + organizationId: body.organizationId, + userId: body.userId, + semesterId: body.semesterId, + }) + .execute(); + + const res = await this.ckOrganizationManagerBeforeCreate(body); + return res; + } } diff --git a/packages/api/src/feature/organization/service/organization.service.ts b/packages/api/src/feature/organization/service/organization.service.ts index 486ca76..3cb7d9b 100644 --- a/packages/api/src/feature/organization/service/organization.service.ts +++ b/packages/api/src/feature/organization/service/organization.service.ts @@ -1,13 +1,28 @@ -import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from "@nestjs/common"; import { ApiOrg001RequestParam, ApiOrg001ResponseOK, ApiOrg002RequestBody, - ApiOrg002ResponseOK, + ApiOrg002ResponseCreated, + ApiOrg003RequestBody, + ApiOrg003ResponseCreated, + ApiOrg004RequestBody, + ApiOrg004ResponseOK, + ApiOrg005RequestBody, + ApiOrg005ResponseCreated, + ApiOrg006RequestBody, + ApiOrg006ResponseCreated, } from "@sparcs-students/interface/api/organization/index"; +import { OrganizationPresidentTypeE } from "@sparcs-students/interface/common/enum/organization.enum"; import { SemesterPublicService } from "src/feature/semester/semester.public.service"; +import { UserPublicService } from "src/feature/user/service/user.public.service"; import { OrganizationRepository } from "../repository/organization.repository"; @@ -16,6 +31,7 @@ export class OrganizationService { constructor( private readonly organizationRepository: OrganizationRepository, private readonly semesterPublicService: SemesterPublicService, + private readonly userPublicService: UserPublicService, ) {} async getOrganizationsBySemesterId( @@ -60,7 +76,7 @@ export class OrganizationService { async postOrganization( body: ApiOrg002RequestBody, - ): Promise { + ): Promise { const ck = await this.organizationRepository.ckOrganizationBeforeCreate(body); if (ck > 0) { @@ -80,4 +96,188 @@ export class OrganizationService { return { organizationId }; } + + async postOrganizationPresident( + body: ApiOrg003RequestBody, + ): Promise { + // president 가 있으면 president를 retire처리 하고, 새로운 president를 생성함. + + const ckAlready = + await this.organizationRepository.ckOrganizationPresidentAlready( + body.userId, + ); + + if (ckAlready > 0) { + throw new HttpException( + "Organization President already exists", + HttpStatus.BAD_REQUEST, + ); + } + + const { ignorePrev, ...bodyExceptIgnorePrev } = body; + if (!ignorePrev) { + const ck = + await this.organizationRepository.ckOrganizationPresidentBeforeCreate( + bodyExceptIgnorePrev, + ); + if (ck > 0) { + // 기존의 president 존재 -> replace하기 + const previousDay = new Date(body.startTerm); // body.startTerm 복사 + previousDay.setDate(previousDay.getDate() - 1); // 하루 감소 + const update = + await this.organizationRepository.updateOrganizationPresidentRetire( + ck, // organizationPresidentId + previousDay, // endTerm + ); + if (update < 1) { + throw new HttpException( + "Failed to update organization president", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } else if ( + body.organizationPresidentTypeE === OrganizationPresidentTypeE.Chief + ) { + // ignorePrev: true 가 cheif인 경우를 거르기 위함 + + throw new HttpException( + "Chief cannot be ignorePrev", + HttpStatus.BAD_REQUEST, + ); + } + const organizationPresidentId = + await this.organizationRepository.createOrganizationPresident( + bodyExceptIgnorePrev, + ); + + if (organizationPresidentId < 1) { + throw new HttpException( + "Failed to create organization President", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return { organizationPresidentId }; + } + + async putOrganizationPresidentRetire( + body: ApiOrg004RequestBody, + ): Promise { + const resSelectPresident = + await this.organizationRepository.selectOrganizationPresidentById( + body.organizationPresidentId, + ); + if (resSelectPresident.length === 0) { + throw new NotFoundException("Organization President not found"); + } else if (resSelectPresident[0].endTerm !== null) { + throw new HttpException( + "Organization President already retired", + HttpStatus.BAD_REQUEST, + ); + } + const update = + await this.organizationRepository.updateOrganizationPresidentRetire( + body.organizationPresidentId, + body.endTerm, + ); + if (update < 1) { + throw new HttpException( + "Failed to update organization president", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const res = + await this.organizationRepository.selectOrganizationPresidentById( + body.organizationPresidentId, + ); + if (res.length === 0 || res.length > 1) { + throw new HttpException("Unreachable", HttpStatus.INTERNAL_SERVER_ERROR); + } + return { + organizationPresidentId: res[0].id, + organizationId: res[0].organizationId, + userId: res[0].userId, + startTerm: res[0].startTerm, + endTerm: res[0].endTerm, + organizationPresidentTypeE: res[0].organizationPresidentTypeEnumId, + phoneNumber: res[0].phoneNumber, + }; + } + + async postOrganizationMember( + body: ApiOrg005RequestBody, + ): Promise { + await this.userPublicService.getUserById(body.userId); // user 존재하는지 확인 + + const ckMemberAlready = + await this.organizationRepository.ckOrganizationMemberBeforeCreate(body); + if (ckMemberAlready > 0) { + throw new HttpException( + "Organization Member already exists", + HttpStatus.BAD_REQUEST, + ); + } + const organizationMemberId = + await this.organizationRepository.createOrganizationMember(body); + if (organizationMemberId < 1) { + throw new HttpException( + "Failed to create organization member", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return { organizationMemberId }; + } + + async postOrganizationManager( + body: ApiOrg006RequestBody, + ): Promise { + await this.userPublicService.getUserById(body.userId); // user 존재하는지 확인 + + // member 존재하는지 확인 + const memberCheck = + await this.organizationRepository.selectOrganizationMemberByUserIdAndOrganizationId( + body.userId, + body.organizationId, + ); + if (memberCheck.length === 0) { + throw new NotFoundException("Organization Member not exists"); + } + + // member의 기간이 semester 기간 안에 있는 지 확인. 일부 겹치는 것은 허용 + const { startTerm, endTerm } = + await this.semesterPublicService.getSemesterById(body.semesterId); + memberCheck.forEach(member => { + if ( + member.startTerm > endTerm || + (member.endTerm !== null && member.endTerm < startTerm) + ) { + // 해당 멤버가 해당 학기 기간에 조금도 겹치지 않는 경우 + throw new HttpException( + "Organization Member is not in semester", + HttpStatus.BAD_REQUEST, + ); + } + }); + const ckManagerAlready = + await this.organizationRepository.ckOrganizationManagerBeforeCreate(body); + if (ckManagerAlready > 0) { + throw new HttpException( + "Organization Manager already exists", + HttpStatus.BAD_REQUEST, + ); + } + const organizationManagerId = + await this.organizationRepository.createOrganizationManager(body); + if (organizationManagerId < 1) { + throw new HttpException( + "Failed to create organization manager", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return { organizationManagerId }; + } } diff --git a/packages/api/src/feature/proposal/project-proposal/service/project-proposal.service.ts b/packages/api/src/feature/proposal/project-proposal/service/project-proposal.service.ts index 7eec6a4..e5fce0b 100644 --- a/packages/api/src/feature/proposal/project-proposal/service/project-proposal.service.ts +++ b/packages/api/src/feature/proposal/project-proposal/service/project-proposal.service.ts @@ -10,7 +10,7 @@ import { ApiPrp001ResponseOK, ApiPrp002ResponseOK, } from "@sparcs-students/interface/api/proposal/index"; -import { UserPublicService } from "src/feature/user/user.public.service"; +import { UserPublicService } from "@sparcs-students/api/feature/user/service/user.public.service"; import { OrganizationPublicService } from "src/feature/organization/service/organization.public.service"; import { ProjectProposalRepository } from "../repository/project-proposal.repository"; diff --git a/packages/api/src/feature/user/controller/user.controller.ts b/packages/api/src/feature/user/controller/user.controller.ts new file mode 100644 index 0000000..8bfa8d7 --- /dev/null +++ b/packages/api/src/feature/user/controller/user.controller.ts @@ -0,0 +1,26 @@ +import { Body, Controller, Post, UsePipes } from "@nestjs/common"; + +import { ZodPipe } from "@sparcs-students/api/common/pipes/zod-pipe"; + +import { + ApiUsr001RequestBody, + ApiUsr001RequestUrl, + ApiUsr001ResponseCreated, + apiUsr001, +} from "@sparcs-students/interface/api/user/index"; + +import { UserService } from "../service/user.service"; + +@Controller() +export class UserController { + constructor(private readonly userService: UserService) {} + + @Post(ApiUsr001RequestUrl) + @UsePipes(new ZodPipe(apiUsr001)) + async postUser( + @Body() body: ApiUsr001RequestBody, + ): Promise { + const res = await this.userService.postUserStudent(body); + return res; + } +} diff --git a/packages/api/src/feature/user/repository/user.repository.ts b/packages/api/src/feature/user/repository/user.repository.ts new file mode 100644 index 0000000..794a39e --- /dev/null +++ b/packages/api/src/feature/user/repository/user.repository.ts @@ -0,0 +1,109 @@ +import { Injectable, Inject, HttpException, HttpStatus } from "@nestjs/common"; +import { User, UserStudent, UserT } from "@sparcs-students/api/drizzle/schema"; +import { + ApiUsr001RequestBody, + ApiUsr001ResponseCreated, +} from "@sparcs-students/interface/api/user/index"; + +import { and, eq } from "drizzle-orm"; +import { MySql2Database } from "drizzle-orm/mysql2"; +import { DrizzleAsyncProvider } from "src/drizzle/drizzle.provider"; + +@Injectable() +export class UserRepository { + constructor( + @Inject(DrizzleAsyncProvider) private readonly db: MySql2Database, + ) {} + + /** + * @param id User id + * @returns User id에 해당하는 User 객체를 리턴합니다. + */ + async getUserById(userId: number): Promise { + const res = await this.db.select().from(User).where(eq(User.id, userId)); + return res; + } + + /** + * @param name, email + * @returns 0 (해당 id가 없을 때) or userId(있을 때)를 리턴합니다. + * service에서 0이면 create, 0이 아니면 update를 실행합니다. + */ + async ckUserBeforeCreate(name: string, email: string): Promise { + const select = await this.db + .select() + .from(User) + .where(and(eq(User.name, name), eq(User.email, email))) + .limit(1); + if (select.length === 0) { + return 0; + } + return select[0].id; + } + + /** + * @param usr001 req body + * @returns {0,0} (해당 id가 없을 때) or {userId, userStudentId}(있을 때)를 리턴합니다. + * service에서 0이면 create, 0이 아니면 update를 실행합니다. + */ + async ckUserStudentBeforeCreate( + body: ApiUsr001RequestBody, + ): Promise { + const select = await this.db + .select() + .from(User) + .innerJoin(UserStudent, eq(User.id, UserStudent.userId)) + .where( + and( + eq(User.name, body.name), + eq(User.email, body.email), + eq(UserStudent.studentNumber, body.studentNumber), + ), + ) + .limit(1); + if (select.length === 0) { + return { userId: 0, userStudentId: 0 }; + } + return { + userId: select[0].user.id, + userStudentId: select[0].user_student.id, + }; + } + + /** + * @param name,email,studentNumber + * @returns 생성된 userId를 리턴합니다. + */ + async createUserStudent( + body: ApiUsr001RequestBody, + ): Promise { + const result = await this.db.transaction(async tx => { + await tx + .insert(User) + .values({ + name: body.name, + email: body.email, + }) + .execute(); + + const userId = await this.ckUserBeforeCreate(body.name, body.email); + if (userId === 0) { + throw new HttpException( + "User create failed", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + await tx + .insert(UserStudent) + .values({ + userId, + studentNumber: body.studentNumber, + }) + .execute(); + + const res = await this.ckUserStudentBeforeCreate(body); + return res; + }); + return result; + } +} diff --git a/packages/api/src/feature/user/user.public.service.ts b/packages/api/src/feature/user/service/user.public.service.ts similarity index 91% rename from packages/api/src/feature/user/user.public.service.ts rename to packages/api/src/feature/user/service/user.public.service.ts index 3051132..91fe570 100644 --- a/packages/api/src/feature/user/user.public.service.ts +++ b/packages/api/src/feature/user/service/user.public.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { UserT } from "@sparcs-students/api/drizzle/schema"; -import { UserRepository } from "./user.repository"; +import { UserRepository } from "../repository/user.repository"; @Injectable() export class UserPublicService { diff --git a/packages/api/src/feature/user/service/user.service.ts b/packages/api/src/feature/user/service/user.service.ts new file mode 100644 index 0000000..e82557f --- /dev/null +++ b/packages/api/src/feature/user/service/user.service.ts @@ -0,0 +1,39 @@ +import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { + ApiUsr001RequestBody, + ApiUsr001ResponseCreated, +} from "@sparcs-students/interface/api/user/index"; +import { UserRepository } from "../repository/user.repository"; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async postUserStudent( + body: ApiUsr001RequestBody, + ): Promise { + const ck = await this.userRepository.ckUserStudentBeforeCreate(body); + if (ck.userId > 0 || ck.userStudentId > 0) { + throw new HttpException( + "User Student already exists", + HttpStatus.BAD_REQUEST, + ); + } + const { userId, userStudentId } = + await this.userRepository.createUserStudent(body); + if (userId < 1) { + throw new HttpException( + "Failed to create user", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + if (userId < 1) { + throw new HttpException( + "Failed to create userStudent", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return { userId, userStudentId }; + } +} diff --git a/packages/api/src/feature/user/user.module.ts b/packages/api/src/feature/user/user.module.ts index c84ce65..a48fadb 100644 --- a/packages/api/src/feature/user/user.module.ts +++ b/packages/api/src/feature/user/user.module.ts @@ -1,11 +1,14 @@ import { Module } from "@nestjs/common"; import { DrizzleModule } from "@sparcs-students/api/drizzle/drizzle.module"; -import { UserRepository } from "./user.repository"; -import { UserPublicService } from "./user.public.service"; +import { UserRepository } from "./repository/user.repository"; +import { UserPublicService } from "./service/user.public.service"; +import { UserService } from "./service/user.service"; +import { UserController } from "./controller/user.controller"; @Module({ - providers: [UserRepository, UserPublicService], - exports: [UserPublicService], imports: [DrizzleModule], + controllers: [UserController], + providers: [UserRepository, UserPublicService, UserService], + exports: [UserPublicService], }) export class UserModule {} diff --git a/packages/api/src/feature/user/user.repository.ts b/packages/api/src/feature/user/user.repository.ts deleted file mode 100644 index 81fdfc6..0000000 --- a/packages/api/src/feature/user/user.repository.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable, Inject } from "@nestjs/common"; -import { User, UserT } from "@sparcs-students/api/drizzle/schema"; -import { eq } from "drizzle-orm"; -import { MySql2Database } from "drizzle-orm/mysql2"; -import { DrizzleAsyncProvider } from "src/drizzle/drizzle.provider"; - -@Injectable() -export class UserRepository { - constructor( - @Inject(DrizzleAsyncProvider) private readonly db: MySql2Database, - ) {} - - /** - * @param id User id - * @returns User id에 해당하는 User 객체를 리턴합니다. - */ - async getUserById(userId: number): Promise { - const res = await this.db - .select() - .from(User) - .where(eq(User.id, userId)) - .limit(1); - return res; - } -} diff --git a/packages/interface/src/api/_example/endpoint/apiTmp000.ts b/packages/interface/src/api/_example/endpoint/apiTmp000.ts index a7ccaaa..eee34bc 100644 --- a/packages/interface/src/api/_example/endpoint/apiTmp000.ts +++ b/packages/interface/src/api/_example/endpoint/apiTmp000.ts @@ -14,6 +14,7 @@ import { z } from "zod"; const url = (id: string) => `/temporary/students/club/subfeature/${id}`; const method = "GET"; +export const ApiTmp000RequestUrl = "/temporary/students/club/subfeature/:id"; const requestParam = z.object({ id: z.string(), diff --git a/packages/interface/src/api/organization/endpoint/apiOrg001.ts b/packages/interface/src/api/organization/endpoint/apiOrg001.ts index 0abdabf..f81cb95 100644 --- a/packages/interface/src/api/organization/endpoint/apiOrg001.ts +++ b/packages/interface/src/api/organization/endpoint/apiOrg001.ts @@ -13,6 +13,7 @@ import { const url = (semesterId: number) => `/organizations/semester/${semesterId}`; const method = "GET"; +export const ApiOrg001RequestUrl = "/organizations/semester/:semesterId"; const requestParam = z.object({ semesterId: z.coerce.number().int().min(1), @@ -59,8 +60,6 @@ type ApiOrg001ResponseOK = z.infer<(typeof apiOrg001.responseBodyMap)[200]>; export default apiOrg001; -export const ApiOrg001RequestUrl = "/organizations/semester/:semesterId"; - export type { ApiOrg001RequestParam, ApiOrg001RequestQuery, diff --git a/packages/interface/src/api/organization/endpoint/apiOrg002.ts b/packages/interface/src/api/organization/endpoint/apiOrg002.ts index e96795a..ae90cf0 100644 --- a/packages/interface/src/api/organization/endpoint/apiOrg002.ts +++ b/packages/interface/src/api/organization/endpoint/apiOrg002.ts @@ -15,6 +15,7 @@ import { zId } from "@sparcs-students/interface/common/type/ids"; const url = () => `/uapresident/organizations/organization`; const method = "POST"; +export const ApiOrg002RequestUrl = "/uapresident/organizations/organization"; const requestParam = z.object({}); @@ -30,7 +31,7 @@ const requestBody = z.object({ }); const responseBodyMap = { - [HttpStatusCode.Ok]: z.object({ + [HttpStatusCode.Created]: z.object({ organizationId: zId, }), }; @@ -50,15 +51,15 @@ const apiOrg002 = { type ApiOrg002RequestParam = z.infer; type ApiOrg002RequestQuery = z.infer; type ApiOrg002RequestBody = z.infer; -type ApiOrg002ResponseOK = z.infer<(typeof apiOrg002.responseBodyMap)[200]>; +type ApiOrg002ResponseCreated = z.infer< + (typeof apiOrg002.responseBodyMap)[201] +>; export default apiOrg002; -export const ApiOrg002RequestUrl = "/uapresident/organizations/organization"; - export type { ApiOrg002RequestParam, ApiOrg002RequestQuery, ApiOrg002RequestBody, - ApiOrg002ResponseOK, + ApiOrg002ResponseCreated, }; diff --git a/packages/interface/src/api/organization/endpoint/apiOrg003.ts b/packages/interface/src/api/organization/endpoint/apiOrg003.ts new file mode 100644 index 0000000..84ae4c7 --- /dev/null +++ b/packages/interface/src/api/organization/endpoint/apiOrg003.ts @@ -0,0 +1,65 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; +import { OrganizationPresidentTypeE } from "@sparcs-students/interface/common/enum"; +import { zId } from "@sparcs-students/interface/common/type/ids"; +import { zPhoneNumber } from "@sparcs-students/interface/common/type/phoneNumber.type"; + +/** + * @version v0.1 + * @description 총학생회장 권한으로 새로운 기구장을 임명합니다. + * 만약 원래 기구장이 있는 경우, 임기를 종료시키고 새로운 기구장을 임명합니다. + * 만약 vice인 president의 경우, ignorePrev를 true로 설정하면 새로운 기구장을 추가합니다. + * 다만, cheif인 경우 ignorePrev를 true로 설정하면 400에러를 반환합니다. + */ + +const url = () => `/uapresident/organizations/president`; +const method = "POST"; +export const ApiOrg003RequestUrl = "/uapresident/organizations/president"; + +const requestParam = z.object({}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + userId: zId, + organizationId: zId, + startTerm: z.coerce.date(), + endTerm: z.coerce.date().optional(), + organizationPresidentTypeE: z.nativeEnum(OrganizationPresidentTypeE), + phoneNumber: zPhoneNumber, + ignorePrev: z.boolean(), +}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({ + organizationPresidentId: zId, + }), +}; + +const responseErrorMap = {}; + +const apiOrg003 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiOrg003RequestParam = z.infer; +type ApiOrg003RequestQuery = z.infer; +type ApiOrg003RequestBody = z.infer; +type ApiOrg003ResponseCreated = z.infer< + (typeof apiOrg003.responseBodyMap)[201] +>; + +export default apiOrg003; + +export type { + ApiOrg003RequestParam, + ApiOrg003RequestQuery, + ApiOrg003RequestBody, + ApiOrg003ResponseCreated, +}; diff --git a/packages/interface/src/api/organization/endpoint/apiOrg004.ts b/packages/interface/src/api/organization/endpoint/apiOrg004.ts new file mode 100644 index 0000000..62ed369 --- /dev/null +++ b/packages/interface/src/api/organization/endpoint/apiOrg004.ts @@ -0,0 +1,63 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; + +import { zId } from "@sparcs-students/interface/common/type/ids"; +import { OrganizationPresidentTypeE } from "@sparcs-students/interface/common/enum"; +import { zPhoneNumber } from "@sparcs-students/interface/common/type/phoneNumber.type"; + +/** + * @version v0.1 + * @description 총학생회장의 권한으로 기구장의 임기를 종료합니다. + */ + +const url = () => `/uapresident/organizations/president/retire`; +const method = "GET"; +export const ApiOrg004RequestUrl = + "/uapresident/organizations/president/retire"; + +const requestParam = z.object({}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + organizationPresidentId: zId, + endTerm: z.coerce.date(), +}); + +const responseBodyMap = { + [HttpStatusCode.Ok]: z.object({ + organizationPresidentId: zId, + organizationId: zId, + userId: zId, + startTerm: z.coerce.date(), + endTerm: z.coerce.date(), + organizationPresidentTypeE: z.nativeEnum(OrganizationPresidentTypeE), + phoneNumber: zPhoneNumber, + }), +}; + +const responseErrorMap = {}; + +const apiOrg004 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiOrg004RequestParam = z.infer; +type ApiOrg004RequestQuery = z.infer; +type ApiOrg004RequestBody = z.infer; +type ApiOrg004ResponseOK = z.infer<(typeof apiOrg004.responseBodyMap)[200]>; + +export default apiOrg004; + +export type { + ApiOrg004RequestParam, + ApiOrg004RequestQuery, + ApiOrg004RequestBody, + ApiOrg004ResponseOK, +}; diff --git a/packages/interface/src/api/organization/endpoint/apiOrg005.ts b/packages/interface/src/api/organization/endpoint/apiOrg005.ts new file mode 100644 index 0000000..4494a49 --- /dev/null +++ b/packages/interface/src/api/organization/endpoint/apiOrg005.ts @@ -0,0 +1,57 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; +import { zId } from "@sparcs-students/interface/common/type/ids"; + +/** + * @version v0.1 + * @description 기구장의 권한으로 OrganizationMember를 임명합니다. + */ + +const url = () => `/president/organizations/member`; +const method = "POST"; +export const ApiOrg005RequestUrl = "/president/organizations/member"; + +const requestParam = z.object({}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + userId: zId, + organizationId: zId, + startTerm: z.coerce.date(), + endTerm: z.coerce.date().optional(), +}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({ + organizationMemberId: zId, + }), +}; + +const responseErrorMap = {}; + +const apiOrg005 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiOrg005RequestParam = z.infer; +type ApiOrg005RequestQuery = z.infer; +type ApiOrg005RequestBody = z.infer; +type ApiOrg005ResponseCreated = z.infer< + (typeof apiOrg005.responseBodyMap)[201] +>; + +export default apiOrg005; + +export type { + ApiOrg005RequestParam, + ApiOrg005RequestQuery, + ApiOrg005RequestBody, + ApiOrg005ResponseCreated, +}; diff --git a/packages/interface/src/api/organization/endpoint/apiOrg006.ts b/packages/interface/src/api/organization/endpoint/apiOrg006.ts new file mode 100644 index 0000000..af48078 --- /dev/null +++ b/packages/interface/src/api/organization/endpoint/apiOrg006.ts @@ -0,0 +1,56 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; +import { zId } from "@sparcs-students/interface/common/type/ids"; + +/** + * @version v0.1 + * @description 기구장의 권한으로 OrganizationManager를 임명합니다. + */ + +const url = () => `/president/organizations/manager`; +const method = "POST"; +export const ApiOrg006RequestUrl = "/president/organizations/manager"; + +const requestParam = z.object({}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + userId: zId, + organizationId: zId, + semesterId: zId, +}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({ + organizationManagerId: zId, + }), +}; + +const responseErrorMap = {}; + +const apiOrg006 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiOrg006RequestParam = z.infer; +type ApiOrg006RequestQuery = z.infer; +type ApiOrg006RequestBody = z.infer; +type ApiOrg006ResponseCreated = z.infer< + (typeof apiOrg006.responseBodyMap)[201] +>; + +export default apiOrg006; + +export type { + ApiOrg006RequestParam, + ApiOrg006RequestQuery, + ApiOrg006RequestBody, + ApiOrg006ResponseCreated, +}; diff --git a/packages/interface/src/api/organization/index.ts b/packages/interface/src/api/organization/index.ts index 2bbb282..2e8ac23 100644 --- a/packages/interface/src/api/organization/index.ts +++ b/packages/interface/src/api/organization/index.ts @@ -3,3 +3,15 @@ export { default as apiOrg001 } from "./endpoint/apiOrg001"; // default export export * from "./endpoint/apiOrg002"; export { default as apiOrg002 } from "./endpoint/apiOrg002"; // default export 추가 + +export * from "./endpoint/apiOrg003"; +export { default as apiOrg003 } from "./endpoint/apiOrg003"; // default export 추가 + +export * from "./endpoint/apiOrg004"; +export { default as apiOrg004 } from "./endpoint/apiOrg004"; // default export 추가 + +export * from "./endpoint/apiOrg005"; +export { default as apiOrg005 } from "./endpoint/apiOrg005"; // default export 추가 + +export * from "./endpoint/apiOrg006"; +export { default as apiOrg006 } from "./endpoint/apiOrg006"; // default export 추가 diff --git a/packages/interface/src/api/proposal/endpoint/apiPrp001.ts b/packages/interface/src/api/proposal/endpoint/apiPrp001.ts index 0d0b0af..c445b33 100644 --- a/packages/interface/src/api/proposal/endpoint/apiPrp001.ts +++ b/packages/interface/src/api/proposal/endpoint/apiPrp001.ts @@ -15,6 +15,7 @@ import { AgendaAcceptedStatusE } from "@sparcs-students/interface/common/enum"; const url = () => `/student/proposals/project-proposals`; const method = "GET"; +export const ApiPrp001RequestUrl = "/student/proposals/project-proposals"; const requestParam = z.object({}); @@ -64,8 +65,6 @@ type ApiPrp001ResponseOK = z.infer<(typeof apiPrp001.responseBodyMap)[200]>; export default apiPrp001; -export const ApiPrp001RequestUrl = "/student/proposals/project-proposals"; - export type { ApiPrp001RequestParam, ApiPrp001RequestQuery, diff --git a/packages/interface/src/api/proposal/endpoint/apiPrp002.ts b/packages/interface/src/api/proposal/endpoint/apiPrp002.ts index bb6df52..6d83d92 100644 --- a/packages/interface/src/api/proposal/endpoint/apiPrp002.ts +++ b/packages/interface/src/api/proposal/endpoint/apiPrp002.ts @@ -14,6 +14,8 @@ import { zId } from "@sparcs-students/interface/common/type/ids"; const url = () => `/student/proposals/project-proposals`; const method = "GET"; +export const ApiPrp002RequestUrl = + "/student/proposals/project-proposals/project-proposal/projectProposalId"; const requestParam = z.object({ projectProposalId: zId, @@ -76,9 +78,6 @@ type ApiPrp002ResponseOK = z.infer<(typeof apiPrp002.responseBodyMap)[200]>; export default apiPrp002; -export const ApiPrp002RequestUrl = - "/student/proposals/project-proposals/project-proposal/projectProposalId"; - export type { ApiPrp002RequestParam, ApiPrp002RequestQuery, diff --git a/packages/interface/src/api/user/endpoint/apiUsr001.ts b/packages/interface/src/api/user/endpoint/apiUsr001.ts new file mode 100644 index 0000000..842436e --- /dev/null +++ b/packages/interface/src/api/user/endpoint/apiUsr001.ts @@ -0,0 +1,59 @@ +import { HttpStatusCode } from "axios"; +import { z } from "zod"; + +import { zUserName } from "@sparcs-students/interface/common/stringLength"; +import { zId } from "@sparcs-students/interface/common/type/ids"; + +/** + * @version v0.1 + * @description 새로운 학부생이 로그인을 했을 때 userDB에 추가합니다. + */ + +const url = () => `/users/sign-up/student`; +const method = "POST"; +export const ApiUsr001RequestUrl = "/users/sign-up/student"; + +const requestParam = z.object({}); + +const requestQuery = z.object({}); + +const requestBody = z.object({ + name: zUserName, + email: z.coerce.string().email(), + studentNumber: z.coerce.string().max(20), +}); + +const responseBodyMap = { + [HttpStatusCode.Created]: z.object({ + userId: zId, + userStudentId: zId, + }), +}; + +const responseErrorMap = {}; + +const apiUsr001 = { + url, + method, + requestParam, + requestQuery, + requestBody, + responseBodyMap, + responseErrorMap, +}; + +type ApiUsr001RequestParam = z.infer; +type ApiUsr001RequestQuery = z.infer; +type ApiUsr001RequestBody = z.infer; +type ApiUsr001ResponseCreated = z.infer< + (typeof apiUsr001.responseBodyMap)[201] +>; + +export default apiUsr001; + +export type { + ApiUsr001RequestParam, + ApiUsr001RequestQuery, + ApiUsr001RequestBody, + ApiUsr001ResponseCreated, +}; diff --git a/packages/interface/src/api/user/index.ts b/packages/interface/src/api/user/index.ts new file mode 100644 index 0000000..63656e3 --- /dev/null +++ b/packages/interface/src/api/user/index.ts @@ -0,0 +1,2 @@ +export * from "./endpoint/apiUsr001"; +export { default as apiUsr001 } from "./endpoint/apiUsr001"; diff --git a/packages/interface/src/common/type/phoneNumber.type.ts b/packages/interface/src/common/type/phoneNumber.type.ts index b526b43..3ff9bb0 100644 --- a/packages/interface/src/common/type/phoneNumber.type.ts +++ b/packages/interface/src/common/type/phoneNumber.type.ts @@ -1,19 +1,22 @@ import { z } from "zod"; -const krPhoneNumberRegex = /^010-(\d{4})-(\d{4})$/; +export const zPhoneNumber = z.coerce.string().max(20); -const zKrPhoneNumber = z.custom(val => - typeof val === "string" ? krPhoneNumberRegex.test(val) : false, -); +// 아래 KR phonenumber는 폐기: 생각보다 국제학생들의 전화번호는 다이나믹 하기 때문입니다. +// const krPhoneNumberRegex = /^010-(\d{4})-(\d{4})$/; -type KrPhoneNumber = z.infer; +// const zKrPhoneNumber = z.custom(val => +// typeof val === "string" ? krPhoneNumberRegex.test(val) : false, +// ); -// 한국 휴대전화 번호 커스텀 타입 테스트입니다. -// 첫 문자열은 validation을 통과하고, 둘째는 통과하지 못합니다. -const testZKrPhoneNumber = () => { - console.log(zKrPhoneNumber.parse("010-1234-5678")); - console.log(zKrPhoneNumber.parse("00-123-578")); -}; +// type KrPhoneNumber = z.infer; -export { zKrPhoneNumber, testZKrPhoneNumber }; -export type { KrPhoneNumber }; +// // 한국 휴대전화 번호 커스텀 타입 테스트입니다. +// // 첫 문자열은 validation을 통과하고, 둘째는 통과하지 못합니다. +// const testZKrPhoneNumber = () => { +// console.log(zKrPhoneNumber.parse("010-1234-5678")); +// console.log(zKrPhoneNumber.parse("00-123-578")); +// }; + +// export { zKrPhoneNumber, testZKrPhoneNumber }; +// export type { KrPhoneNumber };