Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Map API 수정 및 맛집 삭제 API, 키워드 저장 로직 추가 #34

Merged
merged 16 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"migrate:up:dev": "dotenv -e .development.env -- npx mikro-orm migration:up",
"show:database:schema": "dotenv -e .development.env -- npx mikro-orm schema:create --dump",
"show:database:schema:update": "dotenv -e .development.env -- npx mikro-orm schema:update --no-drop-tables --dump",
"database:schema:update": "dotenv -e .development.env -- npx mikro-orm schema:update --no-drop-tables -r",
"migrate:up": "npx mikro-orm migration:up",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start:dev": "export NODE_ENV=development&& nest start --watch",
Expand Down Expand Up @@ -60,7 +61,8 @@
"passport-kakao": "^1.0.1",
"reflect-metadata": "^0.2.2",
"rimraf": "^5.0.7",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 6 additions & 8 deletions src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
}

async validate(payload: { id: number }, done: VerifiedCallback) {
try {
const userData = await this.userService.findOne({
id: payload.id,
});

done(null, userData);
} catch (err) {
throw new UnauthorizedException('Error', err.message);
const user = await this.userService.findOne({
id: payload.id,
});
if (!user) {
throw new UnauthorizedException('존재하지 않는 유저입니다.');
}
done(null, user);
}
}
1 change: 1 addition & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const IS_DEV = process.env.NODE_ENV === 'development';
export const IS_STAGE = process.env.NODE_ENV === 'stage';
export const IS_PROD = process.env.NODE_ENV === 'production';
export const SEARCH_KEYWORD_MAX_LENGTH = 10;
export const INVITE_LINK_EXPIRATION_DAYS = 7;
1 change: 0 additions & 1 deletion src/common/guards/map-role.guard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

import { UserMapRole } from 'src/entities';
import { UserMapService } from 'src/user-map/user-map.service';

@Injectable()
Expand Down
2 changes: 1 addition & 1 deletion src/common/helper/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class EnvironmentVariables {

@IsString()
DISCORD_WEBHOOK_URL: string;

@IsString()
GPT_KEY: string;
}
Expand Down
9 changes: 6 additions & 3 deletions src/core/exception-filters/custom-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

// import * as Sentry from '@sentry/nestjs';
import { Response } from 'express';

import { EnvType } from 'src/common/helper/env.validation';
import { BaseException } from 'src/exceptions/exception.abstract';
import { UtilService } from 'src/util/util.service';

