Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f519a90
Merge pull request #56 from se-gam/dev
therealjamesjung Jan 3, 2025
2b14be0
Merge pull request #57 from se-gam/dev
therealjamesjung Jan 5, 2025
396acc8
feat: studyroom 활성화 여부 컬럼 추가
RightHennessy Jan 22, 2025
df76f2e
fix: 활성화된 스터디룸만 크롤링하도록 수정
RightHennessy Jan 22, 2025
f598fff
Merge pull request #65 from se-gam/hotfix/studyroom
therealjamesjung Jan 25, 2025
b9087f2
fix: crawler health check시 활성화된 스터디룸만 체크하도록 수정
Im-Siyoun Jan 25, 2025
96b552c
Merge pull request #66 from se-gam/hotfix/studyroom
Im-Siyoun Jan 25, 2025
598f8c9
fix: 스터디룸 크롤러 복구
therealjamesjung Jan 25, 2025
c03e386
Merge pull request #67 from se-gam/hotfix/studyroom-crawler
therealjamesjung Jan 25, 2025
1ea401d
fix: 스터디룸 크롤러 복구
therealjamesjung Jan 28, 2025
fc02848
Merge pull request #68 from se-gam/dev
therealjamesjung Feb 2, 2025
7a632c2
Merge pull request #74 from se-gam/prod-2025
RightHennessy Feb 10, 2025
1b6a8e3
feat: is-boolean pipe
RightHennessy Feb 10, 2025
6061214
feat: 스터디룸 정보 업데이트 api
RightHennessy Feb 10, 2025
a99d933
feat: 어드민용 스터디룸 목록 조회 API 추가
therealjamesjung Feb 11, 2025
c3d0120
docs: 어드민용 API를 swagger문서에서 구분
therealjamesjung Feb 11, 2025
1b494d0
Merge pull request #75 from se-gam/feat/admin-studyroom
therealjamesjung Feb 11, 2025
3f8183d
feat: 2025-1학기 강의 구분 파싱
therealjamesjung Feb 17, 2025
5540372
Merge pull request #76 from se-gam/feat/parse-lecture
therealjamesjung Feb 17, 2025
c38e6e9
feat: 강의 정보 목록 API 추가
therealjamesjung Feb 17, 2025
8dc8b74
Merge pull request #77 from se-gam/feat/parse-lecture
therealjamesjung Feb 17, 2025
c51eaa3
fix: 빌드 디렉토리 수정
therealjamesjung Feb 17, 2025
608f4e4
Merge pull request #78 from se-gam/feat/parse-lecture
therealjamesjung Feb 17, 2025
fd51685
feat: 수동 빌드 기능 추가
Im-Siyoun Feb 19, 2025
f597480
Merge pull request #79 from se-gam/hotfix/enviroment
Im-Siyoun Feb 19, 2025
004e92c
feat: 배치 모듈 Init
Feb 22, 2025
46e3c80
feat: 스터디룸 크롤러 관리 API 추가
Feb 22, 2025
4f6338c
feat: 스터디룸 크롤러 헬스체크 크론잡 관리 API 추가
Feb 22, 2025
32d2cb7
Merge pull request #81 from se-gam/feat/cron-manager
therealjamesjung Feb 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/prod2025.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- prod-2025
workflow_dispatch:

permissions:
contents: read
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"db:init:local": "docker-compose --env-file .local.env -f docker-compose.db.yml up -d",
"db:save:local": "dotenv -e .local.env -- yarn prisma migrate dev",
"db:save:dev": "dotenv -e .dev.env -- yarn prisma migrate dev",
"deploy:local": "dotenv -e .local.env -- node dist/main.js",
"deploy:dev": "dotenv -e .dev.env -- node dist/main.js",
"deploy:prod": "dotenv -e .prod.env -- node dist/main.js",
"deploy:prod2025": "dotenv -e .prod2025.env -- node dist/main.js",
"deploy:local": "dotenv -e .local.env -- node dist/src/main.js",
"deploy:dev": "dotenv -e .dev.env -- node dist/src/main.js",
"deploy:prod": "dotenv -e .prod.env -- node dist/src/main.js",
"deploy:prod2025": "dotenv -e .prod2025.env -- node dist/src/main.js",
"seed:studyroom:local": "dotenv -e .local.env -- npx ts-node ./prisma/seeds/insert-studyrooms.ts",
"seed:studyroom:dev": "dotenv -e .dev.env -- npx ts-node ./prisma/seeds/insert-studyrooms.ts",
"seed:course:local": "dotenv -e .local.env -- npx ts-node ./prisma/seeds/insert-courses.ts",
Expand Down
1 change: 1 addition & 0 deletions parser/lectures_2025-1.json

