diff --git a/src/app.module.ts b/src/app.module.ts index f37de2e..8ace79e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { EventsModule } from './events/evnets.module'; import { MessageService } from './message/message.service'; import { MessageModule } from './message/message.module'; import { LoggerMiddleware } from './global/middleware/logger.middleware'; +import { HealthController } from './health.controller'; @Module({ imports: [ @@ -31,7 +32,7 @@ import { LoggerMiddleware } from './global/middleware/logger.middleware'; SseModule, UserModule, ], - controllers: [SseController], + controllers: [SseController,HealthController], providers: [MessageService], }) export class AppModule implements NestModule { diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 15493ee..2bd16f0 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -11,10 +11,11 @@ import { } from '@nestjs/common'; import axios from 'axios'; import { Request, Response } from 'express'; -import { UserService } from '../user/user.service'; +import { UserService } from 'src/user/user.service'; import { AuthService } from './auth.service'; import { ConfigService } from '@nestjs/config'; import { ApiTags } from '@nestjs/swagger'; +import * as domain from "domain"; @ApiTags('Auth') @Controller('api/v1/auth') @@ -29,9 +30,9 @@ export class AuthController { private readonly refreshTokenPath = '/api/v1/auth/redirect'; constructor( - private readonly config: ConfigService, - private readonly userService: UserService, - private readonly authService: AuthService, + private readonly config: ConfigService, + private readonly userService: UserService, + private readonly authService: AuthService, ) { this.origin = this.config.get('ORIGIN'); this.client_id = this.config.get('REST_API'); @@ -60,24 +61,24 @@ export class AuthController { try { const accessTokenResponse = await axios.post( - 'https://kauth.kakao.com/oauth/token', - data, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'https://kauth.kakao.com/oauth/token', + data, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, }, - }, ); const kakaoUserInfoResponse = await axios.post( - 'https://kapi.kakao.com/v2/user/me', - {}, - { - headers: { - Authorization: 'Bearer ' + accessTokenResponse.data.access_token, - 'Content-Type': 'application/x-www-form-urlencoded', + 'https://kapi.kakao.com/v2/user/me', + {}, + { + headers: { + Authorization: 'Bearer ' + accessTokenResponse.data.access_token, + 'Content-Type': 'application/x-www-form-urlencoded', + }, }, - }, ); const kakaoId = kakaoUserInfoResponse.data.id; @@ -86,20 +87,21 @@ export class AuthController { let user = await this.userService.getUserByKakaoId(kakaoId); if (!user) { user = await this.userService.createUserWithKakaoIdAndUsername( - kakaoId, - nickname, + kakaoId, + nickname, ); } - const { accessToken, refreshToken } = await this.authService.createTokens( - user.id, + const {accessToken, refreshToken} = await this.authService.createTokens( + user.id, ); this.setTokens( - res, - accessToken, - refreshToken, - 'http://localhost:3000/boards', + res, + accessToken, + refreshToken, + 'here-there-fe.vercel.app', + 'https://here-there-fe.vercel.app/boards', ); } catch { throw new BadRequestException(); @@ -108,17 +110,17 @@ export class AuthController { @Get('redirect/refresh') async refresh(@Req() req: Request, @Res() res: Response) { - const { accessToken, refreshToken } = await this.authService.refreshTokens( - req.cookies['refreshToken'], + const {accessToken, refreshToken} = await this.authService.refreshTokens( + req.cookies['refreshToken'], ); - this.setTokens(res, accessToken, refreshToken); + this.setTokens(res, accessToken, refreshToken, 'here-there-fe.vercel.app'); } @Post('logout') async logout(@Res() res: Response) { - res.clearCookie('accessToken', { path: this.accessTokenPath }); - res.clearCookie('refreshToken', { path: this.refreshTokenPath }); + res.clearCookie('accessToken', {path: this.accessTokenPath}); + res.clearCookie('refreshToken', {path: this.refreshTokenPath}); res.sendStatus(204); } @@ -139,28 +141,37 @@ export class AuthController { } private setTokens( - res: Response, - accessToken: string, - refreshToken: string, - redirectUrl?: string, + res: Response, + accessToken: string, + refreshToken: string, + domain: string, + redirectUrl?: string, ) { + // 쿠키 설정 로그 추가 + this.logger.log(`Setting accessToken cookie: ${accessToken}`); + this.logger.log(`Setting refreshToken cookie: ${refreshToken}`); + this.logger.log(`Cookie domain: ${domain}`); + this.logger.log(`Redirect URL: ${redirectUrl}`); + + res.cookie('accessToken', accessToken, { httpOnly: true, - // secure: true, // HTTPS 사용 시 활성화 - sameSite: 'strict', - path: this.accessTokenPath, // 쿠키가 /api/v1 경로에서만 유효 + secure: true, + sameSite: 'none', + //domain, + path: this.accessTokenPath, }); res.cookie('refreshToken', refreshToken, { httpOnly: true, - // secure: true, // HTTPS 사용 시 활성화 - sameSite: 'strict', - path: this.refreshTokenPath, // 쿠키가 /api/v1/auth/redirect 경로에서만 유효 + secure: true, + sameSite: 'none', + //domain, + path: this.refreshTokenPath, }); - this.logger.log( - `Tokens issued - Access Token: ${accessToken}, Refresh Token: ${refreshToken}`, - ); + + this.logger.log(`Tokens issued - Access Token: ${accessToken}, Refresh Token: ${refreshToken}`); if (redirectUrl) { res.redirect(302, redirectUrl); @@ -169,3 +180,6 @@ export class AuthController { } } } + + + diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 539ccc6..83334a8 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -11,8 +11,8 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthGuard implements CanActivate { constructor( - private readonly jwtService: JwtService, - private readonly configService: ConfigService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, ) {} async canActivate(context: ExecutionContext): Promise { @@ -37,4 +37,4 @@ export class AuthGuard implements CanActivate { private extractTokenFromCookie(request: Request): string | undefined { return request.cookies['accessToken']; } -} +} \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 3ea2785..e918dcd 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,12 +2,13 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { UserModule } from '../user/user.module'; import { AuthGuard } from './auth.guard'; @Module({ imports: [ + ConfigModule, JwtModule.registerAsync({ global: true, inject: [ConfigService], @@ -22,6 +23,6 @@ import { AuthGuard } from './auth.guard'; ], controllers: [AuthController], providers: [AuthService, AuthGuard], - exports: [AuthGuard], + exports: [AuthGuard, JwtModule], }) export class AuthModule {} diff --git a/src/board/board.controller.ts b/src/board/board.controller.ts index 0f6be09..6e36ea5 100644 --- a/src/board/board.controller.ts +++ b/src/board/board.controller.ts @@ -33,7 +33,8 @@ import { BoardIdDto } from './dto/boardId.dto'; @ApiTags('Boards') @Controller('api/v1/boards') export class BoardController { - constructor(private readonly boardService: BoardService) {} + constructor(private readonly boardService: BoardService, + private readonly jwtService: JwtService,) {} @ApiQuery({ name: 'page', @@ -60,38 +61,39 @@ export class BoardController { @UseGuards(AuthGuard) @Post() async create( - @Body(ValidationPipe) createBoardDto: CreateBoardDto, - @Token('sub') id: number, + @Body(ValidationPipe) createBoardDto: CreateBoardDto, + @Token('sub') id: number, ): Promise { + console.log('Received CreateBoardDto:', createBoardDto); // 데이터 확인 return this.boardService.createBoard(createBoardDto, id); } @ApiOperation({ summary: '특정 게시물 조회' }) @Get(':boardId') async findOne( - @Param() boardIdDto: BoardIdDto, - @Req() req: Request, + @Param() boardIdDto: BoardIdDto, + @Req() req: Request, ): Promise { // 쿠키에서 JWT 토큰을 추출 - const token = req.cookies['accessToken']; - - if (!token) { - throw new UnauthorizedException('JWT 토큰이 쿠키에 없습니다.'); - } + const token = req.cookies?.['accessToken']; // 쿠키가 없는 경우도 처리 - // JwtService를 이용해 토큰 디코딩 - const jwtService = new JwtService({ secret: 'JWT_SECRET' }); - const decodedToken = jwtService.decode(token) as any; + let userId: number | null = null; - // sub 클레임에서 userId 추출 - const userId = decodedToken?.sub; - if (!userId) { - throw new UnauthorizedException('유효한 사용자 ID가 아닙니다.'); + // 토큰이 있는 경우 디코딩 + if (token) { + try { + const decodedToken = this.jwtService.decode(token) as any; + userId = decodedToken?.sub || null; + } catch (error) { + console.warn('JWT 디코딩 실패:', error.message); + userId = null; // 디코딩 실패 시 userId를 null로 설정 + } } return this.boardService.findOne(boardIdDto.boardId, userId); } + @ApiOperation({ summary: '게시물 업데이트' }) @ApiCookieAuth() @UseGuards(AuthGuard) diff --git a/src/board/board.service.ts b/src/board/board.service.ts index fdee980..d30483c 100644 --- a/src/board/board.service.ts +++ b/src/board/board.service.ts @@ -1,3 +1,4 @@ +import { Request } from 'express'; import { ConflictException, Injectable, @@ -148,22 +149,25 @@ export class BoardService { }; } - async findOne(id: number, userId: number): Promise { + async findOne(id: number, userId: number | null): Promise { const board = await this.boardRepository.findOne({ where: { id }, relations: ['user', 'location'], }); + if (!board) { throw new NotFoundException(`ID가 ${id}인 게시판을 찾을 수 없습니다.`); } - const chatRoom: ChatRoom = - await this.chatRoomService.findChatRoomByBoardId(id); + const chatRoom: ChatRoom = await this.chatRoomService.findChatRoomByBoardId(id); if (!chatRoom) { throw new NotFoundException('게시판에 연결된 채팅방을 찾을 수 없습니다.'); } - return this.boardMapper.toBoardResponseDto(board, userId, chatRoom); + const response = this.boardMapper.toBoardResponseDto(board, userId, chatRoom); + response.editable = userId === board.user.id; + + return response; } async updateBoard( @@ -235,26 +239,36 @@ export class BoardService { } private async getOrCreateLocation( - location: { latitude: number; longitude: number }, - location_name: string, + location: { latitude: number; longitude: number }, + locationName: string, ): Promise { + console.log('Received locationName:', locationName); // 추가 + console.log('Received location:', location); // 추가 + let newLocation: Location = - await this.locationService.findLocationByCoordinates( - location.latitude, - location.longitude, - ); + await this.locationService.findLocationByCoordinates( + location.latitude, + location.longitude, + ); if (!newLocation) { + console.log('Creating new location:', { + latitude: location.latitude, + longitude: location.longitude, + location_name: locationName, // 디버깅 + }); newLocation = await this.locationService.createLocation({ latitude: location.latitude, longitude: location.longitude, - location_name, + location_name: locationName, // 필드명 확인 }); } else { - newLocation.locationName = location_name; + console.log('Updating existing location:', newLocation); + newLocation.locationName = locationName; await this.locationService.updateLocation(newLocation); } return newLocation; } + } diff --git a/src/board/entities/board.entity.ts b/src/board/entities/board.entity.ts index d72326e..987cf29 100644 --- a/src/board/entities/board.entity.ts +++ b/src/board/entities/board.entity.ts @@ -49,7 +49,8 @@ export class Board extends TimeStamp { @ApiProperty({ description: '날짜', nullable: false }) date: string; - @OneToOne(() => ChatRoom, (chatRoom) => chatRoom.board) + @OneToOne(() => ChatRoom, (chatRoom) => chatRoom.board, { eager: true }) @JoinColumn({ name: 'chat_room_id' }) + @ApiProperty({ description: '채팅방' }) chatRoom: ChatRoom; } diff --git a/src/board/repository/board.repository.ts b/src/board/repository/board.repository.ts index 6d60f03..f83b330 100644 --- a/src/board/repository/board.repository.ts +++ b/src/board/repository/board.repository.ts @@ -8,16 +8,20 @@ import { PagingParams } from '../../global/common/type'; @Injectable() export class CustomBoardRepository { constructor( - @InjectRepository(Board) - private readonly boardRepository: Repository, + @InjectRepository(Board) + private readonly boardRepository: Repository, ) {} - async paginateCreatedBoards(userId: number, pagingParams?: PagingParams) { + // 유저가 생성한 게시판을 페이징 처리하여 가져오는 메서드 + async paginateCreatedBoards( + userId: number, + pagingParams?: PagingParams, + ) { const queryBuilder = this.boardRepository - .createQueryBuilder('board') - .leftJoinAndSelect('board.location', 'location') - .where('board.user_id = :userId', { userId }) - .orderBy('board.updatedAt', 'DESC'); + .createQueryBuilder('board') + .leftJoinAndSelect('board.location', 'location') + .where('board.user.id = :userId', { userId }) // 명확히 user.id 참조 + .orderBy('board.updatedAt', 'DESC'); const paginator = buildPaginator({ entity: Board, @@ -42,17 +46,18 @@ export class CustomBoardRepository { }; } + // 유저가 참여한 게시판을 페이징 처리하여 가져오는 메서드 async paginateJoinedBoards( - userId: number, - chatroomIds: number[], - pagingParams?: PagingParams, + userId: number, + chatroomIds: number[], + pagingParams?: PagingParams, ) { const queryBuilder = this.boardRepository - .createQueryBuilder('board') - .leftJoinAndSelect('board.location', 'location') - .where('board.chat_room IN (:...chatroomIds)', { chatroomIds }) - .andWhere('board.user_id != :userId', { userId }) - .orderBy('board.updatedAt', 'DESC'); + .createQueryBuilder('board') + .leftJoinAndSelect('board.location', 'location') + .where('board.chatRoom.id IN (:...chatroomIds)', { chatroomIds }) // 명확한 필드 참조 + .andWhere('board.user.id != :userId', { userId }) + .orderBy('board.updatedAt', 'DESC'); const paginator = buildPaginator({ entity: Board, @@ -76,4 +81,4 @@ export class CustomBoardRepository { }, }; } -} +} \ No newline at end of file diff --git a/src/chat-room/chat-room.service.ts b/src/chat-room/chat-room.service.ts index 2410128..bc86c8b 100644 --- a/src/chat-room/chat-room.service.ts +++ b/src/chat-room/chat-room.service.ts @@ -26,27 +26,27 @@ export class ChatRoomService { private readonly logger = new Logger(ChatRoomService.name); constructor( - @InjectRepository(ChatRoom) - private readonly chatRoomRepository: Repository, - @InjectRepository(User) - private readonly userRepository: Repository, - @InjectRepository(Message) - private readonly messageRepository: Repository, - @InjectRepository(UserChatRoom) - private userChatRoomRepository: Repository, - @Inject(forwardRef(() => BoardService)) - private readonly boardService: BoardService, - @Inject(forwardRef(() => EventsGateway)) - private readonly eventsGateway: EventsGateway, - private readonly sseService: SseService, + @InjectRepository(ChatRoom) + private readonly chatRoomRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(Message) + private readonly messageRepository: Repository, + @InjectRepository(UserChatRoom) + private userChatRoomRepository: Repository, + @Inject(forwardRef(() => BoardService)) + private readonly boardService: BoardService, + @Inject(forwardRef(() => EventsGateway)) + private readonly eventsGateway: EventsGateway, + private readonly sseService: SseService, ) {} /** 게시글에 해당하는 채팅방 생성 */ async createChatRoomForBoard( - queryRunner: QueryRunner, - board: Board, - maxMemberCount: number, - user: User, + queryRunner: QueryRunner, + board: Board, + maxMemberCount: number, + user: User, ): Promise { const chatRoom: ChatRoom = queryRunner.manager.create(ChatRoom, { board: board, @@ -58,11 +58,11 @@ export class ChatRoomService { // 게시글 작성자를 채팅방에 추가 const userChatRoom: UserChatRoom = queryRunner.manager.create( - UserChatRoom, - { - user: user, - chatRoom: savedChatRoom, - }, + UserChatRoom, + { + user: user, + chatRoom: savedChatRoom, + }, ); await queryRunner.manager.save(userChatRoom); @@ -85,10 +85,7 @@ export class ChatRoomService { } // 채팅방에 참가 - async joinChatRoomByBoardId( - boardId: number, - userId: number, - ): Promise { + async joinChatRoomByBoardId(boardId: number, userId: number): Promise { if (!userId) { throw new UnauthorizedException('UserId를 찾을 수 없습니다'); } @@ -99,21 +96,34 @@ export class ChatRoomService { throw new NotFoundException('채팅방을 찾을 수 없습니다.'); } - if (chatRoom.memberCount >= chatRoom.maxMemberCount) { - throw new BadRequestException( - '채팅방의 최대 인원수를 초과할 수 없습니다.', - ); - } - // 동일한 유저가 이미 같은 chatRoomId에 들어가 있는지 확인 const userChatRoom = chatRoom.userChatRooms.find( - (userChatRoom) => userChatRoom.user.id == userId, + (userChatRoom) => userChatRoom.user.id === userId, ); if (userChatRoom) { - throw new ConflictException('user가 이미 방에 들어가있습니다.'); + this.logger.log( + `User ${userId} is already in ChatRoom ${chatRoom.id}. Redirecting to the chat room.`, + ); + + // 이미 방에 참가한 경우에도 멤버 수를 알림 + const user = await this.getUser(userId); + this.sseService.notifyMemberCountChange( + chatRoom.id, + chatRoom.memberCount, + user.username, + ); + + return chatRoom.id; // 기존 방 ID 반환 + } + + if (chatRoom.memberCount >= chatRoom.maxMemberCount) { + throw new BadRequestException( + '채팅방의 최대 인원수를 초과할 수 없습니다.', + ); } + // 새로운 유저를 채팅방에 추가 chatRoom.memberCount += 1; await this.chatRoomRepository.save(chatRoom); @@ -126,22 +136,23 @@ export class ChatRoomService { const user = await this.getUser(userId); this.sseService.notifyMemberCountChange( - chatRoom.id, - chatRoom.memberCount, - user.username, + chatRoom.id, + chatRoom.memberCount, + user.username, ); this.logger.log( - `User ${userId} joined chat room ID: ${chatRoom.id}. Current count: ${chatRoom.memberCount}`, + `User ${userId} joined chat room ID: ${chatRoom.id}. Current count: ${chatRoom.memberCount}`, ); return chatRoom.id; } + // 채팅방에서 나가기 async leaveChatRoomByBoardId( - boardId: number, - userId: number, + boardId: number, + userId: number, ): Promise { const chatRoom = await this.chatRoomRepository.findOne({ where: { board: { id: boardId } }, @@ -164,23 +175,23 @@ export class ChatRoomService { const user = await this.getUser(userId); this.sseService.notifyMemberCountChange( - chatRoom.id, - chatRoom.memberCount, - user.username, + chatRoom.id, + chatRoom.memberCount, + user.username, ); this.logger.log( - `User ${userId} left chat room ID: ${chatRoom.id}. Current count: ${chatRoom.memberCount}`, + `User ${userId} left chat room ID: ${chatRoom.id}. Current count: ${chatRoom.memberCount}`, ); return chatRoom.id; } async sendMessage( - chatRoomId: number, - userId: number, - content: string, - username: string, + chatRoomId: number, + userId: number, + content: string, + username: string, ): Promise { const chatRoom = await this.chatRoomRepository.findOne({ where: { id: chatRoomId }, @@ -211,7 +222,7 @@ export class ChatRoomService { }); this.logger.log( - `채팅방 ${chatRoomId}에 메시지 전송: ${message.content} by ${username}`, + `채팅방 ${chatRoomId}에 메시지 전송: ${message.content} by ${username}`, ); return message; @@ -224,7 +235,7 @@ export class ChatRoomService { } async getRoomUpdatesByBoardId( - boardId: number, + boardId: number, ): Promise> { const chatRoom = await this.findChatRoomByBoardId(boardId); if (!chatRoom) { @@ -241,4 +252,4 @@ export class ChatRoomService { relations: ['userChatRooms', 'userChatRooms.user'], }); } -} +} \ No newline at end of file diff --git a/src/global/config/typeorm.config.ts b/src/global/config/typeorm.config.ts index 86727a5..99a5395 100644 --- a/src/global/config/typeorm.config.ts +++ b/src/global/config/typeorm.config.ts @@ -23,3 +23,4 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory { }; } } + diff --git a/src/health.controller.ts b/src/health.controller.ts new file mode 100644 index 0000000..1f3c66b --- /dev/null +++ b/src/health.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + checkHealth() { + return { status: 'OK' }; + } +} diff --git a/src/location/location.service.ts b/src/location/location.service.ts index 2e01d5d..afc359b 100644 --- a/src/location/location.service.ts +++ b/src/location/location.service.ts @@ -12,11 +12,18 @@ export class LocationService { ) {} async createLocation(data: CreateLocationDto): Promise { - const location = this.locationRepository.create(data); - await this.locationRepository.save(location); - return location; + const location = this.locationRepository.create({ + latitude: data.latitude, + longitude: data.longitude, + locationName: data.location_name, + }); + + return this.locationRepository.save(location); } + + + async findLocationByCoordinates( latitude: number, longitude: number, diff --git a/src/main.ts b/src/main.ts index 9479ae6..2d2ff34 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,14 +20,17 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('swagger', app, document); - // CORS 설정 추가 app.enableCors({ - origin: 'http://localhost:3000', + origin: ['https://here-there-fe.vercel.app', 'https://meetingsquare.site'], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', credentials: true, - allowedHeaders: 'Content-Type, Authorization', + allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'], // 추가로 필요한 헤더를 명시 + exposedHeaders: ['Set-Cookie'], // 클라이언트가 응답에서 읽을 수 있는 헤더 }); + + + // Socket.IO 어댑터 설정 app.useWebSocketAdapter(new IoAdapter(app)); app.useGlobalPipes(new ValidationPipe({ transform: true }));