Skip to content

Commit 77569be

Browse files
authored
Merge pull request #126 from Yoonhojoon/hotfix/redis
수강 신청 시스템 개선: MClass 모델에 인원 수 필드 추가, 관련 SQL 마이그레이션 파일 생성, EnrollmentS…
2 parents 01af573 + 954b328 commit 77569be

6 files changed

Lines changed: 96 additions & 9 deletions

File tree

artillery/generate-users-with-real-tokens.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async function completeUserRegistration(email, password, name) {
105105
};
106106

107107
const completeData = {
108-
termIds: ["a2774319-cd65-4e59-a0bf-b9f4b145af06"] // 약관 ID
108+
termIds: ["15722639-9ef0-43a2-b128-2504f2b4c8dc"] // 약관 ID
109109
};
110110

111111
const completeResponse = await makeRequest(completeOptions, completeData);

k6/enrollment-only-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const options = {
4646

4747
// 테스트 변수
4848
const BASE_URL = __ENV.BASE_URL || 'http://mclass-alb-616483239.ap-northeast-2.elb.amazonaws.com';
49-
const MCLASS_ID = '1615d720-e902-43a9-82e4-7ab895916683';
49+
const MCLASS_ID = '482ba975-afda-4e60-aa12-b816dceab40a';
5050

5151
// 유틸리티 함수
5252
function generateUUID() {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- MClass 테이블에 인원 수 필드 추가
2+
ALTER TABLE "mclasses" ADD COLUMN "approved_count" INTEGER NOT NULL DEFAULT 0;
3+
ALTER TABLE "mclasses" ADD COLUMN "waitlisted_count" INTEGER NOT NULL DEFAULT 0;
4+
5+
-- 기존 데이터에 대해 실제 인원 수로 업데이트
6+
UPDATE "mclasses"
7+
SET
8+
"approved_count" = (
9+
SELECT COUNT(*)
10+
FROM "enrollments"
11+
WHERE "enrollments"."mclass_id" = "mclasses"."id"
12+
AND "enrollments"."status" = 'APPROVED'
13+
),
14+
"waitlisted_count" = (
15+
SELECT COUNT(*)
16+
FROM "enrollments"
17+
WHERE "enrollments"."mclass_id" = "mclasses"."id"
18+
AND "enrollments"."status" = 'WAITLISTED'
19+
);
20+
21+
-- 인덱스 추가 (성능 최적화)
22+
CREATE INDEX "mclasses_approved_count_idx" ON "mclasses"("approved_count");
23+
CREATE INDEX "mclasses_waitlisted_count_idx" ON "mclasses"("waitlisted_count");

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ model MClass {
4747
startAt DateTime @map("start_at")
4848
updatedAt DateTime @updatedAt @map("updated_at")
4949
waitlistCapacity Int? @map("waitlist_capacity")
50+
// 인원 수 필드 추가
51+
approvedCount Int @default(0) @map("approved_count")
52+
waitlistedCount Int @default(0) @map("waitlisted_count")
5053
enrollmentForm EnrollmentForm?
5154
enrollments Enrollment[]
5255
creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull)

src/domains/enrollment/enrollment.service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,21 @@ export class EnrollmentService {
320320
mclassId,
321321
});
322322

323+
// MClass 인원 수 업데이트 (트랜잭션 내부에서)
324+
if (status === 'APPROVED') {
325+
await tx.$executeRaw`
326+
UPDATE mclasses
327+
SET approved_count = approved_count + 1
328+
WHERE id = ${mclassId}
329+
`;
330+
} else if (status === 'WAITLISTED') {
331+
await tx.$executeRaw`
332+
UPDATE mclasses
333+
SET waitlisted_count = waitlisted_count + 1
334+
WHERE id = ${mclassId}
335+
`;
336+
}
337+
323338
return enrollment;
324339
},
325340
{ timeout: 5000 }
@@ -484,6 +499,21 @@ export class EnrollmentService {
484499
await this.promoteWaitlistInTransaction(tx, enrollment.mclassId);
485500
}
486501

502+
// MClass 인원 수 업데이트 (취소 시 감소)
503+
if (previousStatus === 'APPROVED') {
504+
await tx.$executeRaw`
505+
UPDATE mclasses
506+
SET approved_count = GREATEST(approved_count - 1, 0)
507+
WHERE id = ${enrollment.mclassId}
508+
`;
509+
} else if (previousStatus === 'WAITLISTED') {
510+
await tx.$executeRaw`
511+
UPDATE mclasses
512+
SET waitlisted_count = GREATEST(waitlisted_count - 1, 0)
513+
WHERE id = ${enrollment.mclassId}
514+
`;
515+
}
516+
487517
logger.info('Enrollment 취소 완료', {
488518
enrollmentId,
489519
userId,
@@ -823,6 +853,15 @@ export class EnrollmentService {
823853
mclassId,
824854
});
825855

856+
// MClass 인원 수 업데이트 (대기자 → 승인)
857+
await tx.$executeRaw`
858+
UPDATE mclasses
859+
SET
860+
waitlisted_count = GREATEST(waitlisted_count - 1, 0),
861+
approved_count = approved_count + 1
862+
WHERE id = ${mclassId}
863+
`;
864+
826865
// 대기자 승인 이메일 발송
827866
await this.sendWaitlistApprovalEmail(oldestWaitlist.id);
828867
}

