diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index a9133b5f..298c4fda 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -75,6 +75,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: "23" + # 루트 의존성 캐시 설정 - name: Cache Yarn dependencies for root id: cache-deps @@ -83,6 +84,12 @@ jobs: path: node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + # 환경변수 build에 필요한 .env 생성 + - name: Set up .env file + run: | + cd /home/runner/work/refactor-web39-OctoDocs/refactor-web39-OctoDocs/apps/frontend + echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> .env + # 빌드 실행 - name: Run build run: yarn build @@ -100,11 +107,19 @@ jobs: # 의존성 캐시 복원 - name: Restore Yarn dependencies + id: cache-deps uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + # test에 필요한 의존성 설치 + - name: Install Jest dependencies from test package + run: | + cd /home/runner/work/refactor-web39-OctoDocs/refactor-web39-OctoDocs/apps/backend/test + yarn install + mv node_modules ../ + # 테스트 실행 - name: Run tests run: yarn test diff --git a/apps/backend/src/edge/edge.service.ts b/apps/backend/src/edge/edge.service.ts index 979b6e34..05f50e7d 100644 --- a/apps/backend/src/edge/edge.service.ts +++ b/apps/backend/src/edge/edge.service.ts @@ -58,16 +58,6 @@ export class EdgeService { await this.edgeRepository.remove(edge); } - async findEdgeByFromNodeAndToNode(fromNodeId: number, toNodeId: number) { - return this.edgeRepository.findOne({ - where: { - fromNode: { id: fromNodeId }, - toNode: { id: toNodeId }, - }, - relations: ['fromNode', 'toNode'], - }); - } - async findEdgesByWorkspace(workspaceId: string): Promise { // 워크스페이스 DB에서 해당 워크스페이스의 내부 id를 찾는다 const workspace = await this.workspaceRepository.findOneBy({ diff --git a/apps/backend/src/node/dtos/coordinateResponse.dto.ts b/apps/backend/src/node/dtos/coordinateResponse.dto.ts deleted file mode 100644 index dc7e1475..00000000 --- a/apps/backend/src/node/dtos/coordinateResponse.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNumber, IsObject } from 'class-validator'; -class Coordinate { - @IsNumber() - x: number; - @IsNumber() - y: number; -} -export class CoordinateResponseDto { - @ApiProperty({ - example: 'OO 생성에 성공했습니다.', - description: 'api 요청 결과 메시지', - }) - @IsString() - message: string; - - @ApiProperty({ - example: { - x: 14, - y: 14, - }, - description: 'api 요청 결과 메시지', - }) - @IsObject() - coordinate: Coordinate; -} diff --git a/apps/backend/src/node/dtos/createNode.dto.ts b/apps/backend/src/node/dtos/createNode.dto.ts deleted file mode 100644 index 71e1b113..00000000 --- a/apps/backend/src/node/dtos/createNode.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNumber } from 'class-validator'; - -export class CreateNodeDto { - @ApiProperty({ - example: '노드 제목', - description: '노드 제목', - }) - @IsString() - title: string; - - @ApiProperty({ - example: '14', - description: 'x 좌표입니다.', - }) - @IsNumber() - x: number; - - @ApiProperty({ - example: '14', - description: 'y 좌표입니다.', - }) - @IsNumber() - y: number; -} diff --git a/apps/backend/src/node/dtos/findNodeResponse.dto.ts b/apps/backend/src/node/dtos/findNodeResponse.dto.ts deleted file mode 100644 index 6259ee48..00000000 --- a/apps/backend/src/node/dtos/findNodeResponse.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IsString, IsObject } from 'class-validator'; -import { Node } from '../node.entity'; - -export class FindNodeResponseDto { - @IsString() - message: string; - - @IsObject() - node: Node; -} diff --git a/apps/backend/src/node/dtos/messageResponse.dto.ts b/apps/backend/src/node/dtos/messageResponse.dto.ts deleted file mode 100644 index 555c4329..00000000 --- a/apps/backend/src/node/dtos/messageResponse.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class MessageResponseDto { - @ApiProperty({ - example: 'OO 생성에 성공했습니다.', - description: 'api 요청 결과 메시지', - }) - @IsString() - message: string; -} diff --git a/apps/backend/src/node/dtos/moveNode.dto.ts b/apps/backend/src/node/dtos/moveNode.dto.ts deleted file mode 100644 index ce1d6f45..00000000 --- a/apps/backend/src/node/dtos/moveNode.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsNumber } from 'class-validator'; - -export class MoveNodeDto { - @IsNumber() - x: number; - - @IsNumber() - y: number; -} diff --git a/apps/backend/src/node/dtos/updateNode.dto.ts b/apps/backend/src/node/dtos/updateNode.dto.ts deleted file mode 100644 index 7ec37597..00000000 --- a/apps/backend/src/node/dtos/updateNode.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IsString, IsNumber } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateNodeDto { - @ApiProperty({ - example: '노드 제목', - description: '노드 제목', - }) - @IsString() - title: string; - - @ApiProperty({ - example: '14', - description: 'x 좌표입니다.', - }) - @IsNumber() - x: number; - - @ApiProperty({ - example: '14', - description: 'y 좌표입니다.', - }) - @IsNumber() - y: number; -} diff --git a/apps/backend/src/node/node.controller.spec.ts b/apps/backend/src/node/node.controller.spec.ts index fe34ffa9..4ab82c09 100644 --- a/apps/backend/src/node/node.controller.spec.ts +++ b/apps/backend/src/node/node.controller.spec.ts @@ -1,15 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NodeController } from './node.controller'; import { NodeService } from './node.service'; -import { NodeResponseMessage } from './node.controller'; -import { CreateNodeDto } from './dtos/createNode.dto'; -import { UpdateNodeDto } from './dtos/updateNode.dto'; -import { MoveNodeDto } from './dtos/moveNode.dto'; -import { NodeNotFoundException } from '../exception/node.exception'; describe('NodeController', () => { let controller: NodeController; - let nodeService: NodeService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -32,123 +26,12 @@ describe('NodeController', () => { }).compile(); controller = module.get(NodeController); - nodeService = module.get(NodeService); + // nodeService = module.get(NodeService); }); it('컨트롤러 클래스가 정상적으로 인스턴스화된다.', () => { expect(controller).toBeDefined(); }); - describe('createNode', () => { - it('노드가 성공적으로 만들어진다', async () => { - const dto: CreateNodeDto = { title: 'New Node', x: 1, y: 2 }; - const expectedResponse = { - message: NodeResponseMessage.NODE_CREATED, - }; - - const result = await controller.createNode(dto); - - expect(nodeService.createNode).toHaveBeenCalledWith(dto); - expect(result).toEqual(expectedResponse); - }); - }); - - describe('deleteNode', () => { - it('id에 해당하는 노드를 찾아 삭제한다.', async () => { - const id = 2; - const expectedResponse = { - message: NodeResponseMessage.NODE_DELETED, - }; - - const result = await controller.deleteNode(id); - - expect(nodeService.deleteNode).toHaveBeenCalledWith(id); - expect(result).toEqual(expectedResponse); - }); - - it('id에 해당하는 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeService, 'deleteNode') - .mockRejectedValue(new NodeNotFoundException()); - - await expect(controller.deleteNode(1)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - - describe('updateNode', () => { - it('id에 해당하는 노드를 찾아 갱신한다.', async () => { - const id = 2; - const dto: UpdateNodeDto = { title: 'Updated Node', x: 3, y: 4 }; - const expectedResponse = { - message: NodeResponseMessage.NODE_UPDATED, - }; - - const result = await controller.updateNode(id, dto); - - expect(nodeService.updateNode).toHaveBeenCalledWith(id, dto); - expect(result).toEqual(expectedResponse); - }); - - it('id에 해당하는 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeService, 'updateNode') - .mockRejectedValue(new NodeNotFoundException()); - - await expect( - controller.updateNode(1, new UpdateNodeDto()), - ).rejects.toThrow(NodeNotFoundException); - }); - }); - describe('getCoordinates', () => { - it('id에 해당하는 노드를 찾아 좌표를 반환한다.', async () => { - const id = 2; - const expectedCoor = { x: 3, y: 8 }; - - jest.spyOn(nodeService, 'getCoordinates').mockResolvedValue(expectedCoor); - - await expect(controller.getCoordinates(id)).resolves.toEqual({ - message: NodeResponseMessage.NODE_GET_COORDINAE, - coordinate: expectedCoor, - }); - }); - - it('id에 해당하는 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeService, 'getCoordinates') - .mockRejectedValue(new NodeNotFoundException()); - - await expect(controller.getCoordinates(1)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - - describe('moveNode', () => { - it('id에 해당하는 노드를 찾아 이동시킨다.', async () => { - const id = 2; - const dto: MoveNodeDto = { x: 3, y: 4 }; - const expectedResponse = { - message: NodeResponseMessage.NODE_MOVED, - }; - - await expect(controller.moveNode(id, dto)).resolves.toEqual( - expectedResponse, - ); - expect(nodeService.moveNode).toHaveBeenCalledWith(id, dto); - }); - - it('id에 해당하는 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeService, 'moveNode') - .mockRejectedValue(new NodeNotFoundException()); - - await expect(controller.moveNode(1, new MoveNodeDto())).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - describe('findNodesByWorkspace', () => {}); }); diff --git a/apps/backend/src/node/node.controller.ts b/apps/backend/src/node/node.controller.ts index 21a32f26..043357da 100644 --- a/apps/backend/src/node/node.controller.ts +++ b/apps/backend/src/node/node.controller.ts @@ -1,134 +1,16 @@ -import { - Controller, - Get, - Post, - Delete, - Patch, - Param, - Body, - HttpCode, - HttpStatus, - ParseIntPipe, -} from '@nestjs/common'; +import { Controller, Get, Param, HttpCode, HttpStatus } from '@nestjs/common'; import { NodeService } from './node.service'; -import { CreateNodeDto } from './dtos/createNode.dto'; -import { UpdateNodeDto } from './dtos/updateNode.dto'; -import { MoveNodeDto } from './dtos/moveNode.dto'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { MessageResponseDto } from './dtos/messageResponse.dto'; -import { CoordinateResponseDto } from './dtos/coordinateResponse.dto'; -import { FindNodeResponseDto } from './dtos/findNodeResponse.dto'; import { FindNodesResponseDto } from './dtos/findNodesResponse.dto.'; export enum NodeResponseMessage { - NODE_RETURNED = '노드와 페이지를 가져왔습니다.', NODES_RETURNED = '워크스페이스의 모든 노드를 가져왔습니다.', - NODE_CREATED = '노드와 페이지를 생성했습니다.', - NODE_UPDATED = '노드와 페이지를 갱신했습니다.', - NODE_DELETED = '노드와 페이지를 삭제했습니다.', - NODE_GET_COORDINAE = '노드의 현재 좌표를 가져왔습니다.', - NODE_MOVED = '노드의 위치를 이동했습니다.', } @Controller('node') export class NodeController { constructor(private readonly nodeService: NodeService) {} - @ApiResponse({ - type: FindNodeResponseDto, - }) - @ApiOperation({ - summary: - '노드와 페이지 정보를 가져옵니다. (페이지 정보 중 id와 title만 가져옵니다.)', - }) - @Get('/:id') - @HttpCode(HttpStatus.OK) - async getNodeById(@Param('id', ParseIntPipe) id: number) { - const node = await this.nodeService.findNodeById(id); - - return { - message: NodeResponseMessage.NODE_RETURNED, - node: node, - }; - } - - @ApiResponse({ - type: MessageResponseDto, - }) - @ApiOperation({ summary: '노드를 생성하면서 페이지도 함께 생성합니다.' }) - @Post('/') - @HttpCode(HttpStatus.CREATED) - async createNode(@Body() body: CreateNodeDto) { - await this.nodeService.createNode(body); - - return { - message: NodeResponseMessage.NODE_CREATED, - }; - } - - @ApiResponse({ - type: MessageResponseDto, - }) - @ApiOperation({ - summary: '노드를 삭제하면서 페이지도 삭제합니다. (delete: cascade)', - }) - @Delete('/:id') - @HttpCode(HttpStatus.OK) - async deleteNode( - @Param('id', ParseIntPipe) id: number, - ): Promise<{ message: string }> { - await this.nodeService.deleteNode(id); - - return { - message: NodeResponseMessage.NODE_DELETED, - }; - } - - @ApiResponse({ - type: MessageResponseDto, - }) - @ApiOperation({ summary: '노드의 제목, 좌표를 수정합니다.' }) - @Patch('/:id') - @HttpCode(HttpStatus.OK) - async updateNode( - @Param('id', ParseIntPipe) id: number, - @Body() body: UpdateNodeDto, - ): Promise<{ message: string }> { - await this.nodeService.updateNode(id, body); - - return { - message: NodeResponseMessage.NODE_UPDATED, - }; - } - - @ApiResponse({ - type: CoordinateResponseDto, - }) - @ApiOperation({ summary: '노드의 좌표를 가져옵니다.' }) - @Get(':id/coordinates') - @HttpCode(HttpStatus.OK) - async getCoordinates(@Param('id', ParseIntPipe) id: number) { - const coordinate = await this.nodeService.getCoordinates(id); - - return { - message: NodeResponseMessage.NODE_GET_COORDINAE, - coordinate: coordinate, - }; - } - - @Patch('/:id/move') - @HttpCode(HttpStatus.OK) - async moveNode( - @Param('id', ParseIntPipe) id: number, - @Body() body: MoveNodeDto, - ) { - await this.nodeService.moveNode(id, body); - - return { - message: NodeResponseMessage.NODE_MOVED, - }; - } - @ApiResponse({ type: FindNodesResponseDto, }) diff --git a/apps/backend/src/node/node.service.spec.ts b/apps/backend/src/node/node.service.spec.ts index e0207f6f..1fd24655 100644 --- a/apps/backend/src/node/node.service.spec.ts +++ b/apps/backend/src/node/node.service.spec.ts @@ -2,12 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NodeService } from './node.service'; import { NodeRepository } from './node.repository'; import { PageRepository } from '../page/page.repository'; -import { NodeNotFoundException } from '../exception/node.exception'; -import { Node } from './node.entity'; -import { Page } from '../page/page.entity'; -import { CreateNodeDto } from './dtos/createNode.dto'; -import { UpdateNodeDto } from './dtos/updateNode.dto'; -import { MoveNodeDto } from './dtos/moveNode.dto'; import { WorkspaceRepository } from '../workspace/workspace.repository'; import { Workspace } from '../workspace/workspace.entity'; @@ -24,12 +18,8 @@ describe('NodeService', () => { { provide: NodeRepository, useValue: { - create: jest.fn(), - save: jest.fn(), - delete: jest.fn(), findOneBy: jest.fn(), findOne: jest.fn(), - update: jest.fn(), findNodesByWorkspace: jest.fn(), }, }, @@ -63,204 +53,6 @@ describe('NodeService', () => { expect(workspaceRepository).toBeDefined(); }); - describe('createNode', () => { - it('새로운 노드를 만들어 새로운 페이지와 연결한다.', async () => { - const dto: CreateNodeDto = { title: 'Node Title', x: 0, y: 0 }; - const node = { - id: 1, - x: 0, - y: 0, - color: '#FFFFFF', - title: 'Node Title', - page: null, - outgoingEdges: [], - incomingEdges: [], - workspace: null, - } as Node; - const page = { id: 1, title: 'Test Page', content: null } as Page; - - jest.spyOn(nodeRepository, 'save').mockResolvedValue(node); - jest.spyOn(pageRepository, 'save').mockResolvedValue(page); - - const result = await service.createNode(dto); - - expect(result).toEqual(node); - expect(nodeRepository.save).toHaveBeenCalledTimes(2); - expect(pageRepository.save).toHaveBeenCalledWith({ - title: dto.title, - content: {}, - }); - }); - }); - - describe('deleteNode', () => { - it('노드를 성공적으로 삭제한다.', async () => { - jest - .spyOn(nodeRepository, 'delete') - .mockResolvedValue({ affected: true } as any); - jest.spyOn(nodeRepository, 'findOneBy').mockResolvedValue(new Node()); - - await service.deleteNode(1); - - expect(nodeRepository.delete).toHaveBeenCalledWith(1); - }); - - it('삭제할 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeRepository, 'delete') - .mockResolvedValue({ affected: false } as any); - await expect(service.deleteNode(1)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - - describe('updateNode', () => { - it('노드를 성공적으로 업데이트한다.', async () => { - const dto: UpdateNodeDto = { title: 'Updated Title', x: 1, y: 1 }; - const node = new Node(); - node.page = new Page(); - - jest.spyOn(nodeRepository, 'findOne').mockResolvedValue(node); - jest.spyOn(pageRepository, 'findOneBy').mockResolvedValue(node.page); - jest.spyOn(nodeRepository, 'save').mockResolvedValue(node); - - const result = await service.updateNode(1, dto); - - expect(result).toEqual(node); - expect(nodeRepository.save).toHaveBeenCalledWith({ - ...node, - x: dto.x, - y: dto.y, - }); - expect(pageRepository.findOneBy).toHaveBeenCalledWith({ - id: node.page.id, - }); - }); - - it('업데이트할 노드가 존재하지 않으면 NodeNotFoundException을 throw한다.', async () => { - jest.spyOn(nodeRepository, 'findOne').mockResolvedValue(undefined); - - await expect(service.updateNode(1, {} as any)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - - describe('getCoordinates', () => { - it('노드 아이디를 받아 해당 노드의 좌표를 반환한다.', async () => { - const node = { id: 1, x: 1, y: 2 } as Node; - - jest.spyOn(nodeRepository, 'findOne').mockResolvedValue(node); - - const coordinates = await service.getCoordinates(1); - - expect(coordinates).toEqual({ x: 1, y: 2 }); - expect(nodeRepository.findOne).toHaveBeenCalledWith({ - relations: ['page'], - select: { - id: true, - page: { - id: true, - title: true, - }, - }, - where: { - id: 1, - }, - }); - }); - - it('노드를 찾을 수 없으면 NodeNotFoundException을 throw한다.', async () => { - jest.spyOn(nodeRepository, 'findOneBy').mockResolvedValue(null); - - await expect(service.getCoordinates(1)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - - describe('findNodeById', () => { - it('존재하는 노드를 아이디로 조회하여 반환한다.', async () => { - const node = { - id: 1, - x: 0, - y: 0, - color: '#FFFFFF', - title: 'Node Title', - page: null, - outgoingEdges: [], - incomingEdges: [], - workspace: null, - } as Node; - jest.spyOn(nodeRepository, 'findOne').mockResolvedValue(node); - - const result = await service.findNodeById(1); - - expect(result).toEqual(node); - expect(nodeRepository.findOne).toHaveBeenCalledWith({ - relations: ['page'], - select: { - id: true, - page: { - id: true, - title: true, - }, - }, - where: { - id: 1, - }, - }); - }); - - it('노드를 찾을 수 없으면 NodeNotFoundException을 던진다.', async () => { - jest.spyOn(nodeRepository, 'findOne').mockResolvedValue(undefined); - await expect(service.findNodeById(1)).rejects.toThrow( - NodeNotFoundException, - ); - expect(nodeRepository.findOne).toHaveBeenCalledWith({ - relations: ['page'], - select: { - id: true, - page: { - id: true, - title: true, - }, - }, - where: { - id: 1, - }, - }); - }); - }); - - describe('moveNode', () => { - it('노드를 성공적으로 이동시킨다.', async () => { - const id = 1; - const dto: MoveNodeDto = { x: 3, y: 4 }; - const node = { id: 1, x: 0, y: 0 } as Node; - - jest.spyOn(service, 'findNodeById').mockResolvedValue(node); - jest - .spyOn(nodeRepository, 'update') - .mockResolvedValue({ affected: true } as any); - - await service.moveNode(id, dto); - - expect(nodeRepository.update).toHaveBeenCalledWith(id, { - x: dto.x, - y: dto.y, - }); - }); - - it('노드를 찾을 수 없으면 NodeNotFoundException을 던진다.', async () => { - jest.spyOn(nodeRepository, 'update').mockRejectedValue(new Error()); - await expect(service.moveNode(1, {} as any)).rejects.toThrow( - NodeNotFoundException, - ); - }); - }); - describe('findNodesByWorkspace', () => { it('workspace에 해당하는 노드 조회 성공', async () => { const currentDate = new Date(); diff --git a/apps/backend/src/node/node.service.ts b/apps/backend/src/node/node.service.ts index 4bbd84ee..a8e363d5 100644 --- a/apps/backend/src/node/node.service.ts +++ b/apps/backend/src/node/node.service.ts @@ -1,11 +1,6 @@ import { Injectable } from '@nestjs/common'; import { NodeRepository } from './node.repository'; -import { PageRepository } from '../page/page.repository'; import { Node } from './node.entity'; -import { CreateNodeDto } from './dtos/createNode.dto'; -import { UpdateNodeDto } from './dtos/updateNode.dto'; -import { NodeNotFoundException } from '../exception/node.exception'; -import { MoveNodeDto } from './dtos/moveNode.dto'; import { WorkspaceRepository } from '../workspace/workspace.repository'; import { WorkspaceNotFoundException } from '../exception/workspace.exception'; @@ -13,128 +8,9 @@ import { WorkspaceNotFoundException } from '../exception/workspace.exception'; export class NodeService { constructor( private readonly nodeRepository: NodeRepository, - private readonly pageRepository: PageRepository, private readonly workspaceRepository: WorkspaceRepository, ) {} - async createNode(dto: CreateNodeDto): Promise { - const { title, x, y } = dto; - - // 노드부터 생성한다. - const node = await this.nodeRepository.save({ title, x, y }); - - // 페이지를 생성한다. - const page = await this.pageRepository.save({ title, content: {} }); - - // 페이지와 노드를 서로 연결하여 저장한다. - node.page = page; - return await this.nodeRepository.save(node); - } - - async createLinkedNode(x: number, y: number, pageId: number): Promise { - // 페이지를 조회한다. - const existingPage = await this.pageRepository.findOneBy({ id: pageId }); - - // 노드를 생성한다. - const node = this.nodeRepository.create({ x, y }); - - node.page = existingPage; - return await this.nodeRepository.save(node); - } - - async deleteNode(id: number): Promise { - // 노드를 삭제한다. - const deleteResult = await this.nodeRepository.delete(id); - - // 만약 삭제된 노드가 없으면 노드를 찾지 못한 것 - if (!deleteResult.affected) { - throw new NodeNotFoundException(); - } - } - - async updateNode(id: number, dto: UpdateNodeDto): Promise { - // 노드를 조회한다. - const node = await this.nodeRepository.findOne({ - relations: ['page'], - select: { - id: true, - page: { - id: true, - title: true, // content 제외하고 title만 선택 - }, - }, - where: { - id, - }, - }); - - // 노드가 없으면 NotFound 에러 - if (!node) { - throw new NodeNotFoundException(); - } - - // 노드와 연결된 페이지를 조회한다. - const linkedPage = await this.pageRepository.findOneBy({ - id: node.page.id, - }); - - // 노드 정보를 갱신한다. - const { x, y, title } = dto; - node.x = x; - node.y = y; - linkedPage.title = title; - - return await this.nodeRepository.save(node); - } - - async findNodeById(id: number): Promise { - // 노드를 조회한다. - const node = await this.nodeRepository.findOne({ - relations: ['page'], - select: { - id: true, - page: { - id: true, - title: true, // content 제외하고 title만 선택 - }, - }, - where: { - id, - }, - }); - - // 노드가 없으면 NotFound 에러 - if (!node) { - throw new NodeNotFoundException(); - } - return node; - } - - async getCoordinates(id: number): Promise<{ x: number; y: number }> { - // 노드를 조회한다. - const node = await this.findNodeById(id); - - // 좌표를 반환한다. - return { - x: node.x, - y: node.y, - }; - } - - async moveNode(id: number, dto: MoveNodeDto): Promise { - const { x, y } = dto; - // 갱신할 노드를 조회한다. - const node = await this.findNodeById(id); - - // 노드가 없으면 NotFound 에러 - if (!node) { - throw new NodeNotFoundException(); - } - - // UPDATE 쿼리를 실행한다. - await this.nodeRepository.update(id, { x, y }); - } - async findNodesByWorkspace(workspaceId: string): Promise { // 워크스페이스 DB에서 해당 워크스페이스의 내부 id를 찾는다 const workspace = await this.workspaceRepository.findOneBy({ diff --git a/apps/backend/src/page/dtos/findPagesResponse.dto.ts b/apps/backend/src/page/dtos/findPagesResponse.dto.ts deleted file mode 100644 index adb38a58..00000000 --- a/apps/backend/src/page/dtos/findPagesResponse.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsArray } from 'class-validator'; -import { Page } from '../page.entity'; - -export class FindPagesResponseDto { - @ApiProperty({ - example: 'OO 생성에 성공했습니다.', - description: 'api 요청 결과 메시지', - }) - @IsString() - message: string; - - @ApiProperty({ - example: [ - { - id: 1, - title: '페이지 제목', - content: { - type: 'doc', - content: {}, - }, - }, - ], - description: '모든 Page 배열', - }) - @IsArray() - pages: Partial[]; -} diff --git a/apps/backend/src/page/dtos/updatePage.dto.ts b/apps/backend/src/page/dtos/updatePage.dto.ts deleted file mode 100644 index f190d3b0..00000000 --- a/apps/backend/src/page/dtos/updatePage.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IsString, IsJSON, IsOptional } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdatePageDto { - @ApiProperty({ - example: '페이지 제목입니다.', - description: '페이지 제목.', - }) - @IsString() - @IsOptional() - title?: string; - - @ApiProperty({ - example: "{'doc' : 'type'}", - description: '페이지 내용 JSON 형태', - }) - @IsJSON() - @IsOptional() - content?: JSON; - - @ApiProperty({ - example: '📝', - description: '페이지 이모지', - required: false, - }) - @IsString() - @IsOptional() - emoji?: string; -} diff --git a/apps/backend/src/page/dtos/updatePartialPage.dto.ts b/apps/backend/src/page/dtos/updatePartialPage.dto.ts deleted file mode 100644 index 99dc730c..00000000 --- a/apps/backend/src/page/dtos/updatePartialPage.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IsString, IsJSON, IsOptional, IsNumber } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdatePartialPageDto { - @ApiProperty({ - example: 1, - description: 'page PK', - }) - @IsNumber() - id: number; - - @ApiProperty({ - example: '페이지 제목입니다.', - description: '페이지 제목.', - }) - @IsString() - @IsOptional() - title?: string; - - @ApiProperty({ - example: "{'doc' : 'type'}", - description: '페이지 내용 JSON 형태', - }) - @IsJSON() - @IsOptional() - content?: JSON; - - @ApiProperty({ - example: '📝', - description: '페이지 이모지', - required: false, - }) - @IsString() - @IsOptional() - emoji?: string; -} diff --git a/apps/backend/src/page/page.controller.spec.ts b/apps/backend/src/page/page.controller.spec.ts index 4280b835..cc89cbe4 100644 --- a/apps/backend/src/page/page.controller.spec.ts +++ b/apps/backend/src/page/page.controller.spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PageController } from './page.controller'; import { PageService } from './page.service'; import { CreatePageDto } from './dtos/createPage.dto'; -import { UpdatePageDto } from './dtos/updatePage.dto'; import { PageResponseMessage } from './page.controller'; import { PageNotFoundException } from '../exception/page.exception'; import { Page } from './page.entity'; @@ -23,7 +22,6 @@ describe('PageController', () => { deletePage: jest.fn(), updatePage: jest.fn(), findPageById: jest.fn(), - findPagesByWorkspace: jest.fn(), }, }, ], @@ -116,31 +114,6 @@ describe('PageController', () => { }); }); - describe('updatePage', () => { - it('id에 해당하는 페이지를 찾아 갱신한다.', async () => { - const id = 2; - const dto: UpdatePageDto = { title: 'Updated Node', content: {} as JSON }; - const expectedResponse = { - message: PageResponseMessage.PAGE_UPDATED, - }; - - const result = await controller.updatePage(id, dto); - - expect(pageService.updatePage).toHaveBeenCalledWith(id, dto); - expect(result).toEqual(expectedResponse); - }); - - it('id에 해당하는 페이지가 존재하지 않으면 PageNotFoundException을 throw한다.', async () => { - jest - .spyOn(pageService, 'updatePage') - .mockRejectedValue(new PageNotFoundException()); - - await expect( - controller.updatePage(1, new UpdatePageDto()), - ).rejects.toThrow(PageNotFoundException); - }); - }); - describe('findPageById', () => { it('id에 해당하는 페이지의 상세 정보를 반환한다.', async () => { const expectedPage: Page = { @@ -163,44 +136,4 @@ describe('PageController', () => { }); }); }); - - describe('findPagesByWorkspace', () => { - it('특정 워크스페이스에 존재하는 페이지들을 반환한다.', async () => { - const workspaceId = 'workspace-id'; - const expectedPages = [ - { id: 1, title: 'Page 1', emoji: '📄' }, - { id: 2, title: 'Page 2', emoji: '✏️' }, - ] as Partial[]; - - jest - .spyOn(pageService, 'findPagesByWorkspace') - .mockResolvedValue(expectedPages); - - const result = await controller.findPagesByWorkspace(workspaceId); - - expect(pageService.findPagesByWorkspace).toHaveBeenCalledWith( - workspaceId, - ); - expect(result).toEqual({ - message: PageResponseMessage.PAGES_RETURNED, - pages: expectedPages, - }); - }); - - it('워크스페이스가 존재하지 않을 경우 WorkspaceNotFoundException을 throw한다.', async () => { - const workspaceId = 'invalid-workspace-id'; - - jest - .spyOn(pageService, 'findPagesByWorkspace') - .mockRejectedValue(new WorkspaceNotFoundException()); - - await expect( - controller.findPagesByWorkspace(workspaceId), - ).rejects.toThrow(WorkspaceNotFoundException); - - expect(pageService.findPagesByWorkspace).toHaveBeenCalledWith( - workspaceId, - ); - }); - }); }); diff --git a/apps/backend/src/page/page.controller.ts b/apps/backend/src/page/page.controller.ts index 9043c232..4fbd9b24 100644 --- a/apps/backend/src/page/page.controller.ts +++ b/apps/backend/src/page/page.controller.ts @@ -3,7 +3,6 @@ import { Get, Post, Delete, - Patch, Param, Body, HttpCode, @@ -12,19 +11,15 @@ import { } from '@nestjs/common'; import { PageService } from './page.service'; import { CreatePageDto } from './dtos/createPage.dto'; -import { UpdatePageDto } from './dtos/updatePage.dto'; import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { MessageResponseDto } from './dtos/messageResponse.dto'; -import { FindPagesResponseDto } from './dtos/findPagesResponse.dto'; import { FindPageResponseDto } from './dtos/findPageResponse.dto'; import { CreatePageResponseDto } from './dtos/createPageResponse.dto'; export enum PageResponseMessage { PAGE_CREATED = '페이지와 노드를 생성했습니다.', - PAGE_UPDATED = '페이지와 노드를 갱신했습니다.', PAGE_DELETED = '페이지와 노드를 삭제했습니다.', PAGE_RETURNED = '페이지를 가져왔습니다.', - PAGES_RETURNED = '워크스페이스의 모든 페이지를 가져왔습니다', } @Controller('page') @@ -70,40 +65,6 @@ export class PageController { }; } - @ApiResponse({ - type: MessageResponseDto, - }) - @ApiOperation({ summary: '페이지 제목, 내용을 수정합니다.' }) - @Patch('/:id') - @HttpCode(HttpStatus.OK) - async updatePage( - @Param('id', ParseIntPipe) id: number, - @Body() body: UpdatePageDto, - ): Promise { - await this.pageService.updatePage(id, body); - - return { - message: PageResponseMessage.PAGE_UPDATED, - }; - } - - @ApiResponse({ - type: FindPagesResponseDto, - }) - @ApiOperation({ summary: '특정 워크스페이스의 모든 페이지를 가져옵니다.' }) - @Get('/workspace/:workspaceId') - @HttpCode(HttpStatus.OK) - async findPagesByWorkspace( - @Param('workspaceId') workspaceId: string, // Snowflake ID - ): Promise { - const pages = await this.pageService.findPagesByWorkspace(workspaceId); - - return { - message: PageResponseMessage.PAGES_RETURNED, - pages, - }; - } - @ApiResponse({ type: FindPageResponseDto, }) diff --git a/apps/backend/src/page/page.repository.ts b/apps/backend/src/page/page.repository.ts index 1fa7cee4..7f511a57 100644 --- a/apps/backend/src/page/page.repository.ts +++ b/apps/backend/src/page/page.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { Page } from './page.entity'; import { InjectDataSource } from '@nestjs/typeorm'; -import { UpdatePartialPageDto } from './dtos/updatePartialPage.dto'; @Injectable() export class PageRepository extends Repository { @@ -20,8 +19,4 @@ export class PageRepository extends Repository { }, }); } - - async bulkUpdate(pages: UpdatePartialPageDto[]) { - await Promise.all(pages.map((page) => this.update(page.id, page))); - } } diff --git a/apps/backend/src/page/page.service.spec.ts b/apps/backend/src/page/page.service.spec.ts index bc6e528d..3be1c5f7 100644 --- a/apps/backend/src/page/page.service.spec.ts +++ b/apps/backend/src/page/page.service.spec.ts @@ -6,10 +6,8 @@ import { Page } from './page.entity'; import { Node } from '../node/node.entity'; import { Workspace } from '../workspace/workspace.entity'; import { CreatePageDto } from './dtos/createPage.dto'; -import { UpdatePageDto } from './dtos/updatePage.dto'; import { PageNotFoundException } from '../exception/page.exception'; import { WorkspaceRepository } from '../workspace/workspace.repository'; -import { WorkspaceNotFoundException } from '../exception/workspace.exception'; const RED_LOCK_TOKEN = 'RED_LOCK'; type RedisLock = { acquire(): Promise<{ release: () => void }>; @@ -184,64 +182,6 @@ describe('PageService', () => { }); }); - describe('updatePage', () => { - it('id에 해당하는 페이지를 찾아 성공적으로 갱신한다.', async () => { - const dto: UpdatePageDto = { - title: 'Updated Title', - content: {} as JSON, - emoji: '📝', - }; - const originDate = new Date(); - const originPage: Page = { - id: 1, - title: 'origin title', - content: {} as JSON, - node: null, - createdAt: originDate, - updatedAt: originDate, - version: 1, - emoji: null, - workspace: null, - }; - const newDate = new Date(); - const newPage: Page = { - id: 1, - title: 'Updated Title', - content: {} as JSON, - node: null, - createdAt: newDate, - updatedAt: newDate, - version: 1, - emoji: '📝', - workspace: null, - }; - jest.spyOn(pageRepository, 'findOneBy').mockResolvedValue(originPage); - jest.spyOn(pageRepository, 'save').mockResolvedValue(newPage); - jest.spyOn(redisLock, 'acquire').mockResolvedValue({ - release: jest.fn(), - }); - const result = await service.updatePage(1, dto); - - expect(result).toEqual(newPage); - expect(pageRepository.findOneBy).toHaveBeenCalledWith({ - id: 1, - }); - expect(pageRepository.save).toHaveBeenCalledWith(newPage); - }); - - it('id에 해당하는 페이지가 없을 경우 PageNotFoundException을 throw한다.', async () => { - jest - .spyOn(nodeRepository, 'findOneBy') - .mockResolvedValue({ affected: false } as any); - jest.spyOn(redisLock, 'acquire').mockResolvedValue({ - release: jest.fn(), - }); - await expect(service.updatePage(1, new UpdatePageDto())).rejects.toThrow( - PageNotFoundException, - ); - }); - }); - describe('findPageById', () => { it('id에 해당하는 페이지를 찾아 성공적으로 반환한다.', async () => { const newDate = new Date(); @@ -289,94 +229,4 @@ describe('PageService', () => { ); }); }); - - describe('findPagesByWorkspace', () => { - it('특정 워크스페이스에 존재하는 페이지들을 content 없이 반환한다.', async () => { - const workspaceId = '123456789012345678'; // Snowflake ID - const workspace = { - id: 1, - snowflakeId: workspaceId, - owner: null, - title: 'Test Workspace', - description: null, - visibility: 'private', - createdAt: new Date(), - updatedAt: new Date(), - thumbnailUrl: null, - edges: [], - pages: [], - nodes: [], - } as Workspace; - - const page1: Page = { - id: 1, - title: 'Page 1', - content: {} as JSON, - node: null, - createdAt: new Date(), - updatedAt: new Date(), - version: 1, - emoji: '📄', - workspace, - }; - - const expectedPageList = [ - { id: page1.id, title: page1.title, emoji: page1.emoji }, - ] as Partial[]; - - jest.spyOn(workspaceRepository, 'findOneBy').mockResolvedValue(workspace); - jest - .spyOn(pageRepository, 'findPagesByWorkspace') - .mockResolvedValue(expectedPageList); - - const result = await service.findPagesByWorkspace(workspaceId); - - expect(result).toEqual(expectedPageList); - expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({ - snowflakeId: workspaceId, - }); - expect(pageRepository.findPagesByWorkspace).toHaveBeenCalledWith( - workspace.id, - ); - }); - - it('워크스페이스가 존재하지 않을 경우, WorkspaceNotFoundException을 던진다.', async () => { - const workspaceId = '123456789012345678'; - - jest.spyOn(workspaceRepository, 'findOneBy').mockResolvedValue(null); - - await expect(service.findPagesByWorkspace(workspaceId)).rejects.toThrow( - WorkspaceNotFoundException, - ); - - expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({ - snowflakeId: workspaceId, - }); - expect(pageRepository.findPagesByWorkspace).not.toHaveBeenCalled(); - }); - - it('워크스페이스에 페이지가 없을 경우, 빈 배열을 반환한다.', async () => { - const workspaceId = '123456789012345678'; - const workspace = { - id: 1, - snowflakeId: workspaceId, - }; - - jest - .spyOn(workspaceRepository, 'findOneBy') - .mockResolvedValue(workspace as Workspace); - - jest.spyOn(pageRepository, 'findPagesByWorkspace').mockResolvedValue([]); - - const result = await service.findPagesByWorkspace(workspaceId); - - expect(result).toEqual([]); - expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({ - snowflakeId: workspaceId, - }); - expect(pageRepository.findPagesByWorkspace).toHaveBeenCalledWith( - workspace.id, - ); - }); - }); }); diff --git a/apps/backend/src/page/page.service.ts b/apps/backend/src/page/page.service.ts index bc90a03e..f195fdfb 100644 --- a/apps/backend/src/page/page.service.ts +++ b/apps/backend/src/page/page.service.ts @@ -4,8 +4,6 @@ import { WorkspaceRepository } from '../workspace/workspace.repository'; import { PageRepository } from './page.repository'; import { Page } from './page.entity'; import { CreatePageDto } from './dtos/createPage.dto'; -import { UpdatePageDto } from './dtos/updatePage.dto'; -import { UpdatePartialPageDto } from './dtos/updatePartialPage.dto'; import { PageNotFoundException } from '../exception/page.exception'; import { WorkspaceNotFoundException } from '../exception/workspace.exception'; @@ -61,32 +59,8 @@ export class PageService { } } - async updatePage(id: number, dto: UpdatePageDto): Promise { - // 갱신할 페이지를 조회한다. - // 페이지를 조회한다. - const page = await this.pageRepository.findOneBy({ id }); - - // 페이지가 없으면 NotFound 에러 - if (!page) { - throw new PageNotFoundException(); - } - // 페이지 정보를 갱신한다. - const newPage = Object.assign({}, page, dto); - - // 변경된 페이지를 저장 - return await this.pageRepository.save(newPage); - } - - async updateBulkPage(pages: UpdatePartialPageDto[]) { - await this.pageRepository.bulkUpdate(pages); - } - async findPageById(id: number): Promise { // 페이지를 조회한다. - // const page = await this.pageRepository.findOne({ - // where: { id }, - // relations: ['node'], - // }); const page = await this.pageRepository .createQueryBuilder('page') @@ -100,17 +74,4 @@ export class PageService { } return page; } - - async findPagesByWorkspace(workspaceId: string): Promise[]> { - // 워크스페이스 DB에서 해당 워크스페이스의 내부 id를 찾는다 - const workspace = await this.workspaceRepository.findOneBy({ - snowflakeId: workspaceId, - }); - - if (!workspace) { - throw new WorkspaceNotFoundException(); - } - - return await this.pageRepository.findPagesByWorkspace(workspace.id); - } } diff --git a/apps/backend/test/package.json b/apps/backend/test/package.json new file mode 100644 index 00000000..912c580f --- /dev/null +++ b/apps/backend/test/package.json @@ -0,0 +1,43 @@ +{ + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^2.0.12", + "@types/ws": "^8.5.13", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 9ced5b4a..f7460416 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,37 +1,47 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "tailwindcss"; import tsconfigPaths from "vite-tsconfig-paths"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import removeConsole from "vite-plugin-remove-console"; +function getHostFromUrl(url: string) { + // 정규식을 사용해 http:// 또는 https:// 프로토콜을 제거하고 + // 호스트 이름만 반환 + return url.replace(/^https?:\/\//, ""); +} // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - TanStackRouterVite({ - routesDirectory: "./src/app/routes", - generatedRouteTree: "./src/app/routeTree.gen.ts", - }), - react(), - tsconfigPaths(), - removeConsole(), - ], - css: { - postcss: { - plugins: [tailwindcss()], +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + console.log(`vite_api_url: ${env.VITE_API_URL}`); + return { + plugins: [ + TanStackRouterVite({ + routesDirectory: "./src/app/routes", + generatedRouteTree: "./src/app/routeTree.gen.ts", + }), + react(), + tsconfigPaths(), + removeConsole(), + ], + css: { + postcss: { + plugins: [tailwindcss()], + }, }, - }, - server: { - host: "0.0.0.0", - port: 5173, - watch: { - usePolling: true, - interval: 1000, + server: { + host: "0.0.0.0", + port: 5173, + watch: { + usePolling: true, + interval: 1000, + }, + hmr: { + protocol: "wss", + clientPort: 443, + path: "hmr/", + }, + allowedHosts: [getHostFromUrl(env.VITE_API_URL)], }, - hmr: { - protocol: "wss", - clientPort: 443, - path: "hmr/", - }, - }, + }; }); diff --git a/compose.local.yml b/compose.local.yml index 29b2a355..0ce1232b 100644 --- a/compose.local.yml +++ b/compose.local.yml @@ -10,7 +10,7 @@ services: networks: - net healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 diff --git a/package.json b/package.json index 7a17e241..cafba661 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "docker:prod:down": "docker compose -f compose.prod.yml down", "docker:prod:clean": "docker compose -v -f compose.prod.yml down", "docker:prod:fclean": "docker compose -v -f compose.prod.yml down --rmi all", - "ssl:generate": "cd services/nginx/ssl && bash ./generate-cert.sh" + "ssl:generate": "cd services/nginx/ssl && bash ./generate-cert.sh", + "reinstall": "rm -rf ./node_modules ./apps/frontend/node_modules ./apps/backend/node_modules ./apps/websocket/node_modules ./yarn.lock && yarn install" }, "dependencies": { "turbo": "^2.3.0" @@ -34,4 +35,4 @@ "apps/*" ], "packageManager": "yarn@1.22.22" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 5b6b835c..251e2584 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2859,17 +2859,17 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.64.2.tgz#be06e7c7966d14ea3e7c82bea1086b463f2f6809" integrity sha512-hdO8SZpWXoADNTWXV9We8CwTkXU88OVWRBcsiFrk7xJQnhm6WRlweDzMD+uH+GnuieTBVSML6xFa17C2cNV8+g== -"@tanstack/query-devtools@5.62.16": - version "5.62.16" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz#a4b71c6b5bbf7575861437ef9a9f232333569255" - integrity sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q== +"@tanstack/query-devtools@5.64.2": + version "5.64.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.64.2.tgz#3d8f8abb17815a7302482b67713cb933c79ed6ba" + integrity sha512-3DautR5UpVZdk/qNIhioZVF7g8fdQZ1U98sBEEk4Tzz3tihSBNMPgwlP40nzgbPEDBIrn/j/oyyvNBVSo083Vw== "@tanstack/react-query-devtools@^5.64.1": - version "5.64.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz#e3ccf56b1b30453a4baed2b05d4fa717885ddf97" - integrity sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ== + version "5.64.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.2.tgz#ece61dfad8032305aefd3f0eb044ccd8304ffa1b" + integrity sha512-+ZjJVnPzc8BUV/Eklu2k9T/IAyAyvwoCHqOaOrk2sbU33LFhM52BpX4eyENXn0bx5LwV3DJZgEQlIzucoemfGQ== dependencies: - "@tanstack/query-devtools" "5.62.16" + "@tanstack/query-devtools" "5.64.2" "@tanstack/react-query@^5.59.19": version "5.64.2" @@ -3705,16 +3705,16 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz#b47a398e0e551cb008c60190b804394e6852c863" - integrity sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A== +"@typescript-eslint/eslint-plugin@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz#395014a75112ecdb81142b866ab6bb62e3be0f2a" + integrity sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.20.0" - "@typescript-eslint/type-utils" "8.20.0" - "@typescript-eslint/utils" "8.20.0" - "@typescript-eslint/visitor-keys" "8.20.0" + "@typescript-eslint/scope-manager" "8.21.0" + "@typescript-eslint/type-utils" "8.21.0" + "@typescript-eslint/utils" "8.21.0" + "@typescript-eslint/visitor-keys" "8.21.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" @@ -3737,15 +3737,15 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.20.0.tgz#5caf2230a37094dc0e671cf836b96dd39b587ced" - integrity sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g== +"@typescript-eslint/parser@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.21.0.tgz#312c638aaba4f640d45bfde7c6795a9d75deb088" + integrity sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA== dependencies: - "@typescript-eslint/scope-manager" "8.20.0" - "@typescript-eslint/types" "8.20.0" - "@typescript-eslint/typescript-estree" "8.20.0" - "@typescript-eslint/visitor-keys" "8.20.0" + "@typescript-eslint/scope-manager" "8.21.0" + "@typescript-eslint/types" "8.21.0" + "@typescript-eslint/typescript-estree" "8.21.0" + "@typescript-eslint/visitor-keys" "8.21.0" debug "^4.3.4" "@typescript-eslint/parser@^6.0.0": @@ -3767,13 +3767,13 @@ "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/scope-manager@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz#aaf4198b509fb87a6527c02cfbfaf8901179e75c" - integrity sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw== +"@typescript-eslint/scope-manager@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz#d08d94e2a34b4ccdcc975543c25bb62917437500" + integrity sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA== dependencies: - "@typescript-eslint/types" "8.20.0" - "@typescript-eslint/visitor-keys" "8.20.0" + "@typescript-eslint/types" "8.21.0" + "@typescript-eslint/visitor-keys" "8.21.0" "@typescript-eslint/type-utils@6.21.0": version "6.21.0" @@ -3785,13 +3785,13 @@ debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/type-utils@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz#958171d86b213a3f32b5b16b91db267968a4ef19" - integrity sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA== +"@typescript-eslint/type-utils@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz#2e69d1a93cdbedc73fe694cd6ae4dfedd00430a0" + integrity sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ== dependencies: - "@typescript-eslint/typescript-estree" "8.20.0" - "@typescript-eslint/utils" "8.20.0" + "@typescript-eslint/typescript-estree" "8.21.0" + "@typescript-eslint/utils" "8.21.0" debug "^4.3.4" ts-api-utils "^2.0.0" @@ -3800,10 +3800,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/types@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.20.0.tgz#487de5314b5415dee075e95568b87a75a3e730cf" - integrity sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA== +"@typescript-eslint/types@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5" + integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A== "@typescript-eslint/typescript-estree@6.21.0": version "6.21.0" @@ -3819,13 +3819,13 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz#658cea07b7e5981f19bce5cf1662cb70ad59f26b" - integrity sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA== +"@typescript-eslint/typescript-estree@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz#5ce71acdbed3b97b959f6168afba5a03c88f69a9" + integrity sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg== dependencies: - "@typescript-eslint/types" "8.20.0" - "@typescript-eslint/visitor-keys" "8.20.0" + "@typescript-eslint/types" "8.21.0" + "@typescript-eslint/visitor-keys" "8.21.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -3846,15 +3846,15 @@ "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" -"@typescript-eslint/utils@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.20.0.tgz#53127ecd314b3b08836b4498b71cdb86f4ef3aa2" - integrity sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA== +"@typescript-eslint/utils@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.21.0.tgz#bc4874fbc30feb3298b926e3b03d94570b3999c5" + integrity sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.20.0" - "@typescript-eslint/types" "8.20.0" - "@typescript-eslint/typescript-estree" "8.20.0" + "@typescript-eslint/scope-manager" "8.21.0" + "@typescript-eslint/types" "8.21.0" + "@typescript-eslint/typescript-estree" "8.21.0" "@typescript-eslint/visitor-keys@6.21.0": version "6.21.0" @@ -3864,12 +3864,12 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@8.20.0": - version "8.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz#2df6e24bc69084b81f06aaaa48d198b10d382bed" - integrity sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA== +"@typescript-eslint/visitor-keys@8.21.0": + version "8.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz#a89744c4cdc83b5c761eb5878befe6c33d1481b2" + integrity sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w== dependencies: - "@typescript-eslint/types" "8.20.0" + "@typescript-eslint/types" "8.21.0" eslint-visitor-keys "^4.2.0" "@uiw/color-convert@2.3.4": @@ -5545,9 +5545,9 @@ ejs@^3.1.10: jake "^10.8.5" electron-to-chromium@^1.5.73: - version "1.5.83" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz#3f74078f0c83e24bf7e692eaa855a998d1bec34f" - integrity sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ== + version "1.5.84" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.84.tgz#8e334ca206bb293a20b16418bf454783365b0a95" + integrity sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g== elkjs@^0.9.3: version "0.9.3" @@ -6110,9 +6110,9 @@ fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fast-uri@^3.0.1: - version "3.0.5" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.5.tgz#19f5f9691d0dab9b85861a7bb5d98fca961da9cd" - integrity sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== fast-xml-parser@4.4.1: version "4.4.1" @@ -6287,9 +6287,9 @@ fraction.js@^4.3.7: integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== framer-motion@^11.11.11: - version "11.18.1" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.1.tgz#2d832ac22671c1cd90338cac4a572825328e3089" - integrity sha512-EQa8c9lWVOm4zlz14MsBJWr8woq87HsNmsBnQNvcS0hs8uzw6HtGAxZyIU7EGTVpHD1C1n01ufxRyarXcNzpPg== + version "11.18.2" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718" + integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w== dependencies: motion-dom "^11.18.1" motion-utils "^11.18.1" @@ -6418,9 +6418,9 @@ get-stream@^6.0.0: integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-tsconfig@^4.7.5: - version "4.9.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.9.0.tgz#704ae2ce2a94935921675dd19c05508b713a405d" - integrity sha512-52n24W52sIueosRe0XZ8Ex5Yle+WbhfCKnV/gWXpbVR8FXNTfqdKEKUSypKso66VRHTvvcQxL44UTZbJRlCTnw== + version "4.10.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" + integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== dependencies: resolve-pkg-maps "^1.0.0" @@ -7339,9 +7339,9 @@ jiti@^1.21.6: integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== jotai@^2.6.4: - version "2.11.0" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.11.0.tgz#923f8351e0b2d721036af892c0ae25625049d120" - integrity sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ== + version "2.11.1" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.11.1.tgz#9fd12aaba8458783482c77c8d7038d6e974fc679" + integrity sha512-41Su098mpHIX29hF/XOpDb0SqF6EES7+HXfrhuBqVSzRkxX48hD5i8nGsEewWZNAsBWJCTTmuz8M946Ih2PfcQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -10684,13 +10684,13 @@ typeorm@^0.3.20: yargs "^17.6.2" typescript-eslint@^8.11.0: - version "8.20.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.20.0.tgz#76d4ea6a483fd49830a7e8baccaed10f76d1e57b" - integrity sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA== + version "8.21.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.21.0.tgz#78bdb83a6d771f0312b128297d84a3111885fd08" + integrity sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw== dependencies: - "@typescript-eslint/eslint-plugin" "8.20.0" - "@typescript-eslint/parser" "8.20.0" - "@typescript-eslint/utils" "8.20.0" + "@typescript-eslint/eslint-plugin" "8.21.0" + "@typescript-eslint/parser" "8.21.0" + "@typescript-eslint/utils" "8.21.0" typescript@5.7.2: version "5.7.2" @@ -10966,9 +10966,9 @@ vite-tsconfig-paths@^5.1.0: tsconfck "^3.0.3" vite@^5.4.10: - version "5.4.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" - integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== + version "5.4.13" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.13.tgz#59994ccf14783e40e2763b950db432cc299cd9fc" + integrity sha512-7zp3N4YSjXOSAFfdBe9pPD3FrO398QlJ/5QpFGm3L8xDP1IxDn1XRxArPw4ZKk5394MM8rcTVPY4y1Hvo62bog== dependencies: esbuild "^0.21.3" postcss "^8.4.43"