icon
13장 : 실전 프로젝트

백엔드 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.tsAuthModule을 임포트합니다.

// 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.tssrc/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에서 FileFolder 엔티티를 관리하고 있으므로, 별도의 모듈은 당장 필요 없습니다. 하지만 DDD 관점에서 나중에 파일과 폴더를 자체 도메인으로 분리할 때 유용합니다.

AppModule에 ProjectModule 등록

src/app.module.tsProjectModule을 임포트합니다.

// 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) 을 구현하는 방법에 대해 알아보겠습니다.