Skip to content

Commit

Permalink
Merge pull request #1039 from academic-relations/dev
Browse files Browse the repository at this point in the history
merge dev into main
  • Loading branch information
pbc1017 authored Sep 5, 2024
2 parents d78784f + 1200b4b commit a37d7c9
Show file tree
Hide file tree
Showing 50 changed files with 975 additions and 582 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"api": "dotenv -- pnpm -F api",
"interface": "pnpm -F interface",
"dev": "dotenv -- pnpm -r --stream --parallel dev",
"dev:no-db": "dotenv -- pnpm -r --stream --parallel dev:no-db",
"mock:front": "NEXT_PUBLIC_API_MOCK_MODE=1 dotenv -- pnpm -r --stream --parallel mock:front",
"mock:back": "NEXT_PUBLIC_API_MOCK_MODE=1 dotenv -- pnpm -r --stream --parallel mock:back",
"db": "dotenv -- pnpm -F api db",
Expand Down
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"license": "UNLICENSED",
"scripts": {
"dev": "pnpm db up -d && pnpm start:dev",
"dev:no-db": "pnpm start:dev",
"prod": "pnpm migrate:prod && pnpm start:prod",
"db": "docker compose -f ../../.docker/docker-compose.dev.yml -p ar-002-clubs",
"env": "echo $NODE_ENV",
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/feature/activity/activity.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DrizzleModule } from "src/drizzle/drizzle.module";
import { ClubModule } from "../club/club.module";
import { FileModule } from "../file/file.module";

import { ClubRegistrationModule } from "../registration/club-registration/club-registration.module";

import ActivityActivityTermController from "./controller/activity.activity-term.controller";
import ActivityController from "./controller/activity.controller";
import ActivityActivityTermRepository from "./repository/activity.activity-term.repository";
Expand All @@ -14,7 +16,7 @@ import ActivityPublicService from "./service/activity.public.service";
import ActivityService from "./service/activity.service";

