diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 9078ba9..395ba0b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; // Add this line +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { HistoriesModule } from './histories/histories.module'; +import { BooksModule } from './books/books.module'; import { dbConfig } from './config'; @Module({ - imports: [TypeOrmModule.forRoot(dbConfig), HistoriesModule], + imports: [TypeOrmModule.forRoot(dbConfig), HistoriesModule, BooksModule], controllers: [AppController], providers: [AppService], }) diff --git a/backend/src/books/books.controller.ts b/backend/src/books/books.controller.ts new file mode 100644 index 0000000..4dc53dd --- /dev/null +++ b/backend/src/books/books.controller.ts @@ -0,0 +1,144 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UsePipes, + NotFoundException, +} from '@nestjs/common'; +import { + ApiOperation, + ApiResponse, + ApiTags, + ApiParam, + ApiBody, +} from '@nestjs/swagger'; +import { ZodValidationPipe } from '@anatine/zod-nestjs'; +import { BooksService } from './books.service'; +import { + BookDto, + BookGetResponseDto, + BookDetailResponseDto, + CreateBookCopyRequestDto, + CreateBookCopyResponseDto, + BookCopySearchResponseDto, + UpdateBookRequestDto, +} from './dto/books.dto'; +import { PaginationOptionsDto } from 'src/common/dtos/page-options.dto'; + +@ApiTags('books') +@Controller('books') +@UsePipes(ZodValidationPipe) +export class BooksController { + constructor(private readonly booksService: BooksService) {} + + @Get() + @ApiOperation({ summary: '도서 목록 조회' }) + @ApiResponse({ + status: 200, + description: '도서 목록 조회 성공', + type: BookGetResponseDto, + }) + async findAll( + @Query() paginationOption: PaginationOptionsDto, + ): Promise { + const [items, count] = await this.booksService.findAll(paginationOption); + const categories = Object.entries( + Object.groupBy( + items.map((item) => item.category), + (x) => x.name, + ), + ) + .map(([name, count]) => ({ name, count: count?.length ?? 0 })) + .filter((item) => item.count > 0); + + return { + items, + categories, + meta: { + itemCount: items.length, + currentPage: paginationOption.page, + itemsPerPage: paginationOption.take, + totalItems: count, + totalPages: Math.ceil(count / paginationOption.take), + }, + }; + } + + @Get(':id') + @ApiOperation({ summary: '도서 상세 정보 조회' }) + @ApiParam({ name: 'id', description: '도서 ID' }) + @ApiResponse({ + status: 200, + description: '도서 상세 정보 조회 성공', + type: BookDetailResponseDto, + }) + async findOne(@Param('id') id: number): Promise { + const book = await this.booksService.findOne(id); + if (!book) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + return book; + } + + @Post(':id/book-copies') + @ApiOperation({ summary: '도서 복본 생성' }) + @ApiParam({ name: 'id', description: '도서 ID' }) + @ApiBody({ type: CreateBookCopyRequestDto }) + @ApiResponse({ + status: 201, + description: '도서 복본 생성 성공', + type: CreateBookCopyResponseDto, + }) + async createCopy( + @Param('id') id: number, + @Body() createBookCopyDto: CreateBookCopyRequestDto, + ): Promise { + return this.booksService.createCopy(id, createBookCopyDto); + } + + @Get(':id/book-copies') + @ApiOperation({ summary: '도서 복본 목록 조회' }) + @ApiParam({ name: 'id', description: '도서 ID' }) + @ApiResponse({ + status: 200, + description: '도서 복본 목록 조회 성공', + type: BookCopySearchResponseDto, + }) + async findCopies( + @Param('id') id: number, + ): Promise { + return this.booksService.findCopies(id); + } + + @Put(':id') + @ApiOperation({ summary: '도서 정보 수정' }) + @ApiParam({ name: 'id', description: '도서 ID' }) + @ApiBody({ type: UpdateBookRequestDto }) + @ApiResponse({ + status: 200, + description: '도서 정보 수정 성공', + type: BookDto, + }) + async update( + @Param('id') id: number, + @Body() updateBookDto: UpdateBookRequestDto, + ): Promise { + return this.booksService.update(id, updateBookDto); + } + + @Delete(':id') + @ApiOperation({ summary: '도서 삭제' }) + @ApiParam({ name: 'id', description: '도서 ID' }) + @ApiResponse({ + status: 204, + description: '도서 삭제 성공', + }) + async remove(@Param('id') id: number): Promise { + await this.booksService.remove(id); + } +} diff --git a/backend/src/books/books.module.ts b/backend/src/books/books.module.ts new file mode 100644 index 0000000..d1328da --- /dev/null +++ b/backend/src/books/books.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BooksController } from './books.controller'; +import { BooksService } from './books.service'; +import { Book, BookCopy } from '../entities'; + +@Module({ + imports: [TypeOrmModule.forFeature([Book, BookCopy])], + controllers: [BooksController], + providers: [BooksService], + exports: [BooksService], +}) +export class BooksModule {} diff --git a/backend/src/books/books.service.ts b/backend/src/books/books.service.ts new file mode 100644 index 0000000..a18f68a --- /dev/null +++ b/backend/src/books/books.service.ts @@ -0,0 +1,142 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Book, BookCopy } from '../entities'; +import { PaginationOptionsDto } from '../common/dtos/page-options.dto'; +import { + CreateBookCopyRequestDto, + CreateBookCopyResponseDto, + BookCopySearchResponseDto, + UpdateBookRequestDto, + BookDetailResponseDto, + BookDto, +} from './dto/books.dto'; +import { BOOK_STATUS, getStatusString } from './constants'; + +@Injectable() +export class BooksService { + constructor( + @InjectRepository(Book) + private readonly bookRepository: Repository, + @InjectRepository(BookCopy) + private readonly bookCopyRepository: Repository, + ) {} + + async findAll(options: PaginationOptionsDto): Promise<[Book[], number]> { + return this.bookRepository.findAndCount({ + take: options.take, + skip: (options.page - 1) * options.take, + order: { createdAt: options.order }, + relations: ['category'], + }); + } + + private mapBookCopyToDto(copy: BookCopy) { + if (!copy.id) { + throw new Error('Book copy ID is required'); + } + return { + id: copy.id, + callSign: copy.callSign, + donator: copy.donator, + status: getStatusString(copy.status), + dueDate: copy.lendings?.[0]?.returnedAt || null, + isLendable: copy.status === BOOK_STATUS.OK, + isReserved: Boolean(copy.reservations?.length), + }; + } + + async findOne(id: number): Promise { + const book = await this.bookRepository.findOne({ + where: { id }, + relations: ['category', 'books', 'books.lendings', 'books.reservations'], + }); + + if (!book) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + + const bookCopies = (book.books || []).map((x) => this.mapBookCopyToDto(x)); + + return { + book, + bookCopies, + }; + } + + async createCopy( + id: number, + createBookCopyDto: CreateBookCopyRequestDto, + ): Promise { + const book = await this.bookRepository.findOne({ + where: { id }, + relations: ['category'], + }); + + if (!book) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + + const bookCopy = this.bookCopyRepository.create({ + info: book, + callSign: createBookCopyDto.callSign, + donator: createBookCopyDto.donator || null, + status: BOOK_STATUS.OK, + }); + + const savedCopy = await this.bookCopyRepository.save(bookCopy); + + return { + book, + bookCopy: this.mapBookCopyToDto(savedCopy), + }; + } + + async findCopies(id: number): Promise { + const book = await this.bookRepository.findOne({ + where: { id }, + relations: ['books', 'books.lendings', 'books.reservations'], + }); + + if (!book) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + + const bookCopies = (book.books || []).map(this.mapBookCopyToDto.bind(this)); + + return { + items: bookCopies, + meta: { + itemCount: bookCopies.length, + currentPage: 1, + itemsPerPage: bookCopies.length, + totalItems: bookCopies.length, + totalPages: 1, + }, + }; + } + + async update( + id: number, + updateBookDto: UpdateBookRequestDto, + ): Promise { + const book = await this.bookRepository.findOne({ + where: { id }, + relations: ['category'], + }); + + if (!book) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + + Object.assign(book, updateBookDto); + return this.bookRepository.save(book); + } + + async remove(id: number): Promise { + const result = await this.bookRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Book with ID ${id} not found`); + } + } +} diff --git a/backend/src/books/constants.ts b/backend/src/books/constants.ts new file mode 100644 index 0000000..2ca98ab --- /dev/null +++ b/backend/src/books/constants.ts @@ -0,0 +1,18 @@ +export const BOOK_STATUS = { + OK: 0, + LOST: 1, + DAMAGED: 2, + ASSIGNED: 3, +} as const; + +export type BookStatus = (typeof BOOK_STATUS)[keyof typeof BOOK_STATUS]; + +export function getStatusString(status: BookStatus): string { + const statusMap: Record = { + [BOOK_STATUS.OK]: 'AVAILABLE', + [BOOK_STATUS.ASSIGNED]: 'ASSIGNED', + [BOOK_STATUS.LOST]: 'LOST', + [BOOK_STATUS.DAMAGED]: 'DAMAGED', + }; + return statusMap[status] || 'UNKNOWN'; +} diff --git a/backend/src/books/dto/books.dto.ts b/backend/src/books/dto/books.dto.ts new file mode 100644 index 0000000..311c57f --- /dev/null +++ b/backend/src/books/dto/books.dto.ts @@ -0,0 +1,98 @@ +import { z } from 'zod'; +import { createZodDto } from '@anatine/zod-nestjs'; +import { extendApi } from '@anatine/zod-openapi'; +import { createPageSchema } from 'src/common/dtos/page.dto'; +import { metaSchema } from 'src/common/dtos/meta.dto'; +import { intSchema } from 'src/dto'; + +const imageSchema = extendApi(z.string().url().nullable(), { + description: '도서 표지 이미지 URL', +}); + +const isbnSchema = extendApi(z.string(), { + description: '도서 ISBN', + example: '9788065960874', +}); + +export const categoryCountSchema = z.object({ + name: extendApi(z.string(), { description: '카테고리 이름' }), + count: extendApi(intSchema, { description: '카테고리 내 도서 수' }), +}); + +const bookSchema = z.object({ + id: extendApi(intSchema, { description: '도서 ID' }), + title: extendApi(z.string(), { description: '도서 제목' }), + author: extendApi(z.string(), { description: '저자' }), + publisher: extendApi(z.string(), { description: '출판사' }), + isbn: isbnSchema, + image: imageSchema.nullable(), + publishedAt: extendApi(z.date(), { description: '출판일' }).nullable(), + createdAt: extendApi(z.date(), { description: '등록일' }), + updatedAt: extendApi(z.date(), { description: '수정일' }), +}); + +const categorySchema = z.object({ + id: extendApi(intSchema, { description: '카테고리 ID' }), + name: extendApi(z.string(), { description: '카테고리 이름' }), +}); + +const bookCopySchema = z.object({ + id: extendApi(intSchema, { description: '도서 복본 ID' }), + callSign: extendApi(z.string(), { description: '청구기호' }), + donator: extendApi(z.string().nullable(), { description: '기증자' }), + status: extendApi(z.string(), { description: '도서 상태' }), + dueDate: extendApi(z.date().nullable(), { description: '반납 예정일' }), + isLendable: extendApi(z.boolean(), { description: '대출 가능 여부' }), + isReserved: extendApi(z.boolean(), { description: '예약 여부' }), +}); + +const bookSearchResultSchema = z.object({ + book: bookSchema, + category: categorySchema, +}); + +const bookGetResponseSchema = createPageSchema(bookSchema).extend({ + categories: z.array(categoryCountSchema), +}); + +const bookDetailResponseSchema = z.object({ + book: bookSchema, + bookCopies: z.array(bookCopySchema), +}); + +const createBookCopyRequestSchema = z.object({ + isbn: isbnSchema, + callSign: extendApi(z.string(), { description: '청구기호' }), + donator: extendApi(z.string().optional(), { description: '기증자' }), +}); + +const createBookCopyResponseSchema = z.object({ + book: bookSchema, + bookCopy: bookCopySchema, +}); + +const bookCopySearchResponseSchema = createPageSchema(bookCopySchema); + +const updateBookRequestSchema = bookSchema.omit({ id: true }); + +export class CategoryCountDto extends createZodDto(categoryCountSchema) {} +export class BookDto extends createZodDto(bookSchema) {} +export class CategoryDto extends createZodDto(categorySchema) {} +export class BookCopyDto extends createZodDto(bookCopySchema) {} +export class BookSearchResultDto extends createZodDto(bookSearchResultSchema) {} +export class BookGetResponseDto extends createZodDto(bookGetResponseSchema) {} +export class BookDetailResponseDto extends createZodDto( + bookDetailResponseSchema, +) {} +export class CreateBookCopyRequestDto extends createZodDto( + createBookCopyRequestSchema, +) {} +export class CreateBookCopyResponseDto extends createZodDto( + createBookCopyResponseSchema, +) {} +export class BookCopySearchResponseDto extends createZodDto( + bookCopySearchResponseSchema, +) {} +export class UpdateBookRequestDto extends createZodDto( + updateBookRequestSchema, +) {} diff --git a/backend/src/categories/categories.service.ts b/backend/src/categories/categories.service.ts new file mode 100644 index 0000000..d3f485a --- /dev/null +++ b/backend/src/categories/categories.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Category } from '../entities/Category'; +import { Book } from '../entities/Book'; +import { categoryCountSchema } from 'src/books/dto/books.dto'; + +@Injectable() +export class CategoriesService { + constructor( + @InjectRepository(Category) + private readonly categoryRepository: Repository, + @InjectRepository(Book) + private readonly bookRepository: Repository, + ) {} + + async getCategoriesWithCount() { + const categoriesWithCount = await this.categoryRepository + .createQueryBuilder('category') + .select('category.name', 'name') + .addSelect('COUNT(book.id)', 'count') + .leftJoin('category.bookInfos', 'book') + .groupBy('category.id') + .getRawMany(); + + return categoriesWithCount.map((item) => categoryCountSchema.parse(item)); + } +} diff --git a/backend/src/common/dtos/meta.dto.ts b/backend/src/common/dtos/meta.dto.ts new file mode 100644 index 0000000..37ae598 --- /dev/null +++ b/backend/src/common/dtos/meta.dto.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import { createZodDto } from '@anatine/zod-nestjs'; +import { extendApi } from '@anatine/zod-openapi'; +import { intSchema } from 'src/dto'; + +export const metaSchema = z.object({ + totalItems: extendApi(intSchema, { + description: '전체 항목 수', + example: 100, + }), + itemCount: extendApi(intSchema, { + description: '현재 페이지의 항목 수', + example: 10, + }), + itemsPerPage: extendApi(intSchema, { + description: '페이지당 항목 수', + example: 10, + }), + totalPages: extendApi(intSchema, { + description: '전체 페이지 수', + example: 10, + }), + currentPage: extendApi(intSchema, { + description: '현재 페이지 번호', + example: 1, + }), +}); + +export class MetaDto extends createZodDto(metaSchema) {} diff --git a/backend/src/common/dtos/page-options.dto.ts b/backend/src/common/dtos/page-options.dto.ts new file mode 100644 index 0000000..bb915b1 --- /dev/null +++ b/backend/src/common/dtos/page-options.dto.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; +import { createZodDto } from '@anatine/zod-nestjs'; +import { extendApi } from '@anatine/zod-openapi'; +import { positiveSchema } from 'src/dto'; + +export enum Order { + ASC = 'ASC', + DESC = 'DESC', +} +export type PaginationOption = z.infer; +const paginationOptionsSchema = z.object({ + order: extendApi(z.nativeEnum(Order).optional().default(Order.ASC), { + description: '정렬 순서 (ASC: 오름차순, DESC: 내림차순)', + example: Order.ASC, + }), + page: extendApi(positiveSchema.optional().default(1), { + description: '페이지 번호', + example: 1, + }), + take: extendApi(positiveSchema.optional().default(10), { + description: '페이지당 항목 수', + example: 10, + }), +}); + +export class PaginationOptionsDto extends createZodDto( + paginationOptionsSchema, +) { + get skip(): number { + return (this.page - 1) * this.take; + } +} diff --git a/backend/src/common/dtos/page.dto.ts b/backend/src/common/dtos/page.dto.ts new file mode 100644 index 0000000..6532051 --- /dev/null +++ b/backend/src/common/dtos/page.dto.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { extendApi } from '@anatine/zod-openapi'; +import { MetaDto, metaSchema } from './meta.dto'; + +export const createPageSchema = (itemSchema: T) => + z.object({ + items: extendApi(z.array(itemSchema), { + description: '페이지 항목 배열', + }), + meta: extendApi(metaSchema, { + description: '페이지 메타데이터', + }), + }); + +export type PageType = { + items: T[]; + meta: MetaDto; +}; diff --git a/backend/src/dto.ts b/backend/src/dto.ts index 4843d98..a38dae1 100644 --- a/backend/src/dto.ts +++ b/backend/src/dto.ts @@ -1,7 +1,7 @@ import { extendApi } from '@anatine/zod-openapi'; import { z } from 'zod'; -export const intSchema = z.number().int(); +export const intSchema = z.coerce.number().int(); export const bookIdSchema = intSchema.describe('도서 ID'); export const userIdSchema = intSchema.describe('회원 ID'); export const positiveSchema = intSchema diff --git a/backend/src/entities/Book.ts b/backend/src/entities/Book.ts index 38a7a48..33947cc 100644 --- a/backend/src/entities/Book.ts +++ b/backend/src/entities/Book.ts @@ -5,63 +5,81 @@ import { JoinColumn, ManyToOne, OneToMany, + OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { BookInfo } from './BookInfo'; -import { User } from './User'; -import { Lending } from './Lending'; +import { BookCopy } from './BookCopy'; +import { Category } from './Category'; +import { Likes } from './Likes'; import { Reservation } from './Reservation'; +import { Reviews } from './Reviews'; +import { SuperTag } from './SuperTag'; +import { BookInfoSearchKeywords } from './BookInfoSearchKeywords'; -@Index('FK_donator_id_from_user', ['donatorId'], {}) -@Entity('book') +@Index('categoryId', ['categoryId'], {}) +@Entity('book_info') export class Book { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id: number; - @Column('varchar', { name: 'donator', nullable: true, length: 255 }) - donator: string | null; + @Column('varchar', { name: 'title', length: 255 }) + title: string; - @Column('varchar', { name: 'callSign', length: 255 }) - callSign: string; + @Column('varchar', { name: 'author', length: 255 }) + author: string; - @Column('int', { name: 'status' }) - status: number; + @Column('varchar', { name: 'publisher', length: 255 }) + publisher: string; + + @Column('varchar', { name: 'isbn', nullable: true, length: 255 }) + isbn: string; + + @Column('varchar', { name: 'image', nullable: true, length: 255 }) + image: string | null; + + @Column('date', { name: 'publishedAt', nullable: true }) + publishedAt: Date | null; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt?: Date; - - @Column('int') - infoId: number; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt?: Date; + updatedAt: Date; - @Column('int', { name: 'donatorId', nullable: true }) - donatorId: number | null; + @Column('int', { name: 'categoryId' }) + categoryId: number; - @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.books, { - onDelete: 'NO ACTION', - onUpdate: 'NO ACTION', - }) - @JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }]) - info?: BookInfo; + @OneToMany(() => BookCopy, (book) => book.info) + books?: BookCopy[]; - @ManyToOne(() => User, (user) => user.books, { + @ManyToOne(() => Category, (category) => category.bookInfos, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) - @JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }]) - donator2?: User; + @JoinColumn([{ name: 'categoryId', referencedColumnName: 'id' }]) + category: Category; - @OneToMany(() => Lending, (lending) => lending.book) - lendings?: Lending[]; + @OneToMany(() => Likes, (likes) => likes.bookInfo) + likes?: Likes[]; - @OneToMany(() => Reservation, (reservation) => reservation.book) + @OneToMany(() => Reservation, (reservation) => reservation.bookInfo) reservations?: Reservation[]; + + @OneToMany(() => Reviews, (reviews) => reviews.bookInfo) + reviews?: Reviews[]; + + @OneToMany(() => SuperTag, (superTags) => superTags.userId) + superTags?: SuperTag[]; + + @OneToOne( + () => BookInfoSearchKeywords, + (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo, + ) + bookInfoSearchKeyword?: BookInfoSearchKeywords; } diff --git a/backend/src/entities/BookCopy.ts b/backend/src/entities/BookCopy.ts new file mode 100644 index 0000000..7e021e5 --- /dev/null +++ b/backend/src/entities/BookCopy.ts @@ -0,0 +1,68 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Book } from './Book'; +import { User } from './User'; +import { Lending } from './Lending'; +import { Reservation } from './Reservation'; +import { BookStatus } from 'src/books/constants'; + +@Index('FK_donator_id_from_user', ['donatorId'], {}) +@Entity('book') +export class BookCopy { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id?: number; + + @Column('varchar', { name: 'donator', nullable: true, length: 255 }) + donator: string | null; + + @Column('varchar', { name: 'callSign', length: 255 }) + callSign: string; + + @Column('int', { name: 'status' }) + status: BookStatus; + + @Column('datetime', { + name: 'createdAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + createdAt?: Date; + + @Column('int') + infoId: number; + + @Column('datetime', { + name: 'updatedAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + updatedAt?: Date; + + @Column('int', { name: 'donatorId', nullable: true }) + donatorId: number | null; + + @ManyToOne(() => Book, (bookInfo) => bookInfo.books, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }]) + info?: Book; + + @ManyToOne(() => User, (user) => user.books, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }]) + donator2?: User; + + @OneToMany(() => Lending, (lending) => lending.book) + lendings?: Lending[]; + + @OneToMany(() => Reservation, (reservation) => reservation.book) + reservations?: Reservation[]; +} diff --git a/backend/src/entities/BookInfo.ts b/backend/src/entities/BookInfo.ts deleted file mode 100644 index c5e1a01..0000000 --- a/backend/src/entities/BookInfo.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - OneToMany, - OneToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { Book } from './Book'; -import { Category } from './Category'; -import { Likes } from './Likes'; -import { Reservation } from './Reservation'; -import { Reviews } from './Reviews'; -import { SuperTag } from './SuperTag'; -import { BookInfoSearchKeywords } from './BookInfoSearchKeywords'; - -@Index('categoryId', ['categoryId'], {}) -@Entity('book_info') -export class BookInfo { - @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; - - @Column('varchar', { name: 'title', length: 255 }) - title?: string; - - @Column('varchar', { name: 'author', length: 255 }) - author?: string; - - @Column('varchar', { name: 'publisher', length: 255 }) - publisher?: string; - - @Column('varchar', { name: 'isbn', nullable: true, length: 255 }) - isbn?: string | null; - - @Column('varchar', { name: 'image', nullable: true, length: 255 }) - image?: string | null; - - @Column('date', { name: 'publishedAt', nullable: true }) - publishedAt?: string | null; - - @Column('datetime', { - name: 'createdAt', - default: () => "'CURRENT_TIMESTAMP(6)'", - }) - createdAt?: Date; - - @Column('datetime', { - name: 'updatedAt', - default: () => "'CURRENT_TIMESTAMP(6)'", - }) - updatedAt?: Date; - - @Column('int', { name: 'categoryId' }) - categoryId?: number; - - @OneToMany(() => Book, (book) => book.info) - books?: Book[]; - - @ManyToOne(() => Category, (category) => category.bookInfos, { - onDelete: 'NO ACTION', - onUpdate: 'NO ACTION', - }) - @JoinColumn([{ name: 'categoryId', referencedColumnName: 'id' }]) - category?: Category; - - @OneToMany(() => Likes, (likes) => likes.bookInfo) - likes?: Likes[]; - - @OneToMany(() => Reservation, (reservation) => reservation.bookInfo) - reservations?: Reservation[]; - - @OneToMany(() => Reviews, (reviews) => reviews.bookInfo) - reviews?: Reviews[]; - - @OneToMany(() => SuperTag, (superTags) => superTags.userId) - superTags?: SuperTag[]; - - @OneToOne( - () => BookInfoSearchKeywords, - (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo, - ) - bookInfoSearchKeyword?: BookInfoSearchKeywords; -} diff --git a/backend/src/entities/BookInfoSearchKeywords.ts b/backend/src/entities/BookInfoSearchKeywords.ts index 44be542..794d098 100644 --- a/backend/src/entities/BookInfoSearchKeywords.ts +++ b/backend/src/entities/BookInfoSearchKeywords.ts @@ -6,7 +6,7 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; @Index('FK_bookInfoId', ['bookInfoId'], {}) @Entity('book_info_search_keywords') @@ -35,7 +35,7 @@ export class BookInfoSearchKeywords { @Column('int', { name: 'book_info_id' }) bookInfoId?: number; - @OneToOne(() => BookInfo, (bookInfo) => bookInfo.id) + @OneToOne(() => Book, (bookInfo) => bookInfo.id) @JoinColumn([{ name: 'book_info_id', referencedColumnName: 'id' }]) - bookInfo?: BookInfo; + bookInfo?: Book; } diff --git a/backend/src/entities/Category.ts b/backend/src/entities/Category.ts index 9ae73e9..a97e560 100644 --- a/backend/src/entities/Category.ts +++ b/backend/src/entities/Category.ts @@ -5,7 +5,7 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; @Index('id', ['id'], { unique: true }) @Index('name', ['name'], { unique: true }) @@ -17,6 +17,6 @@ export class Category { @Column('varchar', { name: 'name', unique: true, length: 255 }) name: string; - @OneToMany(() => BookInfo, (bookInfo) => bookInfo.category) - bookInfos: BookInfo[]; + @OneToMany(() => Book, (bookInfo) => bookInfo.category) + bookInfos: Book[]; } diff --git a/backend/src/entities/Lending.ts b/backend/src/entities/Lending.ts index 5f9c31c..62e5082 100644 --- a/backend/src/entities/Lending.ts +++ b/backend/src/entities/Lending.ts @@ -6,7 +6,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { Book } from './Book'; +import { BookCopy } from './BookCopy'; import { User } from './User'; @Index('FK_f2adde8c7d298210c39c500d966', ['lendingLibrarianId'], {}) @@ -47,12 +47,12 @@ export class Lending { }) updatedAt: Date; - @ManyToOne(() => Book, (book) => book.lendings, { + @ManyToOne(() => BookCopy, (book) => book.lendings, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: BookCopy; @Column({ name: 'bookId', type: 'int' }) bookId: number; diff --git a/backend/src/entities/Likes.ts b/backend/src/entities/Likes.ts index 70e17a2..8ed86ee 100644 --- a/backend/src/entities/Likes.ts +++ b/backend/src/entities/Likes.ts @@ -7,7 +7,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; import { User } from './User'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; @Index('FK_529dceb01ef681127fef04d755d4', ['userId'], {}) @Index('FK_bookInfo3', ['bookInfoId'], {}) @@ -32,10 +32,10 @@ export class Likes { @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) user: User; - @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.likes, { + @ManyToOne(() => Book, (bookInfo) => bookInfo.likes, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: Book; } diff --git a/backend/src/entities/Reservation.ts b/backend/src/entities/Reservation.ts index 0b16360..5dc84e8 100644 --- a/backend/src/entities/Reservation.ts +++ b/backend/src/entities/Reservation.ts @@ -7,8 +7,8 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; import { User } from './User'; -import { BookInfo } from './BookInfo'; import { Book } from './Book'; +import { BookCopy } from './BookCopy'; @Index('FK_bookInfo', ['bookInfoId'], {}) @Entity('reservation') @@ -47,19 +47,19 @@ export class Reservation { @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) user: User; - @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reservations, { + @ManyToOne(() => Book, (bookInfo) => bookInfo.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: Book; - @ManyToOne(() => Book, (book) => book.reservations, { + @ManyToOne(() => BookCopy, (book) => book.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: BookCopy; @Column('int', { name: 'bookId', nullable: true }) bookId: number | null; diff --git a/backend/src/entities/Reviews.ts b/backend/src/entities/Reviews.ts index 3ce738d..2edb913 100644 --- a/backend/src/entities/Reviews.ts +++ b/backend/src/entities/Reviews.ts @@ -7,7 +7,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; import { User } from './User'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; @Index('FK_529dceb01ef681127fef04d755d3', ['userId'], {}) @Index('FK_bookInfo2', ['bookInfoId'], {}) @@ -59,10 +59,10 @@ export class Reviews { @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) user: User; - @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reviews, { + @ManyToOne(() => Book, (bookInfo) => bookInfo.reviews, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: Book; } diff --git a/backend/src/entities/SuperTag.ts b/backend/src/entities/SuperTag.ts index 72a579e..1eefcad 100644 --- a/backend/src/entities/SuperTag.ts +++ b/backend/src/entities/SuperTag.ts @@ -9,7 +9,7 @@ import { } from 'typeorm'; import { SubTag } from './SubTag'; import { User } from './User'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; @Index('userId', ['userId'], {}) @Index('bookInfoId', ['bookInfoId'], {}) @@ -55,10 +55,10 @@ export class SuperTag { @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) user: User; - @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.superTags, { + @ManyToOne(() => Book, (bookInfo) => bookInfo.superTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: Book; } diff --git a/backend/src/entities/User.ts b/backend/src/entities/User.ts index d1c8659..42dfc57 100644 --- a/backend/src/entities/User.ts +++ b/backend/src/entities/User.ts @@ -5,7 +5,7 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { Book } from './Book'; +import { BookCopy } from './BookCopy'; import { Lending } from './Lending'; import { Likes } from './Likes'; import { Reservation } from './Reservation'; @@ -62,8 +62,8 @@ export class User { }) updatedAt: Date; - @OneToMany(() => Book, (book) => book.donator2) - books: Book[]; + @OneToMany(() => BookCopy, (book) => book.donator2) + books: BookCopy[]; @OneToMany(() => Lending, (lending) => lending.user) lendings: Lending[]; diff --git a/backend/src/entities/UserReservation.ts b/backend/src/entities/UserReservation.ts index 64e9905..3c9f919 100644 --- a/backend/src/entities/UserReservation.ts +++ b/backend/src/entities/UserReservation.ts @@ -1,5 +1,5 @@ import { ViewEntity, ViewColumn, DataSource } from 'typeorm'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; import { Reservation } from './Reservation'; @ViewEntity({ @@ -22,7 +22,7 @@ import { Reservation } from './Reservation'; .addSelect('bi.image', 'image') .addSelect('r.userId', 'userId') .from(Reservation, 'r') - .leftJoin(BookInfo, 'bi', 'r.bookInfoId = bi.id') + .leftJoin(Book, 'bi', 'r.bookInfoId = bi.id') .where('r.status = 0'), }) export class UserReservation { diff --git a/backend/src/entities/VSearchBook.ts b/backend/src/entities/VSearchBook.ts index f1d82dd..772d0f9 100644 --- a/backend/src/entities/VSearchBook.ts +++ b/backend/src/entities/VSearchBook.ts @@ -1,6 +1,6 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; -import { BookInfo } from './BookInfo'; import { Book } from './Book'; +import { BookCopy } from './BookCopy'; import { Category } from './Category'; @ViewEntity('v_search_book', { @@ -29,8 +29,8 @@ import { Category } from './Category'; ' ), TRUE, FALSE)', 'isLendable', ) - .from(Book, 'book') - .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') + .from(BookCopy, 'book') + .leftJoin(Book, 'book_info', 'book_info.id = book.infoId') .leftJoin(Category, 'category', 'book_info.categoryId = category.id'), }) export class VSearchBook { diff --git a/backend/src/entities/VSearchBookByTag.ts b/backend/src/entities/VSearchBookByTag.ts index 5d74c98..e19dc71 100644 --- a/backend/src/entities/VSearchBookByTag.ts +++ b/backend/src/entities/VSearchBookByTag.ts @@ -1,5 +1,5 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; import { Category } from './Category'; import { SubTag } from './SubTag'; import { SuperTag } from './SuperTag'; @@ -29,7 +29,7 @@ import { SuperTag } from './SuperTag'; .where('bi.id = bi.id'), 'lendingCnt', ) - .from(BookInfo, 'bi') + .from(Book, 'bi') .innerJoin(Category, 'c', 'c.id = bi.categoryId') .innerJoin(SuperTag, 'sp', 'sp.bookInfoId = bi.id') .leftJoin(SubTag, 'sb', 'sb.superTagId = sp.id'), diff --git a/backend/src/entities/VStock.ts b/backend/src/entities/VStock.ts index 4b81cd1..c1ea390 100644 --- a/backend/src/entities/VStock.ts +++ b/backend/src/entities/VStock.ts @@ -1,6 +1,6 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; -import { BookInfo } from './BookInfo'; import { Book } from './Book'; +import { BookCopy } from './BookCopy'; import { Category } from './Category'; import { Lending } from './Lending'; import { Reservation } from './Reservation'; @@ -22,8 +22,8 @@ import { Reservation } from './Reservation'; .addSelect("date_format(book.updatedAt, '%Y-%m-%d %T')", 'updatedAt') .addSelect('book_info.categoryId', 'categoryId') .addSelect('category.name', 'category') - .from(Book, 'book') - .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') + .from(BookCopy, 'book') + .leftJoin(Book, 'book_info', 'book_info.id = book.infoId') .leftJoin(Category, 'category', 'book_info.categoryId = category.id') .leftJoin(Lending, 'l', 'book.id = l.bookId') .leftJoin(Reservation, 'r', 'r.bookId = book.id AND r.status = 0') diff --git a/backend/src/entities/VTagsSubDefault.ts b/backend/src/entities/VTagsSubDefault.ts index 2dd66fa..ac3cc08 100644 --- a/backend/src/entities/VTagsSubDefault.ts +++ b/backend/src/entities/VTagsSubDefault.ts @@ -1,5 +1,5 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; -import { BookInfo } from './BookInfo'; +import { Book } from './Book'; import { SuperTag } from './SuperTag'; import { SubTag } from './SubTag'; import { User } from './User'; @@ -23,7 +23,7 @@ import { User } from './User'; ) .from(SuperTag, 'sp') .innerJoin(SubTag, 'sb', 'sb.superTagId = sp.id') - .innerJoin(BookInfo, 'bi', 'bi.id = sp.bookInfoId') + .innerJoin(Book, 'bi', 'bi.id = sp.bookInfoId') .innerJoin(User, 'u', 'u.id = sb.userId'), }) export class VTagsSubDefault { diff --git a/backend/src/entities/index.ts b/backend/src/entities/index.ts index e08aff7..18a42ee 100644 --- a/backend/src/entities/index.ts +++ b/backend/src/entities/index.ts @@ -1,5 +1,5 @@ +export * from './BookCopy'; export * from './Book'; -export * from './BookInfo'; export * from './BookInfoSearchKeywords'; export * from './Category'; export * from './Lending'; diff --git a/backend/src/histories/schema/histories.schema.ts b/backend/src/histories/schema/histories.schema.ts index a57e20d..ea4b2f6 100644 --- a/backend/src/histories/schema/histories.schema.ts +++ b/backend/src/histories/schema/histories.schema.ts @@ -1,4 +1,5 @@ import { createZodDto } from '@anatine/zod-nestjs'; +import { intSchema } from 'src/dto'; import { z } from 'zod'; export const getHistoriesRequestSchema = z.object({ @@ -20,14 +21,14 @@ export const getHistoriesResponseSchema = z .object({ items: z.array( z.object({ - id: z.number().int(), + id: intSchema, lendingCondition: z.string(), login: z.string(), returningCondition: z.string(), - penaltyDays: z.number().int(), + penaltyDays: intSchema, callSign: z.string(), title: z.string(), - bookInfoId: z.number().int(), + bookInfoId: intSchema, createdAt: z.string(), }), ), diff --git a/backend/src/reviews/schema/reviews.schema.ts b/backend/src/reviews/schema/reviews.schema.ts index e7f18cc..0290599 100644 --- a/backend/src/reviews/schema/reviews.schema.ts +++ b/backend/src/reviews/schema/reviews.schema.ts @@ -1,4 +1,5 @@ import { extendApi } from '@anatine/zod-openapi'; +import { intSchema } from 'src/dto'; import { z } from 'zod'; export const createReviewsRequestSchema = z.object({ @@ -71,7 +72,7 @@ export const getMyReviewsRequestSchema = z.object({ .describe( '한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10]', ), - page: z.number().int().optional().describe('해당하는 페이지를 보여준다.'), + page: intSchema.optional().describe('해당하는 페이지를 보여준다.'), sort: z.enum(['asc', 'desc']).optional(), isMyReview: z.boolean().default(false), });