src/domains/mclass/mclass.service.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '../../schemas/mclass/index.js';
77
import { MClassError } from '../../common/exception/mclass/MClassError.js';
88
import logger from '../../config/logger.config.js';
9+
import { redis } from '../../config/redis.config.js';
910

1011
export type MClassPhase = 'UPCOMING' | 'RECRUITING' | 'IN_PROGRESS' | 'ENDED';
1112

@@ -40,9 +41,9 @@ export class MClassService {
4041
constructor(private repository: MClassRepository) {}
4142

4243
/**
43-
* Phase 계산 로직 (실시간 approvedCount 사용)
44+
* Phase 계산 로직 (이미 조회된 approvedCount 사용)
4445
*/
45-
private async calculatePhase(mclass: any): Promise<MClassPhase> {
46+
private calculatePhase(mclass: any): MClassPhase {
4647
const now = new Date();
4748
const {
4849
recruitStartAt,
@@ -51,6 +52,7 @@ export class MClassService {
5152
endAt,
5253
visibility,
5354
capacity,
55+
approvedCount = 0, // 기본값 0
5456
} = mclass;
5557

5658
// 종료됨 (가장 우선 체크)
@@ -68,15 +70,14 @@ export class MClassService {
6870
return 'UPCOMING';
6971
}
7072

71-
// RECRUITING: 모집 중 (실시간 approvedCount 계산)
73+
// RECRUITING: 모집 중 (이미 조회된 approvedCount 사용)
7274
if (
7375
recruitStartAt &&
7476
recruitEndAt &&
7577
now >= recruitStartAt &&
7678
now < recruitEndAt &&
7779
visibility === 'PUBLIC'
7880
) {
79-
const approvedCount = await this.repository.getApprovedCount(mclass.id);
8081
if (capacity === null || approvedCount < capacity) {
8182
return 'RECRUITING';
8283
}
@@ -95,7 +96,7 @@ export class MClassService {
9596
* MClass 데이터에 phase 추가 및 Date를 문자열로 변환
9697
*/
9798
private async addPhaseToMClass(mclass: any): Promise<MClassWithPhase> {
98-
const phase = await this.calculatePhase(mclass);
99+
const phase = this.calculatePhase(mclass);
99100
return {
100101
...mclass,
101102
phase,
@@ -183,13 +184,34 @@ export class MClassService {
183184
logger.info(`[MClassService] MClass 단일 조회 시작: ${id}`);
184185

185186
try {
187+
const cachedMClass = await redis.get(`mclass:${id}`);
188+
if (cachedMClass) {
189+
const mclass = JSON.parse(cachedMClass);
190+
logger.info(`[MClassService] MClass 캐시에서 조회: ${id}`);
191+
return this.addPhaseToMClass(mclass);
192+
}
193+
186194
const mclass = await this.repository.findById(id);
187195
if (!mclass) {
188196
logger.warn(`[MClassService] MClass를 찾을 수 없음: ${id}`);
189197
throw MClassError.notFound(id);
190198
}
191199

192200
const result = await this.addPhaseToMClass(mclass);
201+
202+
// 캐시에 저장 (5분간)
203+
try {
204+
await redis.setex(`mclass:${id}`, 300, JSON.stringify(mclass));
205+
logger.info(`[MClassService] MClass 캐시에 저장: ${id}`);
206+
} catch (cacheError) {
207+
logger.warn(`[MClassService] MClass 캐시 저장 실패: ${id}`, {
208+
error:
209+
cacheError instanceof Error
210+
? cacheError.message
211+
: String(cacheError),
212+
});
213+
}
214+
193215
logger.info(
194216
`[MClassService] MClass 단일 조회 성공: ${id}, Phase: ${result.phase}`
195217
);
@@ -267,7 +289,7 @@ export class MClassService {
267289
}
268290

269291
// 모집 중인 클래스 수정 제한 체크
270-
const currentPhase = await this.calculatePhase(existingMClass);
292+
const currentPhase = this.calculatePhase(existingMClass);
271293
if (currentPhase === 'RECRUITING') {
272294
logger.warn(
273295
`[MClassService] 모집 중인 MClass 수정 시도: ${id}, Phase: ${currentPhase}`
@@ -304,7 +326,7 @@ export class MClassService {
304326
}
305327

306328
// 진행 중인 클래스 삭제 제한 체크
307-
const currentPhase = await this.calculatePhase(existingMClass);
329+
const currentPhase = this.calculatePhase(existingMClass);
308330
if (currentPhase === 'IN_PROGRESS') {
309331
logger.warn(
310332
`[MClassService] 진행 중인 MClass 삭제 시도: ${id}, Phase: ${currentPhase}`

0 commit comments

Comments
 (0)