@Module({
imports: [ClubModule, DrizzleModule, FileModule],
imports: [ClubModule, DrizzleModule, FileModule, ClubRegistrationModule],
controllers: [ActivityController, ActivityActivityTermController],
providers: [
ActivityRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import apiAct013 from "@sparcs-clubs/interface/api/activity/endpoint/apiAct013";
import apiAct014 from "@sparcs-clubs/interface/api/activity/endpoint/apiAct014";
import apiAct015 from "@sparcs-clubs/interface/api/activity/endpoint/apiAct015";
import apiAct016 from "@sparcs-clubs/interface/api/activity/endpoint/apiAct016";
import apiAct017 from "@sparcs-clubs/interface/api/activity/endpoint/apiAct017";

import { ZodPipe } from "@sparcs-clubs/api/common/pipe/zod-pipe";

Expand Down Expand Up @@ -96,6 +97,11 @@ import type {
ApiAct016RequestParam,
ApiAct016ResponseOk,
} from "@sparcs-clubs/interface/api/activity/endpoint/apiAct016";
import type {
ApiAct017RequestBody,
ApiAct017RequestParam,
ApiAct017ResponseOk,
} from "@sparcs-clubs/interface/api/activity/endpoint/apiAct017";

@Controller()
export default class ActivityController {
Expand Down Expand Up @@ -203,6 +209,20 @@ export default class ActivityController {
return {};
}

@Student()
@Delete("/student/activities/activity/:activityId/provisional")
@UsePipes(new ZodPipe(apiAct004))
async deleteStudentActivityProvisional(
@GetStudent() user: GetStudent,
@Param() param: ApiAct004RequestParam,
): Promise<ApiAct004ResponseOk> {
await this.activityService.deleteStudentActivityProvisional(
param.activityId,
user.studentId,
);
return {};
}

@Student()
@Get("/student/activities/available-members")
@UsePipes(new ZodPipe(apiAct010))
Expand Down Expand Up @@ -302,4 +322,20 @@ export default class ActivityController {
});
return result;
}

@Executive()
@Patch("/executive/activities/activity/:activityId/send-back")
@UsePipes(new ZodPipe(apiAct017))
async patchExecutiveActivitySendBack(
@GetExecutive() user: GetExecutive,
@Param() param: ApiAct017RequestParam,
@Body() body: ApiAct017RequestBody,
): Promise<ApiAct017ResponseOk> {
const result = await this.activityService.patchExecutiveActivitySendBack({
executiveId: user.executiveId,
param,
body,
});
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,36 @@ export default class ActivityRepository {
return isInsertionSucceed;
}

/**
* @param param
* @description 동아리활동 반려 사유를 생성합니다.
* activityId와 activityId의의 유효성을 검사하지 않습니다.
* @returns 생성의 성공 여부를 boolean으로 리턴합니다.
*/
async insertActivityFeedback(param: {
activityId: number;
comment: string;
executiveId: number;
}): Promise<boolean> {
const isInsertionSucceed = await this.db.transaction(async tx => {
const [insertionResult] = await tx.insert(ActivityFeedback).values({
activityId: param.activityId,
comment: param.comment,
executiveId: param.executiveId,
});
if (insertionResult.affectedRows > 1)
throw new HttpException(
"unreachable",
HttpStatus.INTERNAL_SERVER_ERROR,
);
if (insertionResult.affectedRows === 0) return false;

return true;
});

return isInsertionSucceed;
}

async selectActivityByActivityId(activityId: number) {
const result = await this.db
.select()
Expand Down Expand Up @@ -349,6 +379,7 @@ export default class ActivityRepository {
evidenceFileIds: Array<string>;
participantIds: Array<number>;
activityDId: number;
activityStatusEnumId: ActivityStatusEnum;
}) {
const isUpdateSucceed = await this.db.transaction(async tx => {
const deletedAt = new Date();
Expand All @@ -357,12 +388,13 @@ export default class ActivityRepository {
.update(Activity)
.set({
name: param.name,
activityStatusEnumId: Number(param.activityTypeEnumId),
activityTypeEnumId: Number(param.activityTypeEnumId),
location: param.location,
purpose: param.purpose,
detail: param.detail,
evidence: param.evidence,
activityDId: param.activityDId,
activityStatusEnumId: Number(param.activityStatusEnumId),
})
.where(eq(Activity.id, param.activityId));
if (activitySetResult.affectedRows !== 1) {
Expand Down Expand Up @@ -504,13 +536,24 @@ export default class ActivityRepository {
* @description 해당 활동의 승인 상태(ActivityStatusEnumId)를 변경합니다.
* 해당 활동의 상태가 이미 승인인 경우 예외(Bad Request)를 발생시킵니다.
* @returns update에 성공했는지 성공여부를 리턴합니다.
* 실패시 예외가 발생하여 항상 true를 리턴해야 합니다.
* 이미 해당 activity의 enumId가 동알할 경우 false를 리턴합니다.
* 이 외의실패시 예외가 발생하여 항상 true를 리턴해야 합니다.
*/
async updateActivityStatusEnumId(param: {
activityId: number;
activityStatusEnumId: ActivityStatusEnum;
}): Promise<boolean> {
const isUpdateSucceed = await this.db.transaction(async tx => {
const activities = await tx
.select()
.from(Activity)
.where(
and(eq(Activity.id, param.activityId), isNull(Activity.deletedAt)),
);
if (activities.length === 0)
throw new HttpException("not found", HttpStatus.NOT_FOUND);
if (activities[0].activityStatusEnumId === param.activityStatusEnumId)
return false;
const [updateResult] = await tx
.update(Activity)
.set({
Expand Down
120 changes: 94 additions & 26 deletions packages/api/src/feature/activity/service/activity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { getKSTDate } from "@sparcs-clubs/api/common/util/util";
import ClubPublicService from "@sparcs-clubs/api/feature/club/service/club.public.service";
import FilePublicService from "@sparcs-clubs/api/feature/file/service/file.public.service";
import { ClubRegistrationPublicService } from "@sparcs-clubs/api/feature/registration/club-registration/service/club-registration.public.service";

import ActivityActivityTermRepository from "../repository/activity.activity-term.repository";
import ActivityRepository from "../repository/activity.repository";
Expand Down Expand Up @@ -44,6 +45,11 @@ import type {
ApiAct016RequestParam,
ApiAct016ResponseOk,
} from "@sparcs-clubs/interface/api/activity/endpoint/apiAct016";
import type {
ApiAct017RequestBody,
ApiAct017RequestParam,
ApiAct017ResponseOk,
} from "@sparcs-clubs/interface/api/activity/endpoint/apiAct017";

@Injectable()
export default class ActivityService {
Expand All @@ -52,6 +58,7 @@ export default class ActivityService {
private activityActivityTermRepository: ActivityActivityTermRepository,
private clubPublicService: ClubPublicService,
private filePublicService: FilePublicService,
private clubRegistrationPublicService: ClubRegistrationPublicService,
) {}

/**
Expand Down Expand Up @@ -419,6 +426,7 @@ export default class ActivityService {
evidenceFileIds: body.evidenceFiles.map(e => e.fileId),
participantIds: body.participants.map(e => e.studentId),
activityDId: activity.activityDId,
activityStatusEnumId: ActivityStatusEnum.Applied,
});
if (!isUpdateSucceed)
throw new HttpException(
Expand Down Expand Up @@ -454,6 +462,13 @@ export default class ActivityService {
e.studentId,
),
);

if (participantIds.length === 0)
throw new HttpException(
"There is no participant in the activity",
HttpStatus.BAD_REQUEST,
);

// 파일 유효한지 검사합니다.
const evidenceFiles = await Promise.all(
body.evidenceFiles.map(key =>
Expand All @@ -474,6 +489,10 @@ export default class ActivityService {
"Failed to insert",
HttpStatus.INTERNAL_SERVER_ERROR,
);

this.clubRegistrationPublicService.resetClubRegistrationStatusEnum(
body.clubId,
);
}

async putStudentActivityProvisional(
Expand All @@ -486,7 +505,6 @@ export default class ActivityService {
await this.checkIsStudentDelegate({ studentId, clubId: activity.clubId });
// 오늘이 활동보고서 작성기간이거나, 예외적 작성기간인지 확인하지 않습니다.
// 해당 활동이 지난 활동기간에 대한 활동인지 확인하지 않습니다.
const lastActivityD = await this.getLastActivityD();

// 제출한 활동 기간들이 지난 활동기간 이내인지 확인하지 않습니다.

Expand All @@ -496,32 +514,29 @@ export default class ActivityService {
this.filePublicService.getFileInfoById(key.fileId),
),
);
// 참여 학생이 지난 활동기간 동아리의 소속원이였는지 확인합니다.
const activityDStartSemester =
await this.clubPublicService.dateToSemesterId(lastActivityD.startTerm);
const activityDEndSemester = await this.clubPublicService.dateToSemesterId(
lastActivityD.endTerm,
);
const members = (
await this.clubPublicService.getMemberFromSemester({
semesterId: activityDStartSemester,
clubId: activity.clubId,
})
).concat(
await this.clubPublicService.getMemberFromSemester({
semesterId: activityDEndSemester,
clubId: activity.clubId,
}),
// 참여 학생이 지난 활동기간 동아리의 소속원이였는지 확인하지 않습니다.
const participantIds = await Promise.all(
body.participants.map(
async e =>
// if (
// !(await this.clubPublicService.isStudentBelongsTo(
// e.studentId,
// body.clubId,
// ))
// )
// throw new HttpException(
// "Some student is not belonged to the club",
// HttpStatus.BAD_REQUEST,
// );
e.studentId,
),
);
body.participants.forEach(participant => {
if (
members.find(e => e.studentId === participant.studentId) === undefined
)
throw new HttpException(
"Some participant is not belonged to the club in the activity duration",
HttpStatus.BAD_REQUEST,
);
});

if (participantIds.length === 0)
throw new HttpException(
"There is no participant in the activity",
HttpStatus.BAD_REQUEST,
);

// PUT 처리를 시작합니다.
const isUpdateSucceed = this.activityRepository.updateActivity({
Expand All @@ -536,12 +551,17 @@ export default class ActivityService {
evidenceFileIds: evidenceFiles.map(e => e.id),
participantIds: body.participants.map(e => e.studentId),
activityDId: activity.activityDId,
activityStatusEnumId: ActivityStatusEnum.Applied,
});
if (!isUpdateSucceed)
throw new HttpException(
"Failed to update",
HttpStatus.INTERNAL_SERVER_ERROR,
);

this.clubRegistrationPublicService.resetClubRegistrationStatusEnum(
activity.clubId,
);
}

/**
Expand Down Expand Up @@ -573,6 +593,7 @@ export default class ActivityService {
id: activity.id,
name: activity.name,
activityTypeEnumId: activity.activityTypeEnumId,
activityStatusEnumId: activity.activityStatusEnumId,
durations,
earliestStartTerm, // 추후 정렬을 위해 추가
latestEndTerm, // 추후 정렬을 위해 추가
Expand All @@ -593,10 +614,27 @@ export default class ActivityService {
id: activity.id,
name: activity.name,
activityTypeEnumId: activity.activityTypeEnumId,
activityStatusEnumId: activity.activityStatusEnumId,
durations: activity.durations,
}));
}

async deleteStudentActivityProvisional(
activityId: number,
studentId: number,
) {
const activity = await this.getActivity({ activityId });
// 학생이 동아리 대표자 또는 대의원이 맞는지 확인합니다.
await this.checkIsStudentDelegate({ studentId, clubId: activity.clubId });

if (!(await this.activityRepository.deleteActivity({ activityId }))) {
throw new HttpException(
"Something got wrong...",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* @param param
* @description getStudentProvisionalActivities와 대응되는 서비스 진입점 입니다.
Expand Down Expand Up @@ -741,7 +779,37 @@ export default class ActivityService {
activityStatusEnumId: ActivityStatusEnum.Approved,
});
if (!isApprovalSucceed)
throw new HttpException(
"the activity is already approved",
HttpStatus.BAD_REQUEST,
);
return {};
}

/**
* @param param
* @description patchExecutiveActivitySendBack의 서비스 진입점입니다.
* 동시성을 고려하지 않고 구현했습니다.
*/
async patchExecutiveActivitySendBack(param: {
executiveId: number;
param: ApiAct017RequestParam;
body: ApiAct017RequestBody;
}): Promise<ApiAct017ResponseOk> {
await this.activityRepository.updateActivityStatusEnumId({
activityId: param.param.activityId,
activityStatusEnumId: ActivityStatusEnum.Rejected,
});

const isInsertionSucceed =
await this.activityRepository.insertActivityFeedback({
activityId: param.param.activityId,
comment: param.body.comment,
executiveId: param.executiveId,
});
if (!isInsertionSucceed)
throw new HttpException("unreachable", HttpStatus.INTERNAL_SERVER_ERROR);

return {};
}
}
Loading

0 comments on commit a37d7c9

Please sign in to comment.