type ResponseBody = {
Expand All @@ -27,7 +27,7 @@ export class CustomExceptionFilter implements ExceptionFilter {
private readonly utilService: UtilService,
) {}

async catch(exception: Error, host: ArgumentsHost) {
async catch(exception: BaseException | Error, host: ArgumentsHost) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋ

const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
Expand All @@ -37,7 +37,10 @@ export class CustomExceptionFilter implements ExceptionFilter {
message: '예상치 못한 에러가 발생했습니다. 노드팀을 채찍질 해주세요',
};

if (exception instanceof HttpException) {
if (exception instanceof BaseException) {
responseBody.statusCode = exception.statusCode;
responseBody.message = exception.composedMessage ?? exception.message;
} else if (exception instanceof HttpException) {
const httpExceptionResponse = exception.getResponse() as
| string
| ResponseBody;
Expand Down
7 changes: 4 additions & 3 deletions src/entities/group-map.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import {
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { v4 as uuid } from 'uuid';

import { GroupMapRepository, UserMap } from 'src/entities';

@Entity({ tableName: 'map', repository: () => GroupMapRepository })
export class GroupMap {
@PrimaryKey()
id: string;
@PrimaryKey({ type: 'uuid' })
id: string = uuid();

@Property({ type: 'string' })
@Property({ type: 'string', unique: true })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class validator로 unique 침범하면, 에러 메시지 커스텀하게 보여줄 수 있도록 하는 건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오~ 좋은데 한번 해볼게 감사링!

name: string;

@OneToMany({ entity: () => UserMap, mappedBy: (userMap) => userMap.map })
Expand Down
3 changes: 1 addition & 2 deletions src/entities/place-for-map.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Entity,
EntityRepositoryType,
ManyToOne,
OneToOne,
PrimaryKeyProp,
Property,
Rel,
Expand Down Expand Up @@ -38,7 +37,7 @@ export class PlaceForMap {
})
likedUserIds: number[];

@OneToOne(() => User)
@ManyToOne(() => User)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good

createdBy: User;

@Property()
Expand Down
7 changes: 7 additions & 0 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export class User {
@OneToMany({ entity: () => UserMap, mappedBy: (userMap) => userMap.user })
userMap = new Collection<UserMap>(this);

@Property({
type: 'json',
comment: '최근 검색어 배열',
default: '[]',
})
recentSearchKeywords: string[];
chanwoonglee marked this conversation as resolved.
Show resolved Hide resolved

@Property()
createdAt: Date = new Date();

Expand Down
5 changes: 5 additions & 0 deletions src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export class UserNotFoundException extends ExceptionOf.USER(
'존재하지 않는 유저입니다.' as const,
) {}

export class PlaceNotFoundException extends ExceptionOf.USER(
HttpStatus.NOT_FOUND,
'지도에 등록되지 않은 장소 입니다.' as const,
) {}

export class DuplicateNicknameException extends ExceptionOf.USER(
HttpStatus.CONFLICT,
'이미 사용중인 닉네임입니다.' as const,
Expand Down
8 changes: 1 addition & 7 deletions src/invite-link/invite-link.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ import {
MockRepository,
MockRepositoryFactory,
} from 'src/common/helper/mock.helper';
import {
InviteLink,
InviteLinkRepository,
User,
UserRepository,
} from 'src/entities/index';
import { UserService } from 'src/user/user.service';
import { InviteLink, InviteLinkRepository, User } from 'src/entities';

import { InviteLinkService } from './invite-link.service';

Expand Down
5 changes: 2 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

// import * as Sentry from '@sentry/nestjs';
// import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { useContainer } from 'class-validator';
import cookieParser from 'cookie-parser';

import { AppModule } from './app.module';
Expand All @@ -27,7 +26,7 @@ async function bootstrap() {

const configService = app.select(AppModule).get(ConfigService);
const utilService = new UtilService(configService);

useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(
new ValidationPipe({
transform: true,
Expand Down
8 changes: 3 additions & 5 deletions src/map/dtos/create-map.dto.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';

import { IsNotEmpty } from 'class-validator';
import { IsNotEmpty, Validate } from 'class-validator';

import { GroupMap } from 'src/entities';
import { IsMapNameUnique } from 'src/map/validator/is-map-name-unique.validator';

export class CreateMapDto implements Partial<GroupMap> {
@ApiProperty()
@IsNotEmpty()
id: string;

@ApiProperty()
@IsNotEmpty()
@Validate(IsMapNameUnique)
name: string;
}
6 changes: 1 addition & 5 deletions src/map/dtos/map-item-for-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

import {
UserMapRepository,
UserMapRole,
UserMapRoleValueType,
} from '../../entities';
import { UserMapRole, UserMapRoleValueType } from '../../entities';

export class MapItemForUserDto {
@ApiProperty()
Expand Down
3 changes: 3 additions & 0 deletions src/map/dtos/map-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class MapResponseDto implements Partial<GroupMap> {
@ApiProperty({ type: MapUser, isArray: true })
users: MapUser[];

@ApiProperty()
registeredPlaceCount: number;

@ApiProperty()
createdAt: Date;

Expand Down
30 changes: 12 additions & 18 deletions src/map/map.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiExcludeEndpoint,
ApiOkResponse,
ApiOperation,
ApiResponse,
Expand All @@ -28,6 +29,7 @@ import { UpdateMapDto } from './dtos/update-map.dto';
import { MapService } from './map.service';

@ApiTags('maps')
@ApiBearerAuth()
@Controller('maps')
export class MapController {
constructor(
Expand All @@ -37,17 +39,10 @@ export class MapController {

@Post()
@ApiOkResponse({ type: MapItemForUserDto })
@ApiOperation({ summary: '새 지도를 생성합니다.' })
@ApiOperation({ summary: '새 지도를 생성합니다' })
@ApiBearerAuth()
@UseAuthGuard([UserRole.USER])
create(@Body() createMapDto: CreateMapDto, @CurrentUser() user: User) {
// ensure alpha numeric
if (!/[A-Za-z0-9-_]/.test(createMapDto.id)) {
throw new BadRequestException(
'지도 아이디는 영문, 숫자, 하이픈만 가능합니다',
);
}

return this.mapService.create(createMapDto, user);
}

Expand All @@ -61,6 +56,7 @@ export class MapController {
}

@Get(':id')
@ApiOperation({ summary: '지도 정보 조회 (포함된 유저 정보, 맛집 개수...)' })
@ApiOkResponse({ type: MapResponseDto })
@ApiBearerAuth()
@UseMapRoleGuard()
Expand All @@ -78,15 +74,13 @@ export class MapController {
return this.mapService.update(id, updateMapDto);
}

// TODO
// @Delete(':id')
// @ApiOkResponse({ type: Number })
// @ApiBearerAuth()
// @UseMapRoleGuard([UserMapRole.ADMIN])
// @UseAuthGuard([UserRole.USER])
// remove(@Param('id') id: string) {
// return this.mapService.remove(id);
// }
@Delete(':id')
@ApiOkResponse({ type: Number })
@ApiBearerAuth()
@ApiExcludeEndpoint()
remove(@Param('id') id: string) {
return this.mapService.remove(id);
}

@Post(':id/invite-links')
@ApiOperation({
Expand Down
7 changes: 4 additions & 3 deletions src/map/map.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';

import { InviteLinkModule } from 'src/invite-link/invite-link.module';

import { GroupMap, UserMap } from '../entities';
import { GroupMap, PlaceForMap, UserMap } from '../entities';
import { UserMapService } from '../user-map/user-map.service';
import { UserModule } from '../user/user.module';
import { MapController } from './map.controller';
import { MapService } from './map.service';
import { IsMapNameUnique } from './validator/is-map-name-unique.validator';

@Module({
imports: [
MikroOrmModule.forFeature([GroupMap, UserMap]),
MikroOrmModule.forFeature([GroupMap, UserMap, PlaceForMap]),
UserModule,
InviteLinkModule,
],
controllers: [MapController],
providers: [MapService, UserMapService],
providers: [MapService, IsMapNameUnique, UserMapService],
exports: [MapService],
})
export class MapModule {}
Loading
Loading