본문 바로가기

프로젝트·파일 API

프로젝트·파일·폴더 서비스와 컨트롤러 및 요청 DTO를 구현합니다.

경로에 나타난 식별자, JWT에서 확인한 사용자, DB에서 읽은 소속을 구분하며 코드를 따라가 보세요. findProjectById()는 id와 ownerId를 함께 조회합니다. 반면 기존 파일을 읽거나 바꾸는 메서드는 fileId로 읽은 뒤 file.project.ownerId를 비교합니다. 여기까지가 현재 코드의 소유자 검사입니다.

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, InternalServerErrorException, 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.');
    }

    // 경로 기반 재귀 삭제: "/src" 삭제 시 "/src/*" 하위 리소스를 함께 정리
    try {
      await this.projectsRepository.manager.transaction(async (manager) => {
        const descendantFolders = await manager
          .createQueryBuilder(Folder, 'folder')
          .where('folder.projectId = :projectId', { projectId: folder.projectId })
          .andWhere('(folder.path = :exactPath OR folder.path LIKE :prefixPath)', {
            exactPath: folder.path,
            prefixPath: `${folder.path}/%`,
          })
          .getMany();

        if (descendantFolders.length === 0) {
          return;
        }

        const folderIds = descendantFolders.map((item) => item.id);

        await manager
          .createQueryBuilder()
          .delete()
          .from(File)
          .where('projectId = :projectId', { projectId: folder.projectId })
          .andWhere('(path = :exactPath OR path LIKE :prefixPath)', {
            exactPath: folder.path,
            prefixPath: `${folder.path}/%`,
          })
          .execute();

        await manager
          .createQueryBuilder()
          .delete()
          .from(Folder)
          .where('id IN (:...folderIds)', { folderIds })
          .execute();
      });
    } catch (error) {
      // 트랜잭션 실패 시 전체 롤백되어 "파일만 지워지고 폴더는 남는" 상태를 방지
      throw new InternalServerErrorException('Failed to delete folder tree. Please retry.');
    }
  }
}

예시

한 사용자가 서로 다른 두 프로젝트를 소유하고 있다고 합시다. 두 번째 프로젝트의 파일 ID를 첫 번째 프로젝트 URL에 넣어 조회하면, 현재 컨트롤러는 URL의 projectId를 받지만 findFileById()에는 fileId와 사용자 ID만 넘깁니다. 파일의 소유자가 같으면 서비스의 소유자 검사를 통과할 수 있습니다. 따라서 “내 파일이다”와 “이 URL의 프로젝트에 속한다”를 같은 판정으로 읽으면 안 됩니다. 중첩 경로의 소속 검사까지 완성하려면 실제 file.projectId와 요청 projectId를 비교하는 계약이 추가로 필요합니다.

폴더 /src를 지울 때는 같은 프로젝트의 정확한 /src와 /src/로 시작하는 하위 경로를 고릅니다. /src2/main.ts와 다른 프로젝트의 /src/main.ts는 제외되어야 합니다. 현재 코드가 파라미터를 바인딩하더라도 SQL LIKE의 %·_는 패턴 문자입니다. 사용자 경로에 이 문자가 허용된다면 리터럴 경로 비교가 되도록 별도의 입력 규칙과 escaping을 정해야 합니다.

삭제 범위 판단 문제

일반 경로만 쓰는 프로젝트에서 대상 폴더는 /src입니다. 파일 경로가 /src/main.ts, /src2/main.ts, /src이고, 다른 프로젝트에도 /src/main.ts가 있습니다. 현재 파일 삭제 조건에 선택되는 행을 고르고, 두 번째 삭제 쿼리가 실패하면 앞선 파일 삭제가 어떻게 되는지 설명해 보세요.

쿼리 조건과 확정 단위 확인

