Skip to content

Commit

Permalink
Merge branch 'dev' into 1399-fix-funding-api-for-executive-feedbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
pbc1017 committed Feb 5, 2025
2 parents c9365cb + 9f95207 commit 0b8deac
Show file tree
Hide file tree
Showing 56 changed files with 1,335 additions and 402 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ export default class ActivityRepository {
}

async fetchSummaries(activityIds: number[]): Promise<IActivitySummary[]> {
if (activityIds.length === 0) return [];
const results = await this.db
.select()
.from(Activity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ClubDelegateDRepository {
/**
* @param id 삭제할 변경 요청의 id
*/
async deleteDelegatChangeRequestById(param: {
async deleteDelegateChangeRequestById(param: {
id: number;
}): Promise<boolean> {
const [result] = await this.db
Expand Down Expand Up @@ -77,18 +77,13 @@ export class ClubDelegateDRepository {
* 3일 이내에 신청된 요청만을 조회합니다.
* 최근에 신청된 요청이 가장 위에 위치합니다.
*/
// TODO: 만료 enum 추가
async findDelegateChangeRequestByClubId(param: { clubId: number }) {
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);

const result = await this.db
.select()
.from(ClubDelegateChangeRequest)
.where(
and(
eq(ClubDelegateChangeRequest.clubId, param.clubId),
gte(ClubDelegateChangeRequest.createdAt, threeDaysAgo),
isNull(ClubDelegateChangeRequest.deletedAt),
),
)
Expand Down
42 changes: 40 additions & 2 deletions packages/api/src/feature/club/delegate/delegate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,40 @@ export default class ClubDelegateService {
private clubPublicService: ClubPublicService,
) {}

/**
* @param clubId 동아리 Id
* @description 해당 동아리의 대표자 변경 요청 중 3일이 지난 요청을 만료(soft delete)합니다.
* **_모든 변경 요청 조회 관련 로직에서 조회 이전에 호출되어야 합니다._**
*/
private async cleanExpiredChangeRequests(param: {
clubId: number;
}): Promise<void> {
const requests =
await this.clubDelegateDRepository.findDelegateChangeRequestByClubId({
clubId: param.clubId,
});

const threeDaysAgo = getKSTDate();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);

await Promise.all(
requests.map(async request => {
if (
request.clubDelegateChangeRequestStatusEnumId ===
ClubDelegateChangeRequestStatusEnum.Applied &&
request.createdAt < threeDaysAgo
) {
logger.debug(
`Found expired change request created on ${request.createdAt}`,
);
await this.clubDelegateDRepository.deleteDelegateChangeRequestById({
id: request.id,
});
}
}),
);
}

async getStudentClubDelegates(
param: { studentId: number } & ApiClb006RequestParam,
): Promise<ApiClb006ResponseOK> {
Expand Down Expand Up @@ -225,6 +259,7 @@ export default class ClubDelegateService {
*
* @description getStudentClubDelegateRequests의 서비스 진입점입니다.
* 동아리 대표자 변경 요청을 조회합니다.
* 조회한 대표자 변경 요청이 3일이 지났다면 soft delete합니다.
*/
async getStudentClubDelegateRequests(param: {
param: ApiClb011RequestParam;
Expand All @@ -243,6 +278,9 @@ export default class ClubDelegateService {
HttpStatus.FORBIDDEN,
);

// 3일이 지난 요청은 soft delete합니다.
await this.cleanExpiredChangeRequests({ clubId: param.param.clubId });

const result =
await this.clubDelegateDRepository.findDelegateChangeRequestByClubId({
clubId: param.param.clubId,
Expand Down Expand Up @@ -360,7 +398,7 @@ export default class ClubDelegateService {
requests.map(request => {
if (request === undefined)
throw new HttpException("No request", HttpStatus.BAD_REQUEST);
return this.clubDelegateDRepository.deleteDelegatChangeRequestById({
return this.clubDelegateDRepository.deleteDelegateChangeRequestById({
id: request.id,
});
}),
Expand Down Expand Up @@ -472,7 +510,7 @@ export default class ClubDelegateService {

if (result.length === 0)
return {
status: HttpStatus.NO_CONTENT,
status: HttpStatus.FORBIDDEN,
data: {},
};
if (result.length > 1)
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/feature/club/service/club.public.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ export default class ClubPublicService {
return true;
}

async checkStudentDelegate(studentId: number, clubId: number) {
if (!(await this.isStudentDelegate(studentId, clubId))) {
throw new HttpException(
"It seems that you are not the delegate of the club.",
HttpStatus.FORBIDDEN,
);
}
}

/**
* @param clubStatusEnumId 동아리 상태 enum id의 배열
* @param studentId 사용중인 학생 id
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/feature/club/service/club.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,17 @@ export class ClubService {
private clubPublicService: ClubPublicService,
) {}

private readonly EXCLUDED_CLUB_IDS: number[] = [112, 113, 121];

async getClubs(): Promise<ApiClb001ResponseOK> {
const result = await this.clubRepository.getClubs();

result.divisions.forEach(division => {
// eslint-disable-next-line no-param-reassign
division.clubs = division.clubs.filter(
club => !this.EXCLUDED_CLUB_IDS.includes(club.id),
);
});
return result;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/feature/funding/model/funding.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class MFunding implements IFunding {

fundingStatusEnum: FundingStatusEnum;

purposeActivity?: Pick<IActivitySummary, "id">;
purposeActivity: Pick<IActivitySummary, "id">;

name: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class VFundingSummary implements IFundingSummary {

expenditureAmount: IFundingSummary["expenditureAmount"];

purposeActivity?: IFundingSummary["purposeActivity"];
purposeActivity: IFundingSummary["purposeActivity"];

approvedAmount?: IFundingSummary["approvedAmount"];

Expand Down
78 changes: 55 additions & 23 deletions packages/api/src/feature/funding/service/funding.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";

import { IActivityDuration } from "@sparcs-clubs/interface/api/activity/type/activity.duration.type";
import { IFileSummary } from "@sparcs-clubs/interface/api/file/type/file.type";
import {
ApiFnd001RequestBody,
Expand Down Expand Up @@ -40,7 +41,10 @@ import {
IFundingResponse,
} from "@sparcs-clubs/interface/api/funding/type/funding.type";
import { IExecutive } from "@sparcs-clubs/interface/api/user/type/user.type";
import { FundingStatusEnum } from "@sparcs-clubs/interface/common/enum/funding.enum";
import {
FundingDeadlineEnum,
FundingStatusEnum,
} from "@sparcs-clubs/interface/common/enum/funding.enum";

import { getKSTDate } from "@sparcs-clubs/api/common/util/util";
import ActivityPublicService from "@sparcs-clubs/api/feature/activity/service/activity.public.service";
Expand Down Expand Up @@ -69,18 +73,16 @@ export default class FundingService {
body: ApiFnd001RequestBody,
studentId: number,
): Promise<ApiFnd001ResponseCreated> {
const user = await this.userPublicService.getStudentById({ id: studentId });
if (!user) {
throw new HttpException("Student not found", HttpStatus.NOT_FOUND);
}
if (
!(await this.clubPublicService.isStudentDelegate(studentId, body.club.id))
) {
throw new HttpException("Student is not delegate", HttpStatus.FORBIDDEN);
}
await this.clubPublicService.checkStudentDelegate(studentId, body.club.id);
await this.checkDeadline([
FundingDeadlineEnum.Writing,
FundingDeadlineEnum.Exception,
]);

const now = getKSTDate();
const activityD = await this.activityPublicService.fetchLastActivityD(now);
await this.validateExpenditureDate(body.expenditureDate, activityD);

const fundingStatusEnum = 1;
const approvedAmount = 0;

Expand Down Expand Up @@ -332,18 +334,17 @@ export default class FundingService {
param: ApiFnd003RequestParam,
studentId: number,
): Promise<ApiFnd003ResponseOk> {
const user = await this.userPublicService.getStudentById({ id: studentId });
if (!user) {
throw new HttpException("Student not found", HttpStatus.NOT_FOUND);
}
if (
!(await this.clubPublicService.isStudentDelegate(studentId, body.club.id))
) {
throw new HttpException("Student is not delegate", HttpStatus.FORBIDDEN);
}
await this.clubPublicService.checkStudentDelegate(studentId, body.club.id);
await this.checkDeadline([
FundingDeadlineEnum.Writing,
FundingDeadlineEnum.Revision,
FundingDeadlineEnum.Exception,
]);

const now = getKSTDate();
const activityD = await this.activityPublicService.fetchLastActivityD(now);
await this.validateExpenditureDate(body.expenditureDate, activityD);

const fundingStatusEnum = 1;
const approvedAmount = 0;

Expand All @@ -358,10 +359,16 @@ export default class FundingService {
studentId: number,
param: ApiFnd004RequestParam,
): Promise<ApiFnd004ResponseOk> {
const user = await this.userPublicService.getStudentById({ id: studentId });
if (!user) {
throw new HttpException("Student not found", HttpStatus.NOT_FOUND);
}
const funding = await this.fundingRepository.fetch(param.id);
await this.clubPublicService.checkStudentDelegate(
studentId,
funding.club.id,
);
await this.checkDeadline([
FundingDeadlineEnum.Writing,
FundingDeadlineEnum.Revision,
FundingDeadlineEnum.Exception,
]);
await this.fundingRepository.delete(param.id);
return {};
}
Expand Down Expand Up @@ -557,4 +564,29 @@ export default class FundingService {

return fundingComment;
}

private async checkDeadline(enums: Array<FundingDeadlineEnum>) {
const today = getKSTDate();
const todayDeadline = await this.fundingDeadlineRepository.fetch(today);
if (enums.find(e => Number(e) === todayDeadline.deadlineEnum) === undefined)
throw new HttpException(
"Today is not a day for funding",
HttpStatus.BAD_REQUEST,
);
}

private async validateExpenditureDate(
expenditureDate: Date,
activityD: IActivityDuration,
) {
if (
expenditureDate < activityD.startTerm ||
expenditureDate > activityD.endTerm
) {
throw new HttpException(
"Expenditure date is not in the range of activity deadline",
HttpStatus.BAD_REQUEST,
);
}
}
}
4 changes: 3 additions & 1 deletion packages/interface/src/api/funding/type/funding.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export const zFunding = z.object({
club: zClub.pick({ id: true }),
activityD: zActivityD.pick({ id: true }),
fundingStatusEnum: z.nativeEnum(FundingStatusEnum),
purposeActivity: zActivitySummary.pick({ id: true }).optional(),
purposeActivity: z.object({
id: zId.nullable(),
}),
name: z.string().max(255).min(1),
expenditureDate: z.coerce.date(),
expenditureAmount: z.coerce.number().int().min(0),
Expand Down
17 changes: 15 additions & 2 deletions packages/web/src/app/executive/activity-report/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import React, { useEffect, useState } from "react";

import Custom404 from "@sparcs-clubs/web/app/not-found";
import AsyncBoundary from "@sparcs-clubs/web/common/components/AsyncBoundary";
import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper";
import PageHead from "@sparcs-clubs/web/common/components/PageHead";
import LoginRequired from "@sparcs-clubs/web/common/frames/LoginRequired";
import { useAuth } from "@sparcs-clubs/web/common/providers/AuthContext";

import ActivityReportDetailFrame from "@sparcs-clubs/web/features/activity-report/frames/ActivityReportDetailFrame";

const ExecutiveActivityReportDetail = () => {
Expand All @@ -31,7 +32,19 @@ const ExecutiveActivityReportDetail = () => {
return <Custom404 />;
}

return <ActivityReportDetailFrame profile={profile} />;
return (
<FlexWrapper direction="column" gap={60}>
<PageHead
items={[
{ name: "집행부원 대시보드", path: "/executive" },
{ name: "활동 보고서", path: "/executive/activity-report" },
]}
title="활동 보고서"
enableLast
/>
<ActivityReportDetailFrame profile={profile} />
</FlexWrapper>
);
};

export default ExecutiveActivityReportDetail;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import React, { useEffect, useState } from "react";

import Custom404 from "@sparcs-clubs/web/app/not-found";
import AsyncBoundary from "@sparcs-clubs/web/common/components/AsyncBoundary";
import LoginRequired from "@sparcs-clubs/web/common/frames/LoginRequired";
import { useAuth } from "@sparcs-clubs/web/common/providers/AuthContext";
import ExecutiveActivityReportChargedFrame from "@sparcs-clubs/web/features/executive/activity-report/frames/ExecutiveActivityReportChargedFrame";

const ExecutiveActivityReport = () => {
const { isLoggedIn, login, profile } = useAuth();
const [loading, setLoading] = useState(true);

useEffect(() => {
if (isLoggedIn !== undefined || profile !== undefined) {
setLoading(false);
}
}, [isLoggedIn, profile]);

if (loading) {
return <AsyncBoundary isLoading={loading} isError />;
}

if (!isLoggedIn) {
return <LoginRequired login={login} />;
}

if (profile?.type !== "executive") {
return <Custom404 />;
}

return <ExecutiveActivityReportChargedFrame />;
};

export default ExecutiveActivityReport;
Loading

0 comments on commit 0b8deac

Please sign in to comment.