백엔드 API 구현
지난 절에서는 "온라인 코드 에디터 및 실시간 협업 도구" 프로젝트의 요구사항을 분석하고 시스템 아키텍처를 설계했습니다. 이제 그 설계를 바탕으로 NestJS 백엔드의 핵심인 API 구현을 시작해 보겠습니다. 이 절에서는 프로젝트의 초기 설정부터 사용자 인증, 그리고 프로젝트 및 파일 관리를 위한 RESTful API 구현까지 다룰 것입니다.
NestJS 프로젝트 초기 설정
먼저 NestJS CLI를 사용하여 새로운 프로젝트를 생성합니다.
# NestJS CLI가 없다면 설치
npm install -g @nestjs/cli
# 새 프로젝트 생성
nest new collaborative-code-editor-backend
# 프로젝트 디렉토리로 이동
cd collaborative-code-editor-backend
# 필요한 기본 패키지 설치
npm install @nestjs/typeorm typeorm pg bcryptjs @nestjs/jwt passport @nestjs/passport passport-jwt @types/passport-jwt @types/bcryptjs dotenv
# WebSocket 및 Socket.IO 관련 패키지는 나중에 설치
# npm install @nestjs/websockets @nestjs/platform-socket.io socket.io @types/socket.io
설치된 주요 패키지 설명:
@nestjs/typeorm
,typeorm
,pg
: TypeORM ORM을 사용하여 PostgreSQL 데이터베이스와 연동합니다.bcryptjs
: 비밀번호 해싱을 위한 라이브러리입니다.@nestjs/jwt
,passport
,@nestjs/passport
,passport-jwt
: JWT(JSON Web Token) 기반 사용자 인증을 구현합니다.dotenv
:.env
파일에서 환경 변수를 로드합니다.
데이터베이스 설정
.env
파일을 생성하고 데이터베이스 연결 정보를 설정합니다.
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=your_db_user
DATABASE_PASSWORD=your_db_password
DATABASE_NAME=code_editor_db
# JWT Secret (보안을 위해 강력하고 긴 문자열 사용)
JWT_SECRET=super_secret_key_please_change_this_in_production
src/app.module.ts
파일을 수정하여 TypeORM 모듈을 설정합니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; // ConfigModule 임포트
@Module({
imports: [
// 환경 변수 로드
ConfigModule.forRoot({
isGlobal: true, // 전역적으로 사용 가능하도록 설정
envFilePath: process.env.NODE_ENV === 'development' ? '.env.development' : '.env', // 환경별 .env 파일 지정 가능
}),
// TypeORM 설정
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 엔티티 파일 경로 지정
synchronize: true, // 개발 단계에서만 true (자동으로 DB 스키마 생성/업데이트). 프로덕션에서는 migration 사용 권장
logging: ['query', 'error'], // 개발 시 쿼리 로그 확인
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
ConfigModule
을 통해 .env
파일을 로드하여 환경 변수를 안전하게 사용할 수 있습니다.
사용자 인증 (Auth) API 구현
사용자 관리를 위한 엔티티, DTO, 서비스, 컨트롤러 및 JWT 전략을 구현합니다.
User Entity
사용자 정보와 TypeORM 매핑을 정의합니다.
// src/auth/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { Project } from '../../project/entities/project.entity'; // Project 엔티티와 관계 설정 (미리 정의했다고 가정)
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, nullable: false })
email: string;
@Column({ nullable: false })
password: string; // 해싱된 비밀번호 저장
@Column({ nullable: true })
nickname: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Project, project => project.owner)
projects: Project[];
}
Auth DTOs
요청(Request) 데이터의 유효성 검사를 위한 DTO를 정의합니다.
npm install class-validator class-transformer
// src/auth/dto/register-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class RegisterUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6, { message: 'Password must be at least 6 characters long' })
password: string;
@IsString()
nickname?: string;
}
// src/auth/dto/login-user.dto.ts
import { IsEmail, IsString } from 'class-validator';
export class LoginUserDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
Auth Service
사용자 관련 비즈니스 로직(회원가입, 로그인)을 처리합니다.
// src/auth/auth.service.ts
import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { RegisterUserDto } from './dto/register-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async register(registerUserDto: RegisterUserDto): Promise<{ message: string }> {
const { email, password, nickname } = registerUserDto;
const existingUser = await this.usersRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 10); // 비밀번호 해싱
const newUser = this.usersRepository.create({
email,
password: hashedPassword,
nickname,
});
await this.usersRepository.save(newUser);
return { message: 'User registered successfully' };
}
async login(loginUserDto: LoginUserDto): Promise<{ accessToken: string }> {
const { email, password } = loginUserDto;
const user = await this.usersRepository.findOne({ where: { email } });
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
// JWT 페이로드 (사용자 ID를 포함)
const payload = { sub: user.id, email: user.email };
const accessToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: '1h', // 토큰 만료 시간
});
return { accessToken };
}
// JWT 전략에서 사용될 사용자 조회 메서드
async validateUser(userId: string): Promise<User> {
return this.usersRepository.findOne({ where: { id: userId } });
}
}
JWT Strategy (Passport)
요청 헤더의 JWT를 검증하고 사용자 정보를 추출합니다.
// src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service'; // AuthService 주입
import { User } from './entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer 토큰에서 JWT 추출
ignoreExpiration: false, // 만료된 토큰 거부
secretOrKey: configService.get<string>('JWT_SECRET'), // JWT Secret
});
}
async validate(payload: any): Promise<User> {
const user = await this.authService.validateUser(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Auth Controller
API 엔드포인트를 정의합니다.
// src/auth/auth.controller.ts
import { Controller, Post, Body, Get, UseGuards, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterUserDto } from './dto/register-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { AuthGuard } from '@nestjs/passport'; // AuthGuard 임포트
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() registerUserDto: RegisterUserDto) {
return this.authService.register(registerUserDto);
}
@Post('login')
async login(@Body() loginUserDto: LoginUserDto) {
return this.authService.login(loginUserDto);
}
@UseGuards(AuthGuard('jwt')) // JWT 가드 적용
@Get('profile')
getProfile(@Req() req) {
// req.user에는 JwtStrategy.validate()에서 반환된 사용자 정보가 담겨있음
const { password, ...result } = req.user; // 비밀번호는 제외하고 반환
return result;
}
}
Auth Module
Auth 관련 모든 컴포넌트들을 묶습니다.
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([User]), // User 엔티티 등록
PassportModule,
JwtModule.registerAsync({ // 비동기적으로 JWT Secret 로드
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtModule], // 다른 모듈에서 AuthService와 JwtModule 사용 가능하도록 내보내기
})
export class AuthModule {}
AppModule에 AuthModule 등록
src/app.module.ts
에 AuthModule
을 임포트합니다.
// src/app.module.ts (수정)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module'; // AuthModule 임포트
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === 'development' ? '.env.development' : '.env' }),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
logging: ['query', 'error'],
}),
AuthModule, // AuthModule 등록
],
controllers: [],
providers: [],
})
export class AppModule {}
프로젝트 및 파일 관리 API 구현
이제 사용자 인증이 완료된 후 접근할 수 있는 프로젝트 및 파일 관리 API를 구현합니다.
Project Entity
// src/project/entities/project.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { User } from '../../auth/entities/user.entity'; // User 엔티티 임포트
import { File } from '../../file/entities/file.entity'; // File 엔티티 임포트 (미리 정의했다고 가정)
import { Folder } from '../../folder/entities/folder.entity'; // Folder 엔티티 임포트 (미리 정의했다고 가정)
@Entity()
export class Project {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
name: string;
@Column({ nullable: false })
ownerId: string; // 소유자 ID
@ManyToOne(() => User, user => user.projects)
owner: User; // User 엔티티와의 관계
@OneToMany(() => File, file => file.project)
files: File[];
@OneToMany(() => Folder, folder => folder.project)
folders: Folder[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
File & Folder Entities
// src/file/entities/file.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity()
export class File {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
name: string;
@Column({ nullable: false })
path: string; // 예: '/src/main.ts'
@Column({ type: 'text', nullable: true })
content: string; // 파일 내용 (TEXT 타입)
@Column({ nullable: true })
type: string; // 파일 타입 (예: 'javascript', 'python')
@Column({ nullable: false })
projectId: string;
@ManyToOne(() => Project, project => project.files, { onDelete: 'CASCADE' }) // 프로젝트 삭제 시 파일도 삭제
project: Project;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// src/folder/entities/folder.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity()
export class Folder {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
name: string;
@Column({ nullable: false })
path: string; // 예: '/src'
@Column({ nullable: false })
projectId: string;
@ManyToOne(() => Project, project => project.folders, { onDelete: 'CASCADE' }) // 프로젝트 삭제 시 폴더도 삭제
project: Project;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Project Service, Controller, DTOs
// src/project/dto/create-project.dto.ts
import { IsString, MinLength } from 'class-validator';
export class CreateProjectDto {
@IsString()
@MinLength(3)
name: string;
}
// src/project/project.service.ts
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Project } from './entities/project.entity';
import { User } from '../auth/entities/user.entity';
import { CreateProjectDto } from './dto/create-project.dto';
import { File } from '../file/entities/file.entity';
import { Folder } from '../folder/entities/folder.entity';
@Injectable()
export class ProjectService {
constructor(
@InjectRepository(Project)
private projectsRepository: Repository<Project>,
@InjectRepository(File)
private filesRepository: Repository<File>,
@InjectRepository(Folder)
private foldersRepository: Repository<Folder>,
) {}
async createProject(createProjectDto: CreateProjectDto, user: User): Promise<Project> {
const newProject = this.projectsRepository.create({
name: createProjectDto.name,
owner: user,
ownerId: user.id,
});
return this.projectsRepository.save(newProject);
}
async findAllUserProjects(userId: string): Promise<Project[]> {
return this.projectsRepository.find({ where: { ownerId: userId }, order: { updatedAt: 'DESC' } });
}
async findProjectById(projectId: string, userId: string): Promise<Project> {
const project = await this.projectsRepository.findOne({
where: { id: projectId, ownerId: userId },
relations: ['files', 'folders'], // 관련 파일과 폴더를 함께 로드
});
if (!project) {
throw new NotFoundException('Project not found or you do not have access.');
}
return project;
}
async updateProject(projectId: string, userId: string, name: string): Promise<Project> {
const project = await this.findProjectById(projectId, userId); // 소유자 확인 포함
project.name = name;
return this.projectsRepository.save(project);
}
async deleteProject(projectId: string, userId: string): Promise<void> {
const project = await this.findProjectById(projectId, userId); // 소유자 확인 포함
await this.projectsRepository.remove(project); // cascade 삭제 설정으로 파일/폴더 함께 삭제
}
// --- 파일/폴더 관리 관련 메서드 (프로젝트 서비스 내에 함께 구현) ---
async createFile(projectId: string, userId: string, name: string, path: string, type?: string, content?: string): Promise<File> {
const project = await this.findProjectById(projectId, userId);
const newFile = this.filesRepository.create({
name,
path: path.startsWith('/') ? path : `/${path}`, // 경로 정규화
type,
content: content || '',
project,
projectId: project.id,
});
return this.filesRepository.save(newFile);
}
async updateFileContent(fileId: string, userId: string, newContent: string): Promise<File> {
const file = await this.filesRepository.findOne({ where: { id: fileId }, relations: ['project'] });
if (!file || file.project.ownerId !== userId) {
throw new UnauthorizedException('File not found or you do not have access.');
}
file.content = newContent;
return this.filesRepository.save(file);
}
async findFileById(fileId: string, userId: string): Promise<File> {
const file = await this.filesRepository.findOne({ where: { id: fileId }, relations: ['project'] });
if (!file || file.project.ownerId !== userId) {
throw new NotFoundException('File not found or you do not have access.');
}
return file;
}
async renameFile(fileId: string, userId: string, newName: string): Promise<File> {
const file = await this.findFileById(fileId, userId);
file.name = newName;
return this.filesRepository.save(file);
}
async deleteFile(fileId: string, userId: string): Promise<void> {
const file = await this.findFileById(fileId, userId);
await this.filesRepository.remove(file);
}
async createFolder(projectId: string, userId: string, name: string, path: string): Promise<Folder> {
const project = await this.findProjectById(projectId, userId);
const newFolder = this.foldersRepository.create({
name,
path: path.startsWith('/') ? path : `/${path}`,
project,
projectId: project.id,
});
return this.foldersRepository.save(newFolder);
}
async renameFolder(folderId: string, userId: string, newName: string): Promise<Folder> {
const folder = await this.foldersRepository.findOne({ where: { id: folderId }, relations: ['project'] });
if (!folder || folder.project.ownerId !== userId) {
throw new UnauthorizedException('Folder not found or you do not have access.');
}
folder.name = newName;
return this.foldersRepository.save(folder);
}
async deleteFolder(folderId: string, userId: string): Promise<void> {
const folder = await this.foldersRepository.findOne({ where: { id: folderId }, relations: ['project'] });
if (!folder || folder.project.ownerId !== userId) {
throw new UnauthorizedException('Folder not found or you do not have access.');
}
await this.foldersRepository.remove(folder);
// TODO: 폴더 내의 모든 파일/하위 폴더도 삭제하는 로직 추가 필요 (재귀적 삭제)
}
}
// src/project/project.controller.ts
import { Controller, Post, Get, Patch, Delete, Body, Param, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ProjectService } from './project.service';
import { CreateProjectDto } from './dto/create-project.dto';
import { User } from '../auth/entities/user.entity'; // 사용자 정보 타입 임포트
import { UpdateProjectDto } from './dto/update-project.dto'; // 추가할 DTO
import { CreateFileDto, UpdateFileContentDto, RenameFileDto } from '../file/dto/file.dto'; // 파일 DTO
import { CreateFolderDto, RenameFolderDto } from '../folder/dto/folder.dto'; // 폴더 DTO
@Controller('projects')
@UseGuards(AuthGuard('jwt')) // 모든 프로젝트 API에 JWT 인증 적용
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Post()
async createProject(@Body() createProjectDto: CreateProjectDto, @Req() req: any) {
const user: User = req.user;
return this.projectService.createProject(createProjectDto, user);
}
@Get()
async getAllUserProjects(@Req() req: any) {
const user: User = req.user;
return this.projectService.findAllUserProjects(user.id);
}
@Get(':projectId')
async getProjectDetails(@Param('projectId') projectId: string, @Req() req: any) {
const user: User = req.user;
return this.projectService.findProjectById(projectId, user.id);
}
@Patch(':projectId')
async updateProject(@Param('projectId') projectId: string, @Body() updateProjectDto: UpdateProjectDto, @Req() req: any) {
const user: User = req.user;
return this.projectService.updateProject(projectId, user.id, updateProjectDto.name);
}
@Delete(':projectId')
@HttpCode(HttpStatus.NO_CONTENT) // 204 No Content 반환
async deleteProject(@Param('projectId') projectId: string, @Req() req: any) {
const user: User = req.user;
await this.projectService.deleteProject(projectId, user.id);
}
// --- 파일 API ---
@Post(':projectId/files')
async createFile(
@Param('projectId') projectId: string,
@Body() createFileDto: CreateFileDto,
@Req() req: any,
) {
const user: User = req.user;
return this.projectService.createFile(projectId, user.id, createFileDto.name, createFileDto.path, createFileDto.type, createFileDto.content);
}
@Get(':projectId/files/:fileId')
async getFileContent(
@Param('projectId') projectId: string,
@Param('fileId') fileId: string,
@Req() req: any,
) {
const user: User = req.user;
// 실제로는 파일 내용만 반환하거나, DTO로 변환하여 필요한 정보만 반환
return this.projectService.findFileById(fileId, user.id);
}
@Patch(':projectId/files/:fileId/content')
async updateFileContent(
@Param('projectId') projectId: string,
@Param('fileId') fileId: string,
@Body() updateFileContentDto: UpdateFileContentDto,
@Req() req: any,
) {
const user: User = req.user;
return this.projectService.updateFileContent(fileId, user.id, updateFileContentDto.content);
}
@Patch(':projectId/files/:fileId/rename')
async renameFile(
@Param('projectId') projectId: string,
@Param('fileId') fileId: string,
@Body() renameFileDto: RenameFileDto,
@Req() req: any,
) {
const user: User = req.user;
return this.projectService.renameFile(fileId, user.id, renameFileDto.newName);
}
@Delete(':projectId/files/:fileId')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteFile(
@Param('projectId') projectId: string,
@Param('fileId') fileId: string,
@Req() req: any,
) {
const user: User = req.user;
await this.projectService.deleteFile(fileId, user.id);
}
// --- 폴더 API ---
@Post(':projectId/folders')
async createFolder(
@Param('projectId') projectId: string,
@Body() createFolderDto: CreateFolderDto,
@Req() req: any,
) {
const user: User = req.user;
return this.projectService.createFolder(projectId, user.id, createFolderDto.name, createFolderDto.path);
}
@Patch(':projectId/folders/:folderId/rename')
async renameFolder(
@Param('projectId') projectId: string,
@Param('folderId') folderId: string,
@Body() renameFolderDto: RenameFolderDto,
@Req() req: any,
) {
const user: User = req.user;
return this.projectService.renameFolder(folderId, user.id, renameFolderDto.newName);
}
@Delete(':projectId/folders/:folderId')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteFolder(
@Param('projectId') projectId: string,
@Param('folderId') folderId: string,
@Req() req: any,
) {
const user: User = req.user;
await this.projectService.deleteFolder(folderId, user.id);
}
}
@UseGuards(AuthGuard('jwt'))
: 이 데코레이터를 사용하여 특정 컨트롤러 또는 특정 메서드에 JWT 인증 가드를 적용할 수 있습니다. 요청에 유효한 JWT가 없으면 401 Unauthorized 응답을 반환합니다.@Req() req: any
: 요청 객체에서req.user
를 통해 인증된 사용자 정보를 가져올 수 있습니다. (JwtStrategy에서 반환한 값)- 파일 및 폴더 DTO는
src/file/dto/file.dto.ts
와src/folder/dto/folder.dto.ts
에 유사한 방식으로 정의합니다.
Project, File, Folder Modules
각 기능을 모듈로 묶고 AppModule
에 등록합니다.
// src/project/project.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Project } from './entities/project.entity';
import { ProjectService } from './project.service';
import { ProjectController } from './project.controller';
import { File } from '../file/entities/file.entity'; // File 엔티티 임포트
import { Folder } from '../folder/entities/folder.entity'; // Folder 엔티티 임포트
@Module({
imports: [
TypeOrmModule.forFeature([Project, File, Folder]), // 프로젝트, 파일, 폴더 엔티티 등록
],
controllers: [ProjectController],
providers: [ProjectService],
exports: [ProjectService], // 필요한 경우 ProjectService 내보내기
})
export class ProjectModule {}
// src/file/file.module.ts (현재는 ProjectModule에 통합되어 있으므로, 추후 분리 시 사용)
// import { Module } from '@nestjs/common';
// import { TypeOrmModule } from '@nestjs/typeorm';
// import { File } from './entities/file.entity';
// @Module({
// imports: [TypeOrmModule.forFeature([File])],
// providers: [],
// exports: [],
// })
// export class FileModule {}
마찬가지로 folder.module.ts
도 만듭니다. 현재는 ProjectModule
에서 File
과 Folder
엔티티를 관리하고 있으므로, 별도의 모듈은 당장 필요 없습니다. 하지만 DDD 관점에서 나중에 파일과 폴더를 자체 도메인으로 분리할 때 유용합니다.
AppModule에 ProjectModule 등록
src/app.module.ts
에 ProjectModule
을 임포트합니다.
// src/app.module.ts (최종 수정)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { ProjectModule } from './project/project.module'; // ProjectModule 임포트
import { User } from './auth/entities/user.entity'; // 엔티티 추가
import { Project } from './project/entities/project.entity'; // 엔티티 추가
import { File } from './file/entities/file.entity'; // 엔티티 추가
import { Folder } from './folder/entities/folder.entity'; // 엔티티 추가
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === 'development' ? '.env.development' : '.env' }),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [User, Project, File, Folder], // 모든 엔티티 등록
synchronize: true,
logging: ['query', 'error'],
}),
AuthModule,
ProjectModule, // ProjectModule 등록
],
controllers: [],
providers: [],
})
export class AppModule {}
주의: entities
배열에 모든 엔티티를 명시적으로 추가해야 TypeORM이 해당 엔티티를 인식하고 테이블을 생성합니다.
NestJS 애플리케이션 실행
이제 NestJS 애플리케이션을 실행하고 API를 테스트할 수 있습니다.
npm run start:dev
서버가 3000번 포트에서 실행되면, Postman이나 Insomnia 같은 API 클라이언트를 사용하여 구현한 API를 테스트할 수 있습니다.
테스트 시나리오
POST /auth/register
로 사용자 회원가입 (이메일, 비밀번호)
POST /auth/login
으로 로그인 (회원가입한 이메일, 비밀번호) -> accessToken
받기
GET /auth/profile
로 프로필 조회 (Header: Authorization: Bearer <accessToken>
)
POST /projects
로 새 프로젝트 생성 (Header: Authorization: Bearer <accessToken>
, Body: { "name": "My First Project" }
)
GET /projects
로 내 프로젝트 목록 조회 (Header: Authorization: Bearer <accessToken>
)
GET /projects/:projectId
로 특정 프로젝트 상세 조회
POST /projects/:projectId/files
로 파일 생성
GET /projects/:projectId/files/:fileId
로 파일 내용 조회
PATCH /projects/:projectId/files/:fileId/content
로 파일 내용 업데이트 (아직 실시간 아님)
이번 절에서는 "온라인 코드 에디터" 프로젝트의 백엔드 API를 구축하는 초기 단계를 진행했습니다. 사용자 인증, 프로젝트 및 파일/폴더 관리를 위한 기본 RESTful API를 TypeORM, Passport-JWT를 활용하여 구현했습니다. 다음 절에서는 이 백엔드 위에 프로젝트의 핵심 기능인 실시간 협업 기능(WebSocket) 을 구현하는 방법에 대해 알아보겠습니다.