같은 프로젝트의 /src/main.ts와 정확히 /src인 파일 행이 선택됩니다. /src2/main.ts는 슬래시가 포함된 /src/%와 일치하지 않고 다른 프로젝트는 projectId 조건으로 제외됩니다. 이 답은 폴더 조회 결과가 비어 있지 않아 삭제를 계속하고, 경로에 wildcard 문자가 없다는 전제입니다. 같은 transaction manager의 파일 삭제와 폴더 삭제 사이에 오류가 나면 함께 rollback합니다. 트랜잭션 이전의 소유권 조회나 다른 시스템의 작업까지 되돌린다는 의미는 아닙니다.

실패 처리 포인트
  • 트랜잭션 롤백: 하위 파일/폴더 삭제 중 하나라도 실패하면 전체를 취소해 데이터 정합성을 유지합니다.
  • 재시도 가능 에러 메시지: API 응답에 재시도 가능 여부를 알리는 메시지를 담아 프론트엔드가 사용자에게 안내할 수 있게 합니다.
  • 운영 로그 분리: folderId, projectId, userId를 구조화 로그로 남겨 장애 분석 시간을 줄입니다.

인증 주체로 프로젝트 범위를 좁히고 폴더 삭제를 함께 확정합니다

소유 검사·프로젝트 경로 선택·File/Folder 동일 TX 삭제·함께 commit/rollback으로 상태가 변한다.

Nest 프로젝트 API · 소유권과 트랜잭션

인증 주체로 프로젝트 범위를 좁히고 폴더 삭제를 함께 확정합니다

JWT 인증은 사용자를 정합니다. 서비스는 그 사용자의 프로젝트·리소스만 처리해야 합니다. 삭제 트랜잭션의 성공·롤백과 현재 예제에 추가할 URL·경로 검증을 구분합니다.

소유자 거부와 삭제 거래의 두 결과 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 거래 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다. 삭제 트랜잭션 아니요 예 성공 오류 JWT에서 사용자 확인 req.user.id 폴더 소유자? 인증 사용자와 비교 거부 · 삭제 없음 UnauthorizedException 같은 project 경로 선택 exactPath 또는 prefixPath File → Folder 삭제 같은 transaction manager 삭제 완료? 오류 발생 여부 함께 commit 삭제 확정 함께 rollback 실패 응답 500
소유자 거부와 삭제 거래의 두 결과 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 거래 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다. 삭제 거래 예 아니요 성공 오류 JWT 사용자 확인 req.user.id 폴더 소유자? 사용자와 비교 같은 project 경로 선택 exact / prefix File → Folder 삭제 같은 manager 삭제 완료? 오류 발생 여부 함께 commit 삭제 확정 거부 · 삭제 없음 Unauthorized Exception 함께 rollback 실패 응답 500

그림은 거래 안에서 대상·하위 폴더 조회 결과가 비어 있지 않은 삭제 경로입니다. 조회 결과가 비면 파일·폴더 삭제 없이 반환합니다.

소유권

요청부터 소유권 확인까지

  1. 인증된 사용자

    AuthGuard(jwt)와 JwtStrategy가 정한 req.user.id를 사용합니다. 본문의 사용자 ID를 신원으로 믿지 않습니다.

  2. 프로젝트 조회·생성 경계

    목록은 ownerId로 제한하고 개별 프로젝트는 id + ownerId로 찾습니다. 파일·폴더 생성도 이 조회를 먼저 통과합니다.

  3. 기존 파일·폴더 경계

    리소스의 project.ownerId를 인증 주체와 비교합니다. JWT가 유효해도 다른 소유자의 리소스는 허용하지 않습니다.

삭제 단위

폴더 트리 삭제의 현재 순서

  1. 대상 폴더와 소유자 확인

    현재 예제는 트랜잭션 전에 폴더와 project를 읽어 소유자를 확인합니다.

  2. 같은 프로젝트의 경로 선택

    트랜잭션 manager로 projectId와 정확한 경로 또는 하위 prefix에 맞는 폴더를 찾습니다.

  3. 파일 → 폴더 삭제

    같은 프로젝트의 대상 경로 파일을 지운 뒤 조회한 폴더 ID들을 지웁니다.

  4. 함께 commit하거나 rollback

    삭제 중 오류가 나면 트랜잭션을 취소하고 500으로 실패합니다. rollback은 앞선 소유자 검증이나 외부 작업까지 포함하지 않습니다.

