diff --git a/BE/package-lock.json b/BE/package-lock.json index 629a9f3a..391a320f 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -1,17 +1,20 @@ { - "name": "be", + "name": "boostus-be", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "be", + "name": "boostus-be", "version": "0.0.1", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/platform-express": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -216,6 +219,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2128,6 +2132,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.9.tgz", "integrity": "sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.1.0", "iterare": "1.2.1", @@ -2160,6 +2165,7 @@ "integrity": "sha512-a00B0BM4X+9z+t3UxJqIZlemIwCQdYoPKrMcM+ky4z3pkqqG1eTWexjs+YXpGObnLnjtMPVKWlcZHp3adDYvUw==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2195,11 +2201,32 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.9", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz", "integrity": "sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -2574,6 +2601,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2687,6 +2715,7 @@ "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2757,6 +2786,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -2819,6 +2854,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -3500,6 +3536,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3549,6 +3586,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3959,6 +3997,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4157,6 +4196,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -4200,6 +4240,25 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4822,6 +4881,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4882,6 +4942,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6129,6 +6190,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -6994,6 +7056,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.31", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.31.tgz", + "integrity": "sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7882,6 +7950,7 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8061,7 +8130,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/require-directory": { "version": "2.1.1", @@ -8158,6 +8228,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8755,6 +8826,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9082,6 +9154,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9229,6 +9302,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9429,6 +9503,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -9478,6 +9561,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -9547,6 +9631,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/BE/package.json b/BE/package.json index 6c961740..46ed79dc 100644 --- a/BE/package.json +++ b/BE/package.json @@ -22,7 +22,10 @@ "dependencies": { "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/platform-express": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 86628031..21b2541e 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { ProjectModule } from './project/project.module'; +import { BlogModule } from './blog/blog.module'; @Module({ - imports: [], + imports: [ProjectModule, BlogModule], controllers: [AppController], providers: [AppService], }) diff --git a/BE/src/blog/blog.controller.ts b/BE/src/blog/blog.controller.ts new file mode 100644 index 00000000..758360a0 --- /dev/null +++ b/BE/src/blog/blog.controller.ts @@ -0,0 +1,6 @@ +import { Controller } from '@nestjs/common'; + +@Controller('/api/blogs') +export class BlogController { + constructor() {} +} diff --git a/BE/src/blog/blog.module.ts b/BE/src/blog/blog.module.ts new file mode 100644 index 00000000..684a8929 --- /dev/null +++ b/BE/src/blog/blog.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BlogController } from './blog.controller'; +import { BlogsService } from './blog.service'; + +@Module({ + imports: [], + controllers: [BlogController], + providers: [BlogsService], +}) +export class BlogModule {} diff --git a/BE/src/blog/blog.service.ts b/BE/src/blog/blog.service.ts new file mode 100644 index 00000000..895a0e5e --- /dev/null +++ b/BE/src/blog/blog.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BlogService { + constructor() {} +} diff --git a/BE/src/main.ts b/BE/src/main.ts index f76bc8d9..9ea6d5b3 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -1,8 +1,21 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + whitelist: true, // DTO에 정의되지 않은 속성 제거 + forbidNonWhitelisted: true, // DTO 외 속성 들어오면 400 Bad Request + }), + ); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/BE/src/project/dto/create-project.dto.ts b/BE/src/project/dto/create-project.dto.ts new file mode 100644 index 00000000..7613bf67 --- /dev/null +++ b/BE/src/project/dto/create-project.dto.ts @@ -0,0 +1,68 @@ +import { Type } from 'class-transformer'; +import { + IsArray, + IsDateString, + IsInt, + IsOptional, + IsString, + IsUrl, + MaxLength, + ValidateNested, +} from 'class-validator'; + +class ProjectMemberDto { + @IsString() + @MaxLength(255) + nickname: string; + + @IsOptional() + @IsUrl() + avatarUrl?: string; +} + +export class CreateProjectDto { + @IsOptional() + @IsUrl() + thumbnailUrl?: string; + + @IsString() + @MaxLength(255) + title: string; // required + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + summary?: string; + + @IsOptional() + @IsString() + contents?: string; + + @IsUrl() + repositoryUrl: string; // required + + @IsOptional() + @IsUrl() + demoUrl?: string; + + @IsOptional() + @IsInt() + cohort?: number; + + @IsOptional() + @IsDateString() + startDate?: string; // YYYY-MM-DD + + @IsOptional() + @IsDateString() + endDate?: string; // YYYY-MM-DD + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ProjectMemberDto) + members?: ProjectMemberDto[]; +} diff --git a/BE/src/project/dto/get-project.query.dto.ts b/BE/src/project/dto/get-project.query.dto.ts new file mode 100644 index 00000000..4fb9cdf6 --- /dev/null +++ b/BE/src/project/dto/get-project.query.dto.ts @@ -0,0 +1,11 @@ +import { IsInt, IsOptional, IsString } from 'class-validator'; + +export class GetProjectQueryDto { + @IsOptional() + @IsString({ each: true }) + stack?: string[]; + + @IsOptional() + @IsInt() + cohort?: number; +} diff --git a/BE/src/project/dto/update-project.dto.ts b/BE/src/project/dto/update-project.dto.ts new file mode 100644 index 00000000..1d7791fc --- /dev/null +++ b/BE/src/project/dto/update-project.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateProjectDto } from './create-project.dto'; + +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} diff --git a/BE/src/project/project.controller.ts b/BE/src/project/project.controller.ts new file mode 100644 index 00000000..15a3c885 --- /dev/null +++ b/BE/src/project/project.controller.ts @@ -0,0 +1,51 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { ProjectsService } from './project.service'; +import { GetProjectQueryDto } from './dto/get-project.query.dto'; +import { CreateProjectDto } from './dto/create-project.dto'; +import { UpdateProjectDto } from './dto/update-project.dto'; +// import { ResponseMessage } from '../core/response/response-message.decorator'; + +@Controller('/api/projects') +export class ProjectsController { + constructor(private readonly projectsService: ProjectsService) {} + + @Get() + // @ResponseMessage('프로젝트 목록 조회 성공') + findAll(@Query() query: GetProjectQueryDto) { + return this.projectsService.findAll(query); + } + + @Get(':id') + // @ResponseMessage('프로젝트 상세 조회 성공') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.projectsService.findOne(id); + } + + @Post() + // @ResponseMessage('프로젝트 생성 성공') + create(@Body() dto: CreateProjectDto) { + return this.projectsService.create(dto); + } + + @Patch(':id') + // @ResponseMessage('프로젝트 수정 성공') + update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateProjectDto) { + return this.projectsService.update(id, dto); + } + + @Delete(':id') + // @ResponseMessage('프로젝트 삭제 성공') + remove(@Param('id', ParseIntPipe) id: number) { + return this.projectsService.remove(id); + } +} diff --git a/BE/src/project/project.module.ts b/BE/src/project/project.module.ts new file mode 100644 index 00000000..3e1fd8b6 --- /dev/null +++ b/BE/src/project/project.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProjectsController } from './project.controller'; +import { ProjectsService } from './project.service'; + +@Module({ + imports: [], + controllers: [ProjectsController], + providers: [ProjectsService], +}) +export class ProjectModule {} diff --git a/BE/src/project/project.service.ts b/BE/src/project/project.service.ts new file mode 100644 index 00000000..6c272abc --- /dev/null +++ b/BE/src/project/project.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +// import { PrismaService } from '../prisma/prisma.service'; +import { CreateProjectDto } from './dto/create-project.dto'; +import { GetProjectQueryDto } from './dto/get-project.query.dto'; +import { UpdateProjectDto } from './dto/update-project.dto'; + +@Injectable() +export class ProjectsService { + constructor() {} + + async findAll(query: GetProjectQueryDto) {} + + async findOne(id: number) {} + + async create(dto: CreateProjectDto) {} + + async update(id: number, dto: UpdateProjectDto) {} + + async remove(id: number) {} +}