feat(be): implement study group main api#3456
Conversation
… and invitation code checks
|
@nhjbest22 |
|
@RyuRaseul |
|
@RyuRaseul |
|
추가적으로 주기적으로 Heartbeat 처럼 FE에서 BE 서버로 일정 시간마다 StudyGroup 참가하고 있는 사용자의 누적 참여 시간을 보내줘야 할 거 같아요. |
|
추가적으로 |
|
오늘 만나서 얘기 ㄱㄱ |
There was a problem hiding this comment.
Pull request overview
Implements backend support for “Study” groups in the client API, including persistence (Prisma models/migration), REST endpoints for managing/joining study groups, and initial websocket/Redis adapter plumbing for realtime features.
Changes:
- Add
GroupTagandStudyInfoPrisma models (and migration) to support tagging and study-specific settings (capacity/invitation). - Introduce
/studyREST API (create/list/update/delete/join) with DTO validation and service logic. - Add Redis module + Socket.IO Redis adapter wiring and extend JWT auth guard to support websocket contexts.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependencies for Socket.IO + Redis adapter/ioredis. |
| apps/backend/package.json | Adds websocket/redis-related runtime deps (@nestjs/websockets, @nestjs/platform-socket.io, socket.io, ioredis, @socket.io/redis-adapter). |
| apps/backend/tsconfig.json | Adds @libs/redis path alias. |
| apps/backend/prisma/schema.prisma | Adds GroupTag and StudyInfo models and relations. |
| apps/backend/prisma/migrations/20260223130454_add_grouptag_and_studyinfo_model/migration.sql | Creates group_tag and study_info tables and FKs. |
| apps/backend/libs/redis/tsconfig.lib.json | New lib tsconfig for redis library build output. |
| apps/backend/libs/redis/src/redis.module.ts | Provides a global ioredis client via DI with ping-based connection check. |
| apps/backend/libs/redis/src/redis-io.adapter.ts | Implements Socket.IO Redis adapter integration for Nest websocket adapter. |
| apps/backend/libs/redis/src/index.ts | Exports redis module and adapter. |
| apps/backend/libs/auth/src/jwt/jwt-auth.guard.ts | Extends JWT guard to extract auth/groupId correctly for websocket contexts. |
| apps/backend/apps/client/src/study/study.service.ts | Core study-group CRUD/join logic + tag derivation from shared problems. |
| apps/backend/apps/client/src/study/study.controller.ts | Exposes /study HTTP routes for study-group operations. |
| apps/backend/apps/client/src/study/study.module.ts | Declares Study module (controller/service). |
| apps/backend/apps/client/src/study/study.gateway.ts | Adds a websocket gateway for the study namespace (connection logging). |
| apps/backend/apps/client/src/study/dto/study.dto.ts | Defines create/update DTOs and validation rules. |
| apps/backend/apps/client/src/main.ts | Wires RedisIoAdapter into app bootstrap for websocket scaling. |
| apps/backend/apps/client/src/app.module.ts | Registers StudyModule and RedisModule in the client app. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const tags = await this.prisma.problemTag.findMany({ | ||
| where: { | ||
| problemId: { | ||
| in: problemIds | ||
| } | ||
| }, | ||
| select: { | ||
| tagId: true | ||
| }, | ||
| distinct: ['tagId'] | ||
| }) |
| sharedProblems: { | ||
| connect: problemIds?.map((problmeId) => ({ id: problmeId })) | ||
| }, |
| async deleteStudyGroup(groupId: number) { | ||
| const studyGroup = await this.prisma.group.findUnique({ | ||
| where: { | ||
| id: groupId | ||
| }, | ||
| select: { id: true } | ||
| }) | ||
|
|
||
| if (!studyGroup) { | ||
| throw new EntityNotExistException('StudyGroup') | ||
| } | ||
|
|
||
| await this.prisma.group.delete({ | ||
| where: { | ||
| id: groupId | ||
| } | ||
| }) |
| if ( | ||
| studyGroup.studyInfo && | ||
| studyGroup.studyInfo.capacity <= studyGroup._count.userGroup | ||
| ) | ||
| throw new ConflictFoundException('Study group capacity exceeded') | ||
|
|
||
| if ( | ||
| studyGroup.studyInfo?.invitationCode && | ||
| studyGroup.studyInfo.invitationCode !== invitation | ||
| ) | ||
| throw new ConflictFoundException('Invalid invitation code') | ||
|
|
||
| return await this.prisma.userGroup.create({ | ||
| data: { | ||
| userId, | ||
| groupId | ||
| } | ||
| }) |
| import { StudyService } from './study.service' | ||
|
|
||
| @Module({ | ||
| controllers: [StudyController], | ||
| providers: [StudyService], |
| async createStudyGroup(userId: number, dto: CreateStudyDto) { | ||
| const { groupName, description, capacity, problemIds, invitationCode } = dto | ||
|
|
||
| const tags = await this.prisma.problemTag.findMany({ | ||
| where: { | ||
| problemId: { | ||
| in: problemIds | ||
| } | ||
| }, | ||
| select: { | ||
| tagId: true | ||
| }, | ||
| distinct: ['tagId'] | ||
| }) | ||
|
|
||
| return await this.prisma.group.create({ | ||
| data: { | ||
| groupName, | ||
| groupType: GroupType.Study, | ||
| description, | ||
| config: { | ||
| showOnList: true, | ||
| allowJoinFromSearch: true, | ||
| allowJoinWithURL: false, | ||
| requireApprovalBeforeJoin: false | ||
| }, | ||
| userGroup: { | ||
| create: { | ||
| userId, | ||
| isGroupLeader: true | ||
| } | ||
| }, | ||
| sharedProblems: { | ||
| connect: problemIds?.map((problmeId) => ({ id: problmeId })) | ||
| }, | ||
| groupTag: { | ||
| createMany: { | ||
| data: tags.map((tag) => ({ tagId: tag.tagId })) | ||
| } | ||
| }, | ||
| studyInfo: { | ||
| create: { | ||
| capacity, | ||
| invitationCode | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| } |
| userGroup: { | ||
| select: { | ||
| user: { | ||
| select: { | ||
| username: true | ||
| } | ||
| }, | ||
| isGroupLeader: true | ||
| } | ||
| }, |
| UseGroupLeaderGuard | ||
| } from '@libs/auth' | ||
| import { GroupIDPipe } from '@libs/pipe' | ||
| import { CreateStudyDto, type UpdateStudyDto } from './dto/study.dto' |
| update: { | ||
| capacity, | ||
| invitationCode |
| if ( | ||
| studyGroup.studyInfo && | ||
| studyGroup.studyInfo.capacity <= studyGroup._count.userGroup | ||
| ) | ||
| throw new ConflictFoundException('Study group capacity exceeded') |
Description
Additional context
Before submitting the PR, please make sure you do the following
fixes #123).