/src

경로 범위의 구체적 예

일반 경로 /src는 정확히 같은 /src와 /src/… 하위를 선택합니다. /src2는 이 prefix에 속하지 않습니다. 같은 경로라도 다른 projectId는 제외합니다.

추가할 계약

현재 코드와 추가 검증의 경계

기존 리소스 API는 소유자를 검사하지만 URL의 projectId를 서비스에 전달하지 않습니다. 중첩 route 계약을 완성하려면 리소스의 실제 프로젝트와 URL도 일치시켜야 합니다.

현재 경로 처리는 선행 /를 붙이는 수준입니다. ..·중복 구분자·루트의 규칙을 정하고 SQL LIKE의 %·_가 사용자 경로에서 wildcard가 되지 않도록 검증·escaping을 추가해야 합니다. 바인딩 parameter만으로 wildcard 의미가 사라지지는 않습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 트랜잭션 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
4소유자 거부와 삭제 트랜잭션의 두 결과
5폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 트랜잭션 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다.
36그림은 트랜잭션 안에서 대상·하위 폴더 조회 결과가 비어 있지 않은 삭제 경로입니다. 조회 결과가 비면 파일·폴더 삭제 없이 반환합니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
소유자와 프로젝트

폴더 소유를 확인하고 같은 프로젝트 경로를 선택합니다.

소유 실패: 삭제 없음 · 경로: 같은 프로젝트허용된 범위만 삭제 후보가 됩니다.
같은 TX 삭제

File·Folder를 같은 transaction 안에서 삭제합니다.

변경: File·Folder · 범위: 같은 TX연관 삭제의 원자적 범위를 묶습니다.
함께 완료·rollback

성공은 함께 확정하고 오류는 함께 되돌립니다.

성공: 둘 다 commit · 실패: 둘 다 rollback · wildcard: 추가 계약두 데이터 변경이 같은 완료 결과를 가집니다.

인증 주체로 프로젝트 범위를 좁히고 폴더 삭제를 함께 확정합니다

소유 검사·프로젝트 경로 선택·File/Folder 동일 TX 삭제·함께 commit/rollback으로 상태가 변한다.

Nest 프로젝트 API · 소유권과 트랜잭션

인증 주체로 프로젝트 범위를 좁히고 폴더 삭제를 함께 확정합니다

JWT 인증은 사용자를 정합니다. 서비스는 그 사용자의 프로젝트·리소스만 처리해야 합니다. 삭제 트랜잭션의 성공·롤백과 현재 예제에 추가할 URL·경로 검증을 구분합니다.

소유자 거부와 삭제 거래의 두 결과 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 거래 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다. 삭제 트랜잭션 아니요 예 성공 오류 JWT에서 사용자 확인 req.user.id 폴더 소유자? 인증 사용자와 비교 거부 · 삭제 없음 UnauthorizedException 같은 project 경로 선택 exactPath 또는 prefixPath File → Folder 삭제 같은 transaction manager 삭제 완료? 오류 발생 여부 함께 commit 삭제 확정 함께 rollback 실패 응답 500
소유자 거부와 삭제 거래의 두 결과 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 거래 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다. 삭제 거래 예 아니요 성공 오류 JWT 사용자 확인 req.user.id 폴더 소유자? 사용자와 비교 같은 project 경로 선택 exact / prefix File → Folder 삭제 같은 manager 삭제 완료? 오류 발생 여부 함께 commit 삭제 확정 거부 · 삭제 없음 Unauthorized Exception 함께 rollback 실패 응답 500