Large diffs are not rendered by default.

Binary file added parser/lectures_2025-1.xlsx
Binary file not shown.
39 changes: 39 additions & 0 deletions parser/parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pandas as pd
import json

FILE_NAME = 'lectures_2025-1'
INVALID_SCHOOLS = ['유형2', '-', '대학', '연계전공']


raw_lectures = pd.read_excel(f'{FILE_NAME}.xlsx')
schools = [school for school in raw_lectures['개설대학'].unique() if school not in INVALID_SCHOOLS]

lectures = dict()

for _, row in raw_lectures.iterrows():
if row['학수번호'] not in lectures:
if row['개설대학'] in INVALID_SCHOOLS:
school = '기타'
else:
school = row['개설대학']

lectures[row['학수번호']] = {
'id': row['학수번호'],
'school': school,
'name': row['교과목명'],
}
else:
if lectures[row['학수번호']]['school'] == '기타':
if row['개설대학'] not in INVALID_SCHOOLS:
lectures[row['학수번호']]['school'] = row['개설대학']


result = [x for x in lectures.values()]


with open(f'{FILE_NAME}.json', 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)




Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "studyroom" ADD COLUMN "is_active" BOOLEAN NOT NULL DEFAULT true;
1 change: 1 addition & 0 deletions prisma/schema/studyroom/studyroom.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ model Studyroom {
minUsers Int @map("min_users")
maxUsers Int @map("max_users")
isCinema Boolean @map("is_cinema")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UserModule } from 'src/user/user.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { configModule } from './modules/config.module';
import { BatchModule } from 'src/batch/batch.module';

@Module({
imports: [
Expand All @@ -23,6 +24,7 @@ import { configModule } from './modules/config.module';
RestaurantModule,
GodokModule,
NoticeModule,
BatchModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
11 changes: 11 additions & 0 deletions src/attendance/attendance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { AssignmentAttendanceListDto } from './dto/assignment-attendance.dto';
import { CourseAttendanceListDto } from './dto/course-attendace-list.dto';
import { CourseAttendanceDto } from './dto/course-attendance.dto';
import { LectureAttendanceListDto } from './dto/lecture-attendance.dto';
import { LectureListDto } from './dto/lecture-list.dto';

import * as LECTURES from '../../parser/lectures_2025-1.json';

@ApiTags('출석 API')
@Controller('attendance')
Expand Down Expand Up @@ -93,4 +96,12 @@ export class AttendanceController {
): Promise<AssignmentAttendanceListDto> {
return this.attendanceService.getAssignmentAttendance(user);
}

@Version('1')
@Get('lecture/list')
@ApiOperation({ summary: '강의 목록' })
@ApiCreatedResponse({ type: LectureListDto })
async getLectureList(): Promise<LectureListDto> {
return LectureListDto.from(LECTURES);
}
}
44 changes: 44 additions & 0 deletions src/attendance/dto/lecture-list.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiProperty } from '@nestjs/swagger';
import { LectureInfo } from '../types/lecture-info';

export class LectureDto {
@ApiProperty({
description: '학수번호',
type: String,
})
id!: string;

@ApiProperty({
description: '강의명',
type: String,
})
name!: string;

@ApiProperty({
description: '주관 학과',
type: String,
})
school!: string;

static from(lecture: LectureInfo) {
return {
id: lecture.id,
name: lecture.name,
school: lecture.school,
};
}
}

export class LectureListDto {
@ApiProperty({
description: '강의 목록',
type: [LectureDto],
})
lectures!: LectureDto[];

static from(lectures: LectureInfo[]) {
return {
lectures: lectures.map((lecture) => LectureDto.from(lecture)),
};
}
}
5 changes: 5 additions & 0 deletions src/attendance/types/lecture-info.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type LectureInfo = {
id: string;
name: string;
school: string;
};
103 changes: 103 additions & 0 deletions src/batch/batch.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Body, Controller, Get, Post, UseGuards, Version } from '@nestjs/common';
import { ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { BatchService } from './batch.service';
import { StudyroomBatchInfoDto } from './dto/studyroomBatchInfo.dto';
import { CronTimePayload } from './payload/cron-time.payload';
import { AdminApiGuard } from 'src/auth/guard/admin.guard';

@ApiTags('배치 API')
@Controller('batch')
export class BatchController {
constructor(private readonly batchService: BatchService) {}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 정보 조회 API',
})
@ApiOkResponse({ type: StudyroomBatchInfoDto })
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Get('studyroom')
async getStudyroom() {
return this.batchService.getStudyroomBatchInfo();
}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 활성화 API',
})
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Post('studyroom/activate')
async activateStudyroomSlotCrawler() {
return this.batchService.activateStudyroomSlotCrawler();
}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 비활성화 API',
})
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Post('studyroom/deactivate')
async deactivateStudyroomSlotCrawler() {
return this.batchService.deactivateStudyroomSlotCrawler();
}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 헬스 체크 활성화 API',
})
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Post('studyroom/health-check/activate')
async activateStudyroomSlotCrawlerHealthCheck() {
return this.batchService.activateStudyroomSlotCrawlerHealthCheck();
}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 헬스 체크 비활성화 API',
})
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Post('studyroom/health-check/deactivate')
async deactivateStudyroomSlotCrawlerHealthCheck() {
return this.batchService.deactivateStudyroomSlotCrawlerHealthCheck();
}

