diff --git a/src/answers/dtos/answer.response.dto.ts b/src/answers/dtos/answer.response.dto.ts index 7b87e54..c95c191 100644 --- a/src/answers/dtos/answer.response.dto.ts +++ b/src/answers/dtos/answer.response.dto.ts @@ -1,5 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsDate, IsOptional, IsString, IsUUID } from 'class-validator'; +import { + IsBoolean, + IsDate, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { Answer } from '../../entities/answer.entity'; export class AnswerDetailDto { diff --git a/src/app.module.ts b/src/app.module.ts index e05efd0..abce7d3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { LetterModule } from './letters/letter.module'; import { AppController } from './app.controller'; import { AnswerModule } from './answers/answer.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { MailsModule } from './mails/mails.module'; @Module({ imports: [ PrometheusModule.register(), @@ -37,6 +38,7 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus'; CommonModule, LetterModule, AnswerModule, + MailsModule, ], controllers: [AppController], providers: [], diff --git a/src/entities/letter.entity.ts b/src/entities/letter.entity.ts index 82ced11..c2c4b65 100644 --- a/src/entities/letter.entity.ts +++ b/src/entities/letter.entity.ts @@ -6,7 +6,7 @@ export class Letter extends BaseEntity { @Column({ name: 'sender_id', comment: '편지 쓰는 사용자 아이디', - nullable: false, + nullable: true, }) senderId: string; diff --git a/src/letters/dtos/letter.request.dto.ts b/src/letters/dtos/letter.request.dto.ts index 571ddd2..cecece4 100644 --- a/src/letters/dtos/letter.request.dto.ts +++ b/src/letters/dtos/letter.request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; import { Letter } from '../../entities/letter.entity'; export class CreateLetterDto { @@ -21,6 +21,11 @@ export class CreateLetterDto { @ApiProperty({ description: '고양이 이름', default: 'umu' }) @IsString() catName: string; + + @ApiProperty({ description: '답장하기 일 경우, 답장하는 메일의 아이디' }) + @IsOptional() + @IsUUID('all') + replyMailId: string; } export function toEntity( diff --git a/src/letters/letter.module.ts b/src/letters/letter.module.ts index 77e3991..3f0ee3d 100644 --- a/src/letters/letter.module.ts +++ b/src/letters/letter.module.ts @@ -2,8 +2,10 @@ import { Module } from '@nestjs/common'; import { LetterService } from './letter.service'; import { LetterController } from './letter.controller'; import { LetterRepository } from './letter.repository'; +import { MailsModule } from 'src/mails/mails.module'; @Module({ + imports: [MailsModule], providers: [LetterService, LetterRepository], controllers: [LetterController], }) diff --git a/src/letters/letter.service.ts b/src/letters/letter.service.ts index 0b0c12d..bfbed62 100644 --- a/src/letters/letter.service.ts +++ b/src/letters/letter.service.ts @@ -4,10 +4,14 @@ import { createResponse } from 'src/utils/response.utils'; import { LetterDetailDto } from './dtos/letter.response.dto'; import { Response } from 'src/common/interface'; import { CreateLetterDto, toEntity } from './dtos/letter.request.dto'; +import { MailsService } from 'src/mails/mails.service'; @Injectable() export class LetterService { - constructor(private readonly lettersRepository: LetterRepository) {} + constructor( + private readonly lettersRepository: LetterRepository, + private readonly mailsService: MailsService, + ) {} /** * 편지를 생성한다. @@ -25,6 +29,12 @@ export class LetterService { ): Promise> { const letter = toEntity(userId, createLetterDto); const newLetter = await this.lettersRepository.createLetter(letter); + if (createLetterDto.replyMailId) { + await this.mailsService.updateReplyMail({ + mailId: createLetterDto.replyMailId, + replyLetterId: newLetter.id, + }); + } return createResponse(new LetterDetailDto(newLetter)); } diff --git a/src/mails/dtos/mails.request.dto.ts b/src/mails/dtos/mails.request.dto.ts new file mode 100644 index 0000000..b227f42 --- /dev/null +++ b/src/mails/dtos/mails.request.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class SaveMailRequestDTO { + @ApiProperty({ description: '저장할 편지 아이디' }) + @IsUUID('all') + letterId: string; +} + +export class ReadMailRequestDTO { + @ApiProperty({ description: '읽음처리할 메일 아이디' }) + @IsUUID('all') + mailId: string; +} + +export class UpdateMailRequetDTO { + @ApiProperty({ description: '메일 아이디' }) + @IsUUID('all') + mailId: string; + + @ApiProperty({ description: '답장으로 저장할 편지 아이디' }) + @IsUUID('all') + replyLetterId: string; +} diff --git a/src/mails/dtos/mails.response.dto.ts b/src/mails/dtos/mails.response.dto.ts new file mode 100644 index 0000000..79532e1 --- /dev/null +++ b/src/mails/dtos/mails.response.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Mail } from '../entities/mail.entity'; +import { IsBoolean, IsDate, IsString, IsUUID } from 'class-validator'; + +export class LetterFromMailResponseDTO { + @ApiProperty() + @IsUUID('all') + id: string; + + @ApiProperty() + @IsDate() + createdAt: Date; + + @ApiProperty() + @IsDate() + updatedAt: Date; + + @ApiProperty() + @IsString() + senderId: string; + + @ApiProperty() + @IsString() + senderNickname: string; + + @ApiProperty() + @IsString() + receiverNickname: string; + + @ApiProperty() + @IsString() + content: string; + + @ApiProperty() + @IsString() + catName: string; + + @ApiProperty() + @IsBoolean() + isRespond: boolean; + + constructor(letter: Mail['letter'] & { replyLetterId: string | null }) { + this.id = letter.id; + this.createdAt = letter.createdAt; + this.updatedAt = letter.updatedAt; + this.senderId = letter.senderId; + this.senderNickname = letter.senderNickname; + this.receiverNickname = letter.receiverNickname; + this.content = letter.content; + this.catName = letter.catName; + this.isRespond = !!letter.replyLetterId; + } +} diff --git a/src/mails/entities/mail.entity.ts b/src/mails/entities/mail.entity.ts new file mode 100644 index 0000000..2bd9731 --- /dev/null +++ b/src/mails/entities/mail.entity.ts @@ -0,0 +1,51 @@ +import { BaseEntity } from 'src/common/entities/base.entity'; +import { Letter } from 'src/entities/letter.entity'; +import { User } from 'src/entities/user.entity'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; + +@Entity({ name: 'mails' }) +export class Mail extends BaseEntity { + @Column({ + type: 'uuid', + comment: '메일을 보유한 사용자 아이디', + name: 'user_id', + }) + userId: string; + + @ManyToOne(() => User, (user) => user.id, { + createForeignKeyConstraints: false, + }) + @JoinColumn({ name: 'user_id', referencedColumnName: 'id' }) + user: User; + + @Column({ type: 'uuid', comment: '메일의 편지 아이디', name: 'letter_id' }) + letterId: string; + + @ManyToOne(() => Letter, (Letter) => Letter.id, { + createForeignKeyConstraints: false, + }) + @JoinColumn({ name: 'letter_id', referencedColumnName: 'id' }) + letter: Letter; + + @Column({ + type: 'uuid', + comment: '답장하기를 통해 보낸 편지 아이디', + name: 'received_letter_id', + nullable: true, + }) + replyLetterId: string; + + @OneToOne(() => Letter, (Letter) => Letter.id, { + createForeignKeyConstraints: false, + nullable: true, + }) + @JoinColumn({ name: 'received_letter_id', referencedColumnName: 'id' }) + replyLetter: Letter; + + @Column({ + name: 'is_read', + comment: '편지 확인 여부', + default: false, + }) + isRead: boolean; +} diff --git a/src/mails/mails.controller.ts b/src/mails/mails.controller.ts new file mode 100644 index 0000000..7ad9704 --- /dev/null +++ b/src/mails/mails.controller.ts @@ -0,0 +1,60 @@ +import { AccessGuard } from 'src/auth/guards/acess.guard'; +import { MailsService } from './mails.service'; +import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'; +import { AuthUser } from 'src/auth/decorators/auth-user.decorator'; +import { + ApiBearerAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { LetterFromMailResponseDTO } from './dtos/mails.response.dto'; +import { + ReadMailRequestDTO, + SaveMailRequestDTO, +} from './dtos/mails.request.dto'; + +@ApiTags('Mails API') +@Controller('mails') +@ApiBearerAuth() +@UseGuards(AccessGuard) +export class MailsController { + constructor(private readonly mailsService: MailsService) {} + + @Get() + @ApiOkResponse({ + type: [LetterFromMailResponseDTO], + }) + async getMyMails(@AuthUser() { id }) { + return await this.mailsService.getMyMails(id); + } + + @ApiOperation({ + summary: '편지 보관하기', + description: + '편지 보관하기 기능 수행 시 호출. 내가 받은 편지(Mail)에 저장된다', + }) + @Put() + async saveMails( + @AuthUser() { id }, + @Body() saveMailRequestDTO: SaveMailRequestDTO, + ) { + return await this.mailsService.saveMails(id, saveMailRequestDTO); + } + + @ApiOperation({ + summary: '메일 읽기', + description: '우체국에 저장된 메일을 읽음처리 한다', + }) + @Post('read') + @ApiCreatedResponse({ + type: Boolean, + }) + async readMails( + @AuthUser() { id }, + @Body() readMailRequestDTO: ReadMailRequestDTO, + ) { + return await this.mailsService.readMail(id, readMailRequestDTO); + } +} diff --git a/src/mails/mails.module.ts b/src/mails/mails.module.ts new file mode 100644 index 0000000..ecf8ac8 --- /dev/null +++ b/src/mails/mails.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { MailsController } from './mails.controller'; +import { MailsService } from './mails.service'; + +@Module({ + controllers: [MailsController], + providers: [MailsService], + exports: [MailsService], +}) +export class MailsModule {} diff --git a/src/mails/mails.service.ts b/src/mails/mails.service.ts new file mode 100644 index 0000000..ed6abce --- /dev/null +++ b/src/mails/mails.service.ts @@ -0,0 +1,87 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { Mail } from './entities/mail.entity'; +import { LetterFromMailResponseDTO } from './dtos/mails.response.dto'; +import { + ReadMailRequestDTO, + SaveMailRequestDTO, + UpdateMailRequetDTO, +} from './dtos/mails.request.dto'; +import { Letter } from 'src/entities/letter.entity'; + +@Injectable() +export class MailsService { + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + async getMyMails(userId: string) { + const myMails = await this.dataSource.getRepository(Mail).find({ + where: { + userId, + }, + relations: { + letter: true, + }, + }); + return myMails.map( + (mail) => + new LetterFromMailResponseDTO({ + ...mail.letter, + replyLetterId: mail.replyLetterId, + }), + ); + } + + async saveMails(id: string, saveMailRequestDTO: SaveMailRequestDTO) { + const repository = this.dataSource.getRepository(Mail); + const newMails = repository.create({ + userId: id, + letterId: saveMailRequestDTO.letterId, + }); + + await repository.save(newMails); + + return newMails; + } + + async readMail(id: string, readMailRequestDTO: ReadMailRequestDTO) { + const repository = this.dataSource.getRepository(Mail); + const currentMail = await repository.findOne({ + where: { + userId: id, + id: readMailRequestDTO.mailId, + }, + }); + + if (!currentMail) { + throw new BadRequestException(); + } + + currentMail.isRead = true; + await repository.save(currentMail); + + return true; + } + + async updateReplyMail(updateMailRequetDTO: UpdateMailRequetDTO) { + const repository = this.dataSource.getRepository(Mail); + const currentMail = await repository.findOne({ + where: { + id: updateMailRequetDTO.mailId, + }, + }); + + const existLetter = await this.dataSource + .createQueryBuilder() + .from(Letter, 'lt') + .where('id = :letterId', { + letterId: updateMailRequetDTO.replyLetterId, + }); + if (existLetter) { + currentMail.replyLetterId = updateMailRequetDTO.replyLetterId; + await repository.save(currentMail); + return true; + } + return false; + } +}