그림은 거래 안에서 대상·하위 폴더 조회 결과가 비어 있지 않은 삭제 경로입니다. 조회 결과가 비면 파일·폴더 삭제 없이 반환합니다.

소유권

요청부터 소유권 확인까지

  1. 인증된 사용자

    AuthGuard(jwt)와 JwtStrategy가 정한 req.user.id를 사용합니다. 본문의 사용자 ID를 신원으로 믿지 않습니다.

  2. 프로젝트 조회·생성 경계

    목록은 ownerId로 제한하고 개별 프로젝트는 id + ownerId로 찾습니다. 파일·폴더 생성도 이 조회를 먼저 통과합니다.

  3. 기존 파일·폴더 경계

    리소스의 project.ownerId를 인증 주체와 비교합니다. JWT가 유효해도 다른 소유자의 리소스는 허용하지 않습니다.

삭제 단위

폴더 트리 삭제의 현재 순서

  1. 대상 폴더와 소유자 확인

    현재 예제는 트랜잭션 전에 폴더와 project를 읽어 소유자를 확인합니다.

  2. 같은 프로젝트의 경로 선택

    트랜잭션 manager로 projectId와 정확한 경로 또는 하위 prefix에 맞는 폴더를 찾습니다.

  3. 파일 → 폴더 삭제

    같은 프로젝트의 대상 경로 파일을 지운 뒤 조회한 폴더 ID들을 지웁니다.

  4. 함께 commit하거나 rollback

    삭제 중 오류가 나면 트랜잭션을 취소하고 500으로 실패합니다. rollback은 앞선 소유자 검증이나 외부 작업까지 포함하지 않습니다.

/src

경로 범위의 구체적 예

일반 경로 /src는 정확히 같은 /src와 /src/… 하위를 선택합니다. /src2는 이 prefix에 속하지 않습니다. 같은 경로라도 다른 projectId는 제외합니다.

추가할 계약

현재 코드와 추가 검증의 경계

기존 리소스 API는 소유자를 검사하지만 URL의 projectId를 서비스에 전달하지 않습니다. 중첩 route 계약을 완성하려면 리소스의 실제 프로젝트와 URL도 일치시켜야 합니다.

현재 경로 처리는 선행 /를 붙이는 수준입니다. ..·중복 구분자·루트의 규칙을 정하고 SQL LIKE의 %·_가 사용자 경로에서 wildcard가 되지 않도록 검증·escaping을 추가해야 합니다. 바인딩 parameter만으로 wildcard 의미가 사라지지는 않습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 트랜잭션 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
4소유자 거부와 삭제 트랜잭션의 두 결과
5폴더 소유자 검사가 실패하면 삭제하지 않는다. 같은 프로젝트의 경로를 선택한 뒤 트랜잭션 안에서 File과 Folder를 삭제하여 함께 확정하거나 오류 시 함께 되돌린다. URL 프로젝트 소속과 wildcard 검증은 별도 추가 계약이다.
36그림은 트랜잭션 안에서 대상·하위 폴더 조회 결과가 비어 있지 않은 삭제 경로입니다. 조회 결과가 비면 파일·폴더 삭제 없이 반환합니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
소유자와 프로젝트

폴더 소유를 확인하고 같은 프로젝트 경로를 선택합니다.

소유 실패: 삭제 없음 · 경로: 같은 프로젝트허용된 범위만 삭제 후보가 됩니다.
같은 TX 삭제

File·Folder를 같은 transaction 안에서 삭제합니다.

변경: File·Folder · 범위: 같은 TX연관 삭제의 원자적 범위를 묶습니다.
함께 완료·rollback

성공은 함께 확정하고 오류는 함께 되돌립니다.

성공: 둘 다 commit · 실패: 둘 다 rollback · wildcard: 추가 계약두 데이터 변경이 같은 완료 결과를 가집니다.
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에 유사한 방식으로 정의합니다.

이 페이지 목차

이 페이지 목차