From 72730abcf88f86e0e1999566161545eb7ac2f4a7 Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Tue, 6 Jan 2026 14:45:48 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EC=B4=88=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.module.ts | 3 +- .../__test__/markdown.controller.spec.ts | 20 +++++++++ .../__test__/markdown.service.spec.ts | 18 ++++++++ .../src/markdown/dto/create-markdown.dto.ts | 1 + .../src/markdown/dto/update-markdown.dto.ts | 4 ++ .../src/markdown/entities/markdown.entity.ts | 1 + backend/src/markdown/markdown.controller.ts | 45 +++++++++++++++++++ backend/src/markdown/markdown.module.ts | 9 ++++ backend/src/markdown/markdown.service.ts | 26 +++++++++++ 9 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 backend/src/markdown/__test__/markdown.controller.spec.ts create mode 100644 backend/src/markdown/__test__/markdown.service.spec.ts create mode 100644 backend/src/markdown/dto/create-markdown.dto.ts create mode 100644 backend/src/markdown/dto/update-markdown.dto.ts create mode 100644 backend/src/markdown/entities/markdown.entity.ts create mode 100644 backend/src/markdown/markdown.controller.ts create mode 100644 backend/src/markdown/markdown.module.ts create mode 100644 backend/src/markdown/markdown.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 3ea54222..ca8d4501 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -4,9 +4,10 @@ import { AppService } from './app.service'; import { WorkspaceModule } from './workspace/workspace.module'; import { WidgetModule } from './widget/widget.module'; import { CursorModule } from './cursor/cursor.module'; +import { MarkdownModule } from './markdown/markdown.module'; @Module({ - imports: [WorkspaceModule, WidgetModule, CursorModule], + imports: [WorkspaceModule, WidgetModule, CursorModule, MarkdownModule], controllers: [AppController], providers: [AppService], }) diff --git a/backend/src/markdown/__test__/markdown.controller.spec.ts b/backend/src/markdown/__test__/markdown.controller.spec.ts new file mode 100644 index 00000000..1d49ca64 --- /dev/null +++ b/backend/src/markdown/__test__/markdown.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MarkdownController } from '../markdown.controller'; +import { MarkdownService } from '../markdown.service'; + +describe('MarkdownController', () => { + let controller: MarkdownController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MarkdownController], + providers: [MarkdownService], + }).compile(); + + controller = module.get(MarkdownController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/markdown/__test__/markdown.service.spec.ts b/backend/src/markdown/__test__/markdown.service.spec.ts new file mode 100644 index 00000000..e614a6df --- /dev/null +++ b/backend/src/markdown/__test__/markdown.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MarkdownService } from '../markdown.service'; + +describe('MarkdownService', () => { + let service: MarkdownService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MarkdownService], + }).compile(); + + service = module.get(MarkdownService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/markdown/dto/create-markdown.dto.ts b/backend/src/markdown/dto/create-markdown.dto.ts new file mode 100644 index 00000000..12283c13 --- /dev/null +++ b/backend/src/markdown/dto/create-markdown.dto.ts @@ -0,0 +1 @@ +export class CreateMarkdownDto {} diff --git a/backend/src/markdown/dto/update-markdown.dto.ts b/backend/src/markdown/dto/update-markdown.dto.ts new file mode 100644 index 00000000..eda5212f --- /dev/null +++ b/backend/src/markdown/dto/update-markdown.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateMarkdownDto } from './create-markdown.dto'; + +export class UpdateMarkdownDto extends PartialType(CreateMarkdownDto) {} diff --git a/backend/src/markdown/entities/markdown.entity.ts b/backend/src/markdown/entities/markdown.entity.ts new file mode 100644 index 00000000..869d1910 --- /dev/null +++ b/backend/src/markdown/entities/markdown.entity.ts @@ -0,0 +1 @@ +export class Markdown {} diff --git a/backend/src/markdown/markdown.controller.ts b/backend/src/markdown/markdown.controller.ts new file mode 100644 index 00000000..9f297edd --- /dev/null +++ b/backend/src/markdown/markdown.controller.ts @@ -0,0 +1,45 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from '@nestjs/common'; +import { MarkdownService } from './markdown.service'; +import { CreateMarkdownDto } from './dto/create-markdown.dto'; +import { UpdateMarkdownDto } from './dto/update-markdown.dto'; + +@Controller('markdown') +export class MarkdownController { + constructor(private readonly markdownService: MarkdownService) {} + + @Post() + create(@Body() createMarkdownDto: CreateMarkdownDto) { + return this.markdownService.create(createMarkdownDto); + } + + @Get() + findAll() { + return this.markdownService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.markdownService.findOne(+id); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() updateMarkdownDto: UpdateMarkdownDto, + ) { + return this.markdownService.update(+id, updateMarkdownDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.markdownService.remove(+id); + } +} diff --git a/backend/src/markdown/markdown.module.ts b/backend/src/markdown/markdown.module.ts new file mode 100644 index 00000000..32184449 --- /dev/null +++ b/backend/src/markdown/markdown.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { MarkdownService } from './markdown.service'; +import { MarkdownController } from './markdown.controller'; + +@Module({ + controllers: [MarkdownController], + providers: [MarkdownService], +}) +export class MarkdownModule {} diff --git a/backend/src/markdown/markdown.service.ts b/backend/src/markdown/markdown.service.ts new file mode 100644 index 00000000..1ad1e02b --- /dev/null +++ b/backend/src/markdown/markdown.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { CreateMarkdownDto } from './dto/create-markdown.dto'; +import { UpdateMarkdownDto } from './dto/update-markdown.dto'; + +@Injectable() +export class MarkdownService { + create(createMarkdownDto: CreateMarkdownDto) { + return 'This action adds a new markdown'; + } + + findAll() { + return `This action returns all markdown`; + } + + findOne(id: number) { + return `This action returns a #${id} markdown`; + } + + update(id: number, updateMarkdownDto: UpdateMarkdownDto) { + return `This action updates a #${id} markdown`; + } + + remove(id: number) { + return `This action removes a #${id} markdown`; + } +} From 50aa246d995db96865a04019305c705c4b0987bd Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Tue, 6 Jan 2026 14:51:15 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=A0=AF=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EC=9C=84=EC=A0=AF=EC=9D=84=20?= =?UTF-8?q?=EC=B0=BE=EB=8F=84=EB=A1=9D=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 나중에 마크다운에서 위젯 타입으로 쉽게 위젯을 찾기 위해 메서드를 추가했습니다. --- backend/src/widget/widget.memory.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/backend/src/widget/widget.memory.service.ts b/backend/src/widget/widget.memory.service.ts index 2a9fd173..b0ab6a20 100644 --- a/backend/src/widget/widget.memory.service.ts +++ b/backend/src/widget/widget.memory.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { IWidgetService } from './widget.interface'; import { CreateWidgetDto } from './dto/create-widget.dto'; import { UpdateWidgetDto } from './dto/update-widget.dto'; +import { WidgetType } from './dto/widget-content.dto'; @Injectable() export class WidgetMemoryService implements IWidgetService { @@ -40,6 +41,20 @@ export class WidgetMemoryService implements IWidgetService { return Promise.resolve(widget); } + async findOneByWidgetType( + workspaceId: string, + widgetType: WidgetType, + ): Promise { + const widgets = this.getWidgetsMap(workspaceId); + const widget = Array.from(widgets.values()).find( + (widget) => widget.data.content.widgetType === widgetType, + ); + if (!widget) { + return Promise.resolve(null); + } + return Promise.resolve(widget); + } + async update( workspaceId: string, updateWidgetDto: UpdateWidgetDto, From 1572e9d0179ed7d171b36df2543630790079ff7e Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Tue, 6 Jan 2026 18:16:09 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EC=A0=9C=EA=B3=B5=ED=95=98=EB=8A=94=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=A7=8C=EB=93=A4=EC=96=B4=EB=86=93=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/markdown.controller.spec.ts | 20 --- .../__test__/markdown.service.spec.ts | 18 --- .../src/markdown/dto/create-markdown.dto.ts | 1 - backend/src/markdown/dto/get-markdown.dto.ts | 8 ++ .../src/markdown/dto/update-markdown.dto.ts | 4 - .../src/markdown/entities/markdown.entity.ts | 1 - backend/src/markdown/markdown.controller.ts | 55 +++----- backend/src/markdown/markdown.module.ts | 4 +- backend/src/markdown/markdown.service.ts | 119 +++++++++++++++--- backend/src/widget/widget.interface.ts | 5 + backend/src/widget/widget.module.ts | 1 + 11 files changed, 141 insertions(+), 95 deletions(-) delete mode 100644 backend/src/markdown/__test__/markdown.controller.spec.ts delete mode 100644 backend/src/markdown/__test__/markdown.service.spec.ts delete mode 100644 backend/src/markdown/dto/create-markdown.dto.ts create mode 100644 backend/src/markdown/dto/get-markdown.dto.ts delete mode 100644 backend/src/markdown/dto/update-markdown.dto.ts delete mode 100644 backend/src/markdown/entities/markdown.entity.ts diff --git a/backend/src/markdown/__test__/markdown.controller.spec.ts b/backend/src/markdown/__test__/markdown.controller.spec.ts deleted file mode 100644 index 1d49ca64..00000000 --- a/backend/src/markdown/__test__/markdown.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MarkdownController } from '../markdown.controller'; -import { MarkdownService } from '../markdown.service'; - -describe('MarkdownController', () => { - let controller: MarkdownController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [MarkdownController], - providers: [MarkdownService], - }).compile(); - - controller = module.get(MarkdownController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/backend/src/markdown/__test__/markdown.service.spec.ts b/backend/src/markdown/__test__/markdown.service.spec.ts deleted file mode 100644 index e614a6df..00000000 --- a/backend/src/markdown/__test__/markdown.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MarkdownService } from '../markdown.service'; - -describe('MarkdownService', () => { - let service: MarkdownService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [MarkdownService], - }).compile(); - - service = module.get(MarkdownService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/backend/src/markdown/dto/create-markdown.dto.ts b/backend/src/markdown/dto/create-markdown.dto.ts deleted file mode 100644 index 12283c13..00000000 --- a/backend/src/markdown/dto/create-markdown.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateMarkdownDto {} diff --git a/backend/src/markdown/dto/get-markdown.dto.ts b/backend/src/markdown/dto/get-markdown.dto.ts new file mode 100644 index 00000000..a17be47a --- /dev/null +++ b/backend/src/markdown/dto/get-markdown.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class GetMarkdownDto { + @ApiProperty({ description: 'Markdown 내용', example: '# Hello, World!' }) + @IsString() + readonly markdown: string; +} diff --git a/backend/src/markdown/dto/update-markdown.dto.ts b/backend/src/markdown/dto/update-markdown.dto.ts deleted file mode 100644 index eda5212f..00000000 --- a/backend/src/markdown/dto/update-markdown.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateMarkdownDto } from './create-markdown.dto'; - -export class UpdateMarkdownDto extends PartialType(CreateMarkdownDto) {} diff --git a/backend/src/markdown/entities/markdown.entity.ts b/backend/src/markdown/entities/markdown.entity.ts deleted file mode 100644 index 869d1910..00000000 --- a/backend/src/markdown/entities/markdown.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class Markdown {} diff --git a/backend/src/markdown/markdown.controller.ts b/backend/src/markdown/markdown.controller.ts index 9f297edd..188d24b1 100644 --- a/backend/src/markdown/markdown.controller.ts +++ b/backend/src/markdown/markdown.controller.ts @@ -1,45 +1,28 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, -} from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiQuery, ApiResponse } from '@nestjs/swagger'; import { MarkdownService } from './markdown.service'; -import { CreateMarkdownDto } from './dto/create-markdown.dto'; -import { UpdateMarkdownDto } from './dto/update-markdown.dto'; +import { GetMarkdownDto } from './dto/get-markdown.dto'; @Controller('markdown') export class MarkdownController { constructor(private readonly markdownService: MarkdownService) {} - @Post() - create(@Body() createMarkdownDto: CreateMarkdownDto) { - return this.markdownService.create(createMarkdownDto); - } - @Get() - findAll() { - return this.markdownService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.markdownService.findOne(+id); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateMarkdownDto: UpdateMarkdownDto, - ) { - return this.markdownService.update(+id, updateMarkdownDto); - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.markdownService.remove(+id); + @ApiQuery({ + name: 'workspaceId', + required: true, + description: '워크스페이스 ID', + example: 'w1', + }) + @ApiResponse({ + status: 200, + description: '생성된 마크다운 문서', + type: GetMarkdownDto, + }) + async find( + @Query('workspaceId') workspaceId: string, + ): Promise { + const markdown = await this.markdownService.generateMarkdown(workspaceId); + return { markdown }; } } diff --git a/backend/src/markdown/markdown.module.ts b/backend/src/markdown/markdown.module.ts index 32184449..63bdee15 100644 --- a/backend/src/markdown/markdown.module.ts +++ b/backend/src/markdown/markdown.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; -import { MarkdownService } from './markdown.service'; import { MarkdownController } from './markdown.controller'; +import { MarkdownService } from './markdown.service'; +import { WidgetModule } from '../widget/widget.module'; @Module({ + imports: [WidgetModule], controllers: [MarkdownController], providers: [MarkdownService], }) diff --git a/backend/src/markdown/markdown.service.ts b/backend/src/markdown/markdown.service.ts index 1ad1e02b..dc5dca78 100644 --- a/backend/src/markdown/markdown.service.ts +++ b/backend/src/markdown/markdown.service.ts @@ -1,26 +1,117 @@ -import { Injectable } from '@nestjs/common'; -import { CreateMarkdownDto } from './dto/create-markdown.dto'; -import { UpdateMarkdownDto } from './dto/update-markdown.dto'; +import { Injectable, Inject } from '@nestjs/common'; +import type { IWidgetService } from '../widget/widget.interface'; +import { WIDGET_SERVICE } from '../widget/widget.interface'; +import { + WidgetType, + GroundRuleContentDto, + TechStackContentDto, +} from '../widget/dto/widget-content.dto'; +import { CreateWidgetDto } from '../widget/dto/create-widget.dto'; @Injectable() export class MarkdownService { - create(createMarkdownDto: CreateMarkdownDto) { - return 'This action adds a new markdown'; - } + constructor( + @Inject(WIDGET_SERVICE) private readonly widgetService: IWidgetService, + ) {} + + private buildGroundRuleSection(widget: CreateWidgetDto | null): string[] { + if (!widget) return []; + + const lines: string[] = []; + const content = widget.data.content as GroundRuleContentDto; + + lines.push('## 1. 📋 Ground Rule'); + lines.push('| Ground Rule| Value| '); + lines.push('| :--- | :--- | '); + + if (content.rules && content.rules.length > 0) { + content.rules.forEach((rule) => { + lines.push(`| ${rule} | - |`); + }); + } - findAll() { - return `This action returns all markdown`; + lines.push(''); + return lines; } - findOne(id: number) { - return `This action returns a #${id} markdown`; + private buildTechStackSection(widget: CreateWidgetDto | null): string[] { + if (!widget) return []; + + const lines: string[] = []; + const content = widget.data.content as TechStackContentDto; + + lines.push('## 2. 🛠 Tech Stack Selection'); + lines.push('| Tech Name | Version |'); + lines.push('| :--- | :--- |'); + + if (content.selectedItems && content.selectedItems.length > 0) { + content.selectedItems.forEach((item) => { + lines.push(`| ${item} | vLatest |`); + }); + } else { + lines.push('| - | - |'); + } + + lines.push(''); + return lines; } - update(id: number, updateMarkdownDto: UpdateMarkdownDto) { - return `This action updates a #${id} markdown`; + private buildElseSection(widget: CreateWidgetDto | null): string[] { + const lines: string[] = []; + + lines.push('## 3. Else'); + lines.push('---'); + + if (widget) { + const content = widget.data.content as { text?: string }; + if (content.text) { + lines.push(content.text); + } + } + + lines.push(''); + return lines; } - remove(id: number) { - return `This action removes a #${id} markdown`; + async generateMarkdown(workspaceId: string): Promise { + const now = new Date(); + const formattedDate = now.toLocaleString('ko-KR', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, + }); + + const markdownParts: string[] = []; + + markdownParts.push('# 🚀 Project Team Align Report'); + markdownParts.push(`> Created at: ${formattedDate}`); + markdownParts.push(''); + + const groundRuleWidget = await this.widgetService.findOneByWidgetType( + workspaceId, + WidgetType.GROUND_RULE, + ); + markdownParts.push(...this.buildGroundRuleSection(groundRuleWidget)); + + const techStackWidget = await this.widgetService.findOneByWidgetType( + workspaceId, + WidgetType.TECH_STACK, + ); + markdownParts.push(...this.buildTechStackSection(techStackWidget)); + + const postItWidget = await this.widgetService.findOneByWidgetType( + workspaceId, + WidgetType.POST_IT, + ); + + markdownParts.push(...this.buildElseSection(postItWidget)); + + markdownParts.push('*Generated by TeamConfig*'); + + return markdownParts.join('\n'); } } diff --git a/backend/src/widget/widget.interface.ts b/backend/src/widget/widget.interface.ts index 3d0ef076..4da66396 100644 --- a/backend/src/widget/widget.interface.ts +++ b/backend/src/widget/widget.interface.ts @@ -1,5 +1,6 @@ import { CreateWidgetDto } from './dto/create-widget.dto'; import { UpdateWidgetDto } from './dto/update-widget.dto'; +import { WidgetType } from './dto/widget-content.dto'; export interface IWidgetService { create( @@ -8,6 +9,10 @@ export interface IWidgetService { ): Promise; findAll(workspaceId: string): Promise; findOne(workspaceId: string, widgetId: string): Promise; + findOneByWidgetType( + workspaceId: string, + widgetType: WidgetType, + ): Promise; update( workspaceId: string, updateWidgetDto: UpdateWidgetDto, diff --git a/backend/src/widget/widget.module.ts b/backend/src/widget/widget.module.ts index 3dc38771..6a2c3d0f 100644 --- a/backend/src/widget/widget.module.ts +++ b/backend/src/widget/widget.module.ts @@ -13,5 +13,6 @@ import { WorkspaceModule } from '../workspace/workspace.module'; useClass: WidgetMemoryService, // 나중에 WidgetRedisService로 교체 가능 }, ], + exports: [WIDGET_SERVICE], }) export class WidgetModule {} From 4a61bd2a119ccb2eb5c9ddb9bf8e0dc873e0f3fb Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Tue, 6 Jan 2026 21:23:38 +0900 Subject: [PATCH 04/17] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/markdown.controller.spec.ts | 37 +++++++ .../__test__/markdown.service.spec.ts | 97 +++++++++++++++++++ backend/src/markdown/markdown.service.ts | 2 - 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 backend/src/markdown/__test__/markdown.controller.spec.ts create mode 100644 backend/src/markdown/__test__/markdown.service.spec.ts diff --git a/backend/src/markdown/__test__/markdown.controller.spec.ts b/backend/src/markdown/__test__/markdown.controller.spec.ts new file mode 100644 index 00000000..c6cf190c --- /dev/null +++ b/backend/src/markdown/__test__/markdown.controller.spec.ts @@ -0,0 +1,37 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MarkdownController } from '../markdown.controller'; +import { MarkdownService } from '../markdown.service'; + +describe('MarkdownController', () => { + let controller: MarkdownController; + const markdownServiceMock = { + generateMarkdown: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MarkdownController], + providers: [ + { + provide: MarkdownService, + useValue: markdownServiceMock, + }, + ], + }).compile(); + + controller = module.get(MarkdownController); + }); + + it('컨트롤러가 정의되어 있어야 한다', () => { + expect(controller).toBeDefined(); + }); + + it('generateMarkdown 결과를 반환한다', async () => { + markdownServiceMock.generateMarkdown.mockResolvedValue('# md'); + + const result = await controller.find('w1'); + + expect(markdownServiceMock.generateMarkdown).toHaveBeenCalledWith('w1'); + expect(result).toEqual({ markdown: '# md' }); + }); +}); diff --git a/backend/src/markdown/__test__/markdown.service.spec.ts b/backend/src/markdown/__test__/markdown.service.spec.ts new file mode 100644 index 00000000..d56bdbec --- /dev/null +++ b/backend/src/markdown/__test__/markdown.service.spec.ts @@ -0,0 +1,97 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { MarkdownService } from '../markdown.service'; +import { IWidgetService, WIDGET_SERVICE } from '../../widget/widget.interface'; +import { WidgetType } from '../../widget/dto/widget-content.dto'; + +type MockWidgetService = { + [P in keyof IWidgetService]: jest.Mock; +}; + +describe('MarkdownService', () => { + let service: MarkdownService; + let widgetServiceMock: MockWidgetService; + const workspaceId = 'w1'; + + beforeEach(async () => { + widgetServiceMock = { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + findOneByWidgetType: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MarkdownService, + { + provide: WIDGET_SERVICE, + useValue: widgetServiceMock, + }, + ], + }).compile(); + + service = module.get(MarkdownService); + jest.useFakeTimers().setSystemTime(new Date('2024-01-01T12:00:00Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('모든 위젯이 없으면 기본 헤더와 Else 섹션만 반환한다', async () => { + widgetServiceMock.findOneByWidgetType.mockResolvedValue(null); + + const markdown = await service.generateMarkdown(workspaceId); + + expect(markdown).toContain('# 🚀 Project Team Align Report'); + expect(markdown).toContain('## 3. Else'); + }); + + it('각 위젯을 마크다운 섹션으로 변환한다', async () => { + widgetServiceMock.findOneByWidgetType + .mockImplementationOnce(() => ({ + data: { + content: { + widgetType: WidgetType.GROUND_RULE, + rules: ['Folder', 'Commit'], + }, + }, + })) + .mockImplementationOnce(() => ({ + data: { + content: { + widgetType: WidgetType.TECH_STACK, + selectedItems: ['React', 'NestJS'], + }, + }, + })) + .mockImplementationOnce(() => ({ + data: { + content: { + widgetType: WidgetType.POST_IT, + text: '기타 메모', + }, + }, + })); + + const markdown = await service.generateMarkdown(workspaceId); + const lines = markdown.split('\n'); + + // Ground Rule 섹션 + expect(lines).toContain('## 1. 📋 Ground Rule'); + expect(lines).toContain('| Folder | - |'); + expect(lines).toContain('| Commit | - |'); + + // Tech Stack 섹션 + expect(lines).toContain('## 2. 🛠 Tech Stack Selection'); + expect(lines).toContain('| React | vLatest |'); + expect(lines).toContain('| NestJS | vLatest |'); + + // Else 섹션 (Post-it) + expect(lines).toContain('## 3. Else'); + expect(lines).toContain('기타 메모'); + }); +}); diff --git a/backend/src/markdown/markdown.service.ts b/backend/src/markdown/markdown.service.ts index dc5dca78..271cfca6 100644 --- a/backend/src/markdown/markdown.service.ts +++ b/backend/src/markdown/markdown.service.ts @@ -48,8 +48,6 @@ export class MarkdownService { content.selectedItems.forEach((item) => { lines.push(`| ${item} | vLatest |`); }); - } else { - lines.push('| - | - |'); } lines.push(''); From cbd57d6361c01e60a5a8a4101351a17d44a14710 Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Thu, 8 Jan 2026 13:58:46 +0900 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20axios=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + package-lock.json | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f06a98c5..931d0b1a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.17", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.561.0", diff --git a/package-lock.json b/package-lock.json index 4cf65515..e7e6d459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.17", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.561.0", @@ -9739,6 +9740,17 @@ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "license": "MIT" }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -12899,6 +12911,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -13043,7 +13075,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -13060,7 +13091,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13070,7 +13100,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" From ed6b588fd779466aa8a9a9c169dd00314a2a05c7 Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Thu, 8 Jan 2026 14:00:15 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20axios=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=EC=A0=81=EC=9D=B8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/common/api/apiClient.ts | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 frontend/src/common/api/apiClient.ts diff --git a/frontend/src/common/api/apiClient.ts b/frontend/src/common/api/apiClient.ts new file mode 100644 index 00000000..f932a0ea --- /dev/null +++ b/frontend/src/common/api/apiClient.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; + +const getApiUrl = () => { + return import.meta.env.MODE === 'production' + ? window.location.origin + : 'http://localhost:3000'; +}; + +export const apiClient = axios.create({ + baseURL: getApiUrl(), + // 일단 10초로 설정 + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// 기본적인 에러 처리 하기 +apiClient.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // 에러 처리 로직 + if (error.response) { + // 서버 응답 에러 + console.error('API Error:', error.response.data); + } else if (error.request) { + // 요청은 보냈지만 응답을 받지 못함 + console.error('Network Error:', error.request); + } else { + // 요청 설정 중 에러 + console.error('Error:', error.message); + } + return Promise.reject(error); + }, +); From 84696458d344406cb8ce07ea4da20192df5bb034 Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Thu, 8 Jan 2026 14:01:13 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EB=A7=88?= =?UTF-8?q?=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EA=B4=80=EB=A0=A8=20isLoading=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20+=20axios=EB=A1=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=ED=9B=85=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/common/api/markdownApi.ts | 19 +++++++++ frontend/src/common/hooks/useMarkdown.ts | 50 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 frontend/src/common/api/markdownApi.ts create mode 100644 frontend/src/common/hooks/useMarkdown.ts diff --git a/frontend/src/common/api/markdownApi.ts b/frontend/src/common/api/markdownApi.ts new file mode 100644 index 00000000..db8b683b --- /dev/null +++ b/frontend/src/common/api/markdownApi.ts @@ -0,0 +1,19 @@ +import { apiClient } from './apiClient'; + +export interface GetMarkdownResponse { + markdown: string; +} + +export const markdownApi = { + /** + * 워크스페이스 ID로 마크다운 문서를 생성합니다. + * @param workspaceId 워크스페이스 ID + * @returns 생성된 마크다운 문서 + */ + getMarkdown: async (workspaceId: string): Promise => { + const response = await apiClient.get('/markdown', { + params: { workspaceId }, + }); + return response.data.markdown; + }, +}; diff --git a/frontend/src/common/hooks/useMarkdown.ts b/frontend/src/common/hooks/useMarkdown.ts new file mode 100644 index 00000000..da8c3994 --- /dev/null +++ b/frontend/src/common/hooks/useMarkdown.ts @@ -0,0 +1,50 @@ +import { useState, useCallback } from 'react'; +import { markdownApi } from '../api/markdownApi'; + +interface UseMarkdownReturn { + markdown: string; + isLoading: boolean; + error: Error | null; + fetchMarkdown: (workspaceId: string) => Promise; + clearMarkdown: () => void; +} + +/** + * 마크다운 문서를 가져오고 관리하는 hook + */ +export const useMarkdown = (): UseMarkdownReturn => { + const [markdown, setMarkdown] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchMarkdown = useCallback(async (workspaceId: string) => { + setIsLoading(true); + setError(null); + + try { + const markdownData = await markdownApi.getMarkdown(workspaceId); + setMarkdown(markdownData); + } catch (err) { + const error = + err instanceof Error ? err : new Error('마크다운 생성에 실패했습니다.'); + setError(error); + console.error('마크다운 생성 실패:', err); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + const clearMarkdown = useCallback(() => { + setMarkdown(''); + setError(null); + }, []); + + return { + markdown, + isLoading, + error, + fetchMarkdown, + clearMarkdown, + }; +}; From 8a7b1bdb5872d2fb27d2b770d1e72f3fece742c6 Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Thu, 8 Jan 2026 14:03:20 +0900 Subject: [PATCH 08/17] =?UTF-8?q?fix:=20'api'=20=EB=84=A3=EC=96=B4?= =?UTF-8?q?=EC=84=9C=20=EB=B0=B1=EC=97=94=EB=93=9C=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/common/api/apiClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/common/api/apiClient.ts b/frontend/src/common/api/apiClient.ts index f932a0ea..d3ca8459 100644 --- a/frontend/src/common/api/apiClient.ts +++ b/frontend/src/common/api/apiClient.ts @@ -7,7 +7,7 @@ const getApiUrl = () => { }; export const apiClient = axios.create({ - baseURL: getApiUrl(), + baseURL: getApiUrl() + '/api', // 일단 10초로 설정 timeout: 10000, headers: { From 38d188a4076ccb7cddaff316986547d700ce675f Mon Sep 17 00:00:00 2001 From: davidpro08 Date: Thu, 8 Jan 2026 14:04:47 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EA=B3=A0=20=EB=B0=9B=EC=9D=80=20=EA=B0=92=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/workspace/WorkSpacePage.tsx | 18 ++++++++++-- .../workspace/components/ExportModal.tsx | 28 ++++--------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/frontend/src/pages/workspace/WorkSpacePage.tsx b/frontend/src/pages/workspace/WorkSpacePage.tsx index cb2b7ebc..7a66b0d2 100644 --- a/frontend/src/pages/workspace/WorkSpacePage.tsx +++ b/frontend/src/pages/workspace/WorkSpacePage.tsx @@ -6,6 +6,7 @@ import TechStackModal from '@/features/widgets/techStack/components/modal/TechSt import { getRandomColor } from '@/utils/getRandomColor'; import { useSocket } from '@/common/hooks/useSocket'; +import { useMarkdown } from '@/common/hooks/useMarkdown'; import CanvasContent from '@/features/canvas/CanvasContent'; import ToolBar from '@/pages/workspace/components/toolbar/ToolBar'; import type { Cursor } from '@/common/types/cursor'; @@ -33,6 +34,9 @@ function WorkSpacePage() { const [hoveredUser, setHoveredUser] = useState(null); const [hoverPosition, setHoverPosition] = useState({ top: 0, left: 0 }); + // 마크다운 관리 hook + const { markdown: exportMarkdown, fetchMarkdown } = useMarkdown(); + // Dragging State const [draggingId, setDraggingId] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); @@ -126,6 +130,16 @@ function WorkSpacePage() { setHoveredUser(null); }; + const handleExportClick = useCallback(async () => { + try { + await fetchMarkdown(workspaceId); + setIsExportModalOpen(true); + } catch (error) { + // 일단 alert를 사용했는데, 그냥 마크다운 내용으로 (마크다운 생성 실패)를 보내는 것도 나쁘지 않을 것 같습니다! + alert('마크다운 생성에 실패했습니다.'); + } + }, [workspaceId, fetchMarkdown]); + return (
- setIsExportModalOpen(true)} /> + {/* Main Workspace */}
@@ -172,7 +186,7 @@ function WorkSpacePage() { setIsExportModalOpen(false)} - techStack={techStack} + markdown={exportMarkdown} />
); diff --git a/frontend/src/pages/workspace/components/ExportModal.tsx b/frontend/src/pages/workspace/components/ExportModal.tsx index d4bdc109..589e5ee6 100644 --- a/frontend/src/pages/workspace/components/ExportModal.tsx +++ b/frontend/src/pages/workspace/components/ExportModal.tsx @@ -3,28 +3,10 @@ import { LuFileText, LuX, LuCopy, LuCheck } from 'react-icons/lu'; interface ExportModalProps { isOpen: boolean; onClose: () => void; - techStack: Set; + markdown: string; } -function ExportModal({ isOpen, onClose, techStack }: ExportModalProps) { - const generateMarkdown = () => { - const techs = Array.from(techStack) - .map((t) => `| ${t} | vLatest | Selected |`) - .join('\n'); - - return `# 🚀 Project Team Align Report -> Created at: ${new Date().toLocaleString()} - - -## 2. 🛠 Tech Stack Selection -| Tech Name | Version | Status | -| :--- | :--- | :--- | -${techs.length ? techs : '| None | - | - |'} - ---- -*Generated by TeamConfig*`; - }; - +function ExportModal({ isOpen, onClose, markdown }: ExportModalProps) { if (!isOpen) return null; return ( @@ -55,14 +37,14 @@ ${techs.length ? techs : '| None | - | - |'}
-            {generateMarkdown()}
+            {markdown}
           
@@ -75,7 +57,7 @@ ${techs.length ? techs : '| None | - | - |'}