Skip to content
Open
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
33 changes: 15 additions & 18 deletions src/activity/activity.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { Controller, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guard/auth.guard';
import { BaseResponse } from 'src/global/base/base-response';
import { ExtractPayload } from 'src/global/decorator/extract-payload.decorator';
import { ActivityService } from './activity.service';
import { ActivityDetailResponse } from './dto/activity-detail-response.dto';

@Controller('api/activities')
@UseGuards(JwtAuthGuard)
Expand All @@ -13,17 +10,17 @@ import { ActivityDetailResponse } from './dto/activity-detail-response.dto';
export class ActivityController {
constructor(private readonly activityService: ActivityService) {}

@Get(':activityId')
@ApiOperation({
summary: '활동 세부 정보 조회',
description: '활동 세부 정보를 조회한다. 리뷰는 따로 API 호출 해주세요!',
})
@ApiParam({ name: 'activityId', description: '세부 정보를 조회할 활동 아이디' })
@ApiOkResponse({ type: ActivityDetailResponse })
async getActivityDetail(
@ExtractPayload() userId: number,
@Param('activityId') activityId: number,
): Promise<BaseResponse<ActivityDetailResponse>> {
return BaseResponse.of(await this.activityService.getActivityDetail(userId, activityId));
}
// @Get(':activityId')
// @ApiOperation({
// summary: '활동 세부 정보 조회',
// description: '활동 세부 정보를 조회한다. 리뷰는 따로 API 호출 해주세요!',
// })
// @ApiParam({ name: 'activityId', description: '세부 정보를 조회할 활동 아이디' })
// @ApiOkResponse({ type: ActivityDetailResponse })
// async getActivityDetail(
// @ExtractPayload() userId: number,
// @Param('activityId') activityId: number,
// ): Promise<BaseResponse<ActivityDetailResponse>> {
// return BaseResponse.of(await this.activityService.getActivityDetail(userId, activityId));
// }
}
12 changes: 3 additions & 9 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiBearerAuth, ApiHeaders, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { BaseResponse } from 'src/global/base/base-response';
import { ExtractPayload } from 'src/global/decorator/extract-payload.decorator';
import { ExtractToken } from 'src/global/decorator/extract-token.decorator';
import { AuthService } from './auth.service';
import { AppleOAuthRequest } from './dto/apple-oauth-request.dto';
Expand All @@ -12,7 +11,6 @@ import { JwtResponse } from './dto/jwt-response.dto';
import { KakaoOAuthRequest } from './dto/kakao-oauth-request.dto';
import { KakaoOAuthResponse } from './dto/kakao-oauth-response.dto';
import { LoginRequest } from './dto/login-request.dto';
import { JwtAuthGuard } from './guard/auth.guard';

@Controller('api/auth')
@ApiTags('auth')
Expand All @@ -31,19 +29,15 @@ export class AuthController {
}

@Get('reissue')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Access Token 만료시 재발급',
description: 'Access Token과 Refresh Token을 재발급한다.',
})
@ApiBearerAuth()
@ApiHeaders([{ name: 'Authorization', description: 'Refresh Token' }])
@ApiOkResponse({ type: JwtResponse, description: '재발급 성공' })
async reissueToken(
@ExtractPayload() userId: number,
@ExtractToken() refreshToken: string,
): Promise<BaseResponse<JwtResponse>> {
return BaseResponse.of(await this.authService.reissueToken(userId, refreshToken));
async reissueToken(@ExtractToken() refreshToken: string): Promise<BaseResponse<JwtResponse>> {
return BaseResponse.of(await this.authService.reissueToken(refreshToken));
}

@Post('email-check')
Expand Down
3 changes: 2 additions & 1 deletion src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/user/entity/user.entity';
import { UserModule } from 'src/user/user.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategy/jwt.strategy';

@Module({
imports: [JwtModule.register({}), TypeOrmModule.forFeature([User]), PassportModule],
imports: [JwtModule.register({}), TypeOrmModule.forFeature([User]), PassportModule, UserModule],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
Expand Down
14 changes: 5 additions & 9 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { BaseException } from 'src/global/base/base-exception';
import { Password } from 'src/user/entity/password';
import { User } from 'src/user/entity/user.entity';
import { SocialType } from 'src/user/enum/social-type';
import { UserResponseCode } from 'src/user/exception/user-response-code';
import { Repository } from 'typeorm';
import { AppleOAuthResponse } from './dto/apple-oauth-response.dto';
import { JwtResponse } from './dto/jwt-response.dto';
Expand Down Expand Up @@ -107,18 +106,15 @@ export class AuthService {
return await this.userRepository.exist({ where: { email } });
}

async reissueToken(userId: number, refreshToken: string) {
const user = await this.userRepository.findOneBy({ userId });
if (!user) {
throw BaseException.of(UserResponseCode.USER_NOT_FOUND);
}
async reissueToken(refreshToken: string) {
const user = await this.userRepository.findOneBy({ refreshToken });

if (user.refreshToken !== refreshToken) {
if (!user) {
throw BaseException.of(AuthResponseCode.INVALID_TOKEN);
}

const newAccessToken = await this.generateAccessToken(userId);
const newRefreshToken = await this.generateRefreshToken(userId);
const newAccessToken = await this.generateAccessToken(user.userId);
const newRefreshToken = await this.generateRefreshToken(user.userId);

user.refreshToken = newRefreshToken;
await this.userRepository.save(user);
Expand Down
11 changes: 8 additions & 3 deletions src/auth/strategy/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Config } from 'src/global/config/config.type';
import { User } from 'src/user/entity/user.entity';
import { UserService } from 'src/user/user.service';
import { JwtPayloadType } from './type/jwt-payload.type';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private configService: ConfigService<Config>) {
constructor(
private configService: ConfigService<Config>,
private readonly userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('auth.secret', { infer: true }),
});
}

public validate(payload: JwtPayloadType): JwtPayloadType {
return payload;
public async validate(payload: JwtPayloadType): Promise<User> {
return await this.userService.findUserById(payload.sub);
}
}
2 changes: 1 addition & 1 deletion src/auth/strategy/type/jwt-payload.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type JwtPayloadType = {
userId: number;
sub: number;
iat: number;
exp: number;
iss: string;
Expand Down
70 changes: 70 additions & 0 deletions src/place-review/dto/request/patch-place-review-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsDateString,
IsNumber,
IsOptional,
IsString,
Length,
Max,
Min,
} from 'class-validator';
import { IsPastOrPresent } from 'src/global/validation/decorator/is-past-or-present.decorator';

export class PatchPlaceReviewRequest {
@ApiProperty({
description: '방문 날짜 (ISO 8601 형식을 따름)',
example: '2023-09-07',
})
@IsDateString(undefined, { message: '날짜 형식이 아닙니다.' })
@IsPastOrPresent({ message: '방문 날짜는 현재거나 과거여야 합니다.' })
readonly visitedAt: Date;

@ApiProperty({
description: '참여한 활동의 아이디',
example: 1,
required: false,
})
@IsOptional()
@Type(() => Number)
@IsNumber(undefined, { message: '참여한 활동 아이디는 정수여야 합니다.' })
readonly activityId?: number;

@ApiProperty({
description: '리뷰 내용',
example: '즐거운 독서 생활',
})
@IsString({ message: '내용은 문자열이어야 합니다.' })
@Length(1, 300, { message: '내용은 300자 이하여야 합니다.' })
readonly contents: string;

@ApiProperty({
description: '장소 평점, 0점 ~ 5점 사이',
example: 4.2,
})
@Type(() => Number)
@IsNumber(undefined, { message: '평점은 숫자여야 합니다.' })
@Min(0, { message: '평점은 0점 이상 5점 이하여야 합니다.' })
@Max(5, { message: '평점은 0점 이상 5점 이하여야 합니다.' })
readonly rating: number;

@ApiProperty({
description: '댓글 허용 여부',
example: true,
})
@Type(() => Boolean)
@IsBoolean({ message: '댓글 비허용 여부는 boolean 값이어야 합니다.' })
readonly commentAllowed: boolean;

@ApiProperty({
type: [Number],
description: '유지할 이미지의 아이디',
example: [1, 2, 3],
required: false,
})
@IsOptional()
@Transform(params => (params.value ? params.value.split(',').map(Number) : []))
@IsNumber(undefined, { each: true, message: '이미지의 아이디는 정수여야 합니다.' })
readonly remainImages: number[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';

export class PatchPlaceReviewResponse {
@ApiProperty({ description: '수정된 리뷰 아이디', example: 1 })
updatedReviewId: number;

@ApiProperty({ description: '수정된 날짜', example: new Date() })
updatedAt: Date;
}
10 changes: 10 additions & 0 deletions src/place-review/exception/place-review-response-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ export const PlaceReviewResponseCode = {
'REVIEW_004',
'자신의 글을 신고할 수 없습니다.',
),
IMAGES_NUM_EXCEEDED: new ResponseCode(
HttpStatus.NOT_ACCEPTABLE,
'REVIEW_005',
'업로드 할 사진이 너무 많습니다.',
),
IMAGE_NOT_FOUND: new ResponseCode(
HttpStatus.NOT_FOUND,
'REVIEW_006',
'해당 이미지를 찾을 수 없습니다.',
),
};
63 changes: 62 additions & 1 deletion src/place-review/place-review.controller.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { Body, Controller, Delete, Param, Post, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Param,
Patch,
Post,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiCreatedResponse,
ApiExtraModels,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guard/auth.guard';
import { BaseResponse } from 'src/global/base/base-response';
import { GlobalResponseCode } from 'src/global/base/global-respose-code';
import { ExtractPayload } from 'src/global/decorator/extract-payload.decorator';
import { PlaceReviewExistsValidationPipe } from 'src/global/validation/pipe/place-review-exists-validation.pipe';
import { PatchPlaceReviewRequest } from './dto/request/patch-place-review-request.dto';
import { ReportPlaceReviewRequest } from './dto/request/report-place-review-request.dto';
import { DeleteReviewResponse } from './dto/response/delete-review-response.dto';
import { PatchPlaceReviewResponse } from './dto/response/patch-place-review-response.dto';
import { ReportPlaceReviewResponse } from './dto/response/report-place-review-response.dto';
import { PlaceReviewConverter } from './place-review.converter';
import { PlaceReviewService } from './place-review.service';
Expand Down Expand Up @@ -57,4 +74,48 @@ export class PlaceReviewController {
GlobalResponseCode.CREATED,
);
}

@Patch(':reviewId')
@UseInterceptors(FilesInterceptor('images', 3))
@ApiConsumes('multipart/form-data')
@ApiExtraModels(PatchPlaceReviewRequest)
@ApiBody({
schema: {
allOf: [
{
type: 'object',
properties: {
images: {
description: '업로드 할 사진 (최대 3개)',
type: 'array',
items: {
type: 'string',
format: 'binary',
},
maxItems: 3,
},
},
},
{ $ref: getSchemaPath(PatchPlaceReviewRequest) },
],
},
})
@ApiOperation({ summary: '리뷰 수정 API' })
@ApiParam({ name: 'reviewId', description: '수정할 리뷰의 아이디', example: 1 })
@ApiOkResponse({ type: PatchPlaceReviewResponse })
async updatePlaceReview(
@ExtractPayload() userId: number,
@Param('reviewId', PlaceReviewExistsValidationPipe) reviewId: number,
@Body() request: PatchPlaceReviewRequest,
@UploadedFiles() newImages: Express.Multer.File[],
): Promise<BaseResponse<PatchPlaceReviewResponse>> {
const updatedReview = await this.placeReviewService.updatePlaceReview(
request,
userId,
reviewId,
newImages,
);

return BaseResponse.of(PlaceReviewConverter.toPatchPlaceReviewResponse(updatedReview));
}
}
28 changes: 27 additions & 1 deletion src/place-review/place-review.converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { PlaceReviewReportReason } from 'src/place-review/enum/place-review-repo
import { CreatePlaceReviewRequest } from 'src/place/dto/request/create-place-review-request.dto';
import { Place } from 'src/place/entity/place.entity';
import { User } from 'src/user/entity/user.entity';
import { PatchPlaceReviewRequest } from './dto/request/patch-place-review-request.dto';
import { ReportPlaceReviewRequest } from './dto/request/report-place-review-request.dto';
import { PatchPlaceReviewResponse } from './dto/response/patch-place-review-response.dto';
import { ReportPlaceReviewResponse } from './dto/response/report-place-review-response.dto';
import { PlaceReviewImage } from './entity/place-review-image.entity';
import { PlaceReview } from './entity/place-review.entity';
Expand Down Expand Up @@ -48,7 +50,24 @@ export class PlaceReviewConverter implements OnModuleInit {
.build();
}

private static toPlaceReviewImage(imageUrl: string): PlaceReviewImage {
public static async toUpdatePlaceReview(
review: PlaceReview,
request: PatchPlaceReviewRequest,
activity: Activity,
) {
review.visitedAt = request.visitedAt;
review.contents = request.contents;
review.rating = request.rating;
review.commentAllowed = request.commentAllowed;
if (activity) {
review.activity = activity;
}
review.images = undefined;

return review;
}

public static toPlaceReviewImage(imageUrl: string): PlaceReviewImage {
return Builder<PlaceReviewImage>().imageUrl(imageUrl).build();
}

Expand All @@ -71,4 +90,11 @@ export class PlaceReviewConverter implements OnModuleInit {
.reportedPlaceReviewId(contentsReport.placeReivew.reviewId)
.build();
}

public static toPatchPlaceReviewResponse(review: PlaceReview) {
return Builder(PatchPlaceReviewResponse)
.updatedReviewId(review.reviewId)
.updatedAt(review.updatedAt)
.build();
}
}
Loading