@Version('1')
@ApiOperation({
summary: '[어드민] 스터디룸 슬롯 크롤러 배치 크론 시간 변경 API',
})
@UseGuards(AdminApiGuard)
@ApiHeader({
name: 'admin-api-key',
description: 'API key for admin access',
required: true,
})
@Post('studyroom/cron-time')
async changeStudyroomSlotCrawlerCronTime(@Body() body: CronTimePayload) {
return this.batchService.changeStudyroomSlotCrawlerCronTime(body.cronTime);
}
}
11 changes: 11 additions & 0 deletions src/batch/batch.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { BatchController } from './batch.controller';
import { BatchService } from './batch.service';
import { AuthModule } from 'src/auth/auth.module';

@Module({
imports: [AuthModule],
controllers: [BatchController],
providers: [BatchService]
})
export class BatchModule {}
71 changes: 71 additions & 0 deletions src/batch/batch.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { StudyroomBatchInfoDto } from './dto/studyroomBatchInfo.dto';
import { CronTime } from 'cron';

@Injectable()
export class BatchService {
constructor(private schedulerRegistry: SchedulerRegistry) {}

async getStudyroomBatchInfo(): Promise<StudyroomBatchInfoDto> {
const cronJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawler'
);

return {
isRunning: cronJob.running,
cronTime: cronJob.cronTime.source.toString(),
lastFiredAt: cronJob.lastDate(),
};
}

async activateStudyroomSlotCrawler() {
const cronJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawler'
);
const healthCheckJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawlerHealthCheck'
);

cronJob.start();
healthCheckJob.start();
}

async deactivateStudyroomSlotCrawler() {
const cronJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawler'
);
const healthCheckJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawlerHealthCheck'
);

cronJob.stop();
healthCheckJob.stop();
}

async activateStudyroomSlotCrawlerHealthCheck() {
const healthCheckJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawlerHealthCheck'
);

healthCheckJob.start();
}

async deactivateStudyroomSlotCrawlerHealthCheck() {
const healthCheckJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawlerHealthCheck'
);

healthCheckJob.stop();
}

async changeStudyroomSlotCrawlerCronTime(rawCronTime: string) {
const cronJob = this.schedulerRegistry.getCronJob(
'studyroomSlotCrawler'
);

const cronTime = new CronTime(rawCronTime);

cronJob.setTime(cronTime);
}
}
22 changes: 22 additions & 0 deletions src/batch/dto/studyroomBatchInfo.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ApiProperty } from "@nestjs/swagger";
import { StudyroomInfoListDto } from "src/studyroom/dto/studyroom-infp.dto";

export class StudyroomBatchInfoDto {
@ApiProperty({
description: '배치 실행 여부',
type: Boolean,
})
isRunning: boolean;

@ApiProperty({
description: '배치 Cron 표현식',
type: String,
})
cronTime: string;

@ApiProperty({
description: '배치 실행 시간',
type: Date,
})
lastFiredAt: Date;
}
12 changes: 12 additions & 0 deletions src/batch/payload/cron-time.payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class CronTimePayload {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'Cron 표현식',
example: '*/2 * * * * *',
})
cronTime: string;
}
Loading