From fa9559c8dbffa4bd5b5478b2901b55a91372b498 Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 7 Sep 2023 18:19:54 +0900 Subject: [PATCH 01/10] =?UTF-8?q?ci:=20=ED=83=80=EC=9E=85=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=95=B4=EC=86=8C=20(#643)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 참고: https://github.com/actions/runner/issues/659 --- .github/workflows/test.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 906c4daf..cdac6b0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,10 @@ name: Test on: pull_request: +defaults: + run: + shell: bash + jobs: test: name: Test PR @@ -10,6 +14,8 @@ jobs: environment: development steps: + - uses: reviewdog/action-setup@v1 + - name: Checkout uses: actions/checkout@v3 @@ -40,12 +46,7 @@ jobs: pnpm install --frozen-lockfile pnpm --filter='@jiphyeonjeon-42/contracts' build - - if: always() - name: check types (backend) - working-directory: backend - run: pnpm check - - - if: always() - name: check types (contracts) - working-directory: contracts - run: pnpm check + - name: check types + if: always() + run: | + pnpm -r --no-bail --parallel run check | sed -r 's|(.*)( check: )(.*)|\1/\3|' From 93fc7b423d21f3348140b6a73a38106f9d062cbd Mon Sep 17 00:00:00 2001 From: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> Date: Thu, 7 Sep 2023 23:48:34 +0900 Subject: [PATCH 02/10] feat: v2 book api (#746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: v1 api와 다른 부분 수정 * refactor: books API schema default sort value 추가 및 callSign 오타 수정 * refactor: status: zod.enum -> zod.nativeEnum 변경 z.enum으로 했을때 number 반환값에 대한 처리가 되지 않아 enumStatus 생성 후 nativeEnum으로 변경하였습니다. * feat: books//:id v2 구현 * feat: books/search v2 구현 * feat: book/update v2 구현 * feat: books/info/:id v2 구현 * refactor: BookNotFoundError import 오타 수정 * feat: books/info/sorted v2 구현 * feat: books/info/tag v2 구현 book_info.id 로 distinct가 되지 않는 오류가 있어요 * refactor: 의미에 맞는 변수명으로 수정 및 코드 간략화 Co-authored-by: scarf * refactor: book_info 중복 select 부분 처리 Co-authored-by: scarf * feat: books/donator v2 구현 v1 구현 시 얘기했었던 기부자가 유저가 아니더라도 수정될 수 있도록 수정 & email은 더이상 관리하지 않기 때문에 변수명에서 제거 * refactor: 대출 가능 여부 boolean 반환값으로 수정 kysely 사용중 select가 없을 경우 sql syntax 에러로 해당 부분 수정 및 boolean 값 반환되도록 수정 * feat: [get] books/create v2 구현 axios.get 동작 중 발생하는 에러(catch 영역)에 대한 처리를 어떻게 해야할지 모르겠어요 * fix: 임시로 타입 오류 무시 --------- Co-authored-by: scarf --- .../src/entity/entities/VSearchBookByTag.ts | 3 + backend/src/kysely/generated.ts | 2 +- backend/src/kysely/sqlDates.ts | 4 + backend/src/v2/books/errors.ts | 23 ++ backend/src/v2/books/mod.ts | 95 ++++++ backend/src/v2/books/repository.ts | 198 +++++++++++ backend/src/v2/books/service.ts | 313 ++++++++++++++++++ backend/src/v2/routes.ts | 2 + backend/src/v2/shared/responses.ts | 24 ++ contracts/src/books/index.ts | 71 ++-- contracts/src/books/schema.ts | 79 +++-- contracts/src/index.ts | 3 +- contracts/src/shared.ts | 5 +- 13 files changed, 755 insertions(+), 67 deletions(-) create mode 100644 backend/src/v2/books/errors.ts create mode 100644 backend/src/v2/books/mod.ts create mode 100644 backend/src/v2/books/repository.ts create mode 100644 backend/src/v2/books/service.ts diff --git a/backend/src/entity/entities/VSearchBookByTag.ts b/backend/src/entity/entities/VSearchBookByTag.ts index 69834260..f8643b0e 100644 --- a/backend/src/entity/entities/VSearchBookByTag.ts +++ b/backend/src/entity/entities/VSearchBookByTag.ts @@ -36,6 +36,9 @@ export class VSearchBookByTag { @ViewColumn() title: string; + @ViewColumn() + author: string; + @ViewColumn() isbn: number; diff --git a/backend/src/kysely/generated.ts b/backend/src/kysely/generated.ts index 047ccd5d..e8ac8de1 100644 --- a/backend/src/kysely/generated.ts +++ b/backend/src/kysely/generated.ts @@ -22,7 +22,7 @@ export interface BookInfo { publisher: string; isbn: Generated; image: Generated; - publishedAt: Generated; + publishedAt: Generated; createdAt: Generated; updatedAt: Generated; categoryId: number; diff --git a/backend/src/kysely/sqlDates.ts b/backend/src/kysely/sqlDates.ts index 5d0af7b7..6cc5d477 100644 --- a/backend/src/kysely/sqlDates.ts +++ b/backend/src/kysely/sqlDates.ts @@ -39,6 +39,10 @@ export function dateAddDays(expr: Expression, days: number) { return sql`DATE_ADD(${expr}, INTERVAL ${days} DAY)`; } +export function dateSubDays(expr: Expression, days: number) { + return sql`DATE_SUB(${expr}, INTERVAL ${days} DAY)`; +} + /** * {@link left} - {@link right}일 수를 반환합니다. * diff --git a/backend/src/v2/books/errors.ts b/backend/src/v2/books/errors.ts new file mode 100644 index 00000000..fc7e2422 --- /dev/null +++ b/backend/src/v2/books/errors.ts @@ -0,0 +1,23 @@ +export class PubdateFormatError extends Error { + declare readonly _tag: 'FormatError'; + + constructor(exp: string) { + super(`${exp}가 지정된 포맷과 일치하지 않습니다.`); + } + } + +export class IsbnNotFoundError extends Error { + declare readonly _tag: 'ISBN_NOT_FOUND'; + + constructor(exp: string) { + super(`국립중앙도서관 API에서 ISBN(${exp}) 검색이 실패하였습니다.`) + } +} + +export class NaverBookNotFound extends Error { + declare readonly _tag: 'NAVER_BOOK_NOT_FOUND'; + + constructor(exp: string) { + super(`네이버 책검색 API에서 ISBN(${exp}) 검색이 실패하였습니다.`) + } +} \ No newline at end of file diff --git a/backend/src/v2/books/mod.ts b/backend/src/v2/books/mod.ts new file mode 100644 index 00000000..88eed967 --- /dev/null +++ b/backend/src/v2/books/mod.ts @@ -0,0 +1,95 @@ +import { contract } from "@jiphyeonjeon-42/contracts"; +import { initServer } from "@ts-rest/express"; +import { searchAllBooks, searchBookById, searchBookInfoById, searchBookInfoForCreate, searchBookInfosByTag, searchBookInfosSorted, updateBookDonator, updateBookOrBookInfo } from "./service"; +import { BookInfoNotFoundError, BookNotFoundError, bookInfoNotFound, bookNotFound, isbnNotFound, naverBookNotFound, pubdateFormatError } from "../shared"; +import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from "./errors"; +import authValidate from "~/v1/auth/auth.validate"; +import { roleSet } from '~/v1/auth/auth.type'; + +const s = initServer(); +export const books = s.router(contract.books, { + // searchAllBookInfos: async ({ query }) => { + // const result = await searchAllBookInfos(query); + + // return { status: 200, body: result } as const; + // }, + // @ts-expect-error + searchBookInfosByTag: async ({ query }) => { + const result = await searchBookInfosByTag(query); + + return { status: 200, body: result } as const; + }, + // @ts-expect-error + searchBookInfosSorted: async ({ query }) => { + const result = await searchBookInfosSorted(query); + + return { status: 200, body: result } as const; + }, + // @ts-expect-error + searchBookInfoById: async ({ params: { id } }) => { + const result = await searchBookInfoById(id); + + if (result instanceof BookInfoNotFoundError) return bookInfoNotFound; + + return { status: 200, body: result } as const; + }, + // @ts-expect-error + searchAllBooks: async ({ query }) => { + const result = await searchAllBooks(query); + + return { status: 200, body: result } as const; + }, + searchBookInfoForCreate: { + // middleware: [authValidate(roleSet.librarian)], + // @ts-expect-error + handler: async ({ query: { isbnQuery } }) => { + const result = await searchBookInfoForCreate(isbnQuery); + + if (result instanceof IsbnNotFoundError) return isbnNotFound; + + if (result instanceof NaverBookNotFound) return naverBookNotFound; + + return { status: 200, body: result } as const; + }, + }, + // @ts-expect-error + searchBookById: async ({ params: { id } }) => { + const result = await searchBookById({ id }); + + if (result instanceof BookNotFoundError) { + return bookNotFound; + } + + return { + status: 200, + body: result, + } as const; + }, + // createBook: { + // middleware: [authValidate(roleSet.librarian)], + // handler: async ({ body }) => { + + // } + // }, + updateBook: { + // middleware: [authValidate(roleSet.librarian)], + // @ts-expect-error + handler: async ({ body }) => { + const result = await updateBookOrBookInfo(body); + + if (result instanceof PubdateFormatError) { + return pubdateFormatError; + } + return { status: 200, body: '책 정보가 수정되었습니다.' } as const; + }, + }, + updateDonator: { + // middleware: [authValidate(roleSet.librarian)], + // @ts-expect-error + handler: async ({ body }) => { + const result = await updateBookDonator(body); + + return { status: 200, body: '기부자 정보가 수정되었습니다.' } as const; + }, + }, +}); diff --git a/backend/src/v2/books/repository.ts b/backend/src/v2/books/repository.ts new file mode 100644 index 00000000..a2e723e0 --- /dev/null +++ b/backend/src/v2/books/repository.ts @@ -0,0 +1,198 @@ +import { db } from "~/kysely/mod.ts"; +import { sql } from "kysely"; + +import jipDataSource from "~/app-data-source"; +import { VSearchBook, Book, BookInfo, VSearchBookByTag, User } from "~/entity/entities"; +import { Like } from "typeorm"; +import { dateAddDays, dateFormat } from "~/kysely/sqlDates"; + +export const vSearchBookRepo = jipDataSource.getRepository(VSearchBook) +export const bookRepo = jipDataSource.getRepository(Book); +export const bookInfoRepo = jipDataSource.getRepository(BookInfo); +export const vSearchBookByTagRepo = jipDataSource.getRepository(VSearchBookByTag); +export const userRepo = jipDataSource.getRepository(User); + +export const getBookInfosByTag = async (whereQuery: object, sortQuery: object, page: number, limit: number) => { + return await vSearchBookByTagRepo.findAndCount({ + select: [ + 'id', + 'title', + 'author', + 'isbn', + 'image', + 'publishedAt', + 'createdAt', + 'updatedAt', + 'category', + 'superTagContent', + 'subTagContent', + 'lendingCnt' + ], + where: whereQuery, + take: limit, + skip: page * limit, + order: sortQuery + }); +} + +const bookInfoBy = () => + db + .selectFrom('book_info') + .select([ + 'book_info.id', + 'book_info.title', + 'book_info.author', + 'book_info.publisher', + 'book_info.isbn', + 'book_info.image', + 'book_info.publishedAt', + 'book_info.createdAt', + 'book_info.updatedAt' + ]) + +export const getBookInfosSorted = (limit: number) => + bookInfoBy() + .leftJoin('book', 'book_info.id', 'book.infoId') + .leftJoin('category', 'book_info.categoryId', 'category.id') + .leftJoin('lending', 'book.id', 'lending.bookId') + .select('category.name as category') + .select(({ eb }) => eb.fn.count('lending.id').as('lendingCnt')) + .limit(limit) + .groupBy('id') + +export const searchBookInfoSpecById = async ( id: number ) => + bookInfoBy() + .select(({ selectFrom }) => [ + selectFrom('category') + .select('name') + .whereRef('category.id', '=', 'book_info.categoryId') + .as('category'), + ]) + .where('id', '=', id) + .executeTakeFirst(); + +export const searchBooksByInfoId = async ( id: number ) => + db + .selectFrom('book') + .select([ + 'id', + 'callSign', + 'donator', + 'status' + ]) + .where('infoId', '=', id) + .execute(); + +export const getIsLendable = async (id: number) => +{ + const isLended = await db + .selectFrom('lending') + .where('bookId', '=', id) + .where('returnedAt', 'is', null) + .select('id') + .executeTakeFirst(); + + const book = await db + .selectFrom('book') + .where('id', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + const isReserved = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + return book !== undefined && isLended === undefined && isReserved !== undefined; +} + +export const getIsReserved = async (id: number) => +{ + const count = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select(({ eb }) => eb.fn.countAll().as('count')) + .executeTakeFirst(); + + if (Number(count?.count) > 0) + return true; + else + return false; +} + +export const getDuedate = async (id: number, interval = 14) => + db + .selectFrom('lending') + .where('bookId', '=', id) + .orderBy('createdAt', 'desc') + .limit(1) + .select(({ ref }) => { + const createdAt = ref('lending.createdAt'); + + return dateAddDays(createdAt, interval).as('dueDate'); + }) + .executeTakeFirst(); + +type SearchBookListArgs = { query: string; page: number; limit: number }; +export const searchBookListAndCount = async ({ + query, + page, + limit +}: SearchBookListArgs) => { + return await vSearchBookRepo.findAndCount({ + where: [ + { title: Like(`%${query}%`) }, + { author: Like(`%${query}%`) }, + { isbn: Like(`%${query}%`) }, + ], + take: limit, + skip: page * limit, + }); +} + +type UpdateBookArgs = { + id: number, + callSign?: string | undefined, + status?: number | undefined, +}; +export const updateBookById = async ({ + id, + callSign, + status, +}: UpdateBookArgs ) => { + await bookRepo.update(id, {callSign, status}); +} + +type UpdateBookInfoArgs = { + id: number, + title?: string | undefined, + author?: string | undefined, + publisher?: string | undefined, + publishedAt?: string | undefined, + image?: string | undefined, + categoryId?: number | undefined +}; +export const updateBookInfoById = async ({ + id, + title, + author, + publisher, + publishedAt, + image, + categoryId +}: UpdateBookInfoArgs ) => { + await bookInfoRepo.update(id, {title, author, publisher, publishedAt, image, categoryId}); +} + +type UpdateBookDonatorNameArgs = {bookId: number, donator: string, donatorId?: number | null}; +export const updateBookDonatorName = async ({ + bookId, + donator, + donatorId +}: UpdateBookDonatorNameArgs ) => { + await bookRepo.update(bookId, {donator, donatorId}); +} \ No newline at end of file diff --git a/backend/src/v2/books/service.ts b/backend/src/v2/books/service.ts new file mode 100644 index 00000000..01b12741 --- /dev/null +++ b/backend/src/v2/books/service.ts @@ -0,0 +1,313 @@ +import { match } from "ts-pattern"; +import { + searchBookListAndCount, + vSearchBookRepo, + updateBookById, + updateBookInfoById, + searchBookInfoSpecById, + searchBooksByInfoId, + getIsLendable, + getIsReserved, + getDuedate, + getBookInfosSorted, + getBookInfosByTag, + userRepo, + updateBookDonatorName} from "./repository"; +import { BookInfoNotFoundError, Meta, BookNotFoundError } from "../shared"; +import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from "./errors"; +import { dateNow, dateSubDays } from "~/kysely/sqlDates"; +import axios from "axios"; +import { nationalIsbnApiKey, naverBookApiOption } from '~/config'; + +type CategoryList = {name: string, count: number}; +type SearchBookInfosByTag = { query: string, sort: string, page: number, limit: number, category?: string | undefined }; +export const searchBookInfosByTag = async ({ + query, + sort, + page, + limit, + category +}: SearchBookInfosByTag ) => { + let sortQuery = {}; + switch(sort) + { + case 'title': + sortQuery = { title: 'ASC' }; + break; + case 'popular': + sortQuery = { lendingCnt: 'DESC' }; + break; + default: + sortQuery = { createdAt: 'DESC' }; + } + + let whereQuery: Array = [ + { superTagContent: query }, + { subTagContent: query } + ]; + + if (category) + { + whereQuery.push({ category }); + } + + const [bookInfoList, totalItems] = await getBookInfosByTag(whereQuery, sortQuery, page, limit); + let categoryList = new Array ; + bookInfoList.forEach((bookInfo) => { + const index = categoryList.findIndex((item) => bookInfo.category === item.name); + if (index === -1) + categoryList.push({name: bookInfo.category, count: 1}); + else + categoryList[index].count += 1; + }); + const meta = { + totalItems, + itemCount: bookInfoList.length, + itemsPerPage: limit, + totalPages: Math.ceil(bookInfoList.length / limit), + currentPage: page + 1 + } + + return { + items: bookInfoList, + categories: categoryList, + meta + }; +} + +type SearchBookInfosSortedArgs = { sort: string, limit: number }; +export const searchBookInfosSorted = async ({ + sort, + limit, +}: SearchBookInfosSortedArgs ) => { + let items; + if (sort === 'popular') + { + items = await getBookInfosSorted(limit) + .where('lending.createdAt', '>=', dateSubDays(dateNow(), 42)) + .orderBy('lendingCnt', 'desc') + .orderBy('title', 'asc') + .execute(); + } + else { + items = await getBookInfosSorted(limit) + .orderBy('createdAt', 'desc') + .orderBy('title', 'asc') + .execute(); + } + + return { items } as const +} + +export const searchBookInfoById = async (id: number) => { + let bookSpec = await searchBookInfoSpecById(id); + + if (bookSpec === undefined) + return new BookInfoNotFoundError(id); + + if (bookSpec.publishedAt) + { + const date = new Date(bookSpec.publishedAt); + bookSpec.publishedAt = `${date.getFullYear()}년 ${date.getMonth() + 1}월`; + } + + const eachbooks = await searchBooksByInfoId(id); + + const books = await Promise.all( + eachbooks.map(async (eachBook) => { + const isLendable = await getIsLendable(eachBook.id); + const isReserved = await getIsReserved(eachBook.id); + let dueDate; + + if (eachBook.status === 0 && isLendable === false) + { + dueDate = await getDuedate(eachBook.id); + dueDate = dueDate?.dueDate; + } + else + dueDate = '-'; + + return { + ...eachBook, + dueDate, + isLendable, + isReserved + } + }) + ); + + return { + ...bookSpec, + books + } +} + +type SearchAllBooksArgs = { query?: string | undefined, page: number, limit: number }; +export const searchAllBooks = async ({ + query, + page, + limit +}: SearchAllBooksArgs) => { + const [BookList, totalItems] = await searchBookListAndCount({query: query ? query : '', page, limit}); + + const meta: Meta = { + totalItems, + itemCount: BookList.length, + itemsPerPage: limit, + totalPages: Math.ceil(totalItems / limit), + currentPage: page + 1, + } + return {items: BookList, meta}; +} + +type BookInfoForCreate = { + title: string, + author?: string | undefined + isbn: string, + category: string, + publisher: string, + pubdate: string, + image: string, +} +const getInfoInNationalLibrary = async (isbn: string) => { + let bookInfo : BookInfoForCreate | undefined; + let searchResult; + + await axios + .get(`https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`) + .then((res) => { + searchResult = res.data.docs[0]; + const { + TITLE: title, SUBJECT: category, PUBLISHER: publisher, PUBLISH_PREDATE: pubdate, + } = searchResult; + const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice(-3)}/x${isbn}.jpg`; + bookInfo = { + title, image, category, isbn, publisher, pubdate, + }; + }) + .catch(() => { + console.log('Error'); + }) + return (bookInfo); +} + +const getAuthorInNaver = async (isbn: string) => { + let author; + + await axios + .get( + `https://openapi.naver.com/v1/search/book_adv?d_isbn=${isbn}`, + { + headers: { + 'X-Naver-Client-Id': `${naverBookApiOption.client}`, + 'X-Naver-Client-Secret': `${naverBookApiOption.secret}`, + }, + }, + ) + .then((res) => { + author = res.data.items[0].author; + }) + .catch(() => { + console.log('ERROR'); + }) + return (author); +} + +export const searchBookInfoForCreate = async (isbn: string) => { + let bookInfo = await getInfoInNationalLibrary(isbn); + if (bookInfo === undefined) + return new IsbnNotFoundError(isbn); + + bookInfo.author = await getAuthorInNaver(isbn); + if (bookInfo.author === undefined) + return new NaverBookNotFound(isbn); + + return {bookInfo}; +} + +type SearchBookByIdArgs = { id: number }; +export const searchBookById = async ({ + id, +}: SearchBookByIdArgs) => { + const book = await vSearchBookRepo.findOneBy({bookId: id}); + + return match(book) + .with(null, () => new BookNotFoundError(id)) + .otherwise(() => { + return { + id: book?.bookId, + ...book + }; + }); +} + +type UpdateBookArgs = { + bookId: number, + callSign?: string | undefined, + status?: number | undefined +}; +const updateBook = async (book: UpdateBookArgs) => { + return await updateBookById({ id: book.bookId, callSign: book.callSign, status: book.status }); +} + +type UpdateBookInfoArgs = { + bookInfoId: number, + title?: string | undefined, + author?: string | undefined, + publisher?: string | undefined, + publishedAt?: string | undefined, + image?: string | undefined, + categoryId?: number | undefined, +} +const pubdateFormatValidator = (pubdate: string) => { + const regexCondition = /^[0-9]{8}$/; + return regexCondition.test(pubdate) +} +const updateBookInfo = async (book: UpdateBookInfoArgs) => { + if (book.publishedAt && !pubdateFormatValidator(book.publishedAt)) + return new PubdateFormatError(book.publishedAt); + return await updateBookInfoById({ + id: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId + }); +} + +type UpdateBookOrBookInfoArgs = + Omit + & Omit + & { + bookId?: number | undefined, + bookInfoId?: number | undefined, +}; +export const updateBookOrBookInfo = async ( book: UpdateBookOrBookInfoArgs ) => { + if (book.bookId) + await updateBook({ + bookId: book.bookId, + callSign: book.callSign, + status: book.status + }); + if (book.bookInfoId) + return await updateBookInfo({ + bookInfoId: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId + }); +} + +type UpdateBookDonatorArgs = {bookId: number, nickname: string }; +export const updateBookDonator = async ({ + bookId, + nickname +}: UpdateBookDonatorArgs ) => { + const user = await userRepo.findOneBy({nickname}); + return await updateBookDonatorName({bookId, donator: nickname, donatorId: user ? user.id : null}); +} diff --git a/backend/src/v2/routes.ts b/backend/src/v2/routes.ts index 1bff3eb7..6be5946a 100644 --- a/backend/src/v2/routes.ts +++ b/backend/src/v2/routes.ts @@ -5,10 +5,12 @@ import { initServer } from '@ts-rest/express'; import { reviews } from './reviews/mod.ts'; import { histories } from './histories/mod.ts'; import { stock } from './stock/mod.ts'; +import { books } from './books/mod.ts'; const s = initServer(); export default s.router(contract, { reviews, histories, stock, + books }); diff --git a/backend/src/v2/shared/responses.ts b/backend/src/v2/shared/responses.ts index f915bb0f..a36b2544 100644 --- a/backend/src/v2/shared/responses.ts +++ b/backend/src/v2/shared/responses.ts @@ -32,3 +32,27 @@ export const bookNotFound = { description: '검색한 책이 존재하지 않습니다.', } as z.infer, } as const; + +export const pubdateFormatError = { + status: 311, + body: { + code: 'PUBDATE_FORMAT_ERROR', + description: '입력한 pubdate가 알맞은 형식이 아님.' + } +} as const; + +export const isbnNotFound = { + status: 303, + body: { + code: 'ISBN_NOT_FOUND', + description: '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.' + } +} as const; + +export const naverBookNotFound = { + status: 310, + body: { + code: 'NAVER_BOOK_NOT_FOUND', + description: '네이버 책검색 API에서 ISBN 검색이 실패' + } +} as const; \ No newline at end of file diff --git a/contracts/src/books/index.ts b/contracts/src/books/index.ts index 455572b9..a41e70d2 100644 --- a/contracts/src/books/index.ts +++ b/contracts/src/books/index.ts @@ -1,5 +1,5 @@ import { initContract } from "@ts-rest/core"; -import { +import { searchAllBooksQuerySchema, searchAllBooksResponseSchema, searchBookByIdResponseSchema, @@ -8,7 +8,7 @@ import { createBookBodySchema, createBookResponseSchema, categoryNotFoundSchema, - formatErrorSchema, + pubdateFormatErrorSchema, insertionFailureSchema, isbnNotFoundSchema, naverBookNotFoundSchema, @@ -16,14 +16,16 @@ import { updateBookResponseSchema, unknownPatchErrorSchema, nonDataErrorSchema, - searchBookInfosQuerySchema, + searchAllBookInfosQuerySchema, + searchBookInfosByTagQuerySchema, searchBookInfosResponseSchema, searchBookInfosSortedQuerySchema, searchBookInfosSortedResponseSchema, - searchBookInfoByIdQuerySchema, searchBookInfoByIdResponseSchema, updateDonatorBodySchema, - updateDonatorResponseSchema + updateDonatorResponseSchema, + searchBookInfoByIdPathSchema, + searchBookByIdParamSchema } from "./schema"; import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema, serverErrorSchema } from "../shared"; @@ -31,22 +33,22 @@ const c = initContract(); export const booksContract = c.router( { - searchAllBookInfos: { - method: 'GET', - path: '/info/search', - description: '책 정보(book_info)를 검색하여 가져온다.', - query: searchBookInfosQuerySchema, - responses: { - 200: searchBookInfosResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, + // searchAllBookInfos: { + // method: 'GET', + // path: '/info/search', + // description: '책 정보(book_info)를 검색하여 가져온다.', + // query: searchAllBookInfosQuerySchema, + // responses: { + // 200: searchBookInfosResponseSchema, + // 400: badRequestSchema, + // 500: serverErrorSchema, + // }, + // }, searchBookInfosByTag: { method: 'GET', path: '/info/tag', description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.', - query: searchBookInfosQuerySchema, + query: searchBookInfosByTagQuerySchema, responses: { 200: searchBookInfosResponseSchema, 400: badRequestSchema, @@ -67,8 +69,8 @@ export const booksContract = c.router( searchBookInfoById: { method: 'GET', path: '/info/:id', + pathParams: searchBookInfoByIdPathSchema, description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', - query: searchBookInfoByIdQuerySchema, responses: { 200: searchBookInfoByIdResponseSchema, 404: bookInfoNotFoundSchema, @@ -100,27 +102,28 @@ export const booksContract = c.router( }, searchBookById: { method: 'GET', - path: '/:bookId', + path: '/:id', description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + pathParams: searchBookByIdParamSchema, responses: { 200: searchBookByIdResponseSchema, 404: bookNotFoundSchema, 500: serverErrorSchema, } }, - createBook: { - method: 'POST', - path: '/create', - description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', - body: createBookBodySchema, - responses: { - 200: createBookResponseSchema, - 308: insertionFailureSchema, - 309: categoryNotFoundSchema, - 311: formatErrorSchema, - 500: serverErrorSchema, - }, - }, + // createBook: { + // method: 'POST', + // path: '/create', + // description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', + // body: createBookBodySchema, + // responses: { + // 200: createBookResponseSchema, + // 308: insertionFailureSchema, + // 309: categoryNotFoundSchema, + // 311: pubdateFormatErrorSchema, + // 500: serverErrorSchema, + // }, + // }, updateBook: { method: 'PATCH', path: '/update', @@ -130,7 +133,7 @@ export const booksContract = c.router( 204: updateBookResponseSchema, 312: unknownPatchErrorSchema, 313: nonDataErrorSchema, - 311: formatErrorSchema, + 311: pubdateFormatErrorSchema, 500: serverErrorSchema, }, }, @@ -147,4 +150,4 @@ export const booksContract = c.router( }, }, { pathPrefix: '/books' }, -) \ No newline at end of file +) diff --git a/contracts/src/books/schema.ts b/contracts/src/books/schema.ts index 056073e1..eb9d14b4 100644 --- a/contracts/src/books/schema.ts +++ b/contracts/src/books/schema.ts @@ -1,30 +1,36 @@ -import { metaSchema, positiveInt, mkErrorMessageSchema, statusSchema, metaPaginatedSchema } from "../shared"; +import { metaSchema, positiveInt, mkErrorMessageSchema, statusSchema, metaPaginatedSchema, dateLike } from "../shared"; import { z } from "../zodWithOpenapi"; export const commonQuerySchema = z.object({ query: z.string().optional(), - page: positiveInt.default(0), - limit: positiveInt.default(10), + page: positiveInt.default(0).openapi({ example: 0 }), + limit: positiveInt.default(10).openapi({ example: 10 }), }); -export const searchBookInfosQuerySchema = commonQuerySchema.extend({ - sort: z.string(), - category: z.string(), +export const searchAllBookInfosQuerySchema = commonQuerySchema.extend({ + sort: z.enum(["new", "popular", "title"]).default('new'), + category: z.string().optional(), +}); + +export const searchBookInfosByTagQuerySchema = commonQuerySchema.extend({ + query: z.string(), + sort: z.enum(["new", "popular", "title"]).default('new'), + category: z.string().optional(), }); export const searchBookInfosSortedQuerySchema = z.object({ - sort: z.string(), - limit: positiveInt.default(10), + sort: z.enum(["new", "popular"]), + limit: positiveInt.default(10).openapi({ example: 10 }), }); -export const searchBookInfoByIdQuerySchema = z.object({ +export const searchBookInfoByIdPathSchema = z.object({ id: positiveInt, }); export const searchAllBooksQuerySchema = commonQuerySchema; export const searchBookInfoCreateQuerySchema = z.object({ - isbnQuery: z.string(), + isbnQuery: z.string().openapi({ example: '9791191114225' }), }); export const createBookBodySchema = z.object({ @@ -38,17 +44,21 @@ export const createBookBodySchema = z.object({ donator: z.string(), }); +export const searchBookByIdParamSchema = z.object({ + id: positiveInt, +}); + export const updateBookBodySchema = z.object({ - bookInfoId: positiveInt, - categoryId: positiveInt, - title: z.string(), - author: z.string(), - publisher: z.string(), - publishedAt: z.string(), - image: z.string(), - bookId: positiveInt, - callSign: z.string(), - status: statusSchema, + bookInfoId: positiveInt.optional(), + title: z.string().optional(), + author: z.string().optional(), + publisher: z.string().optional(), + publishedAt: z.string().optional(), + image: z.string().optional(), + categoryId: positiveInt.optional(), + bookId: positiveInt.optional(), + callSign: z.string().optional(), + status: statusSchema.optional(), }); export const updateDonatorBodySchema = z.object({ @@ -65,12 +75,19 @@ export const bookInfoSchema = z.object({ image: z.string(), category: z.string(), publishedAt: z.string(), - createdAt: z.string(), - updatedAt: z.string(), - lendingCnt: positiveInt, + createdAt: dateLike, + updatedAt: dateLike, }); -export const searchBookInfosResponseSchema = metaPaginatedSchema(bookInfoSchema) +export const searchBookInfosResponseSchema = metaPaginatedSchema( + bookInfoSchema + .extend({ + publishedAt: dateLike, + }) + .omit({ + publisher: true + }) + ) .extend({ categories: z.array( z.object({ @@ -82,19 +99,21 @@ export const searchBookInfosResponseSchema = metaPaginatedSchema(bookInfoSchema) export const searchBookInfosSortedResponseSchema = z.object({ items: z.array( - bookInfoSchema, + bookInfoSchema.extend({ + publishedAt: dateLike, + lendingCnt: positiveInt, + }), ) }); -export const searchBookInfoByIdResponseSchema = z.object({ - bookInfoSchema, +export const searchBookInfoByIdResponseSchema = bookInfoSchema.extend({ books: z.array( z.object({ id: positiveInt, callSign: z.string(), donator: z.string(), status: statusSchema, - dueDate: z.string(), + dueDate: dateLike, isLendable: positiveInt, isReserved: positiveInt, }), @@ -146,7 +165,7 @@ export const searchBookByIdResponseSchema = z.object({ image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg' }), status: statusSchema.openapi({ example: 0 }), categoryId: positiveInt.openapi({ example: 2}), - callsign: z.string().openapi({ example: 'C5.13.v1.c2' }), + callSign: z.string().openapi({ example: 'C5.13.v1.c2' }), category: z.string().openapi({ example: '네트워크' }), isLendable: positiveInt.openapi({ example: 1 }), }); @@ -167,7 +186,7 @@ export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').des export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe('보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음'); -export const formatErrorSchema = mkErrorMessageSchema('FORMAT_ERROR').describe('입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"'); +export const pubdateFormatErrorSchema = mkErrorMessageSchema('PUBDATE_FORMAT_ERROR').describe('입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"'); export const unknownPatchErrorSchema = mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.'); diff --git a/contracts/src/index.ts b/contracts/src/index.ts index 88dc7add..57546710 100644 --- a/contracts/src/index.ts +++ b/contracts/src/index.ts @@ -5,6 +5,7 @@ import { usersContract } from './users'; import { likesContract } from './likes'; import { stockContract } from './stock'; import { tagContract } from './tags'; +import { booksContract } from './books'; export * from './reviews'; export * from './shared'; @@ -17,7 +18,7 @@ export const contract = c.router( // likes: likesContract, reviews: reviewsContract, histories: historiesContract, - + books: booksContract, stock: stockContract, // TODO(@nyj001012): 태그 서비스 작성 // tags: tagContract, diff --git a/contracts/src/shared.ts b/contracts/src/shared.ts index 47e08dc9..2f188541 100644 --- a/contracts/src/shared.ts +++ b/contracts/src/shared.ts @@ -6,7 +6,10 @@ export const dateLike = z.union([z.date(), z.string()]).transform(String) export const bookInfoIdSchema = positiveInt.describe('개별 도서 ID'); -export const statusSchema = z.enum(["ok", "lost", "damaged"]); +export enum enumStatus { + "ok", "lost", "damaged", "designate" +} +export const statusSchema = z.nativeEnum(enumStatus); /** From 8b7c07f9ad6ce7a3e2fc7a3ec228a7e500deeeee Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 7 Sep 2023 23:53:02 +0900 Subject: [PATCH 03/10] =?UTF-8?q?style:=20prettier=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#761)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: prettier 설정 * style: prettier 적용 --------- Co-authored-by: nocontribute <> --- .eslintrc.json | 2 +- .prettierrc.yml | 7 + backend/src/config/JwtOption.ts | 24 +- backend/src/config/config.type.ts | 10 +- backend/src/config/dbSchema.ts | 18 +- backend/src/config/envObject.ts | 2 +- backend/src/config/getConnectOption.ts | 10 +- backend/src/config/logOption.ts | 4 +- backend/src/config/naverBookApiOption.ts | 12 +- backend/src/config/oauthOption.ts | 24 +- backend/src/entity/entities/Book.ts | 33 +- backend/src/entity/entities/BookInfo.ts | 39 +- .../entity/entities/BookInfoSearchKeywords.ts | 22 +- backend/src/entity/entities/Category.ts | 14 +- backend/src/entity/entities/Lending.ts | 42 +- backend/src/entity/entities/Likes.ts | 26 +- backend/src/entity/entities/Reservation.ts | 35 +- backend/src/entity/entities/Reviews.ts | 40 +- backend/src/entity/entities/SearchKeywords.ts | 14 +- backend/src/entity/entities/SearchLogs.ts | 17 +- backend/src/entity/entities/SubTag.ts | 33 +- backend/src/entity/entities/SuperTag.ts | 22 +- backend/src/entity/entities/User.ts | 46 +- .../src/entity/entities/UserReservation.ts | 52 +- backend/src/entity/entities/VHistories.ts | 83 +- backend/src/entity/entities/VLending.ts | 59 +- .../entity/entities/VLendingForSearchUser.ts | 60 +- backend/src/entity/entities/VSearchBook.ts | 83 +- .../src/entity/entities/VSearchBookByTag.ts | 75 +- backend/src/entity/entities/VStock.ts | 77 +- .../src/entity/entities/VTagsSubDefault.ts | 57 +- .../src/entity/entities/VTagsSuperDefault.ts | 8 +- backend/src/entity/entities/VUserLending.ts | 49 +- backend/src/kysely/generated.ts | 2 +- backend/src/kysely/mod.ts | 2 +- backend/src/kysely/paginated.ts | 4 +- backend/src/kysely/shared.ts | 13 +- backend/src/kysely/sqlDates.ts | 20 +- backend/src/logger.ts | 29 +- backend/src/mysql.ts | 72 +- backend/src/v1/DTO/common.interface.ts | 32 +- backend/src/v1/DTO/cursus.model.ts | 22 +- backend/src/v1/DTO/tags.model.ts | 4 +- backend/src/v1/DTO/users.model.ts | 48 +- backend/src/v1/auth/auth.controller.ts | 57 +- backend/src/v1/auth/auth.jwt.ts | 2 +- backend/src/v1/auth/auth.service.ts | 25 +- backend/src/v1/auth/auth.strategy.ts | 4 +- backend/src/v1/auth/auth.type.ts | 8 +- backend/src/v1/auth/auth.validate.ts | 114 +- .../v1/auth/auth.validateDefaultNullUser.ts | 94 +- .../controller/bookInfoReviews.controller.ts | 12 +- .../controller/utils/errorCheck.ts | 8 +- .../controller/utils/parseCheck.ts | 12 +- .../repository/bookInfoReviews.repository.ts | 31 +- .../service/bookInfoReviews.service.ts | 24 +- backend/src/v1/books/Likes.repository.ts | 4 +- backend/src/v1/books/books.controller.ts | 175 ++- backend/src/v1/books/books.model.ts | 54 +- backend/src/v1/books/books.repository.ts | 57 +- backend/src/v1/books/books.service.spec.ts | 5 +- backend/src/v1/books/books.service.ts | 73 +- backend/src/v1/books/books.type.ts | 132 +- backend/src/v1/books/likes.service.ts | 34 +- backend/src/v1/cursus/cursus.controller.ts | 21 +- backend/src/v1/cursus/cursus.service.ts | 141 +- .../src/v1/histories/histories.controller.ts | 14 +- .../src/v1/histories/histories.repository.ts | 7 +- backend/src/v1/histories/histories.service.ts | 5 +- .../src/v1/lendings/lendings.controller.ts | 36 +- .../src/v1/lendings/lendings.repository.ts | 66 +- .../src/v1/lendings/lendings.service.spec.ts | 55 +- backend/src/v1/lendings/lendings.service.ts | 89 +- .../src/v1/middlewares/wrapAsyncController.ts | 5 +- .../v1/notifications/notifications.service.ts | 93 +- .../reservations/reservations.controller.ts | 33 +- .../reservations/reservations.repository.ts | 104 +- .../reservations/reservations.service.spec.ts | 6 +- .../v1/reservations/reservations.service.ts | 76 +- .../src/v1/reservations/reservations.type.ts | 32 +- .../reviews/controller/reviews.controller.ts | 28 +- .../src/v1/reviews/controller/reviews.type.ts | 28 +- .../v1/reviews/controller/utils/errorCheck.ts | 23 +- .../v1/reviews/controller/utils/parseCheck.ts | 25 +- .../reviews/repository/reviews.repository.ts | 46 +- .../src/v1/reviews/service/reviews.service.ts | 25 +- .../v1/reviews/service/utils/errorCheck.ts | 15 +- backend/src/v1/routes/auth.routes.ts | 29 +- .../src/v1/routes/bookInfoReviews.routes.ts | 214 ++- backend/src/v1/routes/books.routes.ts | 194 +-- backend/src/v1/routes/cursus.routes.ts | 204 +-- backend/src/v1/routes/histories.routes.ts | 4 +- backend/src/v1/routes/lendings.routes.ts | 608 +++++---- backend/src/v1/routes/reservations.routes.ts | 8 +- backend/src/v1/routes/reviews.routes.ts | 1188 +++++++++-------- .../src/v1/routes/searchKeywords.routes.ts | 5 +- backend/src/v1/routes/stock.routes.ts | 260 ++-- backend/src/v1/routes/tags.routes.ts | 470 +++---- backend/src/v1/routes/users.routes.ts | 11 +- .../booksInfoSearchKeywords.repository.ts | 13 +- .../searchKeywords.controller.ts | 15 +- .../search-keywords/searchKeywords.service.ts | 35 +- backend/src/v1/slack/slack.controller.ts | 2 +- backend/src/v1/slack/slack.service.ts | 35 +- backend/src/v1/stocks/stocks.controller.ts | 4 +- backend/src/v1/stocks/stocks.repository.ts | 38 +- backend/src/v1/stocks/stocks.service.ts | 13 +- backend/src/v1/swagger/swagger.ts | 2 +- backend/src/v1/tags/tags.controller.ts | 103 +- backend/src/v1/tags/tags.repository.ts | 102 +- backend/src/v1/tags/tags.service.ts | 55 +- backend/src/v1/users/users.controller.spec.ts | 13 +- backend/src/v1/users/users.controller.ts | 90 +- backend/src/v1/users/users.repository.ts | 74 +- backend/src/v1/users/users.service.spec.ts | 59 +- backend/src/v1/users/users.service.ts | 34 +- backend/src/v1/users/users.types.ts | 4 +- backend/src/v1/users/users.utils.ts | 2 +- backend/src/v1/utils/dateFormat.ts | 4 +- backend/src/v1/utils/error/errorCode.ts | 3 +- backend/src/v1/utils/error/errorHandler.ts | 5 +- backend/src/v1/utils/isNullish.ts | 2 +- backend/src/v1/utils/parseCheck.ts | 30 +- backend/src/v1/utils/types.ts | 4 +- backend/src/v2/books/errors.ts | 28 +- backend/src/v2/books/mod.ts | 29 +- backend/src/v2/books/repository.ts | 308 ++--- backend/src/v2/books/service.ts | 528 ++++---- backend/src/v2/histories/repository.ts | 13 +- backend/src/v2/reviews/mod.ts | 11 +- backend/src/v2/reviews/repository.ts | 24 +- backend/src/v2/reviews/service.ts | 34 +- backend/src/v2/routes.ts | 2 +- backend/src/v2/shared/responses.ts | 21 +- backend/src/v2/stock/mod.ts | 2 +- backend/src/v2/stock/repository.ts | 6 +- contracts/src/books/index.ts | 302 +++-- contracts/src/books/schema.ts | 285 ++-- contracts/src/index.ts | 2 +- contracts/src/reviews/index.ts | 13 +- contracts/src/reviews/schema.ts | 12 +- contracts/src/shared.ts | 24 +- contracts/src/stock/index.ts | 56 +- contracts/src/stock/schema.ts | 44 +- contracts/src/tags/index.ts | 18 +- contracts/src/tags/schema.ts | 30 +- contracts/src/users/schema.ts | 93 +- package.json | 8 +- pnpm-lock.yaml | 12 + 149 files changed, 4526 insertions(+), 4560 deletions(-) create mode 100644 .prettierrc.yml diff --git a/.eslintrc.json b/.eslintrc.json index e83e38ff..e15ccc99 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@ "jest": true }, "root": true, - "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended"], + "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended", "prettier"], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "import"], "parserOptions": { diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..6589e8e5 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +semi: true +singleQuote: true +useTabs: false +tabWidth: 2 +trailingComma: all +printWidth: 100 +arrowParens: always diff --git a/backend/src/config/JwtOption.ts b/backend/src/config/JwtOption.ts index ffd0a186..0bd71179 100644 --- a/backend/src/config/JwtOption.ts +++ b/backend/src/config/JwtOption.ts @@ -5,19 +5,21 @@ import { Mode } from './modeOption'; import { match } from 'ts-pattern'; type getJwtOption = (mode: Mode) => (option: OauthUrlOption) => JwtOption; -export const getJwtOption: getJwtOption = (mode) => ({ redirectURL, clientURL }) => { - const redirectDomain = new URL(redirectURL).hostname; - const clientDomain = new URL(clientURL).hostname; - const secure = mode === 'prod' || mode === 'https'; +export const getJwtOption: getJwtOption = + (mode) => + ({ redirectURL, clientURL }) => { + const redirectDomain = new URL(redirectURL).hostname; + const clientDomain = new URL(clientURL).hostname; + const secure = mode === 'prod' || mode === 'https'; - const issuer = secure ? redirectDomain : 'localhost'; - const domain = match(mode) - .with('prod', () => clientDomain) - .with('https', () => undefined) - .otherwise(() => 'localhost'); + const issuer = secure ? redirectDomain : 'localhost'; + const domain = match(mode) + .with('prod', () => clientDomain) + .with('https', () => undefined) + .otherwise(() => 'localhost'); - return { issuer, domain, secure }; -}; + return { issuer, domain, secure }; + }; export const jwtSecretSchema = z.object({ JWT_SECRET: nonempty }).transform((v) => v.JWT_SECRET); diff --git a/backend/src/config/config.type.ts b/backend/src/config/config.type.ts index 69d1a86e..11bb943d 100644 --- a/backend/src/config/config.type.ts +++ b/backend/src/config/config.type.ts @@ -19,7 +19,7 @@ export type NaverBookApiOption = { /** 네이버 도서 검색 API 시크릿 */ secret: string; -} +}; /** DB 연결 옵션 */ export type ConnectOption = { @@ -34,7 +34,7 @@ export type ConnectOption = { /** DB 이름 */ database: string; -} +}; /** OAuth URL 옵션 */ export type OauthUrlOption = { @@ -43,7 +43,7 @@ export type OauthUrlOption = { /** 집현전 프론트엔드 URL */ clientURL: string; -} +}; /** 42 API OAuth 클라이언트 인증 정보 */ export type Oauth42ApiOption = { @@ -52,7 +52,7 @@ export type Oauth42ApiOption = { /** 42 API OAuth 클라이언트 시크릿 */ secret: string; -} +}; /** npm 로깅 레벨 */ export type LogLevel = keyof typeof levels; @@ -64,4 +64,4 @@ export type LogLevelOption = { /** 콘솔 로깅 레벨 */ readonly consoleLogLevel: 'error' | 'debug'; -} +}; diff --git a/backend/src/config/dbSchema.ts b/backend/src/config/dbSchema.ts index 7c072271..cf5f7073 100644 --- a/backend/src/config/dbSchema.ts +++ b/backend/src/config/dbSchema.ts @@ -1,13 +1,17 @@ import { envObject, nonempty } from './envObject'; /** RDS 연결 옵션 파싱을 위한 스키마 */ -export const rdsSchema = envObject('RDS_HOSTNAME', 'RDS_USERNAME', 'RDS_PASSWORD', 'RDS_DB_NAME') - .transform((v) => ({ - host: v.RDS_HOSTNAME, - username: v.RDS_USERNAME, - password: v.RDS_PASSWORD, - database: v.RDS_DB_NAME, - })); +export const rdsSchema = envObject( + 'RDS_HOSTNAME', + 'RDS_USERNAME', + 'RDS_PASSWORD', + 'RDS_DB_NAME', +).transform((v) => ({ + host: v.RDS_HOSTNAME, + username: v.RDS_USERNAME, + password: v.RDS_PASSWORD, + database: v.RDS_DB_NAME, +})); /** MYSQL 연결 옵션 파싱을 위한 스키마 */ const mysqlSchema = envObject('MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DATABASE') diff --git a/backend/src/config/envObject.ts b/backend/src/config/envObject.ts index ace38068..b690733b 100644 --- a/backend/src/config/envObject.ts +++ b/backend/src/config/envObject.ts @@ -15,7 +15,7 @@ export const url = z.string().trim().url(); * @param keys 환경변수 키 목록 */ export const envObject = (...keys: T) => { - type Keys = T[ number ]; + type Keys = T[number]; const env = Object.fromEntries(keys.map((key) => [key, nonempty])); return z.object(env as Record); diff --git a/backend/src/config/getConnectOption.ts b/backend/src/config/getConnectOption.ts index 80eee87e..a08e857e 100644 --- a/backend/src/config/getConnectOption.ts +++ b/backend/src/config/getConnectOption.ts @@ -13,8 +13,10 @@ const getConnectOptionSchema = (mode: Mode) => { /** * 환경변수에서 DB 연결 옵션을 파싱하는 함수 */ -export const getConnectOption = (mode: Mode) => (processEnv: NodeJS.ProcessEnv): ConnectOption => { - const connectOptionSchema = getConnectOptionSchema(mode); +export const getConnectOption = + (mode: Mode) => + (processEnv: NodeJS.ProcessEnv): ConnectOption => { + const connectOptionSchema = getConnectOptionSchema(mode); - return connectOptionSchema.parse(processEnv); -}; + return connectOptionSchema.parse(processEnv); + }; diff --git a/backend/src/config/logOption.ts b/backend/src/config/logOption.ts index 1477cdbf..e30af6d2 100644 --- a/backend/src/config/logOption.ts +++ b/backend/src/config/logOption.ts @@ -18,8 +18,8 @@ export const colors: Record = { } as const; export const getLogLevelOption = (mode: RuntimeMode): LogLevelOption => { - const logLevel = (mode === 'production' ? 'http' : 'debug'); - const consoleLogLevel = (mode === 'production' ? 'error' : 'debug'); + const logLevel = mode === 'production' ? 'http' : 'debug'; + const consoleLogLevel = mode === 'production' ? 'error' : 'debug'; return { logLevel, consoleLogLevel } as const; }; diff --git a/backend/src/config/naverBookApiOption.ts b/backend/src/config/naverBookApiOption.ts index 73b3f7eb..282d4671 100644 --- a/backend/src/config/naverBookApiOption.ts +++ b/backend/src/config/naverBookApiOption.ts @@ -2,11 +2,13 @@ import { NaverBookApiOption } from './config.type'; import { envObject } from './envObject'; -const naverBookApiSchema = envObject('NAVER_BOOK_SEARCH_CLIENT_ID', 'NAVER_BOOK_SEARCH_SECRET') - .transform((v) => ({ - client: v.NAVER_BOOK_SEARCH_CLIENT_ID, - secret: v.NAVER_BOOK_SEARCH_SECRET, - })); +const naverBookApiSchema = envObject( + 'NAVER_BOOK_SEARCH_CLIENT_ID', + 'NAVER_BOOK_SEARCH_SECRET', +).transform((v) => ({ + client: v.NAVER_BOOK_SEARCH_CLIENT_ID, + secret: v.NAVER_BOOK_SEARCH_SECRET, +})); export const getNaverBookApiOption = (processEnv: NodeJS.ProcessEnv): NaverBookApiOption => { const option = naverBookApiSchema.parse(processEnv); diff --git a/backend/src/config/oauthOption.ts b/backend/src/config/oauthOption.ts index 30b9d0c5..fa811c58 100644 --- a/backend/src/config/oauthOption.ts +++ b/backend/src/config/oauthOption.ts @@ -12,15 +12,19 @@ export const oauth42Schema = z.object({ CLIENT_SECRET: nonempty, }); -export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption => oauthUrlSchema - .transform((v) => ({ - redirectURL: v.REDIRECT_URL, - clientURL: v.CLIENT_URL, - })).parse(processEnv); +export const getOauthUrlOption = (processEnv: NodeJS.ProcessEnv): OauthUrlOption => + oauthUrlSchema + .transform((v) => ({ + redirectURL: v.REDIRECT_URL, + clientURL: v.CLIENT_URL, + })) + .parse(processEnv); // eslint-disable-next-line max-len -export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption => oauth42Schema - .transform((v) => ({ - id: v.CLIENT_ID, - secret: v.CLIENT_SECRET, - })).parse(processEnv); +export const getOauth42ApiOption = (processEnv: NodeJS.ProcessEnv): Oauth42ApiOption => + oauth42Schema + .transform((v) => ({ + id: v.CLIENT_ID, + secret: v.CLIENT_SECRET, + })) + .parse(processEnv); diff --git a/backend/src/entity/entities/Book.ts b/backend/src/entity/entities/Book.ts index 5435d1fa..38a7a48e 100644 --- a/backend/src/entity/entities/Book.ts +++ b/backend/src/entity/entities/Book.ts @@ -1,5 +1,11 @@ import { - Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; import { BookInfo } from './BookInfo'; import { User } from './User'; @@ -8,55 +14,54 @@ import { Reservation } from './Reservation'; @Index('FK_donator_id_from_user', ['donatorId'], {}) @Entity('book') - export class Book { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'donator', nullable: true, length: 255 }) - donator: string | null; + donator: string | null; @Column('varchar', { name: 'callSign', length: 255 }) - callSign: string; + callSign: string; @Column('int', { name: 'status' }) - status: number; + status: number; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt?: Date; + createdAt?: Date; @Column('int') - infoId: number; + infoId: number; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt?: Date; + updatedAt?: Date; @Column('int', { name: 'donatorId', nullable: true }) - donatorId: number | null; + donatorId: number | null; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.books, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }]) - info?: BookInfo; + info?: BookInfo; @ManyToOne(() => User, (user) => user.books, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }]) - donator2?: User; + donator2?: User; @OneToMany(() => Lending, (lending) => lending.book) - lendings?: Lending[]; + lendings?: Lending[]; @OneToMany(() => Reservation, (reservation) => reservation.book) - reservations?: Reservation[]; + reservations?: Reservation[]; } diff --git a/backend/src/entity/entities/BookInfo.ts b/backend/src/entity/entities/BookInfo.ts index 9ef8b3b0..a0c19bc4 100644 --- a/backend/src/entity/entities/BookInfo.ts +++ b/backend/src/entity/entities/BookInfo.ts @@ -20,66 +20,63 @@ import { BookInfoSearchKeywords } from './BookInfoSearchKeywords'; @Entity('book_info') export class BookInfo { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'title', length: 255 }) - title?: string; + title?: string; @Column('varchar', { name: 'author', length: 255 }) - author?: string; + author?: string; @Column('varchar', { name: 'publisher', length: 255 }) - publisher?: string; + publisher?: string; @Column('varchar', { name: 'isbn', nullable: true, length: 255 }) - isbn?: string | null; + isbn?: string | null; @Column('varchar', { name: 'image', nullable: true, length: 255 }) - image?: string | null; + image?: string | null; @Column('date', { name: 'publishedAt', nullable: true }) - publishedAt?: string | null; + publishedAt?: string | null; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt?: Date; + createdAt?: Date; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt?: Date; + updatedAt?: Date; @Column('int', { name: 'categoryId' }) - categoryId?: number; + categoryId?: number; @OneToMany(() => Book, (book) => book.info) - books?: Book[]; + books?: Book[]; @ManyToOne(() => Category, (category) => category.bookInfos, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'categoryId', referencedColumnName: 'id' }]) - category?: Category; + category?: Category; @OneToMany(() => Likes, (likes) => likes.bookInfo) - likes?: Likes[]; + likes?: Likes[]; @OneToMany(() => Reservation, (reservation) => reservation.bookInfo) - reservations?: Reservation[]; + reservations?: Reservation[]; @OneToMany(() => Reviews, (reviews) => reviews.bookInfo) - reviews?: Reviews[]; + reviews?: Reviews[]; @OneToMany(() => SuperTag, (superTags) => superTags.userId) - superTags?: SuperTag[]; + superTags?: SuperTag[]; - @OneToOne( - () => BookInfoSearchKeywords, - (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo, - ) - bookInfoSearchKeyword?: BookInfoSearchKeywords; + @OneToOne(() => BookInfoSearchKeywords, (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo) + bookInfoSearchKeyword?: BookInfoSearchKeywords; } diff --git a/backend/src/entity/entities/BookInfoSearchKeywords.ts b/backend/src/entity/entities/BookInfoSearchKeywords.ts index 83d556c9..a51b03ee 100644 --- a/backend/src/entity/entities/BookInfoSearchKeywords.ts +++ b/backend/src/entity/entities/BookInfoSearchKeywords.ts @@ -1,36 +1,34 @@ -import { - Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { BookInfo } from './BookInfo'; @Index('FK_bookInfoId', ['bookInfoId'], {}) @Entity('book_info_search_keywords') export class BookInfoSearchKeywords { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'disassembled_title', length: 255 }) - disassembledTitle?: string; + disassembledTitle?: string; @Column('varchar', { name: 'disassembled_author', length: 255 }) - disassembledAuthor?: string; + disassembledAuthor?: string; @Column('varchar', { name: 'disassembled_publisher', length: 255 }) - disassembledPublisher?: string; + disassembledPublisher?: string; @Column('varchar', { name: 'title_initials', length: 255 }) - titleInitials?: string; + titleInitials?: string; @Column('varchar', { name: 'author_initials', length: 255 }) - authorInitials?: string; + authorInitials?: string; @Column('varchar', { name: 'publisher_initials', length: 255 }) - publisherInitials?: string; + publisherInitials?: string; @Column('int', { name: 'book_info_id' }) - bookInfoId?: number; + bookInfoId?: number; @OneToOne(() => BookInfo, (bookInfo) => bookInfo.id) @JoinColumn([{ name: 'book_info_id', referencedColumnName: 'id' }]) - bookInfo?: BookInfo; + bookInfo?: BookInfo; } diff --git a/backend/src/entity/entities/Category.ts b/backend/src/entity/entities/Category.ts index 27c77fec..cd6cca5c 100644 --- a/backend/src/entity/entities/Category.ts +++ b/backend/src/entity/entities/Category.ts @@ -1,10 +1,4 @@ -import { - Column, - Entity, - Index, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { BookInfo } from './BookInfo'; @Index('id', ['id'], { unique: true }) @@ -12,11 +6,11 @@ import { BookInfo } from './BookInfo'; @Entity('category', { schema: '42library' }) export class Category { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('varchar', { name: 'name', unique: true, length: 255 }) - name: string; + name: string; @OneToMany(() => BookInfo, (bookInfo) => bookInfo.category) - bookInfos: BookInfo[]; + bookInfos: BookInfo[]; } diff --git a/backend/src/entity/entities/Lending.ts b/backend/src/entity/entities/Lending.ts index bd77d783..4a321f4f 100644 --- a/backend/src/entity/entities/Lending.ts +++ b/backend/src/entity/entities/Lending.ts @@ -1,84 +1,76 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Book } from './Book'; import { User } from './User'; - @Index('FK_f2adde8c7d298210c39c500d966', ['lendingLibrarianId'], {}) - @Index('FK_returningLibrarianId', ['returningLibrarianId'], {}) +@Index('FK_f2adde8c7d298210c39c500d966', ['lendingLibrarianId'], {}) +@Index('FK_returningLibrarianId', ['returningLibrarianId'], {}) @Entity('lending', { schema: '42library' }) - export class Lending { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'lendingLibrarianId' }) - lendingLibrarianId: number; + lendingLibrarianId: number; @Column('varchar', { name: 'lendingCondition', length: 255 }) - lendingCondition: string; + lendingCondition: string; @Column('int', { name: 'returningLibrarianId', nullable: true }) - returningLibrarianId: number | null; + returningLibrarianId: number | null; @Column('varchar', { name: 'returningCondition', nullable: true, length: 255, }) - returningCondition: string | null; + returningCondition: string | null; @Column('datetime', { name: 'returnedAt', nullable: true }) - returnedAt: Date | null; + returnedAt: Date | null; @Column('timestamp', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt: Date; + createdAt: Date; @Column('timestamp', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt: Date; + updatedAt: Date; @ManyToOne(() => Book, (book) => book.lendings, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: Book; @Column({ name: 'bookId', type: 'int' }) - bookId: number; + bookId: number; @ManyToOne(() => User, (user) => user.lendings, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @Column({ name: 'userId', type: 'int' }) - userId: number; + userId: number; @ManyToOne(() => User, (user) => user.lendings2, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'lendingLibrarianId', referencedColumnName: 'id' }]) - lendingLibrarian: User; + lendingLibrarian: User; @ManyToOne(() => User, (user) => user.lendings3, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'returningLibrarianId', referencedColumnName: 'id' }]) - returningLibrarian: User; + returningLibrarian: User; } diff --git a/backend/src/entity/entities/Likes.ts b/backend/src/entity/entities/Likes.ts index b173470c..86a147c5 100644 --- a/backend/src/entity/entities/Likes.ts +++ b/backend/src/entity/entities/Likes.ts @@ -1,42 +1,34 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; - @Index('FK_529dceb01ef681127fef04d755d4', ['userId'], {}) - @Index('FK_bookInfo3', ['bookInfoId'], {}) +@Index('FK_529dceb01ef681127fef04d755d4', ['userId'], {}) +@Index('FK_bookInfo3', ['bookInfoId'], {}) @Entity('likes', { schema: '42library' }) - export class Likes { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) - isDeleted: boolean; + isDeleted: boolean; @ManyToOne(() => User, (user) => user.likes, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.likes, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/Reservation.ts b/backend/src/entity/entities/Reservation.ts index ea652c1d..d8292147 100644 --- a/backend/src/entity/entities/Reservation.ts +++ b/backend/src/entity/entities/Reservation.ts @@ -1,66 +1,59 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; import { Book } from './Book'; - @Index('FK_bookInfo', ['bookInfoId'], {}) +@Index('FK_bookInfo', ['bookInfoId'], {}) @Entity('reservation') export class Reservation { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('datetime', { name: 'endAt', nullable: true }) - endAt: Date | null; + endAt: Date | null; @Column('datetime', { name: 'createdAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('int', { name: 'status', default: () => '0' }) - status: number; + status: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; - @Column('int', { name: 'userId' }) - userId: number; + @Column('int', { name: 'userId' }) + userId: number; @ManyToOne(() => User, (user) => user.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; @ManyToOne(() => Book, (book) => book.reservations, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) - book: Book; + book: Book; @Column('int', { name: 'bookId', nullable: true }) - bookId: number | null; + bookId: number | null; } diff --git a/backend/src/entity/entities/Reviews.ts b/backend/src/entity/entities/Reviews.ts index 611e4e67..62b1e75d 100644 --- a/backend/src/entity/entities/Reviews.ts +++ b/backend/src/entity/entities/Reviews.ts @@ -1,69 +1,61 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { BookInfo } from './BookInfo'; - @Index('FK_529dceb01ef681127fef04d755d3', ['userId'], {}) - @Index('FK_bookInfo2', ['bookInfoId'], {}) +@Index('FK_529dceb01ef681127fef04d755d3', ['userId'], {}) +@Index('FK_bookInfo2', ['bookInfoId'], {}) @Entity('reviews') - export class Reviews { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('datetime', { name: 'createdAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => "'CURRENT_TIMESTAMP(6)'", }) - updatedAt: Date; + updatedAt: Date; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) - isDeleted: boolean; + isDeleted: boolean; @Column('int', { name: 'deleteUserId', nullable: true }) - deleteUserId: number | null; + deleteUserId: number | null; @Column('text', { name: 'content' }) - content: string; + content: string; @Column('tinyint', { name: 'disabled', width: 1, default: () => "'0'" }) - disabled: boolean; + disabled: boolean; @Column('int', { name: 'disabledUserId', nullable: true }) - disabledUserId: number | null; + disabledUserId: number | null; @ManyToOne(() => User, (user) => user.reviews, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reviews, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/SearchKeywords.ts b/backend/src/entity/entities/SearchKeywords.ts index aca0cfb6..b7407de6 100644 --- a/backend/src/entity/entities/SearchKeywords.ts +++ b/backend/src/entity/entities/SearchKeywords.ts @@ -1,22 +1,20 @@ -import { - Column, Entity, OneToMany, PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { SearchLogs } from './SearchLogs'; @Entity('search_keywords') export class SearchKeywords { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('varchar', { name: 'keyword', length: 255 }) - keyword?: string; + keyword?: string; @Column('varchar', { name: 'disassembled_keyword', length: 255 }) - disassembledKeyword?: string; + disassembledKeyword?: string; @Column('varchar', { name: 'initial_consonants', length: 255 }) - initialConsonants?: string; + initialConsonants?: string; @OneToMany(() => SearchLogs, (searchLogs) => searchLogs.searchKeyword) - searchLogs?: SearchLogs[]; + searchLogs?: SearchLogs[]; } diff --git a/backend/src/entity/entities/SearchLogs.ts b/backend/src/entity/entities/SearchLogs.ts index 6e1b1d1b..35249e8e 100644 --- a/backend/src/entity/entities/SearchLogs.ts +++ b/backend/src/entity/entities/SearchLogs.ts @@ -1,29 +1,22 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { SearchKeywords } from './SearchKeywords'; @Index('FK_searchKeywordId', ['searchKeywordId'], {}) @Entity('search_logs') export class SearchLogs { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id?: number; + id?: number; @Column('int', { name: 'search_keyword_id' }) - searchKeywordId?: number; + searchKeywordId?: number; @Column('varchar', { name: 'timestamp', length: 255 }) - timestamp?: string; + timestamp?: string; @ManyToOne(() => SearchKeywords, (SearchKeyword) => SearchKeyword.id, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'search_keyword_id', referencedColumnName: 'id' }]) - searchKeyword?: SearchKeywords; + searchKeyword?: SearchKeywords; } diff --git a/backend/src/entity/entities/SubTag.ts b/backend/src/entity/entities/SubTag.ts index 99a4c8f6..2124b394 100644 --- a/backend/src/entity/entities/SubTag.ts +++ b/backend/src/entity/entities/SubTag.ts @@ -1,11 +1,4 @@ -import { - Column, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; import { SuperTag } from './SuperTag'; @@ -14,52 +7,52 @@ import { SuperTag } from './SuperTag'; @Entity('sub_tag', { schema: 'jip_dev' }) export class SubTag { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'superTagId' }) - superTagId: number; + superTagId: number; @Column('datetime', { name: 'createdAt', default: () => 'current_timestamp(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'current_timestamp(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('tinyint', { name: 'isDeleted', default: () => '0' }) - isDeleted: number; + isDeleted: number; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('varchar', { name: 'content', length: 42 }) - content: string; + content: string; @Column('tinyint', { name: 'isPublic' }) - isPublic: number; + isPublic: number; @ManyToOne(() => User, (user) => user.subTag, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => SuperTag, (superTag) => superTag.subTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'superTagId', referencedColumnName: 'id' }]) - superTag: SuperTag; + superTag: SuperTag; @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfoId: number; + bookInfoId: number; } diff --git a/backend/src/entity/entities/SuperTag.ts b/backend/src/entity/entities/SuperTag.ts index 25a6fd98..72a579e6 100644 --- a/backend/src/entity/entities/SuperTag.ts +++ b/backend/src/entity/entities/SuperTag.ts @@ -16,49 +16,49 @@ import { BookInfo } from './BookInfo'; @Entity('super_tag', { schema: 'jip_dev' }) export class SuperTag { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('int', { name: 'userId' }) - userId: number; + userId: number; @Column('int', { name: 'bookInfoId' }) - bookInfoId: number; + bookInfoId: number; @Column('datetime', { name: 'createdAt', default: () => 'current_timestamp(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'current_timestamp(6)', }) - updatedAt: Date; + updatedAt: Date; @Column('tinyint', { name: 'isDeleted', default: () => '0' }) - isDeleted: number; + isDeleted: number; @Column('int', { name: 'updateUserId' }) - updateUserId: number; + updateUserId: number; @Column('varchar', { name: 'content', length: 42 }) - content: string; + content: string; @OneToMany(() => SubTag, (subTag) => subTag.superTag) - subTags: SubTag[]; + subTags: SubTag[]; @ManyToOne(() => User, (user) => user.superTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) - user: User; + user: User; @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.superTags, { onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }) @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) - bookInfo: BookInfo; + bookInfo: BookInfo; } diff --git a/backend/src/entity/entities/User.ts b/backend/src/entity/entities/User.ts index 1820c90f..6d7bf39e 100644 --- a/backend/src/entity/entities/User.ts +++ b/backend/src/entity/entities/User.ts @@ -1,10 +1,4 @@ -import { - Column, - Entity, - Index, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Book } from './Book'; import { Lending } from './Lending'; import { Likes } from './Likes'; @@ -19,19 +13,19 @@ import { SuperTag } from './SuperTag'; @Entity('user') export class User { @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) - id: number; + id: number; @Column('varchar', { name: 'email', unique: true, length: 255 }) - email: string; + email: string; @Column('varchar', { name: 'password', length: 255, select: false }) - password: string; + password: string; @Column('varchar', { name: 'nickname', nullable: true, length: 255 }) - nickname: string | null; + nickname: string | null; @Column('int', { name: 'intraId', nullable: true, unique: true }) - intraId: number | null; + intraId: number | null; @Column('varchar', { name: 'slack', @@ -39,53 +33,53 @@ export class User { unique: true, length: 255, }) - slack: string | null; + slack: string | null; @Column('datetime', { name: 'penaltyEndDate', default: () => 'CURRENT_TIMESTAMP', }) - penaltyEndDate: Date; + penaltyEndDate: Date; @Column('tinyint', { name: 'role', default: () => '0' }) - role: number; + role: number; @Column('datetime', { name: 'createdAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - createdAt: Date; + createdAt: Date; @Column('datetime', { name: 'updatedAt', default: () => 'CURRENT_TIMESTAMP(6)', }) - updatedAt: Date; + updatedAt: Date; @OneToMany(() => Book, (book) => book.donator2) - books: Book[]; + books: Book[]; @OneToMany(() => Lending, (lending) => lending.user) - lendings: Lending[]; + lendings: Lending[]; @OneToMany(() => Lending, (lending) => lending.lendingLibrarian) - lendings2: Lending[]; + lendings2: Lending[]; @OneToMany(() => Lending, (lending) => lending.returningLibrarian) - lendings3: Lending[]; + lendings3: Lending[]; @OneToMany(() => Likes, (likes) => likes.user) - likes: Likes[]; + likes: Likes[]; @OneToMany(() => Reservation, (reservation) => reservation.user) - reservations: Reservation[]; + reservations: Reservation[]; @OneToMany(() => Reviews, (reviews) => reviews.user) - reviews: Reviews[]; + reviews: Reviews[]; @OneToMany(() => SubTag, (subtag) => subtag.userId) - subTag: SubTag[]; + subTag: SubTag[]; @OneToMany(() => SuperTag, (superTags) => superTags.userId) - superTags: SuperTag[]; + superTags: SuperTag[]; } diff --git a/backend/src/entity/entities/UserReservation.ts b/backend/src/entity/entities/UserReservation.ts index 56e6e75d..64e99055 100644 --- a/backend/src/entity/entities/UserReservation.ts +++ b/backend/src/entity/entities/UserReservation.ts @@ -3,53 +3,53 @@ import { BookInfo } from './BookInfo'; import { Reservation } from './Reservation'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('r.id', 'reservationId') - .addSelect('r.bookInfoId', 'reservedBookInfoId') - .addSelect('r.createdAt', 'reservationDate') - .addSelect('r.endAt', 'endAt') - .addSelect( - `(SELECT COUNT(*) + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('r.id', 'reservationId') + .addSelect('r.bookInfoId', 'reservedBookInfoId') + .addSelect('r.createdAt', 'reservationDate') + .addSelect('r.endAt', 'endAt') + .addSelect( + `(SELECT COUNT(*) FROM reservation WHERE (status = 0) AND (bookInfoId = reservedBookInfoId) AND (createdAt <= reservationDate))`, - 'ranking', - ) - .addSelect('bi.title', 'title') - .addSelect('bi.author', 'author') - .addSelect('bi.image', 'image') - .addSelect('r.userId', 'userId') - .from(Reservation, 'r') - .leftJoin(BookInfo, 'bi', 'r.bookInfoId = bi.id') - .where('r.status = 0'), + 'ranking', + ) + .addSelect('bi.title', 'title') + .addSelect('bi.author', 'author') + .addSelect('bi.image', 'image') + .addSelect('r.userId', 'userId') + .from(Reservation, 'r') + .leftJoin(BookInfo, 'bi', 'r.bookInfoId = bi.id') + .where('r.status = 0'), }) export class UserReservation { @ViewColumn() - reservationId: number; + reservationId: number; @ViewColumn() - reservedBookInfoId: number; + reservedBookInfoId: number; @ViewColumn() - reservationDate: Date; + reservationDate: Date; @ViewColumn() - endAt: Date; + endAt: Date; @ViewColumn() - ranking: number; + ranking: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - userId: number; + userId: number; } diff --git a/backend/src/entity/entities/VHistories.ts b/backend/src/entity/entities/VHistories.ts index dae4c939..a7c5e4e5 100644 --- a/backend/src/entity/entities/VHistories.ts +++ b/backend/src/entity/entities/VHistories.ts @@ -2,74 +2,83 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; // TODO: 대출자 id로 검색 가능하게 @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.id', 'id') - .addSelect('lendingCondition', 'lendingCondition') - .addSelect('u.nickname', 'login') - .addSelect('l.returningCondition', 'returningCondition') - .addSelect(` + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.id', 'id') + .addSelect('lendingCondition', 'lendingCondition') + .addSelect('u.nickname', 'login') + .addSelect('l.returningCondition', 'returningCondition') + .addSelect( + ` CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END - `, 'penaltyDays') - .addSelect('b.callSign', 'callSign') - .addSelect('bi.title', 'title') - .addSelect('bi.id', 'bookInfoId') - .addSelect('bi.image', 'image') - .addSelect('DATE_FORMAT(l.createdAt, "%Y-%m-%d")', 'createdAt') - .addSelect('DATE_FORMAT(l.returnedAt, "%Y-%m-%d")', 'returnedAt') - .addSelect('DATE_FORMAT(l.updatedAt, "%Y-%m-%d")', 'updatedAt') - .addSelect("DATE_FORMAT(DATE_ADD(l.createdAt, interval 14 day), '%Y-%m-%d')", 'dueDate') - .addSelect('(SELECT nickname FROM user WHERE user.id = lendingLibrarianId)', 'lendingLibrarianNickName') - .addSelect('(SELECT nickname FROM user WHERE user.id = returningLibrarianId)', 'returningLibrarianNickname') - .from('lending', 'l') - .innerJoin('user', 'u', 'l.userId = u.id') - .innerJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoId = bi.id'), + `, + 'penaltyDays', + ) + .addSelect('b.callSign', 'callSign') + .addSelect('bi.title', 'title') + .addSelect('bi.id', 'bookInfoId') + .addSelect('bi.image', 'image') + .addSelect('DATE_FORMAT(l.createdAt, "%Y-%m-%d")', 'createdAt') + .addSelect('DATE_FORMAT(l.returnedAt, "%Y-%m-%d")', 'returnedAt') + .addSelect('DATE_FORMAT(l.updatedAt, "%Y-%m-%d")', 'updatedAt') + .addSelect("DATE_FORMAT(DATE_ADD(l.createdAt, interval 14 day), '%Y-%m-%d')", 'dueDate') + .addSelect( + '(SELECT nickname FROM user WHERE user.id = lendingLibrarianId)', + 'lendingLibrarianNickName', + ) + .addSelect( + '(SELECT nickname FROM user WHERE user.id = returningLibrarianId)', + 'returningLibrarianNickname', + ) + .from('lending', 'l') + .innerJoin('user', 'u', 'l.userId = u.id') + .innerJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoId = bi.id'), }) export class VHistories { @ViewColumn() - id: number; + id: number; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - returningCondition: string; + returningCondition: string; @ViewColumn() - penaltyDays: number; + penaltyDays: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - image: string; + image: string; @ViewColumn() - createdAt: Date; + createdAt: Date; @ViewColumn() - returnedAt: Date; + returnedAt: Date; @ViewColumn() - updatedAt: Date; + updatedAt: Date; @ViewColumn() - dueDate: Date; + dueDate: Date; @ViewColumn() - lendingLibrarianNickName: string; + lendingLibrarianNickName: string; @ViewColumn() - returningLibrarianNickname: string; + returningLibrarianNickname: string; } diff --git a/backend/src/entity/entities/VLending.ts b/backend/src/entity/entities/VLending.ts index 4e7fdb9f..71f086ca 100644 --- a/backend/src/entity/entities/VLending.ts +++ b/backend/src/entity/entities/VLending.ts @@ -1,55 +1,58 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.id', 'id') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('u.nickname', 'login') - .addSelect('CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, now()) END', 'penaltyDays') - .addSelect('b.id', 'bookId') - .addSelect('b.callSign', 'callSign') - .addSelect('bi.title', 'title') - .addSelect('bi.image', 'image') - .addSelect('date_format(l.createdAt, \'%Y-%m-%d\')', 'createdAt') - .addSelect('date_format(l.returnedAt, \'%Y-%m-%d\')', 'returnedAt') - .addSelect('date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), \'%Y-%m-%d\')', 'dueDate') - .from('lending', 'l') - .innerJoin('user', 'u', 'l.userId = u.id') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.id', 'id') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('u.nickname', 'login') + .addSelect( + 'CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, now()) END', + 'penaltyDays', + ) + .addSelect('b.id', 'bookId') + .addSelect('b.callSign', 'callSign') + .addSelect('bi.title', 'title') + .addSelect('bi.image', 'image') + .addSelect("date_format(l.createdAt, '%Y-%m-%d')", 'createdAt') + .addSelect("date_format(l.returnedAt, '%Y-%m-%d')", 'returnedAt') + .addSelect("date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), '%Y-%m-%d')", 'dueDate') + .from('lending', 'l') + .innerJoin('user', 'u', 'l.userId = u.id') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), }) export class VLending { @ViewColumn() - id: number; + id: number; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - penaltyDays: number; + penaltyDays: number; @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - createdAt: Date; + createdAt: Date; @ViewColumn() - returnedAt: Date; + returnedAt: Date; @ViewColumn() - dueDate: Date; + dueDate: Date; } diff --git a/backend/src/entity/entities/VLendingForSearchUser.ts b/backend/src/entity/entities/VLendingForSearchUser.ts index cb5c3c6c..6690c251 100644 --- a/backend/src/entity/entities/VLendingForSearchUser.ts +++ b/backend/src/entity/entities/VLendingForSearchUser.ts @@ -1,52 +1,58 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity('v_lending_for_search_user', { - expression: (Data: DataSource) => Data - .createQueryBuilder() - .addSelect('u.id', 'userId') - .addSelect('bi.id', 'bookInfoId') - .addSelect('l.createdAt', 'lendDate') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('bi.image', 'image') - .addSelect('bi.author', 'author') - .addSelect('bi.title', 'title') - .addSelect('DATE_ADD(l.createdAt, INTERVAL 14 DAY)', 'duedate') - .addSelect('CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', 'overDueDay') - .addSelect('(SELECT COUNT(r.id) FROM reservation r WHERE r.bookInfoId = bi.id AND r.status = 0)', 'reservedNum') - .from('lending', 'l') - .where('l.returnedAt is NULL') - .innerJoin('user', 'u', 'l.userId = u.id') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .addSelect('u.id', 'userId') + .addSelect('bi.id', 'bookInfoId') + .addSelect('l.createdAt', 'lendDate') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('bi.image', 'image') + .addSelect('bi.author', 'author') + .addSelect('bi.title', 'title') + .addSelect('DATE_ADD(l.createdAt, INTERVAL 14 DAY)', 'duedate') + .addSelect( + 'CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', + 'overDueDay', + ) + .addSelect( + '(SELECT COUNT(r.id) FROM reservation r WHERE r.bookInfoId = bi.id AND r.status = 0)', + 'reservedNum', + ) + .from('lending', 'l') + .where('l.returnedAt is NULL') + .innerJoin('user', 'u', 'l.userId = u.id') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id'), }) export class VLendingForSearchUser { @ViewColumn() - userId: number; + userId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - lendDate: Date; + lendDate: Date; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - title: string; + title: string; @ViewColumn() - duedate: Date; + duedate: Date; @ViewColumn() - overDueDay: Date; + overDueDay: Date; @ViewColumn() - reservedNum: number; + reservedNum: number; } diff --git a/backend/src/entity/entities/VSearchBook.ts b/backend/src/entity/entities/VSearchBook.ts index 6986aa84..f1d82ddd 100644 --- a/backend/src/entity/entities/VSearchBook.ts +++ b/backend/src/entity/entities/VSearchBook.ts @@ -4,74 +4,75 @@ import { Book } from './Book'; import { Category } from './Category'; @ViewEntity('v_search_book', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('book.infoId', 'bookInfoId') - .addSelect('book_info.title', 'title') - .addSelect('book_info.author', 'author') - .addSelect('book_info.publisher', 'publisher') - .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') - .addSelect('book_info.isbn', 'isbn') - .addSelect('book_info.image', 'image') - .addSelect('book.callSign', 'callSign') - .addSelect('book.id', 'bookId') - .addSelect('book.status', 'status') - .addSelect('book.donator', 'donator') - .addSelect('book_info.categoryId', 'categoryId') - .addSelect('category.name', 'category') - .addSelect( - ' IF((\n' - + ' IF((select COUNT(*) from lending as l where l.bookId = book.id and l.returnedAt is NULL) = 0, TRUE, FALSE)\n' - + ' AND\n' - + ' IF((select COUNT(*) from book as b where (b.id = book.id and b.status = 0)) = 1, TRUE, FALSE)\n' - + ' AND\n' - + ' IF((select COUNT(*) from reservation as r where (r.bookId = book.id and status = 0)) = 0, TRUE, FALSE)\n' - + ' ), TRUE, FALSE)', - 'isLendable', - ) - .from(Book, 'book') - .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') - .leftJoin(Category, 'category', 'book_info.categoryId = category.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('book.infoId', 'bookInfoId') + .addSelect('book_info.title', 'title') + .addSelect('book_info.author', 'author') + .addSelect('book_info.publisher', 'publisher') + .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') + .addSelect('book_info.isbn', 'isbn') + .addSelect('book_info.image', 'image') + .addSelect('book.callSign', 'callSign') + .addSelect('book.id', 'bookId') + .addSelect('book.status', 'status') + .addSelect('book.donator', 'donator') + .addSelect('book_info.categoryId', 'categoryId') + .addSelect('category.name', 'category') + .addSelect( + ' IF((\n' + + ' IF((select COUNT(*) from lending as l where l.bookId = book.id and l.returnedAt is NULL) = 0, TRUE, FALSE)\n' + + ' AND\n' + + ' IF((select COUNT(*) from book as b where (b.id = book.id and b.status = 0)) = 1, TRUE, FALSE)\n' + + ' AND\n' + + ' IF((select COUNT(*) from reservation as r where (r.bookId = book.id and status = 0)) = 0, TRUE, FALSE)\n' + + ' ), TRUE, FALSE)', + 'isLendable', + ) + .from(Book, 'book') + .leftJoin(BookInfo, 'book_info', 'book_info.id = book.infoId') + .leftJoin(Category, 'category', 'book_info.categoryId = category.id'), }) export class VSearchBook { @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - donator: string; + donator: string; @ViewColumn() - publisher: string; + publisher: string; @ViewColumn() - publishedAt: string; + publishedAt: string; @ViewColumn() - isbn: string; + isbn: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - status: number; + status: number; @ViewColumn() - categoryId: string; + categoryId: string; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - category: string; + category: string; @ViewColumn() - isLendable: boolean; + isLendable: boolean; } diff --git a/backend/src/entity/entities/VSearchBookByTag.ts b/backend/src/entity/entities/VSearchBookByTag.ts index f8643b0e..5d74c981 100644 --- a/backend/src/entity/entities/VSearchBookByTag.ts +++ b/backend/src/entity/entities/VSearchBookByTag.ts @@ -5,66 +5,71 @@ import { SubTag } from './SubTag'; import { SuperTag } from './SuperTag'; @ViewEntity('v_search_book_by_tag', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .distinctOn(['bi.id']) - .select('bi.id', 'id') - .addSelect('bi.title', 'title') - .addSelect('bi.author', 'author') - .addSelect('bi.isbn', 'isbn') - .addSelect('bi.image', 'image') - .addSelect('bi.publishedAt', 'publishedAt') - .addSelect('bi.createdAt', 'createdAt') - .addSelect('bi.updatedAt', 'updatedAt') - .addSelect('c.name', 'category') - .addSelect('sp.content', 'superTagContent') - .addSelect('sb.content', 'subTagContent') - .addSelect((subQuery) => subQuery - .select('COUNT(l.id)') - .from('book', 'b') - .leftJoin('lending', 'l', 'l.bookId = b.id') - .innerJoin('book_info', 'bi2', 'bi2.id = b.infoId') - .where('bi.id = bi.id'), 'lendingCnt') - .from(BookInfo, 'bi') - .innerJoin(Category, 'c', 'c.id = bi.categoryId') - .innerJoin(SuperTag, 'sp', 'sp.bookInfoId = bi.id') - .leftJoin(SubTag, 'sb', 'sb.superTagId = sp.id'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .distinctOn(['bi.id']) + .select('bi.id', 'id') + .addSelect('bi.title', 'title') + .addSelect('bi.author', 'author') + .addSelect('bi.isbn', 'isbn') + .addSelect('bi.image', 'image') + .addSelect('bi.publishedAt', 'publishedAt') + .addSelect('bi.createdAt', 'createdAt') + .addSelect('bi.updatedAt', 'updatedAt') + .addSelect('c.name', 'category') + .addSelect('sp.content', 'superTagContent') + .addSelect('sb.content', 'subTagContent') + .addSelect( + (subQuery) => + subQuery + .select('COUNT(l.id)') + .from('book', 'b') + .leftJoin('lending', 'l', 'l.bookId = b.id') + .innerJoin('book_info', 'bi2', 'bi2.id = b.infoId') + .where('bi.id = bi.id'), + 'lendingCnt', + ) + .from(BookInfo, 'bi') + .innerJoin(Category, 'c', 'c.id = bi.categoryId') + .innerJoin(SuperTag, 'sp', 'sp.bookInfoId = bi.id') + .leftJoin(SubTag, 'sb', 'sb.superTagId = sp.id'), }) export class VSearchBookByTag { @ViewColumn() - id: number; + id: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - isbn: number; + isbn: number; @ViewColumn() - image: string; + image: string; @ViewColumn() - publishedAt: string; + publishedAt: string; @ViewColumn() - createdAt: string; + createdAt: string; @ViewColumn() - updatedAt: string; + updatedAt: string; @ViewColumn() - category: string; + category: string; @ViewColumn() - superTagContent: string; + superTagContent: string; @ViewColumn() - subTagContent: string; + subTagContent: string; @ViewColumn() - lendingCnt: number; + lendingCnt: number; } export default VSearchBookByTag; diff --git a/backend/src/entity/entities/VStock.ts b/backend/src/entity/entities/VStock.ts index 18113f66..4b81cd10 100644 --- a/backend/src/entity/entities/VStock.ts +++ b/backend/src/entity/entities/VStock.ts @@ -6,72 +6,71 @@ import { Lending } from './Lending'; import { Reservation } from './Reservation'; @ViewEntity('v_stock', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('book.infoId', 'bookInfoId') - .addSelect('book_info.title', 'title') - .addSelect('book_info.author', 'author') - .addSelect('book_info.publisher', 'publisher') - .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') - .addSelect('book_info.isbn', 'isbn') - .addSelect('book_info.image', 'image') - .addSelect('book.callSign', 'callSign') - .addSelect('book.id', 'bookId') - .addSelect('book.status', 'status') - .addSelect('book.donator', 'donator') - .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') - .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') - .groupBy('book.id') - .having('COUNT(l.id) = COUNT(l.returnedAt) AND COUNT(r.id) = 0') - .where('book.status = 0'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('book.infoId', 'bookInfoId') + .addSelect('book_info.title', 'title') + .addSelect('book_info.author', 'author') + .addSelect('book_info.publisher', 'publisher') + .addSelect("DATE_FORMAT(book_info.publishedAt, '%Y%m%d')", 'publishedAt') + .addSelect('book_info.isbn', 'isbn') + .addSelect('book_info.image', 'image') + .addSelect('book.callSign', 'callSign') + .addSelect('book.id', 'bookId') + .addSelect('book.status', 'status') + .addSelect('book.donator', 'donator') + .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') + .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') + .groupBy('book.id') + .having('COUNT(l.id) = COUNT(l.returnedAt) AND COUNT(r.id) = 0') + .where('book.status = 0'), }) export class VStock { @ViewColumn() - bookId: number; + bookId: number; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - author: string; + author: string; @ViewColumn() - donator: string; + donator: string; @ViewColumn() - publisher: string; + publisher: string; @ViewColumn() - publishedAt: string; + publishedAt: string; @ViewColumn() - isbn: string; + isbn: string; @ViewColumn() - image: string; + image: string; @ViewColumn() - status: number; + status: number; @ViewColumn() - categoryId: number; + categoryId: number; @ViewColumn() - callSign: string; + callSign: string; @ViewColumn() - category: string; + category: string; @ViewColumn() - updatedAt: Date; + updatedAt: Date; } - - diff --git a/backend/src/entity/entities/VTagsSubDefault.ts b/backend/src/entity/entities/VTagsSubDefault.ts index c933ac9c..ca3ad9d0 100644 --- a/backend/src/entity/entities/VTagsSubDefault.ts +++ b/backend/src/entity/entities/VTagsSubDefault.ts @@ -5,56 +5,55 @@ import { SubTag } from './SubTag'; import { User } from './User'; @ViewEntity('v_tags_sub_default', { - expression: (Data: DataSource) => Data.createQueryBuilder() - .select('sp.bookInfoId', 'bookInfoId') - .addSelect('bi.title', 'title') - .addSelect('sb.id', 'id') - .addSelect('DATE_FORMAT(sb.createdAt, "%Y-%m-%d")', 'createdAt') - .addSelect('u.nickname', 'login') - .addSelect('sb.content', 'content') - .addSelect('sp.id', 'superTagId') - .addSelect('sp.content', 'superContent') - .addSelect('sb.isPublic', 'isPublic') - .addSelect('sb.isDeleted', 'isDeleted') - .addSelect('CASE WHEN sb.isPublic = 1 THEN \'public\' ELSE 1 \'private\' END', 'visibility') - .from(SuperTag, 'sp') - .innerJoin(SubTag, 'sb', 'sb.superTagId = sp.id') - .innerJoin(BookInfo, 'bi', 'bi.id = sp.bookInfoId') - .innerJoin(User, 'u', 'u.id = sb.userId'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('sp.bookInfoId', 'bookInfoId') + .addSelect('bi.title', 'title') + .addSelect('sb.id', 'id') + .addSelect('DATE_FORMAT(sb.createdAt, "%Y-%m-%d")', 'createdAt') + .addSelect('u.nickname', 'login') + .addSelect('sb.content', 'content') + .addSelect('sp.id', 'superTagId') + .addSelect('sp.content', 'superContent') + .addSelect('sb.isPublic', 'isPublic') + .addSelect('sb.isDeleted', 'isDeleted') + .addSelect("CASE WHEN sb.isPublic = 1 THEN 'public' ELSE 1 'private' END", 'visibility') + .from(SuperTag, 'sp') + .innerJoin(SubTag, 'sb', 'sb.superTagId = sp.id') + .innerJoin(BookInfo, 'bi', 'bi.id = sp.bookInfoId') + .innerJoin(User, 'u', 'u.id = sb.userId'), }) export class VTagsSubDefault { @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - id: number; + id: number; @ViewColumn() - createdAt: string; + createdAt: string; @ViewColumn() - login: string; + login: string; @ViewColumn() - content: string; + content: string; @ViewColumn() - superTagId: number; + superTagId: number; @ViewColumn() - superContent: string; + superContent: string; @ViewColumn() - isPublic: boolean; + isPublic: boolean; @ViewColumn() - isDeleted: boolean; + isDeleted: boolean; @ViewColumn() - visibility: string; + visibility: string; } - - diff --git a/backend/src/entity/entities/VTagsSuperDefault.ts b/backend/src/entity/entities/VTagsSuperDefault.ts index beb8ee26..d5f5255b 100644 --- a/backend/src/entity/entities/VTagsSuperDefault.ts +++ b/backend/src/entity/entities/VTagsSuperDefault.ts @@ -50,14 +50,14 @@ import { ObjectLiteral, SelectQueryBuilder } from 'typeorm/browser'; }) export class VTagsSuperDefault { @ViewColumn() - content: string; + content: string; @ViewColumn() - count: number; + count: number; @ViewColumn() - type: string; + type: string; @ViewColumn() - createdAt: string; + createdAt: string; } diff --git a/backend/src/entity/entities/VUserLending.ts b/backend/src/entity/entities/VUserLending.ts index 4adfb01b..39f60460 100644 --- a/backend/src/entity/entities/VUserLending.ts +++ b/backend/src/entity/entities/VUserLending.ts @@ -1,45 +1,46 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; @ViewEntity({ - expression: (Data: DataSource) => Data - .createQueryBuilder() - .select('l.userId', 'userId') - .addSelect('date_format(l.createdAt, \'%Y-%m-%d\')', 'lendDate') - .addSelect('l.lendingCondition', 'lendingCondition') - .addSelect('bi.id', 'bookInfoId') - .addSelect('bi.title', 'title') - .addSelect('date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), \'%Y-%m-%d\')', 'duedate') - .addSelect('bi.image', 'image') - .addSelect('CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', 'overDueDay') - .from('lending', 'l') - .leftJoin('book', 'b', 'l.bookId = b.id') - .leftJoin('book_info', 'bi', 'b.infoid = bi.id') - .where('l.returnedAt IS NULL'), + expression: (Data: DataSource) => + Data.createQueryBuilder() + .select('l.userId', 'userId') + .addSelect("date_format(l.createdAt, '%Y-%m-%d')", 'lendDate') + .addSelect('l.lendingCondition', 'lendingCondition') + .addSelect('bi.id', 'bookInfoId') + .addSelect('bi.title', 'title') + .addSelect("date_format(DATE_ADD(l.createdAt, INTERVAL 14 DAY), '%Y-%m-%d')", 'duedate') + .addSelect('bi.image', 'image') + .addSelect( + 'CASE WHEN DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) < 0 THEN 0 ELSE DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) END', + 'overDueDay', + ) + .from('lending', 'l') + .leftJoin('book', 'b', 'l.bookId = b.id') + .leftJoin('book_info', 'bi', 'b.infoid = bi.id') + .where('l.returnedAt IS NULL'), }) export class VUserLending { @ViewColumn() - userId: number; + userId: number; @ViewColumn() - lendDate: Date; + lendDate: Date; @ViewColumn() - lendingCondition: string; + lendingCondition: string; @ViewColumn() - bookInfoId: number; + bookInfoId: number; @ViewColumn() - title: string; + title: string; @ViewColumn() - duedate: Date; + duedate: Date; @ViewColumn() - image: string; + image: string; @ViewColumn() - overDueDay: number; + overDueDay: number; } - - diff --git a/backend/src/kysely/generated.ts b/backend/src/kysely/generated.ts index e8ac8de1..7a1de228 100644 --- a/backend/src/kysely/generated.ts +++ b/backend/src/kysely/generated.ts @@ -1,4 +1,4 @@ -import type { ColumnType, SqlBool } from "kysely"; +import type { ColumnType, SqlBool } from 'kysely'; export type Generated = T extends ColumnType ? ColumnType diff --git a/backend/src/kysely/mod.ts b/backend/src/kysely/mod.ts index b0e24ea3..fcd06fe5 100644 --- a/backend/src/kysely/mod.ts +++ b/backend/src/kysely/mod.ts @@ -18,7 +18,7 @@ const dialect = new MysqlDialect({ export const db = new Kysely({ dialect, - log: event => console.log('kysely:', event.query.sql, event.query.parameters), + log: (event) => console.log('kysely:', event.query.sql, event.query.parameters), }); export type Database = typeof db; diff --git a/backend/src/kysely/paginated.ts b/backend/src/kysely/paginated.ts index 414e1138..30e582ff 100644 --- a/backend/src/kysely/paginated.ts +++ b/backend/src/kysely/paginated.ts @@ -1,12 +1,12 @@ import { SelectQueryBuilder } from 'kysely'; type Paginated = { - items: O[], + items: O[]; meta: { totalItems: number; totalPages: number; }; -} +}; export const metaPaginated = async ( qb: SelectQueryBuilder, diff --git a/backend/src/kysely/shared.ts b/backend/src/kysely/shared.ts index 58fe8de1..1eb4f97e 100644 --- a/backend/src/kysely/shared.ts +++ b/backend/src/kysely/shared.ts @@ -8,15 +8,12 @@ const throwIf = (value: T, ok: (v: T) => boolean) => { throw new Error(`값이 예상과 달리 ${value}입니다`); }; -export type Visibility = 'public' | 'hidden' | 'all' +export type Visibility = 'public' | 'hidden' | 'all'; const roles = ['user', 'cadet', 'librarian', 'staff'] as const; -export type Role = typeof roles[number] +export type Role = (typeof roles)[number]; -const fromEnum = (role: number): Role => - throwIf(roles[role], (v) => v === undefined); +const fromEnum = (role: number): Role => throwIf(roles[role], (v) => v === undefined); -export const toRole = (role: Role): number => - throwIf(roles.indexOf(role), (v) => v === -1); +export const toRole = (role: Role): number => throwIf(roles.indexOf(role), (v) => v === -1); -export const roleSchema = z.number().int().min(0).max(3) - .transform(fromEnum); +export const roleSchema = z.number().int().min(0).max(3).transform(fromEnum); diff --git a/backend/src/kysely/sqlDates.ts b/backend/src/kysely/sqlDates.ts index 6cc5d477..60ca8e7e 100644 --- a/backend/src/kysely/sqlDates.ts +++ b/backend/src/kysely/sqlDates.ts @@ -12,10 +12,7 @@ export const dateNow = () => sql`NOW()`; * * @todo(@scarf005): 임의의 날짜 형식을 타입 안전하게 사용 */ -export function dateFormat( - expr: Expression, - format: '%Y-%m-%d', -): RawBuilder; +export function dateFormat(expr: Expression, format: '%Y-%m-%d'): RawBuilder; export function dateFormat( expr: Expression, @@ -30,10 +27,7 @@ export function dateFormat(expr: Expression, format: '%Y-%m-%d') { * * @see {@link https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-add | DATE_ADD} */ -export function dateAddDays( - expr: Expression, - days: number, -): RawBuilder; +export function dateAddDays(expr: Expression, days: number): RawBuilder; export function dateAddDays(expr: Expression, days: number) { return sql`DATE_ADD(${expr}, INTERVAL ${days} DAY)`; @@ -48,19 +42,13 @@ export function dateSubDays(expr: Expression, days: number) { * * @see {@link https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_datediff | DATEDIFF} */ -export function dateDiff( - left: Expression, - right: Expression, -): RawBuilder; +export function dateDiff(left: Expression, right: Expression): RawBuilder; export function dateDiff( left: Expression, right: Expression, ): RawBuilder; -export function dateDiff( - left: Expression, - right: Expression, -) { +export function dateDiff(left: Expression, right: Expression) { return sql`DATEDIFF(${left}, ${right})`; } diff --git a/backend/src/logger.ts b/backend/src/logger.ts index ebd9295c..84c9d0a9 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -1,19 +1,12 @@ import morgan from 'morgan'; import path from 'path'; -import { - addColors, - createLogger, - format, - transports, -} from 'winston'; +import { addColors, createLogger, format, transports } from 'winston'; import WinstonDaily from 'winston-daily-rotate-file'; import { logFormatOption, logLevelOption } from '~/config'; const { colors, levels } = logFormatOption; -const { - combine, timestamp, printf, colorize, errors, -} = format; +const { combine, timestamp, printf, colorize, errors } = format; addColors(colors); @@ -34,10 +27,7 @@ const logFormat = combine( const consoleOpts = { handleExceptions: true, level: logLevelOption.consoleLogLevel, - format: combine( - colorize({ all: true }), - timestamp({ format: logTimestampFormat }), - ), + format: combine(colorize({ all: true }), timestamp({ format: logTimestampFormat })), }; const logger = createLogger({ @@ -65,14 +55,11 @@ const logger = createLogger({ ], }); -const morganMiddleware = morgan( - ':method :url :status :res[content-length] - :response-time ms', - { - stream: { - // Use the http severity - write: (message: string) => logger.http(message), - }, +const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', { + stream: { + // Use the http severity + write: (message: string) => logger.http(message), }, -); +}); export { logger, morganMiddleware }; diff --git a/backend/src/mysql.ts b/backend/src/mysql.ts index 4940f01f..e8a8de61 100644 --- a/backend/src/mysql.ts +++ b/backend/src/mysql.ts @@ -18,10 +18,7 @@ export const executeQuery = async (queryText: string, values: any[] = []): Promi logger.debug(`Executing query: ${queryText} (${values})`); let result; try { - const queryResult: [ - any, - FieldPacket[] - ] = await connection.query(queryText, values); + const queryResult: [any, FieldPacket[]] = await connection.query(queryText, values); [result] = queryResult; } catch (e) { if (e instanceof Error) { @@ -35,61 +32,62 @@ export const executeQuery = async (queryText: string, values: any[] = []): Promi return result; }; -export const makeExecuteQuery = (connection: mysql.PoolConnection) => async ( - queryText: string, - values: any[] = [], -): Promise => { - logger.debug(`Executing query: ${queryText} (${values})`); - let result; - try { - const queryResult: [ - any, - FieldPacket[] - ] = await connection.query(queryText, values); - [result] = queryResult; - } catch (e) { - if (e instanceof Error) { - logger.error(e); - throw new Error(DBError); +export const makeExecuteQuery = + (connection: mysql.PoolConnection) => + async (queryText: string, values: any[] = []): Promise => { + logger.debug(`Executing query: ${queryText} (${values})`); + let result; + try { + const queryResult: [any, FieldPacket[]] = await connection.query(queryText, values); + [result] = queryResult; + } catch (e) { + if (e instanceof Error) { + logger.error(e); + throw new Error(DBError); + } + throw e; } - throw e; - } - return result; -}; + return result; + }; type lending = { lendingId: number; -} +}; type UserRow = { userId: number; lending: lending[]; -} +}; export const queryTest = async () => { const connection = await pool.getConnection(); - const rows = await connection.query(` + const rows = (await connection.query(` SELECT id AS userId FROM user LIMIT 50; - `) as unknown as UserRow[][]; - const newRows = Promise.all(rows[0].map(async (row) => { - const lendings = await connection.query(` + `)) as unknown as UserRow[][]; + const newRows = Promise.all( + rows[0].map(async (row) => { + const lendings = (await connection.query( + ` SELECT id AS lendingId FROM lending WHERE userId = ? - `, [row.userId]) as unknown as lending[][]; - const newRow = row; - [newRow.lending] = lendings; - // eslint-disable-next-line no-console - console.log(lendings[0]); - return newRow; - })); + `, + [row.userId], + )) as unknown as lending[][]; + const newRow = row; + [newRow.lending] = lendings; + // eslint-disable-next-line no-console + console.log(lendings[0]); + return newRow; + }), + ); // eslint-disable-next-line no-console console.log(newRows); diff --git a/backend/src/v1/DTO/common.interface.ts b/backend/src/v1/DTO/common.interface.ts index 1f2ba1c3..3ccc8bc4 100644 --- a/backend/src/v1/DTO/common.interface.ts +++ b/backend/src/v1/DTO/common.interface.ts @@ -1,23 +1,23 @@ export type Meta = { - totalItems: number, - itemCount: number, - itemsPerPage: number, - totalPages: number, - currentPage: number -} + totalItems: number; + itemCount: number; + itemsPerPage: number; + totalPages: number; + currentPage: number; +}; export type searchQuery = { - nickname: string, - page?: string, - limit?: string, -} + nickname: string; + page?: string; + limit?: string; +}; export type createQuery = { - email: string, - password: string, -} + email: string; + password: string; +}; export type categoryWithBookCount = { - name: string, - bookCount: number, -} + name: string; + bookCount: number; +}; diff --git a/backend/src/v1/DTO/cursus.model.ts b/backend/src/v1/DTO/cursus.model.ts index 73eb8d21..5e828882 100644 --- a/backend/src/v1/DTO/cursus.model.ts +++ b/backend/src/v1/DTO/cursus.model.ts @@ -45,7 +45,7 @@ export type UserProjectFrom42 = { 'active?': boolean; }; teams: object[]; -} +}; export type UserProject = { id: UserProjectFrom42['id']; @@ -56,7 +56,7 @@ export type UserProject = { marked: UserProjectFrom42['marked']; marked_at: UserProjectFrom42['marked_at']; updated_at: UserProjectFrom42['updated_at']; -} +}; export type ProjectFrom42 = { id: number; @@ -73,9 +73,9 @@ export type ProjectFrom42 = { repository: string; cursus: Cursus[]; campus: Campus[]; - videos: [], + videos: []; project_sessions: object[]; -} +}; export type ProjectInfo = { id: number; @@ -87,7 +87,7 @@ export type ProjectInfo = { name: string; slug: string; }[]; -} +}; export type Campus = { id: number; @@ -113,7 +113,7 @@ export type Campus = { public: boolean; email_extension: string; default_hidden_phone: boolean; -} +}; export type Cursus = { id: number; @@ -121,13 +121,13 @@ export type Cursus = { name: string; slug: string; kind: string; -} +}; export type ProjectWithCircle = { [key: string]: { project_ids: number[]; - } -} + }; +}; export type BooksWithProjectInfo = { book_info_id: number; @@ -135,7 +135,7 @@ export type BooksWithProjectInfo = { id: number; circle: number; }[]; -} +}; export type RecommendedBook = { id: number; @@ -145,4 +145,4 @@ export type RecommendedBook = { image: string; publishedAt: string; subjects: string[]; -} +}; diff --git a/backend/src/v1/DTO/tags.model.ts b/backend/src/v1/DTO/tags.model.ts index 0f9c6e31..afcc3ab9 100644 --- a/backend/src/v1/DTO/tags.model.ts +++ b/backend/src/v1/DTO/tags.model.ts @@ -7,7 +7,7 @@ export type subDefaultTag = { content: string; superContent: string; visibility: 'public' | 'private'; -} +}; export type superDefaultTag = { id: number; @@ -15,4 +15,4 @@ export type superDefaultTag = { login: string; count: number; type: 'super' | 'default'; -} +}; diff --git a/backend/src/v1/DTO/users.model.ts b/backend/src/v1/DTO/users.model.ts index 7b145ca7..587e78fa 100644 --- a/backend/src/v1/DTO/users.model.ts +++ b/backend/src/v1/DTO/users.model.ts @@ -1,28 +1,28 @@ export type Lending = { - userId: number, - bookInfoId: number, - lendDate: Date, - lendingCondition: string, - image: string, - author: string, - title: string, - duedate: Date, - overDueDay: number, - reservedNum: number, -} + userId: number; + bookInfoId: number; + lendDate: Date; + lendingCondition: string; + image: string; + author: string; + title: string; + duedate: Date; + overDueDay: number; + reservedNum: number; +}; export type User = { - id: number, - email: string, - nickname: string, - intraId: number, - slack?: string, - penaltyEndDate?: Date, - overDueDay: number, - role: number, - reservations?: [], - lendings?: Lending[], -} + id: number; + email: string; + nickname: string; + intraId: number; + slack?: string; + penaltyEndDate?: Date; + overDueDay: number; + role: number; + reservations?: []; + lendings?: Lending[]; +}; export type PrivateUser = User & { - password: string, -} + password: string; +}; diff --git a/backend/src/v1/auth/auth.controller.ts b/backend/src/v1/auth/auth.controller.ts index 52aadbea..d8f340e4 100644 --- a/backend/src/v1/auth/auth.controller.ts +++ b/backend/src/v1/auth/auth.controller.ts @@ -39,13 +39,23 @@ export const getToken = async (req: Request, res: Response, next: NextFunction): } catch (error: any) { const errorNumber = parseInt(error.message ? error.message : error.errorCode, 10); if (errorNumber === 203) { - res.status(status.BAD_REQUEST).send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; } - res.status(status.SERVICE_UNAVAILABLE).send(``); + res + .status(status.SERVICE_UNAVAILABLE) + .send( + ``, + ); return; } - } else { await authJwt.saveJwt(req, res, user[0]); } + } else { + await authJwt.saveJwt(req, res, user[0]); + } res.status(302).redirect(`${oauthUrlOption.clientURL}/auth`); } catch (error: any) { const errorNumber = parseInt(error.message ? error.message : error.errorCode, 10); @@ -99,7 +109,9 @@ export const login = async (req: Request, res: Response, next: NextFunction): Pr throw new ErrorResponse(errorCode.NO_INPUT, 400); } /* 여기에 id, password의 유효성 검증 한번 더 할 수도 있음 */ - const user: { items: models.PrivateUser[] } = await usersService.searchUserWithPasswordByEmail(id); + const user: { items: models.PrivateUser[] } = await usersService.searchUserWithPasswordByEmail( + id, + ); if (user.items.length === 0) { return next(new ErrorResponse(errorCode.NO_ID, 401)); } @@ -146,40 +158,55 @@ export const intraAuthentication = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { +): Promise => { try { const usersService = new UsersService(); const { intraProfile, id } = req.user as any; const { intraId, nickName } = intraProfile; const user: { items: models.User[] } = await usersService.searchUserById(id); if (user.items.length === 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; // return next(new ErrorResponse(errorCode.NO_USER, 410)); } if (user.items[0].role !== role.user) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); // return next(new ErrorResponse(errorCode.ALREADY_AUTHENTICATED, 401)); } const intraList: models.User[] = await usersService.searchUserByIntraId(intraId); if (intraList.length !== 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); return; // return next(new ErrorResponse(errorCode.ALREADY_AUTHENTICATED, 401)); } const affectedRow = await authService.updateAuthenticationUser(id, intraId, nickName); if (affectedRow === 0) { - res.status(status.BAD_REQUEST) - .send(``); + res + .status(status.BAD_REQUEST) + .send( + ``, + ); // return next(new ErrorResponse(errorCode.NON_AFFECTED, 401)); } await updateSlackIdByUserId(user.items[0].id); await authJwt.saveJwt(req, res, user.items[0]); - res.status(status.OK) - .send(``); + res + .status(status.OK) + .send( + ``, + ); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 100 && errorNumber < 200) { diff --git a/backend/src/v1/auth/auth.jwt.ts b/backend/src/v1/auth/auth.jwt.ts index b9903ea3..fa48fac1 100644 --- a/backend/src/v1/auth/auth.jwt.ts +++ b/backend/src/v1/auth/auth.jwt.ts @@ -28,7 +28,7 @@ export const issueJwt = (user: User) => { * 설정값 설명 * expires: 밀리세컨드 값으로 설정해야하고, 1000 * 60 * 480 = 8시간으로 설정 */ -export const saveJwt = async (req: Request, res: Response, user: User) : Promise => { +export const saveJwt = async (req: Request, res: Response, user: User): Promise => { const token = issueJwt(user); res.cookie('access_token', token, { ...cookieOptions, diff --git a/backend/src/v1/auth/auth.service.ts b/backend/src/v1/auth/auth.service.ts index 273beb05..a0b1275e 100644 --- a/backend/src/v1/auth/auth.service.ts +++ b/backend/src/v1/auth/auth.service.ts @@ -10,12 +10,15 @@ export const updateAuthenticationUser = async ( id: number, intraId: number, nickname: string, -) : Promise => { - const result : ResultSetHeader = await executeQuery(` +): Promise => { + const result: ResultSetHeader = await executeQuery( + ` UPDATE user SET intraId = ?, nickname = ?, role = ? WHERE id = ? - `, [intraId, nickname, role.cadet, id]); + `, + [intraId, nickname, role.cadet, id], + ); return result.affectedRows; }; @@ -34,10 +37,16 @@ export const getAccessToken = async (): Promise => { 'Content-Type': 'application/json', }, data: queryString, - }).then((response) => { - accessToken = response.data.access_token; - }).catch((error) => { - throw new ErrorResponse(httpStatus[500], httpStatus.INTERNAL_SERVER_ERROR, '42 API로부터 토큰을 받아오는데 실패했습니다.'); - }); + }) + .then((response) => { + accessToken = response.data.access_token; + }) + .catch((error) => { + throw new ErrorResponse( + httpStatus[500], + httpStatus.INTERNAL_SERVER_ERROR, + '42 API로부터 토큰을 받아오는데 실패했습니다.', + ); + }); return accessToken; }; diff --git a/backend/src/v1/auth/auth.strategy.ts b/backend/src/v1/auth/auth.strategy.ts index cb7d81ae..3fc79a79 100644 --- a/backend/src/v1/auth/auth.strategy.ts +++ b/backend/src/v1/auth/auth.strategy.ts @@ -40,9 +40,7 @@ export const FtAuthentication = new FortyTwoStrategy( export const JwtStrategy = new JWTStrategy( { - jwtFromRequest: ExtractJwt.fromExtractors([ - (req: Request) => req?.cookies?.access_token, - ]), + jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => req?.cookies?.access_token]), secretOrKey: jwtOption.secret, ignoreExpiration: false, issuer: jwtOption.issuer, diff --git a/backend/src/v1/auth/auth.type.ts b/backend/src/v1/auth/auth.type.ts index cf4691b3..24f9cf57 100644 --- a/backend/src/v1/auth/auth.type.ts +++ b/backend/src/v1/auth/auth.type.ts @@ -2,10 +2,10 @@ /* eslint-disable no-shadow */ export const enum role { - user = 0, - cadet, - librarian, - staff, + user = 0, + cadet, + librarian, + staff, } export const roleSet = { diff --git a/backend/src/v1/auth/auth.validate.ts b/backend/src/v1/auth/auth.validate.ts index e70d59a5..4ecff381 100644 --- a/backend/src/v1/auth/auth.validate.ts +++ b/backend/src/v1/auth/auth.validate.ts @@ -11,62 +11,66 @@ import { role } from './auth.type'; const usersService = new UsersService(); -const authValidate = (roles: role[]) => async ( - req: Request, - res: Response, - next: Function, -) : Promise => { - try { - if (!req.cookies.access_token) { - if (roles.includes(role.user)) { - req.user = { - intraProfile: null, id: null, role: role.user, nickname: null, - }; - return next(); +const authValidate = + (roles: role[]) => + async (req: Request, res: Response, next: Function): Promise => { + try { + if (!req.cookies.access_token) { + if (roles.includes(role.user)) { + req.user = { + intraProfile: null, + id: null, + role: role.user, + nickname: null, + }; + return next(); + } + throw new ErrorResponse(errorCode.NO_TOKEN, 401); + } + // 토큰 복호화 + const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); + const { id } = verifyCheck as any; + const user: { items: User[] } = await usersService.searchUserById(id); + // User가 없는 경우 + if (user.items.length === 0) { + throw new ErrorResponse(errorCode.NO_USER, 410); + } + // 권한이 있지 않은 경우 + if (!roles.includes(user.items[0].role)) { + throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); + } + req.user = { + intraProfile: req.user, + id, + role: user.items[0].role, + nickname: user.items[0].nickname, + }; + next(); + } catch (error: any) { + switch (error.message) { + // 토큰에 대한 오류를 판단합니다. + case 'INVALID_TOKEN': + case 'TOKEN_IS_ARRAY': + case 'NO_USER': + return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); + case 'EXPIRED_TOKEN': + return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); + default: + break; + } + if (error instanceof ErrorResponse) { + next(error); + } + const errorNumber = parseInt(error.message, 10); + if (errorNumber >= 100 && errorNumber < 200) { + next(new ErrorResponse(error.message, status.BAD_REQUEST)); + } else if (error.message === 'DB error') { + next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); + } else { + logger.error(error); + next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - throw new ErrorResponse(errorCode.NO_TOKEN, 401); - } - // 토큰 복호화 - const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); - const { id } = verifyCheck as any; - const user: { items: User[] } = await usersService.searchUserById(id); - // User가 없는 경우 - if (user.items.length === 0) { - throw new ErrorResponse(errorCode.NO_USER, 410); - } - // 권한이 있지 않은 경우 - if (!roles.includes(user.items[0].role)) { - throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); - } - req.user = { - intraProfile: req.user, id, role: user.items[0].role, nickname: user.items[0].nickname, - }; - next(); - } catch (error: any) { - switch (error.message) { - // 토큰에 대한 오류를 판단합니다. - case 'INVALID_TOKEN': - case 'TOKEN_IS_ARRAY': - case 'NO_USER': - return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); - case 'EXPIRED_TOKEN': - return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); - default: - break; - } - if (error instanceof ErrorResponse) { - next(error); - } - const errorNumber = parseInt(error.message, 10); - if (errorNumber >= 100 && errorNumber < 200) { - next(new ErrorResponse(error.message, status.BAD_REQUEST)); - } else if (error.message === 'DB error') { - next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); - } else { - logger.error(error); - next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } -}; + }; export default authValidate; diff --git a/backend/src/v1/auth/auth.validateDefaultNullUser.ts b/backend/src/v1/auth/auth.validateDefaultNullUser.ts index 17f60864..272234b0 100644 --- a/backend/src/v1/auth/auth.validateDefaultNullUser.ts +++ b/backend/src/v1/auth/auth.validateDefaultNullUser.ts @@ -11,56 +11,54 @@ import { role } from './auth.type'; const usersService = new UsersService(); -const authValidateDefaultNullUser = (roles: role[]) => async ( - req: Request, - res: Response, - next: Function, -) : Promise => { - if (!req.cookies.access_token) { - req.user = { intraProfile: null, id: null, role: null }; - next(); - } else { - try { - // 토큰 복호화 - const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); - const { id } = verifyCheck as any; - const user: { items: User[] } = await usersService.searchUserById(id); - // User가 없는 경우 - if (user.items.length === 0) { - throw new ErrorResponse(errorCode.NO_USER, 410); - } - // 권한이 있지 않은 경우 - if (!roles.includes(user.items[0].role)) { - throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); - } - req.user = { intraProfile: req.user, id, role: user.items[0].role }; +const authValidateDefaultNullUser = + (roles: role[]) => + async (req: Request, res: Response, next: Function): Promise => { + if (!req.cookies.access_token) { + req.user = { intraProfile: null, id: null, role: null }; next(); - } catch (error: any) { - switch (error.message) { - // 토큰에 대한 오류를 판단합니다. - case 'INVALID_TOKEN': - case 'TOKEN_IS_ARRAY': - case 'NO_USER': - return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); - case 'EXPIRED_TOKEN': - return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); - default: - break; - } - if (error instanceof ErrorResponse) { - next(error); - } - const errorNumber = parseInt(error.message, 10); - if (errorNumber >= 100 && errorNumber < 200) { - next(new ErrorResponse(error.message, status.BAD_REQUEST)); - } else if (error.message === 'DB error') { - next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); - } else { - logger.error(error); - next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); + } else { + try { + // 토큰 복호화 + const verifyCheck = verify(req.cookies.access_token, jwtOption.secret); + const { id } = verifyCheck as any; + const user: { items: User[] } = await usersService.searchUserById(id); + // User가 없는 경우 + if (user.items.length === 0) { + throw new ErrorResponse(errorCode.NO_USER, 410); + } + // 권한이 있지 않은 경우 + if (!roles.includes(user.items[0].role)) { + throw new ErrorResponse(errorCode.NO_AUTHORIZATION, 403); + } + req.user = { intraProfile: req.user, id, role: user.items[0].role }; + next(); + } catch (error: any) { + switch (error.message) { + // 토큰에 대한 오류를 판단합니다. + case 'INVALID_TOKEN': + case 'TOKEN_IS_ARRAY': + case 'NO_USER': + return next(new ErrorResponse(errorCode.TOKEN_NOT_VALID, status.UNAUTHORIZED)); + case 'EXPIRED_TOKEN': + return next(new ErrorResponse(errorCode.EXPIRATION_TOKEN, status.GONE)); + default: + break; + } + if (error instanceof ErrorResponse) { + next(error); + } + const errorNumber = parseInt(error.message, 10); + if (errorNumber >= 100 && errorNumber < 200) { + next(new ErrorResponse(error.message, status.BAD_REQUEST)); + } else if (error.message === 'DB error') { + next(new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR)); + } else { + logger.error(error); + next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); + } } } - } -}; + }; export default authValidateDefaultNullUser; diff --git a/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts b/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts index af7255c5..3c40be90 100644 --- a/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts +++ b/backend/src/v1/book-info-reviews/controller/bookInfoReviews.controller.ts @@ -1,19 +1,13 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import * as parseCheck from './utils/parseCheck'; import * as bookInfoReviewsService from '../service/bookInfoReviews.service'; import * as errorCheck from './utils/errorCheck'; -export const getBookInfoReviewsPage = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getBookInfoReviewsPage = async (req: Request, res: Response, next: NextFunction) => { const bookInfoId = errorCheck.bookInfoParseCheck(req?.params?.bookInfoId); const reviewsId = parseCheck.reviewsIdParse(req?.query?.reviewsId); - const sort : 'asc' | 'desc' = parseCheck.sortParse(req?.query?.sort); + const sort: 'asc' | 'desc' = parseCheck.sortParse(req?.query?.sort); const limit = parseInt(String(req?.query?.limit), 10); return res .status(status.OK) diff --git a/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts b/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts index 45419aeb..44c7d49d 100644 --- a/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts +++ b/backend/src/v1/book-info-reviews/controller/utils/errorCheck.ts @@ -1,16 +1,14 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; -export const bookInfoParseCheck = ( - bookInfoId : string, -) => { - let result : number; +export const bookInfoParseCheck = (bookInfoId: string) => { + let result: number; if (bookInfoId.trim() === '') { throw new ErrorResponse(errorCode.INVALID_INPUT, 400); } try { result = parseInt(bookInfoId, 10); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.INVALID_INPUT, 400); } return result; diff --git a/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts b/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts index c0452725..aee3b957 100644 --- a/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts +++ b/backend/src/v1/book-info-reviews/controller/utils/parseCheck.ts @@ -1,18 +1,14 @@ -export const reviewsIdParse = ( - reviewsId : any, -) => { - let result : number; +export const reviewsIdParse = (reviewsId: any) => { + let result: number; try { result = parseInt(reviewsId, 10); - } catch (error : any) { + } catch (error: any) { result = NaN; } return result; }; -export const sortParse = ( - sort : any, -) : 'asc' | 'desc' => { +export const sortParse = (sort: any): 'asc' | 'desc' => { if (sort === 'asc' || sort === 'desc') { return sort; } diff --git a/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts b/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts index 3d233509..97e42a1a 100644 --- a/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts +++ b/backend/src/v1/book-info-reviews/repository/bookInfoReviews.repository.ts @@ -1,12 +1,19 @@ import { executeQuery } from '~/mysql'; -export const getBookinfoReviewsPageNoOffset = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc', limit: number) => { - const bookInfoIdQuery = (Number.isNaN(bookInfoId)) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; +export const getBookinfoReviewsPageNoOffset = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', + limit: number, +) => { + const bookInfoIdQuery = Number.isNaN(bookInfoId) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; const sign = sort === 'asc' ? '>' : '<'; - const reviewIdQuery = (Number.isNaN(reviewsId)) ? '' : `AND reviews.id ${sign} ${reviewsId}`; + const reviewIdQuery = Number.isNaN(reviewsId) ? '' : `AND reviews.id ${sign} ${reviewsId}`; const sortQuery = `ORDER BY reviews.id ${sort}`; - if (bookInfoIdQuery === '') { return []; } - const limitQuery = (Number.isNaN(limit)) ? 'LIMIT 10' : `LIMIT ${limit}`; + if (bookInfoIdQuery === '') { + return []; + } + const limitQuery = Number.isNaN(limit) ? 'LIMIT 10' : `LIMIT ${limit}`; const reviews = await executeQuery(` SELECT @@ -27,13 +34,17 @@ export const getBookinfoReviewsPageNoOffset = async (bookInfoId: number, reviews ${sortQuery} ${limitQuery} `); - return (reviews); + return reviews; }; -export const getBookInfoReviewsCounts = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc') => { - const bookInfoIdQuery = (Number.isNaN(bookInfoId)) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; +export const getBookInfoReviewsCounts = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', +) => { + const bookInfoIdQuery = Number.isNaN(bookInfoId) ? '' : `AND reviews.bookInfoId = ${bookInfoId}`; const sign = sort === 'asc' ? '>' : '<'; - const reviewIdQuery = (Number.isNaN(reviewsId)) ? '' : `AND reviews.id ${sign} ${reviewsId}`; + const reviewIdQuery = Number.isNaN(reviewsId) ? '' : `AND reviews.id ${sign} ${reviewsId}`; const counts = await executeQuery(` SELECT COUNT(*) as counts @@ -43,5 +54,5 @@ export const getBookInfoReviewsCounts = async (bookInfoId: number, reviewsId: nu ${bookInfoIdQuery} ${reviewIdQuery} `); - return (counts[0].counts); + return counts[0].counts; }; diff --git a/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts b/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts index f54eea16..3c62bcd8 100644 --- a/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts +++ b/backend/src/v1/book-info-reviews/service/bookInfoReviews.service.ts @@ -1,11 +1,23 @@ import * as bookInfoReviewsRepository from '../repository/bookInfoReviews.repository'; -export const getPageNoOffset = async (bookInfoId: number, reviewsId: number, sort: 'asc' | 'desc', limit: number) => { - const items = await bookInfoReviewsRepository - .getBookinfoReviewsPageNoOffset(bookInfoId, reviewsId, sort, limit); - const counts = await bookInfoReviewsRepository - .getBookInfoReviewsCounts(bookInfoId, reviewsId, sort); - const itemsPerPage = (Number.isNaN(limit)) ? 10 : limit; +export const getPageNoOffset = async ( + bookInfoId: number, + reviewsId: number, + sort: 'asc' | 'desc', + limit: number, +) => { + const items = await bookInfoReviewsRepository.getBookinfoReviewsPageNoOffset( + bookInfoId, + reviewsId, + sort, + limit, + ); + const counts = await bookInfoReviewsRepository.getBookInfoReviewsCounts( + bookInfoId, + reviewsId, + sort, + ); + const itemsPerPage = Number.isNaN(limit) ? 10 : limit; const finalReviewsId = items[items.length - 1]?.reviewsId; const meta = { totalLeftItems: counts, diff --git a/backend/src/v1/books/Likes.repository.ts b/backend/src/v1/books/Likes.repository.ts index 0921ebdf..bd6a316f 100644 --- a/backend/src/v1/books/Likes.repository.ts +++ b/backend/src/v1/books/Likes.repository.ts @@ -8,7 +8,7 @@ class LikesRepository extends Repository { super(Likes, entityManager); } - async getLikesByBookInfoId(bookInfoId: number) : Promise { + async getLikesByBookInfoId(bookInfoId: number): Promise { const likes = this.find({ where: { bookInfoId, @@ -17,7 +17,7 @@ class LikesRepository extends Repository { return likes; } - async getLikesByUserId(userId: number) : Promise { + async getLikesByUserId(userId: number): Promise { const likes = await this.find({ where: { userId, diff --git a/backend/src/v1/books/books.controller.ts b/backend/src/v1/books/books.controller.ts index 765fcea0..fe4f80d2 100644 --- a/backend/src/v1/books/books.controller.ts +++ b/backend/src/v1/books/books.controller.ts @@ -1,7 +1,5 @@ /* eslint-disable import/no-unresolved */ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -26,21 +24,15 @@ const pubdateFormatValidator = (pubdate: string | Date) => { return true; }; -const bookStatusFormatValidator = (bookStatus : number) => { +const bookStatusFormatValidator = (bookStatus: number) => { if (bookStatus < 0 || bookStatus > 3) { return false; } return true; }; -export const createBook = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const { - title, author, categoryId, pubdate, - } = req.body; +export const createBook = async (req: Request, res: Response, next: NextFunction) => { + const { title, author, categoryId, pubdate } = req.body; if (!(title && author && categoryId && pubdate)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -48,9 +40,7 @@ export const createBook = async ( return next(new ErrorResponse(errorCode.INVALID_PUBDATE_FORNAT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .send(await BooksService.createBook(req.body)); + return res.status(status.OK).send(await BooksService.createBook(req.body)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -64,19 +54,13 @@ export const createBook = async ( return 0; }; -export const createBookInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const isbn = req.query.isbnQuery ? req.query.isbnQuery as string : ''; +export const createBookInfo = async (req: Request, res: Response, next: NextFunction) => { + const isbn = req.query.isbnQuery ? (req.query.isbnQuery as string) : ''; if (isbn === '') { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .send(await BooksService.createBookInfo(isbn)); + return res.status(status.OK).send(await BooksService.createBookInfo(isbn)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -98,9 +82,7 @@ export const searchBookInfo = async ( // URI에 있는 파라미터/쿼리 변수에 저장 let query = req.query?.query ?? ''; query = query.trim(); - const { - page, limit, sort, category, - } = req.query; + const { page, limit, sort, category } = req.query; // 유효한 인자인지 파악 if (Number.isNaN(page) || Number.isNaN(limit)) { @@ -117,9 +99,7 @@ export const searchBookInfo = async ( category, ); logger.info(`[ES_S] : ${JSON.stringify(searchBookInfoResult.items)}`); - return res - .status(status.OK) - .json(searchBookInfoResult); + return res.status(status.OK).json(searchBookInfoResult); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -146,9 +126,9 @@ export const searchBookInfoByTag = async ( const category = parseCheck.stringQueryParse(rawData.category); try { - return res.status(status.OK).json( - await BooksService.searchInfoByTag(query, page, limit, sort, category), - ); + return res + .status(status.OK) + .json(await BooksService.searchInfoByTag(query, page, limit, sort, category)); } catch (error: any) { return next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } @@ -165,9 +145,7 @@ export const getBookById: RequestHandler = async ( } try { const bookInfo = await BooksService.getBookById(req.params.id); - return res - .status(status.OK) - .json(bookInfo); + return res.status(status.OK).json(bookInfo); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -193,9 +171,7 @@ export const getInfoId: RequestHandler = async ( try { const bookInfo = await BooksService.getInfo(req.params.id); logger.info(`[ES_C] : ${JSON.stringify(bookInfo)}`); - return res - .status(status.OK) - .json(bookInfo); + return res.status(status.OK).json(bookInfo); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -220,9 +196,7 @@ export const sortInfo = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await BooksService.sortInfo(limit, sort)); + return res.status(status.OK).json(await BooksService.sortInfo(limit, sort)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -236,11 +210,7 @@ export const sortInfo = async ( return 0; }; -export const search = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search = async (req: Request, res: Response, next: NextFunction) => { const query = String(req.query.query) === 'undefined' ? ' ' : String(req.query.query); const page = parseInt(String(req.query.page), 10); const limit = parseInt(String(req.query.limit), 10); @@ -249,9 +219,7 @@ export const search = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await BooksService.search(query, page, limit)); + return res.status(status.OK).json(await BooksService.search(query, page, limit)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 300 && errorNumber < 400) { @@ -265,11 +233,7 @@ export const search = async ( return 0; }; -export const createLike = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createLike = async (req: Request, res: Response, next: NextFunction) => { // parameters const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); const { id } = req.user as any; @@ -295,17 +259,15 @@ export const createLike = async ( return 0; }; -export const deleteLike = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteLike = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; const parameter = String(req?.params); const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); // parameter 검증 - if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } + if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { + return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); + } // 로직수행 및 에러처리 try { @@ -324,18 +286,16 @@ export const deleteLike = async ( return 0; }; -export const getLikeInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getLikeInfo = async (req: Request, res: Response, next: NextFunction) => { // parameters const { id } = req.user as any; const parameter = String(req?.params); const bookInfoId = parseInt(String(req?.params?.bookInfoId), 10); // parameter 검증 - if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } + if (parameter === 'undefined' || Number.isNaN(bookInfoId)) { + return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); + } // 로직수행 및 에러처리 try { @@ -353,11 +313,7 @@ export const getLikeInfo = async ( return 0; }; -export const updateBookInfo = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateBookInfo = async (req: Request, res: Response, next: NextFunction) => { const bookInfo: types.UpdateBookInfo = { id: req.body.bookInfoId, title: req.body.title, @@ -376,24 +332,51 @@ export const updateBookInfo = async ( if (book.id <= 0 || Number.isNaN(book.id) || bookInfo.id <= 0 || Number.isNaN(bookInfo.id)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - if (!(bookInfo.title || bookInfo.author || bookInfo.publisher || bookInfo.image - || bookInfo.categoryId || bookInfo.publishedAt || book.callSign || book.status !== undefined)) { return next(new ErrorResponse(errorCode.NO_BOOK_INFO_DATA, status.BAD_REQUEST)); } + if ( + !( + bookInfo.title || + bookInfo.author || + bookInfo.publisher || + bookInfo.image || + bookInfo.categoryId || + bookInfo.publishedAt || + book.callSign || + book.status !== undefined + ) + ) { + return next(new ErrorResponse(errorCode.NO_BOOK_INFO_DATA, status.BAD_REQUEST)); + } - if (!isNullish(bookInfo.title)) { bookInfo.title.trim(); } - if (!isNullish(bookInfo.author)) { bookInfo.author.trim(); } - if (!isNullish(bookInfo.publisher)) { bookInfo.publisher.trim(); } - if (!isNullish(bookInfo.image)) { bookInfo.image.trim(); } + if (!isNullish(bookInfo.title)) { + bookInfo.title.trim(); + } + if (!isNullish(bookInfo.author)) { + bookInfo.author.trim(); + } + if (!isNullish(bookInfo.publisher)) { + bookInfo.publisher.trim(); + } + if (!isNullish(bookInfo.image)) { + bookInfo.image.trim(); + } if (!isNullish(bookInfo.publishedAt) && pubdateFormatValidator(bookInfo.publishedAt)) { String(bookInfo.publishedAt).trim(); - } else if (!isNullish(bookInfo.publishedAt) && pubdateFormatValidator(bookInfo.publishedAt) === false) { + } else if ( + !isNullish(bookInfo.publishedAt) && + pubdateFormatValidator(bookInfo.publishedAt) === false + ) { return next(new ErrorResponse(errorCode.INVALID_PUBDATE_FORNAT, status.BAD_REQUEST)); } - if (isNullish(book.callSign) === false) { book.callSign.trim(); } + if (isNullish(book.callSign) === false) { + book.callSign.trim(); + } if (bookStatusFormatValidator(book.status) === false) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - if (book.id) { await BooksService.updateBook(book, bookInfo); } + if (book.id) { + await BooksService.updateBook(book, bookInfo); + } return res.status(status.NO_CONTENT).send(); } catch (error: any) { const errorNumber = parseInt(error.message, 10); @@ -408,30 +391,28 @@ export const updateBookInfo = async ( return 0; }; -export const updateBookDonator = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateBookDonator = async (req: Request, res: Response, next: NextFunction) => { const parsed = searchSchema.safeParse(req.body); if (!parsed.success) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - const { - nicknameOrEmail, page, limit, - } = parsed.data; + const { nicknameOrEmail, page, limit } = parsed.data; let items; let user; try { if (nicknameOrEmail) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), - )); + items = JSON.parse( + JSON.stringify( + await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), + ), + ); } if (items) { - items.items = await Promise.all(items.items.map(async (data: User) => ({ - ...data, - }))); + items.items = await Promise.all( + items.items.map(async (data: User) => ({ + ...data, + })), + ); } if (items.items[0]) { user = items.items[0]; @@ -447,7 +428,9 @@ export const updateBookDonator = async ( if (bookDonator.id <= 0 || Number.isNaN(bookDonator.id)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - if (bookDonator.id) { await BooksService.updateBookDonator(bookDonator); } + if (bookDonator.id) { + await BooksService.updateBookDonator(bookDonator); + } return res.status(status.NO_CONTENT).send(); } catch (error: any) { diff --git a/backend/src/v1/books/books.model.ts b/backend/src/v1/books/books.model.ts index bcf01d6f..8b8c8e69 100644 --- a/backend/src/v1/books/books.model.ts +++ b/backend/src/v1/books/books.model.ts @@ -11,38 +11,38 @@ export type BookInfo = RowDataPacket & { publishedAt?: string | Date; createdAt: Date; updatedAt: Date; -} +}; export type BookEach = RowDataPacket & { - id?: number; - donator: string; - donatorId?: number; - callSign: string; - status: number; - createdAt: Date; - updatedAt: Date; - infoId: number; -} + id?: number; + donator: string; + donatorId?: number; + callSign: string; + status: number; + createdAt: Date; + updatedAt: Date; + infoId: number; +}; export type Book = { - title: string; - author: string; - publisher: string; - isbn: string; - image?: string; - category: string; - publishedAt?: Date; - donator?: string; - callSign: string; - status: number; -} + title: string; + author: string; + publisher: string; + isbn: string; + image?: string; + category: string; + publishedAt?: Date; + donator?: string; + callSign: string; + status: number; +}; export type categoryCount = RowDataPacket & { - name: string; - count: number; -} + name: string; + count: number; +}; export type lending = RowDataPacket & { - lendingCreatedAt: Date; - returningCreatedAt: Date; -} + lendingCreatedAt: Date; + returningCreatedAt: Date; +}; diff --git a/backend/src/v1/books/books.repository.ts b/backend/src/v1/books/books.repository.ts index fdd3933d..a16a2d25 100644 --- a/backend/src/v1/books/books.repository.ts +++ b/backend/src/v1/books/books.repository.ts @@ -4,12 +4,14 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import jipDataSource from '~/app-data-source'; import { VSearchBookByTag } from '~/entity/entities/VSearchBookByTag'; -import { - Book, BookInfo, User, Lending, Category, VSearchBook, -} from '~/entity/entities'; +import { Book, BookInfo, User, Lending, Category, VSearchBook } from '~/entity/entities'; import { number } from 'zod'; import { - CreateBookInfo, LendingBookList, UpdateBook, UpdateBookInfo, UpdateBookDonator, + CreateBookInfo, + LendingBookList, + UpdateBook, + UpdateBookInfo, + UpdateBookDonator, } from './books.type'; class BooksRepository extends Repository { @@ -47,11 +49,7 @@ class BooksRepository extends Repository { return this.users.count({ where: { nickname } }); } - async getBookList( - condition: string, - limit: number, - page: number, - ): Promise { + async getBookList(condition: string, limit: number, page: number): Promise { const searchBook = await this.searchBook.find({ where: [ { title: Like(`%${condition}%`) }, @@ -116,14 +114,10 @@ class BooksRepository extends Repository { } // TODO: refactact sort type - async getLendingBookList( - sort: string, - limit: number, - ): Promise { + async getLendingBookList(sort: string, limit: number): Promise { const order = sort === 'popular' ? 'lendingCnt' : 'createdAt'; - const lendingCondition: string = sort === 'popular' - ? 'and lending.createdAt >= date_sub(now(), interval 42 day)' - : ''; + const lendingCondition: string = + sort === 'popular' ? 'and lending.createdAt >= date_sub(now(), interval 42 day)' : ''; const lendingBookList = this.bookInfo .createQueryBuilder('book_info') @@ -139,11 +133,7 @@ class BooksRepository extends Repository { .addSelect('book_info.updatedAt', 'updatedAt') .addSelect('COUNT(lending.id)', 'lendingCnt') .leftJoin(Book, 'book', 'book.infoId = book_info.id') - .leftJoin( - Lending, - 'lending', - `lending.bookId = book.id ${lendingCondition}`, - ) + .leftJoin(Lending, 'lending', `lending.bookId = book.id ${lendingCondition}`) .leftJoin(Category, 'category', 'category.id = book_info.categoryId') .limit(limit) .groupBy('book_info.id') @@ -154,22 +144,14 @@ class BooksRepository extends Repository { } async getNewCallsignPrimaryNum(categoryId: string | undefined): Promise { - return ( - (await this.bookInfo.countBy({ categoryId: Number(categoryId) })) + 1 - ); + return (await this.bookInfo.countBy({ categoryId: Number(categoryId) })) + 1; } async getOldCallsignNums(categoryAlphabet: string) { return this.books .createQueryBuilder() - .select( - "substring(SUBSTRING_INDEX(callSign, '.', 1),2)", - 'recommendPrimaryNum', - ) - .addSelect( - "substring(SUBSTRING_INDEX(callSign, '.', -1),2)", - 'recommendCopyNum', - ) + .select("substring(SUBSTRING_INDEX(callSign, '.', 1),2)", 'recommendPrimaryNum') + .addSelect("substring(SUBSTRING_INDEX(callSign, '.', -1),2)", 'recommendCopyNum') .where('callsign like :categoryAlphabet', { categoryAlphabet: `${categoryAlphabet}%`, }) @@ -191,9 +173,7 @@ class BooksRepository extends Repository { await this.books.update(bookDonator.id, bookDonator as Book); } - async createBookInfo( - target: CreateBookInfo, - ): Promise { + async createBookInfo(target: CreateBookInfo): Promise { const bookInfo: BookInfo = { title: target.title, author: target.author, @@ -206,9 +186,7 @@ class BooksRepository extends Repository { return this.bookInfo.save(bookInfo); } - async createBook( - target: CreateBookInfo, - ): Promise { + async createBook(target: CreateBookInfo): Promise { const book: Book = { donator: target.donator, donatorId: target.donatorId, @@ -220,7 +198,8 @@ class BooksRepository extends Repository { } async findBooksByIds(idList: number[]) { - const bookList = await this.bookInfo.createQueryBuilder('bi') + const bookList = await this.bookInfo + .createQueryBuilder('bi') .select('bi.id', 'id') .addSelect('bi.title', 'title') .addSelect('bi.author', 'author') diff --git a/backend/src/v1/books/books.service.spec.ts b/backend/src/v1/books/books.service.spec.ts index 4317cc80..df96baf6 100644 --- a/backend/src/v1/books/books.service.spec.ts +++ b/backend/src/v1/books/books.service.spec.ts @@ -4,7 +4,10 @@ import { CreateBookInfo } from './books.type'; describe('BooksService', () => { beforeAll(async () => { - await jipDataSource.initialize().then(() => console.log('good!')).catch((err) => console.log(err)); + await jipDataSource + .initialize() + .then(() => console.log('good!')) + .catch((err) => console.log(err)); }); afterAll(() => { jipDataSource.destroy(); diff --git a/backend/src/v1/books/books.service.ts b/backend/src/v1/books/books.service.ts index f330b844..1438b150 100644 --- a/backend/src/v1/books/books.service.ts +++ b/backend/src/v1/books/books.service.ts @@ -16,8 +16,12 @@ import { import * as models from './books.model'; import BooksRepository from './books.repository'; import { - CreateBookInfo, LendingBookList, UpdateBook, UpdateBookInfo, - categoryIds, UpdateBookDonator, + CreateBookInfo, + LendingBookList, + UpdateBook, + UpdateBookInfo, + categoryIds, + UpdateBookDonator, } from './books.type'; import { categoryWithBookCount } from '../DTO/common.interface'; import * as searchKeywordsService from '../search-keywords/searchKeywords.service'; @@ -27,21 +31,33 @@ const getInfoInNationalLibrary = async (isbn: string) => { let book; let searchResult; await axios - .get(`https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`) + .get( + `https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`, + ) .then((res) => { searchResult = res.data.docs[0]; const { - TITLE: title, SUBJECT: category, PUBLISHER: publisher, PUBLISH_PREDATE: pubdate, + TITLE: title, + SUBJECT: category, + PUBLISHER: publisher, + PUBLISH_PREDATE: pubdate, } = searchResult; - const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice(-3)}/x${isbn}.jpg`; + const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice( + -3, + )}/x${isbn}.jpg`; book = { - title, image, category, isbn, publisher, pubdate, + title, + image, + category, + isbn, + publisher, + pubdate, }; }) .catch(() => { throw new Error(errorCode.ISBN_SEARCH_FAILED); }); - return (book); + return book; }; const getAuthorInNaver = async (isbn: string) => { @@ -64,10 +80,10 @@ const getAuthorInNaver = async (isbn: string) => { .catch(() => { throw new Error(errorCode.ISBN_SEARCH_FAILED_IN_NAVER); }); - return (author); + return author; }; -const getCategoryAlphabet = (categoryId : number): string => { +const getCategoryAlphabet = (categoryId: number): string => { try { const category = Object.values(categoryIds) as string[]; return category[categoryId - 1]; @@ -76,11 +92,7 @@ const getCategoryAlphabet = (categoryId : number): string => { } }; -export const search = async ( - query: string, - page: number, - limit: number, -) => { +export const search = async (query: string, page: number, limit: number) => { const booksRepository = new BooksRepository(); const bookList = await booksRepository.getBookList(query, limit, page); const totalItems = await booksRepository.getTotalItems(query); @@ -110,7 +122,9 @@ export const createBook = async (book: CreateBookInfo) => { let recommendPrimaryNum; if (checkNickName > 1) { - logger.warn(`${errorCode.SLACKID_OVERLAP}: nickname이 중복입니다. 최근에 가입한 user의 ID로 기부가 기록됩니다.`); + logger.warn( + `${errorCode.SLACKID_OVERLAP}: nickname이 중복입니다. 최근에 가입한 user의 ID로 기부가 기록됩니다.`, + ); } if (isbnInBookInfo === 0) { @@ -128,10 +142,12 @@ export const createBook = async (book: CreateBookInfo) => { recommendPrimaryNum = nums.recommendPrimaryNum; recommendCopyNum = nums.recommendCopyNum * 1 + 1; } - const recommendCallSign = `${categoryAlphabet}${recommendPrimaryNum}.${String(book.pubdate).slice(2, 4)}.v1.c${recommendCopyNum}`; + const recommendCallSign = `${categoryAlphabet}${recommendPrimaryNum}.${String( + book.pubdate, + ).slice(2, 4)}.v1.c${recommendCopyNum}`; await booksRepository.createBook({ ...book, callSign: recommendCallSign }); await transactionQueryRunner.commitTransaction(); - return ({ callsign: recommendCallSign }); + return { callsign: recommendCallSign }; } catch (error) { await transactionQueryRunner.rollbackTransaction(); if (error instanceof Error) { @@ -140,7 +156,7 @@ export const createBook = async (book: CreateBookInfo) => { } finally { await transactionQueryRunner.release(); } - return (new Error(errorCode.FAIL_CREATE_BOOK_BY_UNEXPECTED)); + return new Error(errorCode.FAIL_CREATE_BOOK_BY_UNEXPECTED); }; export const createBookInfo = async (isbn: string) => { @@ -149,10 +165,7 @@ export const createBookInfo = async (isbn: string) => { return { bookInfo }; }; -export const sortInfo = async ( - limit: number, - sort: string, -) => { +export const sortInfo = async (limit: number, sort: string) => { const booksRepository = new BooksRepository(); const bookList: LendingBookList[] = await booksRepository.getLendingBookList(sort, limit); return { items: bookList }; @@ -336,10 +349,7 @@ export const searchInfoByTag = async ( default: sortQuery = { createdAt: 'DESC' }; } - const whereQuery: Array = [ - { superTagContent: query }, - { subTagContent: query }, - ]; + const whereQuery: Array = [{ superTagContent: query }, { subTagContent: query }]; if (category) { whereQuery.push({ category }); } @@ -466,7 +476,10 @@ export const getInfo = async (id: string) => { } const { ...rest } = eachBook; return { - ...rest, dueDate, isLendable, isReserved, + ...rest, + dueDate, + isLendable, + isReserved, }; }), ); @@ -490,9 +503,9 @@ export const updateBook = async (book: UpdateBook, bookInfo: UpdateBookInfo) => await booksRepository.updateBook(book); if (bookInfo.id) { await booksRepository.updateBookInfo(bookInfo); - const keyword = await bookInfoSearchKeywordRepository.getBookInfoSearchKeyword( - { bookInfoId: bookInfo.id }, - ); + const keyword = await bookInfoSearchKeywordRepository.getBookInfoSearchKeyword({ + bookInfoId: bookInfo.id, + }); if (keyword?.id) { await bookInfoSearchKeywordRepository.updateBookInfoSearchKeyword(keyword.id, bookInfo); } diff --git a/backend/src/v1/books/books.type.ts b/backend/src/v1/books/books.type.ts index baa40af4..e746a807 100644 --- a/backend/src/v1/books/books.type.ts +++ b/backend/src/v1/books/books.type.ts @@ -1,41 +1,41 @@ export type SearchBookInfoQuery = { - query: string; - sort: string; - page: string; - limit: string; - category: string; -} + query: string; + sort: string; + page: string; + limit: string; + category: string; +}; export type SortInfoType = { - sort: string; - limit: string; -} + sort: string; + limit: string; +}; export type LendingBookList = { - id: number; - title: string; - author: string; - publisher: string; - isbn: string; - image: string; - publishedAt: Date | string; - updatedAt: Date | string; - lendingCnt: number; -} + id: number; + title: string; + author: string; + publisher: string; + isbn: string; + image: string; + publishedAt: Date | string; + updatedAt: Date | string; + lendingCnt: number; +}; export type CreateBookInfo = { - infoId: number; - callSign: string; - title: string; - author: string; - publisher: string; - isbn?: string; - image?: string; - categoryId?: string; - pubdate?: string | null; - donator: string; - donatorId: number | null; -} + infoId: number; + callSign: string; + title: string; + author: string; + publisher: string; + isbn?: string; + image?: string; + categoryId?: string; + pubdate?: string | null; + donator: string; + donatorId: number | null; +}; export type UpdateBookInfo = { id: number; @@ -45,47 +45,47 @@ export type UpdateBookInfo = { publishedAt: string | Date; image: string; categoryId?: string; -} +}; export type UpdateBook = { id: number; callSign: string; status: number; -} +}; export type UpdateBookDonator = { - id: number; - donator: string; - donatorId: number; -} + id: number; + donator: string; + donatorId: number; +}; -export enum categoryIds{ - 'K' = 1, - 'C', - 'O', - 'A', - 'I', - 'G', - 'J', - 'c', - 'F', - 'E', - 'h', - 'H', - 'd', - 'D', - 'k', - 'g', - 'B', - 'e', - 'n', - 'N', - 'j', - 'a', - 'f', - 'L', - 'b', - 'M', - 'i', - 'l', +export enum categoryIds { + 'K' = 1, + 'C', + 'O', + 'A', + 'I', + 'G', + 'J', + 'c', + 'F', + 'E', + 'h', + 'H', + 'd', + 'D', + 'k', + 'g', + 'B', + 'e', + 'n', + 'N', + 'j', + 'a', + 'f', + 'L', + 'b', + 'M', + 'i', + 'l', } diff --git a/backend/src/v1/books/likes.service.ts b/backend/src/v1/books/likes.service.ts index 6abf729e..8ed5d79c 100644 --- a/backend/src/v1/books/likes.service.ts +++ b/backend/src/v1/books/likes.service.ts @@ -3,7 +3,7 @@ import jipDataSource from '~/app-data-source'; import LikesRepository from './Likes.repository'; export default class LikesService { - private readonly likesRepository : LikesRepository; + private readonly likesRepository: LikesRepository; constructor() { this.likesRepository = new LikesRepository(); @@ -22,7 +22,9 @@ export default class LikesService { const LikeArray = await this.likesRepository.find({ where: { userId, bookInfoId, isDeleted: false }, }); - if (LikeArray.length === 0) { throw new Error(errorCode.NONEXISTENT_LIKES); } + if (LikeArray.length === 0) { + throw new Error(errorCode.NONEXISTENT_LIKES); + } } async createLike(userId: number, bookInfoId: number) { @@ -33,9 +35,12 @@ export default class LikesService { await likesRepo.manager.queryRunner?.connect(); await likesRepo.manager.queryRunner?.startTransaction(); try { - const ret = await likesRepo.update({ userId, bookInfoId }, { - isDeleted: false, - }); + const ret = await likesRepo.update( + { userId, bookInfoId }, + { + isDeleted: false, + }, + ); if (ret.affected === 0) { const like = likesRepo.create({ userId, bookInfoId }); await likesRepo.save(like); @@ -54,18 +59,25 @@ export default class LikesService { async deleteLike(userId: number, bookInfoId: number) { // update를 할때 이미 해당 데이터가 존재하는지 검사하지 말라는 이유는?? // UpdateResult { generatedMaps: [], raw: [], affected: 0 } - const { affected } = await this.likesRepository.update({ userId, bookInfoId }, { - isDeleted: true, - }); - if (affected === 0) { throw new Error(errorCode.NONEXISTENT_LIKES); } + const { affected } = await this.likesRepository.update( + { userId, bookInfoId }, + { + isDeleted: true, + }, + ); + if (affected === 0) { + throw new Error(errorCode.NONEXISTENT_LIKES); + } } async getLikeInfo(userId: number, bookInfoId: number) { const LikeArray = await this.likesRepository.find({ where: { bookInfoId, isDeleted: false } }); let isLiked = false; LikeArray.forEach((like: any) => { - if (like.userId === userId && like.isDeleted === 0) { isLiked = true; } + if (like.userId === userId && like.isDeleted === 0) { + isLiked = true; + } }); - return ({ bookInfoId, isLiked, likeNum: LikeArray.length }); + return { bookInfoId, isLiked, likeNum: LikeArray.length }; } } diff --git a/backend/src/v1/cursus/cursus.controller.ts b/backend/src/v1/cursus/cursus.controller.ts index f154c37b..0c70b580 100644 --- a/backend/src/v1/cursus/cursus.controller.ts +++ b/backend/src/v1/cursus/cursus.controller.ts @@ -1,16 +1,11 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import { getAccessToken } from '~/v1/auth/auth.service'; import { RecommendedBook, UserProject, ProjectInfo } from '~/v1/DTO/cursus.model'; import { logger } from '~/logger'; import * as CursusService from './cursus.service'; -export const recommendBook = async ( - req: Request, - res: Response, -) => { +export const recommendBook = async (req: Request, res: Response) => { const { nickname: login } = req.user as any; const limit = req.query.limit ? Number(req.query.limit) : 4; const project = req.query.project as string; @@ -46,21 +41,19 @@ export const recommendBook = async ( return res.status(status.OK).json({ items: bookList, meta }); }; -export const getProjects = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getProjects = async (req: Request, res: Response, next: NextFunction) => { const page = req.query.page as string; const mode = req.query.mode as string; - const accessToken:string = await getAccessToken(); + const accessToken: string = await getAccessToken(); let projects: ProjectInfo[] = []; try { projects = await CursusService.getProjectsInfo(accessToken, page); } catch (error) { return next(error); } - if (projects.length !== 0) { CursusService.saveProjects(projects, mode); } + if (projects.length !== 0) { + CursusService.saveProjects(projects, mode); + } return res.status(200).send({ projects }); }; diff --git a/backend/src/v1/cursus/cursus.service.ts b/backend/src/v1/cursus/cursus.service.ts index e2848713..e70bf1fe 100644 --- a/backend/src/v1/cursus/cursus.service.ts +++ b/backend/src/v1/cursus/cursus.service.ts @@ -36,9 +36,7 @@ export const readFiles = async () => { * @param login 사용자의 닉네임 * @returns 사용자의 intra id */ -export const getIntraId = async ( - login: string, -): Promise => { +export const getIntraId = async (login: string): Promise => { const usersRepo = new UsersRepository(); const user = (await usersRepo.searchUserBy({ nickname: login }, 1, 0))[0]; return user[0].intraId.toString(); @@ -62,27 +60,33 @@ export const getUserProjectFrom42API = async ( 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, - }).then((response) => { - const rawData: UserProjectFrom42[] = response.data; - rawData.forEach((data: UserProjectFrom42) => { - userProject.push({ - id: data.id, - status: data.status, - validated: data['validated?'], - project: data.project, - cursus_ids: data.cursus_ids, - marked: data.marked, - marked_at: data.marked_at, - updated_at: data.updated_at, + }) + .then((response) => { + const rawData: UserProjectFrom42[] = response.data; + rawData.forEach((data: UserProjectFrom42) => { + userProject.push({ + id: data.id, + status: data.status, + validated: data['validated?'], + project: data.project, + cursus_ids: data.cursus_ids, + marked: data.marked, + marked_at: data.marked_at, + updated_at: data.updated_at, + }); }); + }) + .catch((error) => { + if (error.response.status === 401) { + throw new ErrorResponse('401', 401, '권한이 없습니다.'); + } else { + throw new ErrorResponse( + '500', + 500, + '42 API로부터 프로젝트 정보를 받아오는데 실패했습니다.', + ); + } }); - }).catch((error) => { - if (error.response.status === 401) { - throw new ErrorResponse('401', 401, '권한이 없습니다.'); - } else { - throw new ErrorResponse('500', 500, '42 API로부터 프로젝트 정보를 받아오는데 실패했습니다.'); - } - }); return userProject; }; @@ -93,10 +97,7 @@ export const getUserProjectFrom42API = async ( * @param projectId 프로젝트 id * @returns projectId가 포함된 서클 번호 문자열 */ -const findCircle = ( - cursus: ProjectWithCircle, - projectId: number, -) => { +const findCircle = (cursus: ProjectWithCircle, projectId: number) => { let circle: string | null = null; Object.keys(cursus).forEach((key) => { const projectIds = cursus[key].project_ids; @@ -116,10 +117,7 @@ const findCircle = ( * @param projectList 사용자가 진행한 프로젝트 목록 * @returns 아우터 서클에 있는 프로젝트 id 배열 */ -const getOuterProjectIds = ( - cursus: ProjectWithCircle, - projectList: UserProject[] | null, -) => { +const getOuterProjectIds = (cursus: ProjectWithCircle, projectList: UserProject[] | null) => { let outerProjectIds: number[] = []; for (let i = 0; i < projectsInfo.length; i += 1) { const projectId = projectsInfo[i].id; @@ -141,10 +139,7 @@ const getOuterProjectIds = ( * @param circle 서클 번호 * @returns 추천할 프로젝트 id 배열 */ -const getNextProjectIds = ( - cursus: ProjectWithCircle, - circle: string, -) => { +const getNextProjectIds = (cursus: ProjectWithCircle, circle: string) => { const projectIds = cursus[circle].project_ids; let innerProjectIds = projectIds.filter((id) => id !== 0); if (innerProjectIds.length === 0) { @@ -162,14 +157,11 @@ const getNextProjectIds = ( * @param userProject 사용자의 프로젝트 정보 * @returns 사용자에게 추천할 프로젝트 */ -export const getRecommendedProject = async ( - userProject: UserProject[], -) => { - const projectList = userProject.sort((prev, post) => - new Date(post.updated_at).getTime() - new Date(prev.updated_at).getTime()) +export const getRecommendedProject = async (userProject: UserProject[]) => { + const projectList = userProject + .sort((prev, post) => new Date(post.updated_at).getTime() - new Date(prev.updated_at).getTime()) .filter((item: UserProject) => !item.project.name.includes('Exam Rank')); - const recommendedProject = projectList.filter((project) => - project.status === 'in_progress'); + const recommendedProject = projectList.filter((project) => project.status === 'in_progress'); if (recommendedProject.length > 0) { return recommendedProject.map((project) => project.project.id); } @@ -177,9 +169,11 @@ export const getRecommendedProject = async ( const userProjectId = userProject[0].project.id; const circle: string | null = findCircle(cursusInfo, userProjectId); let nextProjectIds: number[] = []; - if (circle) { // Inner Circle + if (circle) { + // Inner Circle nextProjectIds = getNextProjectIds(cursusInfo, circle); - } else { // Outer Circle + } else { + // Outer Circle nextProjectIds = getOuterProjectIds(cursusInfo, projectList); } return nextProjectIds; @@ -191,11 +185,9 @@ export const getRecommendedProject = async ( * @param projectIds 추천할 프로젝트 id 배열 * @returns 추천할 책 id 배열 */ -export const getRecommendedBookInfoIds = async ( - userProjectIds: number[], -) => { +export const getRecommendedBookInfoIds = async (userProjectIds: number[]) => { if (userProjectIds.length === 0) { - return (booksWithProjectInfo.map((book) => book.book_info_id)); + return booksWithProjectInfo.map((book) => book.book_info_id); } const recommendedBookIds: number[] = []; for (let i = 0; i < booksWithProjectInfo.length; i += 1) { @@ -208,7 +200,7 @@ export const getRecommendedBookInfoIds = async ( } } if (recommendedBookIds.length === 0) { - return (booksWithProjectInfo.map((book) => book.book_info_id)); + return booksWithProjectInfo.map((book) => book.book_info_id); } return [...new Set(recommendedBookIds)]; }; @@ -218,9 +210,7 @@ export const getRecommendedBookInfoIds = async ( * @param bookInfoId 추천 도서의 book_info_id * @returns 추천 도서의 프로젝트 이름 배열 */ -const findProjectNamesWithBookInfoId = ( - bookInfoId: number, -) => { +const findProjectNamesWithBookInfoId = (bookInfoId: number) => { const bookWithProjectInfo = booksWithProjectInfo.find((book) => book.book_info_id === bookInfoId); const recommendedProjects: ProjectInfo[] = projectsInfo.filter((info) => { if (bookWithProjectInfo) { @@ -276,7 +266,9 @@ export const getRecommendMeta = async () => { projectName = '기타'; } let circle = projects[j].circle.toString(); - if (circle === '-1') { circle = '아우터 '; } + if (circle === '-1') { + circle = '아우터 '; + } meta.push(`${circle}서클 | ${projectName}`); } } @@ -290,20 +282,18 @@ export const getRecommendMeta = async () => { * @param data 42 API에서 받아온 프로젝트 정보 * @returns */ -const processData = async ( - data: ProjectFrom42[], -) => { +const processData = async (data: ProjectFrom42[]) => { const ftSeoulData = data.filter((project) => { for (let i = 0; i < project.campus.length; i += 1) { if (project.campus[i].id === 29) { for (let j = 0; j < project.cursus.length; j += 1) { if (project.cursus[j].id === 21) { - return (true); + return true; } } } } - return (false); + return false; }); const processedData: ProjectInfo[] = ftSeoulData.map((project) => ({ id: project.id, @@ -316,7 +306,7 @@ const processData = async ( slug: cursus.slug, })), })); - return (processedData); + return processedData; }; /** @@ -324,22 +314,26 @@ const processData = async ( * @param accessToken 42 API에 접근하기 위한 access token * @param pageNumber 프로젝트 정보를 가져올 페이 */ -export const getProjectsInfo = async ( - accessToken: string, - pageNumber: string, -) => { +export const getProjectsInfo = async (accessToken: string, pageNumber: string) => { const uri: string = 'https://api.intra.42.fr/v2/projects'; - const queryString: string = 'sort=id&filter[exam]=false&filter[visible]=true&filter[has_mark]=true&page[size]=100'; + const queryString: string = + 'sort=id&filter[exam]=false&filter[visible]=true&filter[has_mark]=true&page[size]=100'; const pageQuery: string = `&page[number]=${pageNumber}`; - const response = await axios.get(`${uri}?${queryString}${pageQuery}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }).catch((error) => { - if (error.status === 401) { throw new ErrorResponse(status[401], 401, 'Unauthorized'); } else { throw new ErrorResponse('500', 500, 'Internal Server Error'); } - }); + const response = await axios + .get(`${uri}?${queryString}${pageQuery}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .catch((error) => { + if (error.status === 401) { + throw new ErrorResponse(status[401], 401, 'Unauthorized'); + } else { + throw new ErrorResponse('500', 500, 'Internal Server Error'); + } + }); const processedData = await processData(response.data); - return (processedData); + return processedData; }; /** @@ -347,10 +341,7 @@ export const getProjectsInfo = async ( * @param projects 저장할 프로젝트 정보 배열 * @param mode 저장할 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. */ -export const saveProjects = async ( - projects: ProjectInfo[], - mode: string, -) => { +export const saveProjects = async (projects: ProjectInfo[], mode: string) => { const filePath: string = path.join(__dirname, '../../assets', 'projects_info.json'); const jsonString = JSON.stringify(projects, null, 2); if (mode === 'overwrite') { diff --git a/backend/src/v1/histories/histories.controller.ts b/backend/src/v1/histories/histories.controller.ts index 01de6a3a..31abee94 100644 --- a/backend/src/v1/histories/histories.controller.ts +++ b/backend/src/v1/histories/histories.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -8,16 +6,14 @@ import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as historiesService from './histories.service'; // eslint-disable-next-line import/prefer-default-export -export const histories = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const histories = async (req: Request, res: Response, next: NextFunction) => { const query = String(req.query.query) !== 'undefined' ? String(req.query.query) : ''; const who = String(req.query.who) !== 'undefined' ? String(req.query.who) : ''; const page = parseInt(req.query.page as string, 10) ? parseInt(req.query.page as string, 10) : 0; // eslint-disable-next-line max-len - const limit = parseInt(req.query.limit as string, 10) ? parseInt(req.query.limit as string, 10) : 5; + const limit = parseInt(req.query.limit as string, 10) + ? parseInt(req.query.limit as string, 10) + : 5; const type = String(req.query.type) !== 'undefined' ? String(req.query.type) : 'all'; const { id: userId, role: userRole } = req.user as any; diff --git a/backend/src/v1/histories/histories.repository.ts b/backend/src/v1/histories/histories.repository.ts index 1d6d65f9..19e2ed9c 100644 --- a/backend/src/v1/histories/histories.repository.ts +++ b/backend/src/v1/histories/histories.repository.ts @@ -9,8 +9,11 @@ class HistoriesRepository extends Repository { super(VHistories, entityManager); } - async getHistoriesItems(conditions: {}, limit: number, page: number) - : Promise<[VHistories[], number]> { + async getHistoriesItems( + conditions: {}, + limit: number, + page: number, + ): Promise<[VHistories[], number]> { const [histories, count] = await this.findAndCount({ where: conditions, take: limit, diff --git a/backend/src/v1/histories/histories.service.ts b/backend/src/v1/histories/histories.service.ts index 6cdaffc8..67ba1061 100644 --- a/backend/src/v1/histories/histories.service.ts +++ b/backend/src/v1/histories/histories.service.ts @@ -22,10 +22,7 @@ export const getHistories = async ( } else if (type === 'title') { filterQuery.title = Like(`%${query}%`); } else { - filterQuery = [ - { login: Like(`%${query}%`) }, - { title: Like(`%${query}%`) }, - ]; + filterQuery = [{ login: Like(`%${query}%`) }, { title: Like(`%${query}%`) }]; } const historiesRepo = new HistoriesRepository(); const [items, count] = await historiesRepo.getHistoriesItems(filterQuery, limit, page); diff --git a/backend/src/v1/lendings/lendings.controller.ts b/backend/src/v1/lendings/lendings.controller.ts index e47ca818..43eb543f 100644 --- a/backend/src/v1/lendings/lendings.controller.ts +++ b/backend/src/v1/lendings/lendings.controller.ts @@ -1,17 +1,11 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as lendingsService from './lendings.service'; -export const create: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const create: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; if (!req.body.userId || !req.body.bookId) { next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); @@ -37,22 +31,26 @@ export const create: RequestHandler = async ( } }; -const argumentCheck = (sort:string, type:string) => { - if (type !== 'user' && type !== 'title' && type !== 'callSign' && type !== 'all' && type !== 'bookId') { return 0; } +const argumentCheck = (sort: string, type: string) => { + if ( + type !== 'user' && + type !== 'title' && + type !== 'callSign' && + type !== 'all' && + type !== 'bookId' + ) { + return 0; + } return 1; }; -export const search: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const info = req.query; const query = String(info.query) !== 'undefined' ? String(info.query) : ''; const page = parseInt(info.page as string, 10) ? parseInt(info.page as string, 10) : 0; const limit = parseInt(info.limit as string, 10) ? parseInt(info.limit as string, 10) : 5; const sort = info.sort as string; - const type = info.type as string ? info.type as string : 'all'; + const type = (info.type as string) ? (info.type as string) : 'all'; if (!argumentCheck(sort, type)) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -109,11 +107,7 @@ export const returnBook: RequestHandler = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - const result = await lendingsService.returnBook( - id, - req.body.lendingId, - req.body.condition, - ); + const result = await lendingsService.returnBook(id, req.body.lendingId, req.body.condition); res.status(status.OK).json(result); } catch (error: any) { const errorNumber = parseInt(error.message, 10); diff --git a/backend/src/v1/lendings/lendings.repository.ts b/backend/src/v1/lendings/lendings.repository.ts index f265677d..ffc03b37 100644 --- a/backend/src/v1/lendings/lendings.repository.ts +++ b/backend/src/v1/lendings/lendings.repository.ts @@ -1,11 +1,7 @@ -import { - IsNull, MoreThan, QueryRunner, Repository, UpdateResult, -} from 'typeorm'; +import { IsNull, MoreThan, QueryRunner, Repository, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import jipDataSource from '~/app-data-source'; -import { - VUserLending, Reservation, VLending, Lending, User, Book, -} from '~/entity/entities'; +import { VUserLending, Reservation, VLending, Lending, User, Book } from '~/entity/entities'; import { formatDate } from '~/v1/utils/dateFormat'; class LendingRepository extends Repository { @@ -24,30 +20,15 @@ class LendingRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Lending, entityManager); - this.userRepo = new Repository( - User, - entityManager, - ); - - this.userLendingRepo = new Repository( - VUserLending, - entityManager, - ); - - this.bookRepo = new Repository( - Book, - entityManager, - ); - - this.reserveRepo = new Repository( - Reservation, - entityManager, - ); - - this.vlendingRepo = new Repository( - VLending, - entityManager, - ); + this.userRepo = new Repository(User, entityManager); + + this.userLendingRepo = new Repository(VUserLending, entityManager); + + this.bookRepo = new Repository(Book, entityManager); + + this.reserveRepo = new Repository(Reservation, entityManager); + + this.vlendingRepo = new Repository(VLending, entityManager); } async searchLendingCount(conditions: {}, limit: number, page: number) { @@ -59,8 +40,12 @@ class LendingRepository extends Repository { return count; } - async searchLending(conditions: {}, limit: number, page: number, order: {}) - : Promise<[VLending[], number]> { + async searchLending( + conditions: {}, + limit: number, + page: number, + order: {}, + ): Promise<[VLending[], number]> { const [lending, count] = await this.vlendingRepo.findAndCount({ select: [ 'id', @@ -165,25 +150,20 @@ class LendingRepository extends Repository { const updateObject: QueryDeepPartialEntity = { returningLibrarianId, returningCondition, - returnedAt: (new Date()), - updatedAt: (new Date()), + returnedAt: new Date(), + updatedAt: new Date(), }; await this.update(lendingId, updateObject); } - async updateUserPenaltyEndDate( - penaltyEndDate: string, - id: number, - ): Promise { + async updateUserPenaltyEndDate(penaltyEndDate: string, id: number): Promise { const updateObject: QueryDeepPartialEntity = { penaltyEndDate, }; await this.userRepo.update(id, updateObject); } - async searchReservedBook( - bookInfoId: number, - ): Promise { + async searchReservedBook(bookInfoId: number): Promise { const reservation = await this.reserveRepo.findOne({ relations: ['book', 'user', 'bookInfo'], where: { @@ -208,9 +188,7 @@ class LendingRepository extends Repository { return this.reserveRepo.update(reservationId, updateObject); } - async updateReservationToLended( - reservationId: number, - ): Promise { + async updateReservationToLended(reservationId: number): Promise { await this.reserveRepo.update(reservationId, { status: 1 }); } } diff --git a/backend/src/v1/lendings/lendings.service.spec.ts b/backend/src/v1/lendings/lendings.service.spec.ts index ce9e9672..0271a0ec 100644 --- a/backend/src/v1/lendings/lendings.service.spec.ts +++ b/backend/src/v1/lendings/lendings.service.spec.ts @@ -12,44 +12,52 @@ describe('LendingsService', () => { const condition = '이상없음'; it('lend a book (success)', async () => { - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.NO_USER_ID); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.NO_USER_ID, + ); }); it('lend a book (noPermission)', async () => { userId = 1392; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.noPermission); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.noPermission, + ); }); it('lend a book (lendingOverload)', async () => { userId = 1408; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.lendingOverload); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.lendingOverload, + ); }); it('lend a book (LENDING_OVERDUE)', async () => { userId = 1418; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.LENDING_OVERDUE); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.LENDING_OVERDUE, + ); }); it('lend a book (ON_LENDING)', async () => { userId = 1444; bookId = 1; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.ON_LENDING); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.ON_LENDING, + ); }); it('lend a book (ON_RESERVATION)', async () => { bookId = 82; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.ON_RESERVATION); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.ON_RESERVATION, + ); }); it('lend a book (LOST_BOOK)', async () => { bookId = 859; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.LOST_BOOK); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.LOST_BOOK, + ); }); it('lend a book (DAMAGED_BOOK)', async () => { bookId = 858; - expect(await lendingsService.create(userId, bookId, librarianId, condition)) - .toBe(lendingsService.DAMAGED_BOOK); + expect(await lendingsService.create(userId, bookId, librarianId, condition)).toBe( + lendingsService.DAMAGED_BOOK, + ); }); it('search lending record (success)', async () => { @@ -152,18 +160,21 @@ describe('LendingsService', () => { let lendingId = 135; it('return a book (ok)', async () => { - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.ok); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.ok, + ); }); it('return a book (ALREADY_RETURNED)', async () => { - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.ALREADY_RETURNED); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.ALREADY_RETURNED, + ); }); it('return a book (NONEXISTENT_LENDING)', async () => { lendingId = 1000; - expect(await lendingsService.returnBook(librarianId, lendingId, condition)) - .toBe(lendingsService.NONEXISTENT_LENDING); + expect(await lendingsService.returnBook(librarianId, lendingId, condition)).toBe( + lendingsService.NONEXISTENT_LENDING, + ); }); }); diff --git a/backend/src/v1/lendings/lendings.service.ts b/backend/src/v1/lendings/lendings.service.ts index a310456c..ba87c44a 100644 --- a/backend/src/v1/lendings/lendings.service.ts +++ b/backend/src/v1/lendings/lendings.service.ts @@ -22,21 +22,34 @@ export const create = async ( try { await transaction.startTransaction(); const [users, count] = await usersRepository.searchUserBy({ id: userId }, 0, 0); - if (!count) { throw new Error(errorCode.NO_USER_ID); } - if (users[0].role === 0) { throw new Error(errorCode.NO_PERMISSION); } + if (!count) { + throw new Error(errorCode.NO_USER_ID); + } + if (users[0].role === 0) { + throw new Error(errorCode.NO_PERMISSION); + } // user conditions - const numberOfLendings = await lendingRepo.searchLendingCount({ - userId, - }, 0, 0); - if (numberOfLendings >= 2) { throw new Error(errorCode.LENDING_OVERLOAD); } + const numberOfLendings = await lendingRepo.searchLendingCount( + { + userId, + }, + 0, + 0, + ); + if (numberOfLendings >= 2) { + throw new Error(errorCode.LENDING_OVERLOAD); + } const penaltyEndDate = await lendingRepo.getUsersPenalty(userId); const overDueDay = await lendingRepo.getUsersOverDueDay(userId); - if (penaltyEndDate >= new Date() - || overDueDay !== undefined) { throw new Error(errorCode.LENDING_OVERDUE); } + if (penaltyEndDate >= new Date() || overDueDay !== undefined) { + throw new Error(errorCode.LENDING_OVERDUE); + } // book conditions const countOfBookInLending = await lendingRepo.getLendingCountByBookId(bookId); - if (countOfBookInLending !== 0) { throw new Error(errorCode.ON_LENDING); } + if (countOfBookInLending !== 0) { + throw new Error(errorCode.ON_LENDING); + } // 책이 분실, 파손이 아닌지 const book = await lendingRepo.searchBookForLending(bookId); @@ -54,10 +67,17 @@ export const create = async ( // 책 대출 정보 insert await lendingRepo.createLending(userId, bookId, librarianId, condition); // 예약 대출 시 상태값 reservation status 0 -> 1 변경 - if (reservationOfBook) { await lendingRepo.updateReservationToLended(reservationOfBook.id); } + if (reservationOfBook) { + await lendingRepo.updateReservationToLended(reservationOfBook.id); + } await transaction.commitTransaction(); if (users[0].slack) { - await publishMessage(users[0].slack, `:jiphyeonjeon: 대출 알림 :jiphyeonjeon: \n대출 하신 \`${book?.info?.title}\`은(는) ${formatDate(dueDate)}까지 반납해주세요.`); + await publishMessage( + users[0].slack, + `:jiphyeonjeon: 대출 알림 :jiphyeonjeon: \n대출 하신 \`${ + book?.info?.title + }\`은(는) ${formatDate(dueDate)}까지 반납해주세요.`, + ); } } catch (e) { await transaction.rollbackTransaction(); @@ -67,14 +87,10 @@ export const create = async ( } finally { await transaction.release(); } - return ({ dueDate: formatDate(dueDate) }); + return { dueDate: formatDate(dueDate) }; }; -export const returnBook = async ( - librarianId: number, - lendingId: number, - condition: string, -) => { +export const returnBook = async (librarianId: number, lendingId: number, condition: string) => { const transaction = jipDataSource.createQueryRunner(); const lendingRepo = new LendingRepository(transaction); try { @@ -91,7 +107,12 @@ export const returnBook = async ( const today = new Date().setHours(0, 0, 0, 0); const createdDate = new Date(lendingInfo.createdAt); // eslint-disable-next-line max-len - const expecetReturnDate = new Date(createdDate.setDate(createdDate.getDate() + 14)).setHours(0, 0, 0, 0); + const expecetReturnDate = new Date(createdDate.setDate(createdDate.getDate() + 14)).setHours( + 0, + 0, + 0, + 0, + ); if (today > expecetReturnDate) { const todayDate = new Date(); const overDueDays = (today - expecetReturnDate) / 1000 / 60 / 60 / 24; @@ -100,9 +121,15 @@ export const returnBook = async ( // eslint-disable-next-line max-len const originPenaltyEndDate = new Date(penaltyEndDateInDB); if (today < originPenaltyEndDate.setHours(0, 0, 0, 0)) { - confirmedPenaltyEndDate = new Date(originPenaltyEndDate.setDate(originPenaltyEndDate.getDate() + overDueDays)).toISOString().split('T')[0]; + confirmedPenaltyEndDate = new Date( + originPenaltyEndDate.setDate(originPenaltyEndDate.getDate() + overDueDays), + ) + .toISOString() + .split('T')[0]; } else { - confirmedPenaltyEndDate = new Date(todayDate.setDate(todayDate.getDate() + overDueDays)).toISOString().split('T')[0]; + confirmedPenaltyEndDate = new Date(todayDate.setDate(todayDate.getDate() + overDueDays)) + .toISOString() + .split('T')[0]; } await lendingRepo.updateUserPenaltyEndDate(confirmedPenaltyEndDate, lendingInfo.userId); } @@ -117,14 +144,19 @@ export const returnBook = async ( if (updateResult && slackIdReservedUser) { // 예약자에게 슬랙메시지 보내기 const bookTitle = reservationInfo.bookInfo.title; - if (slackIdReservedUser) { await publishMessage(slackIdReservedUser, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${bookTitle}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요.`); } + if (slackIdReservedUser) { + await publishMessage( + slackIdReservedUser, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${bookTitle}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요.`, + ); + } } } await transaction.commitTransaction(); if (reservationInfo) { - return ({ reservedBook: true }); + return { reservedBook: true }; } - return ({ reservedBook: false }); + return { reservedBook: false }; } catch (error) { await transaction.rollbackTransaction(); if (error instanceof Error) { @@ -139,7 +171,7 @@ export const search = async ( query: string, page: number, limit: number, - sort:string, + sort: string, type: string, ) => { const lendingRepo = new LendingRepository(); @@ -165,12 +197,7 @@ export const search = async ( ]); } const orderQuery = sort === 'new' ? { createdAt: 'DESC' } : { createdAt: 'ASC' }; - const [items, count] = await lendingRepo.searchLending( - filterQuery, - limit, - page, - orderQuery, - ); + const [items, count] = await lendingRepo.searchLending(filterQuery, limit, page, orderQuery); const meta: Meta = { totalItems: count, itemCount: items.length, @@ -181,7 +208,7 @@ export const search = async ( return { items, meta }; }; -export const lendingId = async (id:number) => { +export const lendingId = async (id: number) => { const lendingRepo = new LendingRepository(); const data = (await lendingRepo.searchLending({ id }, 0, 0, {}))[0]; return data[0]; diff --git a/backend/src/v1/middlewares/wrapAsyncController.ts b/backend/src/v1/middlewares/wrapAsyncController.ts index 64563f55..fd835025 100644 --- a/backend/src/v1/middlewares/wrapAsyncController.ts +++ b/backend/src/v1/middlewares/wrapAsyncController.ts @@ -1,8 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import { - NextFunction, - Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as errorCode from '~/v1/utils/error/errorCode'; diff --git a/backend/src/v1/notifications/notifications.service.ts b/backend/src/v1/notifications/notifications.service.ts index 1be21ee6..6cefcab5 100644 --- a/backend/src/v1/notifications/notifications.service.ts +++ b/backend/src/v1/notifications/notifications.service.ts @@ -1,18 +1,16 @@ import { executeQuery, makeExecuteQuery, pool } from '~/mysql'; import { publishMessage } from '../slack/slack.service'; -const succeedReservation = async (reservation: { - bookId: number, - bookInfoId: number, -}) => { +const succeedReservation = async (reservation: { bookId: number; bookInfoId: number }) => { const conn = await pool.getConnection(); const transactionExecuteQuery = makeExecuteQuery(conn); try { const candidates: { - id: number - slack: string, - title: string, - }[] = await transactionExecuteQuery(` + id: number; + slack: string; + title: string; + }[] = await transactionExecuteQuery( + ` SELECT reservation.id AS id, user.slack AS slack, @@ -29,9 +27,12 @@ const succeedReservation = async (reservation: { ORDER BY reservation.createdAt DESC LIMIT 1 - `, [reservation.bookInfoId]); + `, + [reservation.bookInfoId], + ); if (candidates.length !== 0) { - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET @@ -39,8 +40,13 @@ const succeedReservation = async (reservation: { endAt = DATE_ADD(NOW(), INTERVAL 3 DAY) WHERE reservation.id = ? - `, [reservation.bookId, candidates[0].id]); - publishMessage(candidates[0].slack, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${candidates[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`); + `, + [reservation.bookId, candidates[0].id], + ); + publishMessage( + candidates[0].slack, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${candidates[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`, + ); } } catch (e) { await conn.rollback(); @@ -53,10 +59,12 @@ const succeedReservation = async (reservation: { }; export const notifyReservation = async () => { - const reservations: [{ - bookId: number, - bookInfoId: number, - }] = await executeQuery(` + const reservations: [ + { + bookId: number; + bookInfoId: number; + }, + ] = await executeQuery(` SELECT reservation.bookId AS bookId, reservation.bookInfoId AS bookInfoId @@ -75,10 +83,10 @@ export const notifyReservation = async () => { export const notifyReservationOverdue = async () => { const reservations: { - slack: string, - title: string, - bookId: number, - bookInfoId: number, + slack: string; + title: string; + bookId: number; + bookInfoId: number; }[] = await executeQuery(` SELECT user.slack AS slack, @@ -96,8 +104,12 @@ export const notifyReservationOverdue = async () => { DATEDIFF(CURDATE(), DATE(reservation.endAt)) = 1 `); reservations.forEach(async (reservation) => { - publishMessage(reservation.slack, `:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservation.title}\`의 예약이 만료되었습니다.`); - const ranks: [{id: number, createdAt: Date}] = await executeQuery(` + publishMessage( + reservation.slack, + `:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservation.title}\`의 예약이 만료되었습니다.`, + ); + const ranks: [{ id: number; createdAt: Date }] = await executeQuery( + ` SELECT id, createdAt @@ -106,20 +118,25 @@ export const notifyReservationOverdue = async () => { WHERE bookInfoId = ? AND status = 0 ORDER BY createdAt ASC - `, [reservation.bookInfoId]); - await executeQuery(` + `, + [reservation.bookInfoId], + ); + await executeQuery( + ` UPDATE reservation SET bookId = ?, endAt = ADDDATE(CURDATE(),1) WHERE id = ? - `, [reservation.bookId, ranks[0].id]); + `, + [reservation.bookId, ranks[0].id], + ); }); }; export const notifyReturningReminder = async () => { - const lendings: [{title: string, slack: string}] = await executeQuery(` + const lendings: [{ title: string; slack: string }] = await executeQuery(` SELECT book_info.title, user.slack @@ -136,15 +153,18 @@ export const notifyReturningReminder = async () => { lending.returnedAt IS NULL `); lendings.forEach(async (lending) => { - publishMessage(lending.slack, `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`의 반납 기한이 다가왔습니다. 3일 내로 반납해주시기 바랍니다.`); + publishMessage( + lending.slack, + `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`의 반납 기한이 다가왔습니다. 3일 내로 반납해주시기 바랍니다.`, + ); }); }; -type Lender = {title: string, slack: string, daysLeft: number}; +type Lender = { title: string; slack: string; daysLeft: number }; // day : 반납까지 남은 기한. // 반납기한이 N일 남은 유저의 목록을 가져옵니다. -export const GetUserFromNDaysLeft = async (day : number) : Promise => { +export const GetUserFromNDaysLeft = async (day: number): Promise => { const LOAN_PERIOD = 14; const daysLeft = LOAN_PERIOD - day; const lendings: Lender[] = await executeQuery(` @@ -166,9 +186,13 @@ export const GetUserFromNDaysLeft = async (day : number) : Promise => return lendings.map(({ ...args }) => ({ ...args, daysLeft: day })); }; -const notifyUser = ({ slack, title, daysLeft }: Lender) => publishMessage(slack, `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${title}\`의 반납 기한이 다가왔습니다. ${daysLeft}일 내로 반납해주시기 바랍니다.`); +const notifyUser = ({ slack, title, daysLeft }: Lender) => + publishMessage( + slack, + `:jiphyeonjeon: 반납 알림 :jiphyeonjeon:\n 대출하신 도서 \`${title}\`의 반납 기한이 다가왔습니다. ${daysLeft}일 내로 반납해주시기 바랍니다.`, + ); -export const notifyUsers = async (userList : Lender[], notifyFn: (_: Lender) => Promise) => { +export const notifyUsers = async (userList: Lender[], notifyFn: (_: Lender) => Promise) => { await Promise.all(userList.map(notifyFn)); }; @@ -180,7 +204,7 @@ export const notifyOverdueManager = async () => { }; export const notifyOverdue = async () => { - const lendings: [{title: string, slack: string}] = await executeQuery(` + const lendings: [{ title: string; slack: string }] = await executeQuery(` SELECT book_info.title, user.slack @@ -197,6 +221,9 @@ export const notifyOverdue = async () => { lending.returnedAt IS NULL `); lendings.forEach(async (lending) => { - publishMessage(lending.slack, `:jiphyeonjeon: 연체 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`가 연체되었습니다. 빠른 시일 내에 반납해주시기 바랍니다.`); + publishMessage( + lending.slack, + `:jiphyeonjeon: 연체 알림 :jiphyeonjeon:\n 대출하신 도서 \`${lending.title}\`가 연체되었습니다. 빠른 시일 내에 반납해주시기 바랍니다.`, + ); }); }; diff --git a/backend/src/v1/reservations/reservations.controller.ts b/backend/src/v1/reservations/reservations.controller.ts index 5daed9b6..4ec347ba 100644 --- a/backend/src/v1/reservations/reservations.controller.ts +++ b/backend/src/v1/reservations/reservations.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import { logger } from '~/logger'; import * as userUtils from '~/v1/users/users.utils'; @@ -8,11 +6,7 @@ import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as reservationsService from './reservations.service'; -export const create: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const create: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.user as any; const bookInfoId = Number.parseInt(req.body.bookInfoId, 10); if (Number.isNaN(bookInfoId)) { @@ -21,9 +15,7 @@ export const create: RequestHandler = async ( try { const createdReservation = await reservationsService.create(id, req.body.bookInfoId); logger.info(`[ES_R] : userId: ${id} bookInfoId: ${bookInfoId}`); - return res - .status(status.OK) - .json(createdReservation); + return res.status(status.OK).json(createdReservation); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { @@ -52,7 +44,7 @@ const filterCheck = (argument: string) => { export const search: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { const info = req.query; - const query = info.query as string ? info.query as string : ''; + const query = (info.query as string) ? (info.query as string) : ''; const page = parseInt(info.page as string, 10) ? parseInt(info.page as string, 10) : 0; const limit = parseInt(info.limit as string, 10) ? parseInt(info.limit as string, 10) : 5; const filter = info.filter as string; @@ -61,9 +53,7 @@ export const search: RequestHandler = async (req: Request, res: Response, next: } try { const searchResult = await reservationsService.search(query, page, limit, filter); - return res - .status(status.OK) - .json(searchResult); + return res.status(status.OK).json(searchResult); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { @@ -74,7 +64,8 @@ export const search: RequestHandler = async (req: Request, res: Response, next: logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const cancel: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { @@ -98,7 +89,8 @@ export const cancel: RequestHandler = async (req: Request, res: Response, next: logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const count: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { @@ -119,7 +111,8 @@ export const count: RequestHandler = async (req: Request, res: Response, next: N logger.error(error); next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } - } return 0; + } + return 0; }; export const userReservations: RequestHandler = async ( @@ -133,9 +126,7 @@ export const userReservations: RequestHandler = async ( return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { - return res - .status(status.OK) - .json(await reservationsService.userReservations(userId)); + return res.status(status.OK).json(await reservationsService.userReservations(userId)); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 500 && errorNumber < 600) { diff --git a/backend/src/v1/reservations/reservations.repository.ts b/backend/src/v1/reservations/reservations.repository.ts index ddbc3c15..e3a1d590 100644 --- a/backend/src/v1/reservations/reservations.repository.ts +++ b/backend/src/v1/reservations/reservations.repository.ts @@ -1,15 +1,6 @@ -import { - Brackets, - IsNull, MoreThan, Not, QueryRunner, Repository, -} from 'typeorm'; - -import { - BookInfo, - User, - Lending, - Book, - Reservation, -} from '~/entity/entities'; +import { Brackets, IsNull, MoreThan, Not, QueryRunner, Repository } from 'typeorm'; + +import { BookInfo, User, Lending, Book, Reservation } from '~/entity/entities'; import jipDataSource from '~/app-data-source'; import { Meta } from '../DTO/common.interface'; @@ -27,18 +18,9 @@ class ReservationsRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Reservation, entityManager); - this.bookInfo = new Repository( - BookInfo, - entityManager, - ); - this.user = new Repository( - User, - entityManager, - ); - this.lending = new Repository( - Lending, - entityManager, - ); + this.bookInfo = new Repository(BookInfo, entityManager); + this.user = new Repository(User, entityManager); + this.lending = new Repository(Lending, entityManager); } // 유저가 대출 패널티 중인지 확인 @@ -59,7 +41,11 @@ class ReservationsRepository extends Repository { .createQueryBuilder('u') .select('u.id') .addSelect('count(u.id)', 'overdueLendingCnt') - .innerJoin('lending', 'l', 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) > 0') + .innerJoin( + 'lending', + 'l', + 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY)) > 0', + ) .where('u.id = :userId', { userId }) .groupBy('u.id') .getExists(); @@ -67,15 +53,19 @@ class ReservationsRepository extends Repository { // 유저가 2권 이상 예약 중인지 확인 async isAllRenderUser(userId: number): Promise { - const [rendUser] = await Promise.all([this.user - .createQueryBuilder('u') - .select('u.id', 'id') - .addSelect('COUNT(r.id)', 'count') - .innerJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') - .where(`u.id = ${userId}`) - .groupBy('u.id') - .getRawOne()]); - if (rendUser?.count >= 2) { return true; } + const [rendUser] = await Promise.all([ + this.user + .createQueryBuilder('u') + .select('u.id', 'id') + .addSelect('COUNT(r.id)', 'count') + .innerJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') + .where(`u.id = ${userId}`) + .groupBy('u.id') + .getRawOne(), + ]); + if (rendUser?.count >= 2) { + return true; + } return false; } @@ -85,7 +75,11 @@ class ReservationsRepository extends Repository { .createQueryBuilder('u') .select('u.id') .addSelect('u.nickname') - .leftJoin('lending', 'l', 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY) > 0') + .leftJoin( + 'lending', + 'l', + 'l.userId = u.id AND l.returnedAt IS NULL AND DATEDIFF(now(), DATE_ADD(l.createdAt, INTERVAL 14 DAY) > 0', + ) .leftJoin('reservation', 'r', 'r.userId = u.id AND r.status = 0') .groupBy('u.id') .having('count(l.id) = 0 AND count(DISTINCT r.id) < 2') @@ -123,8 +117,7 @@ class ReservationsRepository extends Repository { // Todo: return 값 수정할 것 async getReservedBooks(userId: number, bookInfoId: number) { - const reservedBooks = this - .createQueryBuilder('r') + const reservedBooks = this.createQueryBuilder('r') .select('r.id', 'id') .where('r.bookInfoId = :bookInfoId', { bookInfoId }) .andWhere('r.userId = :userId', { userId }) @@ -132,7 +125,7 @@ class ReservationsRepository extends Repository { return reservedBooks; } - async createReservation(userId: number, bookInfoId:number): Promise { + async createReservation(userId: number, bookInfoId: number): Promise { await this.createQueryBuilder() .insert() .into(Reservation) @@ -140,10 +133,13 @@ class ReservationsRepository extends Repository { .execute(); } - async searchReservations(query: string, filter: string, page: number, limit: number): - Promise<{ meta: Meta; items: Reservation[] }> { - const searchAll = this - .createQueryBuilder('r') + async searchReservations( + query: string, + filter: string, + page: number, + limit: number, + ): Promise<{ meta: Meta; items: Reservation[] }> { + const searchAll = this.createQueryBuilder('r') .select('r.id', 'reservationsId') .addSelect('r.endAt', 'endAt') .addSelect('r.createdAt', 'createdAt') @@ -151,7 +147,10 @@ class ReservationsRepository extends Repository { .addSelect('r.userId', 'userId') .addSelect('r.bookId', 'bookId') .addSelect('u.nickname', 'login') - .addSelect('CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END', 'penaltyDays') + .addSelect( + 'CASE WHEN NOW() > u.penaltyEndDate THEN 0 ELSE DATEDIFF(u.penaltyEndDate, NOW()) END', + 'penaltyDays', + ) .addSelect('bi.title', 'title') .addSelect('bi.image', 'image') .addSelect('(SELECT COUNT(*) FROM reservation)', 'count') @@ -159,11 +158,13 @@ class ReservationsRepository extends Repository { .leftJoin('user', 'u', 'r.userId = u.id') .leftJoin('book_info', 'bi', 'r.bookInfoId = bi.id') .leftJoin('book', 'b', 'r.bookId = b.id') - .where(new Brackets((qb) => { - qb.where('bi.title like :query', { query: `%${query}%` }) - .orWhere('u.nickname like :query', { query: `%${query}%` }) - .orWhere('b.callSign like :query', { query: `%${query}%` }); - })); + .where( + new Brackets((qb) => { + qb.where('bi.title like :query', { query: `%${query}%` }) + .orWhere('u.nickname like :query', { query: `%${query}%` }) + .orWhere('b.callSign like :query', { query: `%${query}%` }); + }), + ); switch (filter) { case 'waiting': searchAll.andWhere({ status: 0, bookId: IsNull() }); @@ -179,9 +180,12 @@ class ReservationsRepository extends Repository { default: searchAll.andWhere({ status: 0, bookId: IsNull() }); } - const items = await searchAll.offset(limit * page).limit(limit).getRawMany(); + const items = await searchAll + .offset(limit * page) + .limit(limit) + .getRawMany(); const totalItems = await searchAll.getCount(); - const meta : Meta = { + const meta: Meta = { totalItems, itemCount: items.length, itemsPerPage: limit, diff --git a/backend/src/v1/reservations/reservations.service.spec.ts b/backend/src/v1/reservations/reservations.service.spec.ts index 7887a354..05c3abb9 100644 --- a/backend/src/v1/reservations/reservations.service.spec.ts +++ b/backend/src/v1/reservations/reservations.service.spec.ts @@ -128,14 +128,12 @@ describe('ReservationsServices', () => { it('reservation count (INVALID_INFO_ID)', async () => { bookInfoId = 4242; - expect(await reservationsService.count(bookInfoId)) - .toBe(reservationsService.INVALID_INFO_ID); + expect(await reservationsService.count(bookInfoId)).toBe(reservationsService.INVALID_INFO_ID); }); it('reservation count (NOT_LENDED)', async () => { bookInfoId = 1; - expect(await reservationsService.count(bookInfoId)) - .toBe(reservationsService.NOT_LENDED); + expect(await reservationsService.count(bookInfoId)).toBe(reservationsService.NOT_LENDED); }); it('get user reservation', async () => { diff --git a/backend/src/v1/reservations/reservations.service.ts b/backend/src/v1/reservations/reservations.service.ts index 1cd9063d..110c0ffa 100644 --- a/backend/src/v1/reservations/reservations.service.ts +++ b/backend/src/v1/reservations/reservations.service.ts @@ -7,16 +7,20 @@ import { publishMessage } from '../slack/slack.service'; import ReservationsRepository from './reservations.repository'; export const count = async (bookInfoId: number) => { - const numberOfBookInfo = await executeQuery(` + const numberOfBookInfo = await executeQuery( + ` SELECT COUNT(*) as count FROM book WHERE infoId = ? AND status = 0; - `, [bookInfoId]); + `, + [bookInfoId], + ); if (numberOfBookInfo[0].count === 0) { throw new Error(errorCode.INVALID_INFO_ID); } // bookInfoId가 모두 대출 중이거나 예약 중인지 확인 - const cantReservBookInfo = await executeQuery(` + const cantReservBookInfo = await executeQuery( + ` SELECT COUNT(*) as count FROM book LEFT JOIN lending ON lending.bookId = book.id @@ -24,15 +28,20 @@ export const count = async (bookInfoId: number) => { WHERE book.infoId = ? AND book.status = 0 AND (lending.returnedAt IS NULL OR reservation.status = 0); -`, [bookInfoId]); +`, + [bookInfoId], + ); if (numberOfBookInfo[0].count > cantReservBookInfo[0].count) { throw new Error(errorCode.NOT_LENDED); } - const numberOfReservations = await executeQuery(` + const numberOfReservations = await executeQuery( + ` SELECT COUNT(*) as count FROM reservation WHERE bookInfoId = ? AND status = 0; - `, [bookInfoId]); + `, + [bookInfoId], + ); return numberOfReservations[0]; }; @@ -87,7 +96,7 @@ export const create = async (userId: number, bookInfoId: number) => { } }; -export const search = async (query:string, page: number, limit: number, filter: string) => { +export const search = async (query: string, page: number, limit: number, filter: string) => { const reservationRepo = new ReservationsRepository(); const reservationList = await reservationRepo.searchReservations(query, filter, page, limit); return reservationList; @@ -100,10 +109,11 @@ export const cancel = async (reservationId: number): Promise => { try { // 올바른 예약인지 확인. const reservations: { - status: number, - bookId: string, - title: string, - }[] = await transactionExecuteQuery(` + status: number; + bookId: string; + title: string; + }[] = await transactionExecuteQuery( + ` SELECT reservation.status AS status, reservation.bookId AS bookId, @@ -112,7 +122,9 @@ export const cancel = async (reservationId: number): Promise => { LEFT JOIN book_info ON book_info.id = reservation.bookInfoId WHERE reservation.id = ? - `, [reservationId]); + `, + [reservationId], + ); if (!reservations.length) { throw new Error(errorCode.RESERVATION_NOT_EXIST); } @@ -120,15 +132,19 @@ export const cancel = async (reservationId: number): Promise => { throw new Error(errorCode.NOT_RESERVED); } // 예약 취소 ( 2번 ) 으로 status 변경 - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET status = 2 WHERE id = ?; - `, [reservationId]); + `, + [reservationId], + ); // bookId 가 있는 사람이 취소했으면 ( 0순위 예약자 ) if (reservations[0].bookId) { // 예약자 (취소된 bookInfoId 로 예약한 사람) 중에 가장 빨리 예약한 사람 찾아서 반환 - const candidates: {id: number, slack: string}[] = await transactionExecuteQuery(` + const candidates: { id: number; slack: string }[] = await transactionExecuteQuery( + ` SELECT reservation.id AS id, user.slack AS slack @@ -143,15 +159,23 @@ export const cancel = async (reservationId: number): Promise => { ) AND reservation.status = 0 ORDER BY reservation.createdAt ASC; - `, [reservations[0].bookId]); + `, + [reservations[0].bookId], + ); // 그 사람이 존재한다면 예약 update 하고 예약 알림 보내기 if (candidates.length) { - await transactionExecuteQuery(` + await transactionExecuteQuery( + ` UPDATE reservation SET bookId = ?, endAt = DATE_ADD(NOW(), INTERVAL 3 DAY) WHERE id = ? - `, [reservations[0].bookId, candidates[0].id]); - publishMessage(candidates[0].slack, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservations[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`); + `, + [reservations[0].bookId, candidates[0].id], + ); + publishMessage( + candidates[0].slack, + `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservations[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`, + ); } } conn.commit(); @@ -164,11 +188,14 @@ export const cancel = async (reservationId: number): Promise => { }; export const userCancel = async (userId: number, reservationId: number): Promise => { - const reservations = await executeQuery(` + const reservations = await executeQuery( + ` SELECT userId FROM reservation WHERE id = ? - `, [reservationId]); + `, + [reservationId], + ); if (!reservations.length) { throw new Error(errorCode.RESERVATION_NOT_EXIST); } @@ -192,7 +219,8 @@ export const reservationKeySubstitution = (obj: queriedReservationInfo): reserva }; export const userReservations = async (userId: number) => { - const reservationList = await executeQuery(` + const reservationList = (await executeQuery( + ` SELECT reservation.id as reservationId, reservation.bookInfoId as reservedBookInfoId, reservation.createdAt as reservationDate, @@ -208,7 +236,9 @@ export const userReservations = async (userId: number) => { LEFT JOIN book_info ON reservation.bookInfoId = book_info.id WHERE reservation.userId = ? AND reservation.status = 0; - `, [userId]) as [queriedReservationInfo]; + `, + [userId], + )) as [queriedReservationInfo]; reservationList.forEach((obj) => reservationKeySubstitution(obj)); return reservationList; }; diff --git a/backend/src/v1/reservations/reservations.type.ts b/backend/src/v1/reservations/reservations.type.ts index 3802a8be..0c9a1182 100644 --- a/backend/src/v1/reservations/reservations.type.ts +++ b/backend/src/v1/reservations/reservations.type.ts @@ -1,19 +1,19 @@ export type queriedReservationInfo = { - reservationId: number, - reservedBookInfoId: number, - reservationDate: Date, - endAt: Date, - orderOfReservation: number, - title: string, - image: string, -} + reservationId: number; + reservedBookInfoId: number; + reservationDate: Date; + endAt: Date; + orderOfReservation: number; + title: string; + image: string; +}; export type reservationInfo = { - reservationId: number, - bookInfoId: number, - createdAt: Date, - endAt: Date, - orderOfReservation: number, - title: string, - image: string, -} + reservationId: number; + bookInfoId: number; + createdAt: Date; + endAt: Date; + orderOfReservation: number; + title: string; + image: string; +}; diff --git a/backend/src/v1/reviews/controller/reviews.controller.ts b/backend/src/v1/reviews/controller/reviews.controller.ts index 11097d6a..cc8ec0f0 100644 --- a/backend/src/v1/reviews/controller/reviews.controller.ts +++ b/backend/src/v1/reviews/controller/reviews.controller.ts @@ -1,6 +1,4 @@ -import { - Request, RequestHandler, Response, -} from 'express'; +import { Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as errorCode from '~/v1/utils/error/errorCode'; @@ -47,21 +45,21 @@ export const getReviews: RequestHandler = async (req, res, next) => { } const { id } = parsedId.data; - const { - isMyReview, titleOrNickname, disabled, page, sort, limit, - } = parsedQuery.data; + const { isMyReview, titleOrNickname, disabled, page, sort, limit } = parsedQuery.data; return res .status(status.OK) - .json(await reviewsService.getReviewsPage( - id, - isMyReview, - titleOrNickname ?? '', - disabled, - page, - sort, - limit, - )); + .json( + await reviewsService.getReviewsPage( + id, + isMyReview, + titleOrNickname ?? '', + disabled, + page, + sort, + limit, + ), + ); }; export const updateReviews: RequestHandler = async (req, res, next) => { diff --git a/backend/src/v1/reviews/controller/reviews.type.ts b/backend/src/v1/reviews/controller/reviews.type.ts index 16ba1924..bfd91b3c 100644 --- a/backend/src/v1/reviews/controller/reviews.type.ts +++ b/backend/src/v1/reviews/controller/reviews.type.ts @@ -12,16 +12,22 @@ export const reviewsIdSchema = positiveInt; export const contentSchema = z.string().min(10).max(420); export type Sort = 'ASC' | 'DESC'; -export const sortSchema = z.string().toUpperCase() +export const sortSchema = z + .string() + .toUpperCase() .refine((s): s is Sort => s === 'ASC' || s === 'DESC') .default('DESC' as const); /** 0: 공개, 1: 비공개, -1: 전체 리뷰 */ type Disabled = 0 | 1 | -1; -const disabledSchema = z.coerce.number().int().refine( - (n): n is Disabled => [-1, 0, 1].includes(n), - (n) => ({ message: `0: 공개, 1: 비공개, -1: 전체 리뷰, 입력값: ${n}` }), -).default(-1); +const disabledSchema = z.coerce + .number() + .int() + .refine( + (n): n is Disabled => [-1, 0, 1].includes(n), + (n) => ({ message: `0: 공개, 1: 비공개, -1: 전체 리뷰, 입력값: ${n}` }), + ) + .default(-1); export const queryOptionSchema = z.object({ page: positiveInt.default(0), @@ -34,11 +40,13 @@ export const booleanLikeSchema = z.union([ z.enum(['true', 'false']).transform((v) => v === 'true'), ]); -export const getReviewsSchema = z.object({ - isMyReview: booleanLikeSchema.catch(false), - titleOrNickname: z.string().optional(), - disabled: disabledSchema, -}).merge(queryOptionSchema); +export const getReviewsSchema = z + .object({ + isMyReview: booleanLikeSchema.catch(false), + titleOrNickname: z.string().optional(), + disabled: disabledSchema, + }) + .merge(queryOptionSchema); export const createReviewsSchema = z.object({ bookInfoId: bookInfoIdSchema, diff --git a/backend/src/v1/reviews/controller/utils/errorCheck.ts b/backend/src/v1/reviews/controller/utils/errorCheck.ts index bd9c2338..0f69814e 100644 --- a/backend/src/v1/reviews/controller/utils/errorCheck.ts +++ b/backend/src/v1/reviews/controller/utils/errorCheck.ts @@ -4,9 +4,7 @@ import ReviewsService from '../../service/reviews.service'; const reviewsService = new ReviewsService(); -export const contentParseCheck = ( - content : string, -) => { +export const contentParseCheck = (content: string) => { const result = content.trim(); if (result === '' || result.length < 10 || result.length > 420) { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS_CONTENT, 400); @@ -14,35 +12,28 @@ export const contentParseCheck = ( return result; }; -export const reviewsIdParseCheck = ( - reviewsId : string, -) => { +export const reviewsIdParseCheck = (reviewsId: string) => { if (reviewsId.trim() === '') { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS_ID, 400); } try { return parseInt(reviewsId, 10); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.INVALID_INPUT_REVIEWS, 400); } }; -export const reviewsIdExistCheck = async ( - reviewsId : number, -) => { - let result : number; +export const reviewsIdExistCheck = async (reviewsId: number) => { + let result: number; try { result = await reviewsService.getReviewsUserId(reviewsId); - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.NOT_FOUND_REVIEWS, 404); } return result; }; -export const idAndTokenIdSameCheck = ( - id : number, - tokenId : number, -) => { +export const idAndTokenIdSameCheck = (id: number, tokenId: number) => { if (id !== tokenId) { throw new ErrorResponse(errorCode.UNAUTHORIZED_REVIEWS, 401); } diff --git a/backend/src/v1/reviews/controller/utils/parseCheck.ts b/backend/src/v1/reviews/controller/utils/parseCheck.ts index f6da9b63..75358c12 100644 --- a/backend/src/v1/reviews/controller/utils/parseCheck.ts +++ b/backend/src/v1/reviews/controller/utils/parseCheck.ts @@ -1,28 +1,17 @@ -export const sortParse = ( - sort : any, -) : 'ASC' | 'DESC' => { +export const sortParse = (sort: any): 'ASC' | 'DESC' => { if (sort === 'asc' || sort === 'desc' || sort === 'ASC' || sort === 'DESC') { return sort.toUpperCase(); } return 'DESC'; }; -export const pageParse = ( - page : number, -) : number => (Number.isNaN(page) ? 0 : page); +export const pageParse = (page: number): number => (Number.isNaN(page) ? 0 : page); -export const limitParse = ( - limit : number, -) : number => (Number.isNaN(limit) ? 10 : limit); +export const limitParse = (limit: number): number => (Number.isNaN(limit) ? 10 : limit); -export const stringQueryParse = ( - stringQuery : any, -) : string => ((stringQuery === undefined || null) ? '' : stringQuery.trim()); +export const stringQueryParse = (stringQuery: any): string => + stringQuery === undefined || null ? '' : stringQuery.trim(); -export const booleanQueryParse = ( - booleanQuery : any, -) : boolean => (booleanQuery === 'true'); +export const booleanQueryParse = (booleanQuery: any): boolean => booleanQuery === 'true'; -export const disabledParse = ( - disabled : number, -) : number => (Number.isNaN(disabled) ? -1 : disabled); +export const disabledParse = (disabled: number): number => (Number.isNaN(disabled) ? -1 : disabled); diff --git a/backend/src/v1/reviews/repository/reviews.repository.ts b/backend/src/v1/reviews/repository/reviews.repository.ts index b113f92b..1eff0433 100644 --- a/backend/src/v1/reviews/repository/reviews.repository.ts +++ b/backend/src/v1/reviews/repository/reviews.repository.ts @@ -11,13 +11,10 @@ export default class ReviewsRepository extends Repository { const queryRunner: QueryRunner | undefined = transactionQueryRunner; const entityManager = jipDataSource.createEntityManager(queryRunner); super(Reviews, entityManager); - this.bookInfoRepo = new Repository( - BookInfo, - entityManager, - ); + this.bookInfoRepo = new Repository(BookInfo, entityManager); } - async validateBookInfo(bookInfoId: number) : Promise { + async validateBookInfo(bookInfoId: number): Promise { const bookInfoCount = await this.bookInfoRepo.count({ where: { id: bookInfoId }, }); @@ -28,14 +25,17 @@ export default class ReviewsRepository extends Repository { async createReviews(userId: number, bookInfoId: number, content: string): Promise { await this.insert({ - userId, bookInfoId, content, updateUserId: userId, + userId, + bookInfoId, + content, + updateUserId: userId, }); } async getReviewsPage( reviewerId: number, isMyReview: boolean, - titleOrNickname :string, + titleOrNickname: string, disabled: number, page: number, sort: 'ASC' | 'DESC' | undefined, @@ -58,12 +58,15 @@ export default class ReviewsRepository extends Repository { if (isMyReview === true) { reviews.andWhere({ userId: reviewerId }); } else if (!isMyReview && titleOrNickname !== '') { - reviews.andWhere(`(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`); + reviews.andWhere( + `(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`, + ); } if (disabled !== -1) { reviews.andWhere({ disabled }); } - const ret = await reviews.offset(page * limit) + const ret = await reviews + .offset(page * limit) .limit(limit) .getRawMany(); return ret; @@ -74,7 +77,7 @@ export default class ReviewsRepository extends Repository { isMyReview: boolean, titleOrNickname: string, disabled: number, - ) : Promise { + ): Promise { const reviews = this.createQueryBuilder('reviews') .select('COUNT(*)', 'counts') .leftJoin(User, 'user', 'user.id = reviews.userId') @@ -83,7 +86,9 @@ export default class ReviewsRepository extends Repository { if (isMyReview === true) { reviews.andWhere({ userId: reviewerId }); } else if (!isMyReview && titleOrNickname !== '') { - reviews.andWhere(`(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`); + reviews.andWhere( + `(title LIKE '%${titleOrNickname}%' OR nickname LIKE '%${titleOrNickname}%')`, + ); } if (disabled !== -1) { reviews.andWhere({ disabled }); @@ -92,7 +97,7 @@ export default class ReviewsRepository extends Repository { return ret.counts; } - async getReviewsUserId(reviewsId : number): Promise { + async getReviewsUserId(reviewsId: number): Promise { const ret = await this.findOneOrFail({ select: { userId: true, @@ -105,7 +110,7 @@ export default class ReviewsRepository extends Repository { return ret.userId; } - async getReviews(reviewsId : number): Promise { + async getReviews(reviewsId: number): Promise { const ret = await this.find({ select: { userId: true, @@ -119,7 +124,7 @@ export default class ReviewsRepository extends Repository { return ret; } - async updateReviews(reviewsId : number, userId : number, content : string): Promise { + async updateReviews(reviewsId: number, userId: number, content: string): Promise { await this.update(reviewsId, { content, updateUserId: userId }); } @@ -127,13 +132,10 @@ export default class ReviewsRepository extends Repository { await this.update(reviewId, { isDeleted: true, deleteUserId: deleteUser }); } - async patchReviews(reviewsId : number, userId : number): Promise { - await this.update( - reviewsId, - { - disabled: () => 'IF(disabled=TRUE, FALSE, TRUE)', - disabledUserId: () => `IF(disabled=FALSE, NULL, ${userId})`, - }, - ); + async patchReviews(reviewsId: number, userId: number): Promise { + await this.update(reviewsId, { + disabled: () => 'IF(disabled=TRUE, FALSE, TRUE)', + disabledUserId: () => `IF(disabled=FALSE, NULL, ${userId})`, + }); } } diff --git a/backend/src/v1/reviews/service/reviews.service.ts b/backend/src/v1/reviews/service/reviews.service.ts index a914d580..b2637b9b 100644 --- a/backend/src/v1/reviews/service/reviews.service.ts +++ b/backend/src/v1/reviews/service/reviews.service.ts @@ -2,7 +2,7 @@ import * as errorCheck from './utils/errorCheck'; import ReviewsRepository from '../repository/reviews.repository'; export default class ReviewsService { - private readonly reviewsRepository : ReviewsRepository; + private readonly reviewsRepository: ReviewsRepository; constructor() { this.reviewsRepository = new ReviewsRepository(); @@ -37,12 +37,14 @@ export default class ReviewsService { titleOrNickname, disabled, ); - const itemsPerPage = (Number.isNaN(limit)) ? 10 : limit; + const itemsPerPage = Number.isNaN(limit) ? 10 : limit; const meta = { totalItems: counts, itemsPerPage, - totalPages: parseInt(String(counts / itemsPerPage - + Number((counts % itemsPerPage !== 0) || !counts)), 10), + totalPages: parseInt( + String(counts / itemsPerPage + Number(counts % itemsPerPage !== 0 || !counts)), + 10, + ), firstPage: page === 0, finalPage: page === parseInt(String(counts / itemsPerPage), 10), currentPage: page, @@ -50,18 +52,12 @@ export default class ReviewsService { return { items, meta }; } - async getReviewsUserId( - reviewsId: number, - ) { + async getReviewsUserId(reviewsId: number) { const reviewsUserId = await this.reviewsRepository.getReviewsUserId(reviewsId); return reviewsUserId; } - async updateReviews( - reviewsId: number, - userId: number, - content: string, - ) { + async updateReviews(reviewsId: number, userId: number, content: string) { const reviewsUserId = await errorCheck.updatePossibleCheck(reviewsId); errorCheck.idAndTokenIdSameCheck(reviewsUserId, userId); await this.reviewsRepository.updateReviews(reviewsId, userId, content); @@ -71,10 +67,7 @@ export default class ReviewsService { await this.reviewsRepository.deleteReviews(reviewId, deleteUser); } - async patchReviews( - reviewsId: number, - userId: number, - ) { + async patchReviews(reviewsId: number, userId: number) { await this.reviewsRepository.patchReviews(reviewsId, userId); } } diff --git a/backend/src/v1/reviews/service/utils/errorCheck.ts b/backend/src/v1/reviews/service/utils/errorCheck.ts index ab3af1f0..ad7e5e88 100644 --- a/backend/src/v1/reviews/service/utils/errorCheck.ts +++ b/backend/src/v1/reviews/service/utils/errorCheck.ts @@ -4,15 +4,13 @@ import ReviewsRepository from '../../repository/reviews.repository'; const reviewsRepository = new ReviewsRepository(); -export const updatePossibleCheck = async ( - reviewsId : number, -) => { - let result : any; - let resultId : number; +export const updatePossibleCheck = async (reviewsId: number) => { + let result: any; + let resultId: number; try { result = await reviewsRepository.getReviews(reviewsId); resultId = result[0].userId; - } catch (error : any) { + } catch (error: any) { throw new ErrorResponse(errorCode.NOT_FOUND_REVIEWS, 404); } if (result[0].disabled === 1) { @@ -21,10 +19,7 @@ export const updatePossibleCheck = async ( return resultId; }; -export const idAndTokenIdSameCheck = ( - id : number, - tokenId : number, -) => { +export const idAndTokenIdSameCheck = (id: number, tokenId: number) => { if (id !== tokenId) { throw new ErrorResponse(errorCode.UNAUTHORIZED_REVIEWS, 401); } diff --git a/backend/src/v1/routes/auth.routes.ts b/backend/src/v1/routes/auth.routes.ts index d1b81915..6fe39443 100644 --- a/backend/src/v1/routes/auth.routes.ts +++ b/backend/src/v1/routes/auth.routes.ts @@ -7,7 +7,12 @@ import { oauthUrlOption } from '~/config'; import * as errorCode from '~/v1/utils/error/errorCode'; import { getIntraAuthentication, - getMe, getOAuth, getToken, intraAuthentication, login, logout, + getMe, + getOAuth, + getToken, + intraAuthentication, + login, + logout, } from '~/v1/auth/auth.controller'; export const path = '/auth'; @@ -71,7 +76,14 @@ router.get('/oauth', getOAuth); * message: * type: string */ -router.get('/token', passport.authenticate('42', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/login?errorCode=${errorCode.ACCESS_DENIED}` }), getToken); +router.get( + '/token', + passport.authenticate('42', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/login?errorCode=${errorCode.ACCESS_DENIED}`, + }), + getToken, +); /** * @openapi @@ -312,4 +324,15 @@ router.get('/getIntraAuthentication', getIntraAuthentication); * message: * type: string */ -router.get('/intraAuthentication', passport.authenticate('42Auth', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/mypage?errorCode=${errorCode.ACCESS_DENIED}` }), passport.authenticate('jwt', { session: false, failureRedirect: `${oauthUrlOption.clientURL}/logout` }), intraAuthentication); +router.get( + '/intraAuthentication', + passport.authenticate('42Auth', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/mypage?errorCode=${errorCode.ACCESS_DENIED}`, + }), + passport.authenticate('jwt', { + session: false, + failureRedirect: `${oauthUrlOption.clientURL}/logout`, + }), + intraAuthentication, +); diff --git a/backend/src/v1/routes/bookInfoReviews.routes.ts b/backend/src/v1/routes/bookInfoReviews.routes.ts index 219d70a0..7c64b366 100644 --- a/backend/src/v1/routes/bookInfoReviews.routes.ts +++ b/backend/src/v1/routes/bookInfoReviews.routes.ts @@ -1,115 +1,113 @@ import { Router } from 'express'; import wrapAsyncController from '~/v1/middlewares/wrapAsyncController'; -import { - getBookInfoReviewsPage, -} from '~/v1/book-info-reviews/controller/bookInfoReviews.controller'; +import { getBookInfoReviewsPage } from '~/v1/book-info-reviews/controller/bookInfoReviews.controller'; export const path = '/book-info'; export const router = Router(); router -/** - * @openapi - * /api/book-info/{bookInfoId}/reviews: - * get: - * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. finalReviewsId는 마지막 리뷰의 Id를 반환하며, 반환할 아이디가 존재하지 않는 경우에는 해당 인자를 반환하지 않는다. - * tags: - * - bookInfo/reviews - * parameters: - * - name: bookInfoId - * required: true - * in: path - * schema: - * type: number - * description: bookInfoId에 해당 하는 리뷰 페이지를 반환한다. - * - name: reviewsId - * in: query - * schema: - * type: number - * required: false - * description: 해당 reviewsId를 조건으로 asc 기준 이후, desc 기준 이전의 페이지를 반환한다. 기본값은 첫 페이지를 반환한다. - * - name: sort - * in: query - * schema: - * type: string - * required: false - * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. 기본값은 asd으로 한다. - * - name: limit - * in: query - * schema: - * type: number - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * default(bookInfoId = 1) : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung1, - * content : hello, - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung2, - * content : hello, - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung3, - * content : hello, - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung4, - * content : hello, - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * title: 클린코드, - * nickname : sechung5, - * content : hello, - * } - * ] - * "meta": { - * totalItems: 100, - * itemsPerPage : 5, - * totalPages : 20, - * finalPage : False, - * finalReviewsId : 104 - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * 적절하지 않는 bookInfoId 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - */ + /** + * @openapi + * /api/book-info/{bookInfoId}/reviews: + * get: + * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. finalReviewsId는 마지막 리뷰의 Id를 반환하며, 반환할 아이디가 존재하지 않는 경우에는 해당 인자를 반환하지 않는다. + * tags: + * - bookInfo/reviews + * parameters: + * - name: bookInfoId + * required: true + * in: path + * schema: + * type: number + * description: bookInfoId에 해당 하는 리뷰 페이지를 반환한다. + * - name: reviewsId + * in: query + * schema: + * type: number + * required: false + * description: 해당 reviewsId를 조건으로 asc 기준 이후, desc 기준 이전의 페이지를 반환한다. 기본값은 첫 페이지를 반환한다. + * - name: sort + * in: query + * schema: + * type: string + * required: false + * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. 기본값은 asd으로 한다. + * - name: limit + * in: query + * schema: + * type: number + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * default(bookInfoId = 1) : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung1, + * content : hello, + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung2, + * content : hello, + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung3, + * content : hello, + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung4, + * content : hello, + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * title: 클린코드, + * nickname : sechung5, + * content : hello, + * } + * ] + * "meta": { + * totalItems: 100, + * itemsPerPage : 5, + * totalPages : 20, + * finalPage : False, + * finalReviewsId : 104 + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * 적절하지 않는 bookInfoId 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + */ .get('/:bookInfoId/reviews', wrapAsyncController(getBookInfoReviewsPage)); diff --git a/backend/src/v1/routes/books.routes.ts b/backend/src/v1/routes/books.routes.ts index aee0c720..ff70baaf 100644 --- a/backend/src/v1/routes/books.routes.ts +++ b/backend/src/v1/routes/books.routes.ts @@ -798,7 +798,7 @@ router .get('/create', authValidate(roleSet.librarian), createBookInfo); router -/** + /** * @openapi * /api/books/{id}: * get: @@ -871,7 +871,7 @@ router .get('/:id', getBookById); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * post: @@ -920,7 +920,7 @@ router .post('/info/:bookInfoId/like', authValidate(roleSet.service), createLike); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * delete: @@ -961,7 +961,7 @@ router .delete('/info/:bookInfoId/like', authValidate(roleSet.service), deleteLike); router -/** + /** * @openapi * /api/books/info/{bookInfoId}/like: * get: @@ -1007,98 +1007,98 @@ router .get('/info/:bookInfoId/like', authValidateDefaultNullUser(roleSet.all), getLikeInfo); router -/** - * @openapi - * /api/books/update: - * patch: - * description: 책 정보를 수정합니다. book_info table or book table - * tags: - * - books - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * bookInfoId: - * description: bookInfoId - * type: integer - * nullable: false - * example: 1 - * categoryId: - * description: categoryId - * type: integer - * nullable: false - * example: 1 - * title: - * description: 제목 - * type: string - * nullable: true - * example: "작별인사 (김영하 장편소설)" - * author: - * description: 저자 - * type: string - * nullable: true - * example: "김영하" - * publisher: - * description: 출판사 - * type: string - * nullable: true - * example: "복복서가" - * publishedAt: - * description: 출판연월 - * type: string - * nullable: true - * example: "20200505" - * image: - * description: 표지이미지 - * type: string - * nullable: true - * example: "https://bookthumb-phinf.pstatic.net/cover/223/538/22353804.jpg?type=m1&udate=20220608" - * bookId: - * description: bookId - * type: integer - * nullable: false - * example: 1 - * callSign: - * description: 청구기호 - * type: string - * nullable: true - * example: h1.18.v1.c1 - * status: - * description: 도서 상태 - * type: integer - * nullable: false - * example: 0 - * responses: - * '204': - * description: 성공했을 때 http 상태코드 204(NO_CONTENT) 값을 반환. - * content: - * application: - * schema: - * type: - * description: 성공했을 때 http 상태코드 204 값을 반환. - * '실패 케이스 1': - * description: 예상치 못한 에러로 책 정보 patch에 실패. - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 312 } - * '실패 케이스 2': - * description: 수정할 DATA가 적어도 한 개는 필요. 수정할 DATA가 없음" - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 313 } - * '실패 케이스 3': - * description: 입력한 publishedAt filed가 알맞은 형식이 아님. 기대하는 형식 "20220807" - * content: - * application/json: - * schema: - * type: json - * example : { errorCode: 311 } - */ + /** + * @openapi + * /api/books/update: + * patch: + * description: 책 정보를 수정합니다. book_info table or book table + * tags: + * - books + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * bookInfoId: + * description: bookInfoId + * type: integer + * nullable: false + * example: 1 + * categoryId: + * description: categoryId + * type: integer + * nullable: false + * example: 1 + * title: + * description: 제목 + * type: string + * nullable: true + * example: "작별인사 (김영하 장편소설)" + * author: + * description: 저자 + * type: string + * nullable: true + * example: "김영하" + * publisher: + * description: 출판사 + * type: string + * nullable: true + * example: "복복서가" + * publishedAt: + * description: 출판연월 + * type: string + * nullable: true + * example: "20200505" + * image: + * description: 표지이미지 + * type: string + * nullable: true + * example: "https://bookthumb-phinf.pstatic.net/cover/223/538/22353804.jpg?type=m1&udate=20220608" + * bookId: + * description: bookId + * type: integer + * nullable: false + * example: 1 + * callSign: + * description: 청구기호 + * type: string + * nullable: true + * example: h1.18.v1.c1 + * status: + * description: 도서 상태 + * type: integer + * nullable: false + * example: 0 + * responses: + * '204': + * description: 성공했을 때 http 상태코드 204(NO_CONTENT) 값을 반환. + * content: + * application: + * schema: + * type: + * description: 성공했을 때 http 상태코드 204 값을 반환. + * '실패 케이스 1': + * description: 예상치 못한 에러로 책 정보 patch에 실패. + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 312 } + * '실패 케이스 2': + * description: 수정할 DATA가 적어도 한 개는 필요. 수정할 DATA가 없음" + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 313 } + * '실패 케이스 3': + * description: 입력한 publishedAt filed가 알맞은 형식이 아님. 기대하는 형식 "20220807" + * content: + * application/json: + * schema: + * type: json + * example : { errorCode: 311 } + */ .patch('/update', authValidate(roleSet.librarian), updateBookInfo) .patch('/donator', authValidate(roleSet.librarian), updateBookDonator); diff --git a/backend/src/v1/routes/cursus.routes.ts b/backend/src/v1/routes/cursus.routes.ts index 45e524de..b0d0c737 100644 --- a/backend/src/v1/routes/cursus.routes.ts +++ b/backend/src/v1/routes/cursus.routes.ts @@ -97,106 +97,106 @@ router .get('/recommend/books', limiter, authValidate(roleSet.all), recommendBook); router -/** - * @openapi - * /api/cursus/projects: - * get: - * summary: 42 API를 통해 cursus의 프로젝트 정보를 가져온다. - * description: 42 API를 통해 cursus의 프로젝트를 정보를 가져와서 json으로 저장한다. - * tags: - * - cursus - * parameters: - * - name: page - * in: query - * description: 프로젝트 정보를 가져올 페이지 번호 - * required: true - * schema: - * type: integer - * example: 1 - * default: 1 - * - name: mode - * in: query - * description: 프로젝트 정보를 가져올 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. - * required: true - * schema: - * type: string - * enum: [append, overwrite] - * example: overwrite - * responses: - * '200': - * description: 프로젝트 정보를 성공적으로 가져옴. - * content: - * application/json: - * schema: - * type: object - * example: { - * projects: [ - * { - * id: 1, - * name: "Libft", - * slug: "libft", - * parent: null, - * cursus: [ - * { - * id: 1, - * name: "42", - * slug: "42" - * }, - * { - * id: 8, - * name: "WeThinkCode_", - * slug: "wethinkcode_" - * }, - * { - * id: 10, - * name: "Formation Pole Emploi", - * slug: "formation-pole-emploi" - * } - * ] - * }, - * { - * id: 2, - * name: "GET_Next_Line", - * slug: "get_next_line", - * parent: null, - * cursus: [ - * { - * id: 1, - * name: "42", - * slug: "42" - * }, - * { - * id: 8, - * name: "WeThinkCode_", - * slug: "wethinkcode_" - * }, - * { - * id: 10, - * name: "Formation Pole Emploi", - * slug: "formation-pole-emploi" - * }, - * { - * id: 18, - * name: "Starfleet", - * slug: "starfleet" - * } - * ] - * } - * ] - * } - * '400': - * description: 잘못된 요청 URL입니다. - * content: - * application/json: - * schema: - * type: json - * example: {errorCode: 400} - * '401': - * description: 토큰이 유효하지 않습니다. - * content: - * application/json: - * schema: - * type: json - * example: {errorCode: 401} - */ + /** + * @openapi + * /api/cursus/projects: + * get: + * summary: 42 API를 통해 cursus의 프로젝트 정보를 가져온다. + * description: 42 API를 통해 cursus의 프로젝트를 정보를 가져와서 json으로 저장한다. + * tags: + * - cursus + * parameters: + * - name: page + * in: query + * description: 프로젝트 정보를 가져올 페이지 번호 + * required: true + * schema: + * type: integer + * example: 1 + * default: 1 + * - name: mode + * in: query + * description: 프로젝트 정보를 가져올 모드. append면 기존에 저장된 정보에 추가로 저장하고, overwrite면 기존에 저장된 정보를 덮어쓴다. + * required: true + * schema: + * type: string + * enum: [append, overwrite] + * example: overwrite + * responses: + * '200': + * description: 프로젝트 정보를 성공적으로 가져옴. + * content: + * application/json: + * schema: + * type: object + * example: { + * projects: [ + * { + * id: 1, + * name: "Libft", + * slug: "libft", + * parent: null, + * cursus: [ + * { + * id: 1, + * name: "42", + * slug: "42" + * }, + * { + * id: 8, + * name: "WeThinkCode_", + * slug: "wethinkcode_" + * }, + * { + * id: 10, + * name: "Formation Pole Emploi", + * slug: "formation-pole-emploi" + * } + * ] + * }, + * { + * id: 2, + * name: "GET_Next_Line", + * slug: "get_next_line", + * parent: null, + * cursus: [ + * { + * id: 1, + * name: "42", + * slug: "42" + * }, + * { + * id: 8, + * name: "WeThinkCode_", + * slug: "wethinkcode_" + * }, + * { + * id: 10, + * name: "Formation Pole Emploi", + * slug: "formation-pole-emploi" + * }, + * { + * id: 18, + * name: "Starfleet", + * slug: "starfleet" + * } + * ] + * } + * ] + * } + * '400': + * description: 잘못된 요청 URL입니다. + * content: + * application/json: + * schema: + * type: json + * example: {errorCode: 400} + * '401': + * description: 토큰이 유효하지 않습니다. + * content: + * application/json: + * schema: + * type: json + * example: {errorCode: 401} + */ .get('/projects', getProjects); diff --git a/backend/src/v1/routes/histories.routes.ts b/backend/src/v1/routes/histories.routes.ts index cc3b8e8c..8a58ba7c 100644 --- a/backend/src/v1/routes/histories.routes.ts +++ b/backend/src/v1/routes/histories.routes.ts @@ -1,7 +1,5 @@ import { Router } from 'express'; -import { - histories, -} from '~/v1/histories/histories.controller'; +import { histories } from '~/v1/histories/histories.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; diff --git a/backend/src/v1/routes/lendings.routes.ts b/backend/src/v1/routes/lendings.routes.ts index 3ad119c9..1e15965b 100644 --- a/backend/src/v1/routes/lendings.routes.ts +++ b/backend/src/v1/routes/lendings.routes.ts @@ -1,7 +1,5 @@ import { Router } from 'express'; -import { - create, search, lendingId, returnBook, -} from '~/v1/lendings/lendings.controller'; +import { create, search, lendingId, returnBook } from '~/v1/lendings/lendings.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -9,313 +7,313 @@ export const path = '/lendings'; export const router = Router(); router -/** - * @openapi - * /api/lendings: - * post: - * tags: - * - lendings - * summary: 대출 기록 생성 - * description: 대출 기록을 생성한다. - * requestBody: - * description: bookId와 userId는 각각 대출할 도서와 대출할 회원의 pk, condition은 대출 당시 책 상태를 의미한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * bookId: - * type: integer - * example: 33 - * userId: - * type: integer - * example: 45 - * condition: - * type: string - * example: "이상 없음" - * required: - * - bookId - * - userId - * - condition - * responses: - * '200': - * description: 생성된 대출기록의 반납일자를 반환. - * content: - * application/json: - * schema: - * type: object - * properties: - * dueDate: - * type: date | string - * example: 2022-12-12 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 2 - * '401': - * description: 대출을 생성할 권한이 없는 사용자 - * '500': - * description: db 에러 - * */ + /** + * @openapi + * /api/lendings: + * post: + * tags: + * - lendings + * summary: 대출 기록 생성 + * description: 대출 기록을 생성한다. + * requestBody: + * description: bookId와 userId는 각각 대출할 도서와 대출할 회원의 pk, condition은 대출 당시 책 상태를 의미한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * bookId: + * type: integer + * example: 33 + * userId: + * type: integer + * example: 45 + * condition: + * type: string + * example: "이상 없음" + * required: + * - bookId + * - userId + * - condition + * responses: + * '200': + * description: 생성된 대출기록의 반납일자를 반환. + * content: + * application/json: + * schema: + * type: object + * properties: + * dueDate: + * type: date | string + * example: 2022-12-12 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 2 + * '401': + * description: 대출을 생성할 권한이 없는 사용자 + * '500': + * description: db 에러 + * */ .post('/', authValidate(roleSet.librarian), create) -/** - * @openapi - * /api/lendings/search: - * get: - * tags: - * - lendings - * summary: 대출 기록 정보 조회 - * description: 대출 기록의 정보를 검색하여 보여준다. - * parameters: - * - name: page - * in: query - * description: 검색 결과의 페이지 - * schema: - * type: integer - * default: 1 - * example: 3 - * - name: limit - * in: query - * description: 검색 결과 한 페이지당 보여줄 결과물의 개수 - * schema: - * type: integer - * default: 5 - * example: 3 - * - name: sort - * in: query - * description: 검색 결과를 정렬할 기준 - * schema: - * type: string - * enum: [new, old] - * default: new - * - name: query - * in: query - * description: 대출 기록에서 검색할 단어, 검색 가능한 필드 [user, title, callSign, bookId] - * schema: - * type: string - * example: 파이썬 - * - name: type - * in: query - * description: query를 조회할 항목 - * schema: - * type: string - * enum: [user, title, callSign, bookId] - * responses: - * '200': - * description: 대출 기록을 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * items: - * description: 검색된 책들의 목록 - * type: array - * items: - * type: object - * properties: - * id: - * description: 대출 고유 id - * type: integer - * condition: - * description: 대출 당시 책 상태 - * type: string - * login: - * description: 대출한 카뎃의 인트라 id - * type: string - * penaltyDays: - * description: 현재 대출 기록의 연체 일수 - * type: integer - * callSign: - * description: 대출된 책의 청구기호 - * type: string - * title: - * description: 대출된 책의 제목 - * type: string - * createdAt: - * type: string - * format: date - * dueDate: - * description: 반납기한 - * type: string - * format: date - * example: - * - id: 2 - * condition: 양호 - * login: minkykim - * penaltyDays: 0 - * callSign: O40.15.v1.c1 - * title: "소프트웨어 장인(로버트 C. 마틴 시리즈)" - * dueDate: 2021.09.20 - * - id: 42 - * condition: 이상없음 - * login: jwoo - * penaltyDays: 2 - * callSign: H19.19.v1.c1 - * title: "클린 아키텍처: 소프트웨어 구조와 설계의 원칙" - * dueDate: 2022.06.07 - * meta: - * description: 대출 조회 결과에 대한 요약 정보 - * type: object - * properties: - * totalItems: - * description: 전체 대출 검색 결과 건수 - * type: integer - * example: 2 - * itemCount: - * description: 현재 페이지 검색 결과 수 - * type: integer - * example: 2 - * itemsPerPage: - * description: 페이지 당 검색 결과 수 - * type: integer - * example: 2 - * totalPages: - * description: 전체 결과 페이지 수 - * type: integer - * example: 1 - * currentPage: - * description: 현재 페이지 - * type: integer - * example: 1 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 - * '401': - * description: 대출을 조회할 권한이 없는 사용자 - * '500': - * description: db 에러 - */ + /** + * @openapi + * /api/lendings/search: + * get: + * tags: + * - lendings + * summary: 대출 기록 정보 조회 + * description: 대출 기록의 정보를 검색하여 보여준다. + * parameters: + * - name: page + * in: query + * description: 검색 결과의 페이지 + * schema: + * type: integer + * default: 1 + * example: 3 + * - name: limit + * in: query + * description: 검색 결과 한 페이지당 보여줄 결과물의 개수 + * schema: + * type: integer + * default: 5 + * example: 3 + * - name: sort + * in: query + * description: 검색 결과를 정렬할 기준 + * schema: + * type: string + * enum: [new, old] + * default: new + * - name: query + * in: query + * description: 대출 기록에서 검색할 단어, 검색 가능한 필드 [user, title, callSign, bookId] + * schema: + * type: string + * example: 파이썬 + * - name: type + * in: query + * description: query를 조회할 항목 + * schema: + * type: string + * enum: [user, title, callSign, bookId] + * responses: + * '200': + * description: 대출 기록을 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * description: 검색된 책들의 목록 + * type: array + * items: + * type: object + * properties: + * id: + * description: 대출 고유 id + * type: integer + * condition: + * description: 대출 당시 책 상태 + * type: string + * login: + * description: 대출한 카뎃의 인트라 id + * type: string + * penaltyDays: + * description: 현재 대출 기록의 연체 일수 + * type: integer + * callSign: + * description: 대출된 책의 청구기호 + * type: string + * title: + * description: 대출된 책의 제목 + * type: string + * createdAt: + * type: string + * format: date + * dueDate: + * description: 반납기한 + * type: string + * format: date + * example: + * - id: 2 + * condition: 양호 + * login: minkykim + * penaltyDays: 0 + * callSign: O40.15.v1.c1 + * title: "소프트웨어 장인(로버트 C. 마틴 시리즈)" + * dueDate: 2021.09.20 + * - id: 42 + * condition: 이상없음 + * login: jwoo + * penaltyDays: 2 + * callSign: H19.19.v1.c1 + * title: "클린 아키텍처: 소프트웨어 구조와 설계의 원칙" + * dueDate: 2022.06.07 + * meta: + * description: 대출 조회 결과에 대한 요약 정보 + * type: object + * properties: + * totalItems: + * description: 전체 대출 검색 결과 건수 + * type: integer + * example: 2 + * itemCount: + * description: 현재 페이지 검색 결과 수 + * type: integer + * example: 2 + * itemsPerPage: + * description: 페이지 당 검색 결과 수 + * type: integer + * example: 2 + * totalPages: + * description: 전체 결과 페이지 수 + * type: integer + * example: 1 + * currentPage: + * description: 현재 페이지 + * type: integer + * example: 1 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 value 등 + * '401': + * description: 대출을 조회할 권한이 없는 사용자 + * '500': + * description: db 에러 + */ .get('/search', authValidate(roleSet.librarian), search) -/** - * @openapi - * /api/lendings/{lendingId}: - * get: - * tags: - * - lendings - * summary: 특정 대출 기록 조회 - * description: 특정 대출 기록의 상세 정보를 보여준다. - * parameters: - * - name: lendingId - * in: path - * description: 대출 기록의 고유 아이디 - * required: true - * schema: - * type: integer - * responses: - * '200': - * description: 대출 기록을 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 대출 고유 id - * type: integer - * example: 2 - * condition: - * description: 대출 당시 책 상태 - * type: string - * example: 양호 - * createdAt: - * description: 대출 일자(대출 레코드 생성 일자) - * type: string - * format: date - * example: 2021.09.06. - * login: - * description: 대출한 카뎃의 인트라 id - * type: string - * example: minkykim - * penaltyDays: - * description: 현재 대출 기록의 연체 일수 - * type: integer - * example: 2 - * callSign: - * description: 대출된 책의 청구기호 - * type: string - * example: H1.13.v1.c1 - * title: - * description: 대출된 책의 제목 - * type: string - * example: 소프트웨어 장인(로버트 C. 마틴 시리즈) - * image: - * description: 대출된 책의 표지 - * type: string - * example: https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F1633934%3Ftimestamp%3D20210706193409 - * dueDate: - * description: 반납기한 - * type: string - * format: date - * example: 2021.09.20 - * '400': - * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 lendingId 등 - * '401': - * description: 대출을 조회할 권한이 없는 사용자 - * '500': - * description: db 에러 - */ + /** + * @openapi + * /api/lendings/{lendingId}: + * get: + * tags: + * - lendings + * summary: 특정 대출 기록 조회 + * description: 특정 대출 기록의 상세 정보를 보여준다. + * parameters: + * - name: lendingId + * in: path + * description: 대출 기록의 고유 아이디 + * required: true + * schema: + * type: integer + * responses: + * '200': + * description: 대출 기록을 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 대출 고유 id + * type: integer + * example: 2 + * condition: + * description: 대출 당시 책 상태 + * type: string + * example: 양호 + * createdAt: + * description: 대출 일자(대출 레코드 생성 일자) + * type: string + * format: date + * example: 2021.09.06. + * login: + * description: 대출한 카뎃의 인트라 id + * type: string + * example: minkykim + * penaltyDays: + * description: 현재 대출 기록의 연체 일수 + * type: integer + * example: 2 + * callSign: + * description: 대출된 책의 청구기호 + * type: string + * example: H1.13.v1.c1 + * title: + * description: 대출된 책의 제목 + * type: string + * example: 소프트웨어 장인(로버트 C. 마틴 시리즈) + * image: + * description: 대출된 책의 표지 + * type: string + * example: https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F1633934%3Ftimestamp%3D20210706193409 + * dueDate: + * description: 반납기한 + * type: string + * format: date + * example: 2021.09.20 + * '400': + * description: 잘못된 요청. 잘못 입력된 json key, 유효하지 않은 lendingId 등 + * '401': + * description: 대출을 조회할 권한이 없는 사용자 + * '500': + * description: db 에러 + */ .get('/:id', authValidate(roleSet.librarian), lendingId) -/** - * @openapi - * /api/lendings/return: - * patch: - * tags: - * - lendings - * summary: 반납 처리 - * description: 대출 레코드에 반납 처리를 한다. - * requestBody: - * description: lendingId는 대출 고유 아이디, condition은 반납 당시 책 상태 - * content: - * application/json: - * schema: - * type: object - * properties: - * lendingId: - * type: integer - * condition: - * type: string - * required: - * - lendingId - * - condition - * responses: - * '200': - * description: 반납처리 완료, 반납된 책이 예약이 되어있는지 알려줌 - * content: - * application/json: - * schema: - * type: object - * properties: - * reservedBook: - * description: 반납된 책이 예약이 되어있는지 알려줌 - * type: boolean - * example: true - * '400': - * description: 에러코드 0 dto에러 잘못된 json key, 1 db 에러 알 수 없는 lending id 등 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * '401': - * description: 알 수 없는 사용자 0 로그인 안 된 유저 1 사서권한없음 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * */ + /** + * @openapi + * /api/lendings/return: + * patch: + * tags: + * - lendings + * summary: 반납 처리 + * description: 대출 레코드에 반납 처리를 한다. + * requestBody: + * description: lendingId는 대출 고유 아이디, condition은 반납 당시 책 상태 + * content: + * application/json: + * schema: + * type: object + * properties: + * lendingId: + * type: integer + * condition: + * type: string + * required: + * - lendingId + * - condition + * responses: + * '200': + * description: 반납처리 완료, 반납된 책이 예약이 되어있는지 알려줌 + * content: + * application/json: + * schema: + * type: object + * properties: + * reservedBook: + * description: 반납된 책이 예약이 되어있는지 알려줌 + * type: boolean + * example: true + * '400': + * description: 에러코드 0 dto에러 잘못된 json key, 1 db 에러 알 수 없는 lending id 등 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * '401': + * description: 알 수 없는 사용자 0 로그인 안 된 유저 1 사서권한없음 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * */ .patch('/return', authValidate(roleSet.librarian), returnBook); diff --git a/backend/src/v1/routes/reservations.routes.ts b/backend/src/v1/routes/reservations.routes.ts index 8d83ebd5..e26d259a 100644 --- a/backend/src/v1/routes/reservations.routes.ts +++ b/backend/src/v1/routes/reservations.routes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { - cancel, create, search, count, userReservations, + cancel, + create, + search, + count, + userReservations, } from '~/v1/reservations/reservations.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -88,7 +92,7 @@ export const router = Router(); * description: * type: integer * example: 2 -* '400_case2': + * '400_case2': * description: 예약에 실패한 경우 * content: * application/json: diff --git a/backend/src/v1/routes/reviews.routes.ts b/backend/src/v1/routes/reviews.routes.ts index e4ef1030..1c06f306 100644 --- a/backend/src/v1/routes/reviews.routes.ts +++ b/backend/src/v1/routes/reviews.routes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { - createReviews, updateReviews, getReviews, deleteReviews, patchReviews, + createReviews, + updateReviews, + getReviews, + deleteReviews, + patchReviews, } from '~/v1/reviews/controller/reviews.controller'; import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; @@ -11,610 +15,610 @@ export const router = Router(); router /** - * @openapi - * /api/reviews: - * post: - * description: 책 리뷰를 작성한다. content 길이는 10글자 이상 420글자 이하로 입력하여야 한다. - * tags: - * - reviews - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * bookInfoId: - * type: number - * nullable: false - * required: true - * example: 42 - * content: - * type: string - * nullable: false - * required: true - * example: "책이 좋네요 열글자." - * responses: - * '201': - * description: 리뷰가 DB에 정상적으로 insert됨. - * '400': - * description: 잘못된 요청. - * content: - * application/json: - * schema: - * type: object - * examples: - * 유효하지 않은 content 길이 : - * value: - * errorCode: 801 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + * @openapi + * /api/reviews: + * post: + * description: 책 리뷰를 작성한다. content 길이는 10글자 이상 420글자 이하로 입력하여야 한다. + * tags: + * - reviews + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * bookInfoId: + * type: number + * nullable: false + * required: true + * example: 42 + * content: + * type: string + * nullable: false + * required: true + * example: "책이 좋네요 열글자." + * responses: + * '201': + * description: 리뷰가 DB에 정상적으로 insert됨. + * '400': + * description: 잘못된 요청. + * content: + * application/json: + * schema: + * type: object + * examples: + * 유효하지 않은 content 길이 : + * value: + * errorCode: 801 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .post('/', authValidate(roleSet.all), wrapAsyncController(createReviews)); router -/** - * @openapi - * /api/reviews: - * get: - * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, - * finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. - * tags: - * - reviews - * parameters: - * - name: titleOrNickname - * in: query - * description: 책 제목 또는 닉네임을 검색어로 받는다. - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: number - * description: 해당하는 페이지를 보여준다. - * required: false - * - name: disabled - * in: query - * description: 0이라면 공개 리뷰를, 1이라면 비공개 리뷰를, -1이라면 모든 리뷰를 가져온다. - * required: true - * schema: - * type: number - * - name: limit - * in: query - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * required: false - * schema: - * type: number - * - name: sort - * in: query - * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. - * required: false - * schema: - * type: string - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * bookInfo 기준 : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "sechung", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 6, - * reviewerId : 105, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 7, - * reviewerId : 106, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 8, - * reviewerId : 107, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 9, - * reviewerId : 108, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 10, - * reviewerId : 109, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * ] - * "meta": { - * totalItems: 100, - * itemCount : 10, - * itemsPerPage : 10, - * totalPages : 20, - * currentPage : 1, - * finalPage : False - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 bookInfoId 값: - * value: - * errorCode: 2 - * 적절하지 않는 userId 값: - * value: - * errorCode: 2 - * 적절하지 않는 page 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 사서 권한 없음 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + /** + * @openapi + * /api/reviews: + * get: + * description: 책 리뷰 10개를 반환한다. 최종 페이지의 경우 1 <= n <= 10 개의 값이 반환될 수 있다. content에는 리뷰에 대한 정보를, + * finalPage 에는 해당 페이지가 마지막인지에 대한 여부를 boolean 값으로 반환한다. + * tags: + * - reviews + * parameters: + * - name: titleOrNickname + * in: query + * description: 책 제목 또는 닉네임을 검색어로 받는다. + * schema: + * type: string + * - name: page + * in: query + * schema: + * type: number + * description: 해당하는 페이지를 보여준다. + * required: false + * - name: disabled + * in: query + * description: 0이라면 공개 리뷰를, 1이라면 비공개 리뷰를, -1이라면 모든 리뷰를 가져온다. + * required: true + * schema: + * type: number + * - name: limit + * in: query + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * required: false + * schema: + * type: number + * - name: sort + * in: query + * description: asc, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. + * required: false + * schema: + * type: string + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * bookInfo 기준 : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "sechung", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 6, + * reviewerId : 105, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 7, + * reviewerId : 106, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 8, + * reviewerId : 107, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 9, + * reviewerId : 108, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 10, + * reviewerId : 109, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * ] + * "meta": { + * totalItems: 100, + * itemCount : 10, + * itemsPerPage : 10, + * totalPages : 20, + * currentPage : 1, + * finalPage : False + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 bookInfoId 값: + * value: + * errorCode: 2 + * 적절하지 않는 userId 값: + * value: + * errorCode: 2 + * 적절하지 않는 page 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 사서 권한 없음 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .get('/', authValidate(roleSet.librarian), wrapAsyncController(getReviews)); router -/** - * @openapi - * /api/reviews/my-reviews: - * get: - * description: 자기자신에 대한 모든 Review 데이터를 가져온다. 내부적으로 getReview와 같은 함수를 사용한다. - * tags: - * - reviews - * parameters: - * - name: titleOrNickname - * in: query - * description: 책 제목 또는 닉네임을 검색어로 받는다. - * schema: - * type: string - * - name: limit - * in: query - * schema: - * type: number - * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] - * required: false - * - name: page - * in: query - * schema: - * type: number - * description: 해당하는 페이지를 보여준다. - * required: false - * - name: sort - * in: query - * schema: - * type: string - * description: asd, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. - * required: false - * - name: isMyReview - * in: query - * default: false - * schema: - * type: boolean - * description: true 라면 마이페이지 용도의 리뷰를, false 라면 모든 리뷰를 가져온다. - * responses: - * '200': - * content: - * application/json: - * schema: - * type: object - * examples: - * bookInfo 기준 : - * value: - * items : [ - * { - * reviewsId : 1, - * reviewerId : 100, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "sechung", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 2, - * reviewerId : 101, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 3, - * reviewerId : 102, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 4, - * reviewerId : 103, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 5, - * reviewerId : 104, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 6, - * reviewerId : 105, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 7, - * reviewerId : 106, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 8, - * reviewerId : 107, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 9, - * reviewerId : 108, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * { - * reviewsId : 10, - * reviewerId : 109, - * bookInfoId: 1, - * content : "hello", - * createdAt: "2022-11-09T06:56:15.640Z", - * title: "클린코드", - * nickname : "chanheki", - * intraId: "default@student.42seoul.kr", - * }, - * ] - * "meta": { - * totalItems: 100, - * itemCount : 10, - * itemsPerPage : 10, - * totalPages : 20, - * currentPage : 1, - * finalPage : False - * } - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 page 값: - * value: - * errorCode: 2 - * 적절하지 않는 sort 값: - * value: - * errorCode: 2 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - */ + /** + * @openapi + * /api/reviews/my-reviews: + * get: + * description: 자기자신에 대한 모든 Review 데이터를 가져온다. 내부적으로 getReview와 같은 함수를 사용한다. + * tags: + * - reviews + * parameters: + * - name: titleOrNickname + * in: query + * description: 책 제목 또는 닉네임을 검색어로 받는다. + * schema: + * type: string + * - name: limit + * in: query + * schema: + * type: number + * description: 한 페이지에서 몇 개의 게시글을 가져올 지 결정한다. [default = 10] + * required: false + * - name: page + * in: query + * schema: + * type: number + * description: 해당하는 페이지를 보여준다. + * required: false + * - name: sort + * in: query + * schema: + * type: string + * description: asd, desc 값을 통해 시간순으로 정렬된 페이지를 반환한다. + * required: false + * - name: isMyReview + * in: query + * default: false + * schema: + * type: boolean + * description: true 라면 마이페이지 용도의 리뷰를, false 라면 모든 리뷰를 가져온다. + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * examples: + * bookInfo 기준 : + * value: + * items : [ + * { + * reviewsId : 1, + * reviewerId : 100, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "sechung", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 2, + * reviewerId : 101, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 3, + * reviewerId : 102, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 4, + * reviewerId : 103, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 5, + * reviewerId : 104, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 6, + * reviewerId : 105, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 7, + * reviewerId : 106, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 8, + * reviewerId : 107, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 9, + * reviewerId : 108, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * { + * reviewsId : 10, + * reviewerId : 109, + * bookInfoId: 1, + * content : "hello", + * createdAt: "2022-11-09T06:56:15.640Z", + * title: "클린코드", + * nickname : "chanheki", + * intraId: "default@student.42seoul.kr", + * }, + * ] + * "meta": { + * totalItems: 100, + * itemCount : 10, + * itemsPerPage : 10, + * totalPages : 20, + * currentPage : 1, + * finalPage : False + * } + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 page 값: + * value: + * errorCode: 2 + * 적절하지 않는 sort 값: + * value: + * errorCode: 2 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + */ .get('/my-reviews', authValidate(roleSet.all), wrapAsyncController(getReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * put: - * description: 책 리뷰를 수정한다. 작성자만 수정할 수 있다. content 길이는 10글자 이상 100글자 이하로 입력하여야 한다. - * tags: - * - reviews - * parameters: - * - name: reviewsId - * in: path - * description: 수정할 reviews ID - * required: true - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * content: - * type: string - * nullable: false경 - * example: "책이 좋네요 열글자." - * responses: - * '200': - * description: 리뷰가 DB에 정상적으로 update됨. - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * 유효하지 않은 content 길이 : - * value: - * errorCode: 801 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : - * value : - * errorCode: 801 - * 토큰 Disabled Reviews는 수정할 수 없음. : - * value : - * errorCode: 805 - * '404': - * description: 존재하지 않는 reviewsId. - * content: - * application/json: - * schema: - * type: object - * examples: - * 존재하지 않는 reviewsId : - * value: - * errorCode: 804 - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * put: + * description: 책 리뷰를 수정한다. 작성자만 수정할 수 있다. content 길이는 10글자 이상 100글자 이하로 입력하여야 한다. + * tags: + * - reviews + * parameters: + * - name: reviewsId + * in: path + * description: 수정할 reviews ID + * required: true + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * nullable: false경 + * example: "책이 좋네요 열글자." + * responses: + * '200': + * description: 리뷰가 DB에 정상적으로 update됨. + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * 유효하지 않은 content 길이 : + * value: + * errorCode: 801 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : + * value : + * errorCode: 801 + * 토큰 Disabled Reviews는 수정할 수 없음. : + * value : + * errorCode: 805 + * '404': + * description: 존재하지 않는 reviewsId. + * content: + * application/json: + * schema: + * type: object + * examples: + * 존재하지 않는 reviewsId : + * value: + * errorCode: 804 + */ .put('/:reviewsId', authValidate(roleSet.all), wrapAsyncController(updateReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * patch: - * description: 책 리뷰의 비활성화 여부를 토글 방식으로 변환 - * tags: - * - reviews - * parameters: - * - name: reviewsId - * in: path - * description: 수정할 reviews ID - * required: true - * requestBody: - * required: false - * responses: - * '200': - * description: 리뷰가 DB에 정상적으로 fetch됨. - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * patch: + * description: 책 리뷰의 비활성화 여부를 토글 방식으로 변환 + * tags: + * - reviews + * parameters: + * - name: reviewsId + * in: path + * description: 수정할 reviews ID + * required: true + * requestBody: + * required: false + * responses: + * '200': + * description: 리뷰가 DB에 정상적으로 fetch됨. + */ .patch('/:reviewsId', authValidate(roleSet.librarian), wrapAsyncController(patchReviews)); router -/** - * @openapi - * /api/reviews/{reviewsId}: - * delete: - * description: 책 리뷰를 삭제한다. 작성자와 사서 권한이 있는 사용자만 삭제할 수 있다. - * tags: - * - reviews - * parameters: - * - name: reviewsId - * required: true - * in: path - * description: 들어온 reviewsId에 해당하는 리뷰를 삭제한다. - * responses: - * '200': - * description: 리뷰가 DB에서 정상적으로 delete됨. - * '400': - * content: - * application/json: - * schema: - * type: object - * examples: - * 적절하지 않는 reviewsId 값: - * value: - * errorCode: 800 - * '401': - * description: 권한 없음. - * content: - * application/json: - * schema: - * type: object - * examples: - * 토큰 누락 : - * value: - * errorCode: 100 - * 토큰 유저 존재하지 않음 : - * value : - * errorCode: 101 - * 토큰 만료 : - * value : - * errorCode: 108 - * 토큰 유효하지 않음 : - * value : - * errorCode: 109 - * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : - * value : - * errorCode: 801 - * '404': - * description: 존재하지 않는 reviewsId. - * content: - * application/json: - * schema: - * type: object - * examples: - * 존재하지 않는 reviewsId : - * value: - * errorCode: 804 - */ + /** + * @openapi + * /api/reviews/{reviewsId}: + * delete: + * description: 책 리뷰를 삭제한다. 작성자와 사서 권한이 있는 사용자만 삭제할 수 있다. + * tags: + * - reviews + * parameters: + * - name: reviewsId + * required: true + * in: path + * description: 들어온 reviewsId에 해당하는 리뷰를 삭제한다. + * responses: + * '200': + * description: 리뷰가 DB에서 정상적으로 delete됨. + * '400': + * content: + * application/json: + * schema: + * type: object + * examples: + * 적절하지 않는 reviewsId 값: + * value: + * errorCode: 800 + * '401': + * description: 권한 없음. + * content: + * application/json: + * schema: + * type: object + * examples: + * 토큰 누락 : + * value: + * errorCode: 100 + * 토큰 유저 존재하지 않음 : + * value : + * errorCode: 101 + * 토큰 만료 : + * value : + * errorCode: 108 + * 토큰 유효하지 않음 : + * value : + * errorCode: 109 + * 토큰 userId와 리뷰 userID 불일치 && 사서 권한 없음 : + * value : + * errorCode: 801 + * '404': + * description: 존재하지 않는 reviewsId. + * content: + * application/json: + * schema: + * type: object + * examples: + * 존재하지 않는 reviewsId : + * value: + * errorCode: 804 + */ .delete('/:reviewsId', authValidate(roleSet.all), wrapAsyncController(deleteReviews)); diff --git a/backend/src/v1/routes/searchKeywords.routes.ts b/backend/src/v1/routes/searchKeywords.routes.ts index 457e12e1..83740735 100644 --- a/backend/src/v1/routes/searchKeywords.routes.ts +++ b/backend/src/v1/routes/searchKeywords.routes.ts @@ -1,5 +1,8 @@ import { Router } from 'express'; -import { searchKeywordsAutocomplete, getPopularSearchKeywords } from '../search-keywords/searchKeywords.controller'; +import { + searchKeywordsAutocomplete, + getPopularSearchKeywords, +} from '../search-keywords/searchKeywords.controller'; export const path = '/search-keywords'; export const router = Router(); diff --git a/backend/src/v1/routes/stock.routes.ts b/backend/src/v1/routes/stock.routes.ts index 652ab929..ab3c29da 100644 --- a/backend/src/v1/routes/stock.routes.ts +++ b/backend/src/v1/routes/stock.routes.ts @@ -6,136 +6,136 @@ export const path = '/stock'; export const router = Router(); router -/** - * @openapi - * /api/stock/search: - * get: - * description: 책 재고 정보를 검색해 온다. - * tags: - * - stock - * parameters: - * - in: query - * name: page - * description: 페이지 - * schema: - * type: integer - * - in: query - * name: limit - * description: 한 페이지에 들어올 검색결과 수 - * schema: - * type: integer - * responses: - * '200': - * description: 검색 결과를 반환한다. - * content: - * application/json: - * schema: - * type: object - * properties: - * items: - * description: 재고 정보 목록 - * type: array - * items: - * type: object - * properties: - * bookId: - * description: 도서 번호 - * type: integer - * example: 3 - * bookInfoId: - * description: 도서 정보 번호 - * type: integer - * example: 2 - * title: - * description: 책 제목 - * type: string - * example: "TCP IP 윈도우 소켓 프로그래밍" - * author: - * description: 저자 - * type: string - * example: "김선우" - * donator: - * description: 기부자 닉네임 - * type: string - * example: "" - * publisher: - * description: 출판사 - * type: string - * example: "한빛아카데미" - * pubishedAt: - * description: 출판일 - * type: string - * format: date - * example: 20220522 - * isbn: - * description: isbn - * type: string - * format: number - * example: "9788998756444" - * image: - * description: 이미지 주소 - * type: string - * format: uri - * example: "https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg" - * status: - * description: 책의 상태 정보 - * type: number - * example: 0 - * categoryId: - * description: 책의 캬테고리 번호 - * type: number - * example: 2 - * callSign: - * description: 책의 고유 호출 번호 - * type: string - * example: "C5.13.v1.c2" - * category: - * description: 책의 카테고리 정보 - * type: string - * example: "네트워크" - * updatedAt: - * description: 책 정보의 마지막 변경 날짜 - * type: string - * format: date - * example: "2022-07-09-22:49:33" - * meta: - * description: 재고 수와 관련된 정보 - * type: object - * properties: - * totalItems: - * description: 전체 검색 결과 수 - * type: integer - * example: 1 - * itemCount: - * description: 현재 페이지 검색 결과 수 - * type: integer - * example: 1 - * itemsPerPage: - * description: 페이지 당 검색 결과 수 - * type: integer - * example: 1 - * totalPages: - * description: 전체 결과 페이지 수 - * type: integer - * example: 1 - * currentPage: - * description: 현재 페이지 - * type: integer - * example: 1 - * '500': - * description: Server Error - * content: - * application/json: - * schema: - * type: object - * description: error decription - * properties: - * errorCode: - * type: number - * description: 에러코드 - * example: 1 - * - */ + /** + * @openapi + * /api/stock/search: + * get: + * description: 책 재고 정보를 검색해 온다. + * tags: + * - stock + * parameters: + * - in: query + * name: page + * description: 페이지 + * schema: + * type: integer + * - in: query + * name: limit + * description: 한 페이지에 들어올 검색결과 수 + * schema: + * type: integer + * responses: + * '200': + * description: 검색 결과를 반환한다. + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * description: 재고 정보 목록 + * type: array + * items: + * type: object + * properties: + * bookId: + * description: 도서 번호 + * type: integer + * example: 3 + * bookInfoId: + * description: 도서 정보 번호 + * type: integer + * example: 2 + * title: + * description: 책 제목 + * type: string + * example: "TCP IP 윈도우 소켓 프로그래밍" + * author: + * description: 저자 + * type: string + * example: "김선우" + * donator: + * description: 기부자 닉네임 + * type: string + * example: "" + * publisher: + * description: 출판사 + * type: string + * example: "한빛아카데미" + * pubishedAt: + * description: 출판일 + * type: string + * format: date + * example: 20220522 + * isbn: + * description: isbn + * type: string + * format: number + * example: "9788998756444" + * image: + * description: 이미지 주소 + * type: string + * format: uri + * example: "https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg" + * status: + * description: 책의 상태 정보 + * type: number + * example: 0 + * categoryId: + * description: 책의 캬테고리 번호 + * type: number + * example: 2 + * callSign: + * description: 책의 고유 호출 번호 + * type: string + * example: "C5.13.v1.c2" + * category: + * description: 책의 카테고리 정보 + * type: string + * example: "네트워크" + * updatedAt: + * description: 책 정보의 마지막 변경 날짜 + * type: string + * format: date + * example: "2022-07-09-22:49:33" + * meta: + * description: 재고 수와 관련된 정보 + * type: object + * properties: + * totalItems: + * description: 전체 검색 결과 수 + * type: integer + * example: 1 + * itemCount: + * description: 현재 페이지 검색 결과 수 + * type: integer + * example: 1 + * itemsPerPage: + * description: 페이지 당 검색 결과 수 + * type: integer + * example: 1 + * totalPages: + * description: 전체 결과 페이지 수 + * type: integer + * example: 1 + * currentPage: + * description: 현재 페이지 + * type: integer + * example: 1 + * '500': + * description: Server Error + * content: + * application/json: + * schema: + * type: object + * description: error decription + * properties: + * errorCode: + * type: number + * description: 에러코드 + * example: 1 + * + */ .get('/search', stockSearch) /** diff --git a/backend/src/v1/routes/tags.routes.ts b/backend/src/v1/routes/tags.routes.ts index cfd7b8e9..4c3b2650 100644 --- a/backend/src/v1/routes/tags.routes.ts +++ b/backend/src/v1/routes/tags.routes.ts @@ -19,247 +19,247 @@ export const path = '/tags'; export const router = Router(); router -/** - * @openapi - * /api/tags/super: - * patch: - * description: 슈퍼 태그를 수정한다. - * tags: - * - tags - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정할 태그의 id - * type: integer - * example: 1 - * required: true - * content: - * description: 슈퍼 태그 내용 - * type: string - * example: "수정할_내용_적기" - * responses: - * '200': - * description: 슈퍼 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정된 슈퍼 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '902': - * description: 이미 존재하는 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '906': - * description: 디폴트 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 906 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/super: + * patch: + * description: 슈퍼 태그를 수정한다. + * tags: + * - tags + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정할 태그의 id + * type: integer + * example: 1 + * required: true + * content: + * description: 슈퍼 태그 내용 + * type: string + * example: "수정할_내용_적기" + * responses: + * '200': + * description: 슈퍼 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정된 슈퍼 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '902': + * description: 이미 존재하는 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '906': + * description: 디폴트 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 906 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/super', authValidate(roleSet.librarian), updateSuperTags); router -/** - * @openapi - * /api/tags/sub: - * patch: - * description: 서브 태그를 수정한다. - * tags: - * - tags - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정할 태그의 id - * type: integer - * example: 1 - * required: true - * visibility: - * description: 서브 태그의 공개 여부 - * type: string - * example: public, private - * responses: - * '200': - * description: 서브 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 수정된 서브 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '901': - * description: 권한이 없습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/sub: + * patch: + * description: 서브 태그를 수정한다. + * tags: + * - tags + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정할 태그의 id + * type: integer + * example: 1 + * required: true + * visibility: + * description: 서브 태그의 공개 여부 + * type: string + * example: public, private + * responses: + * '200': + * description: 서브 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 수정된 서브 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '901': + * description: 권한이 없습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/sub', authValidate(roleSet.librarian), updateSubTags); router -/** - * @openapi - * /api/tags/{bookInfoId}/merge: - * patch: - * description: 태그를 병합한다. - * tags: - * - tags - * parameters: - * - in: path - * name: bookInfoId - * description: 병합할 책 정보의 id - * required: true - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * subTagIds: - * description: 병합될 서브 태그의 id 리스트 - * type: list - * required: true - * example: [1, 2, 3, 5, 10] - * superTagId: - * description: 슈퍼 태그의 id. null일 경우, 디폴트 태그로 병합됨을 의미한다. - * type: integer - * required: true - * example: 2 - * responses: - * '200': - * description: 슈퍼 태그 수정 성공. - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * description: 슈퍼 태그의 id - * type: integer - * example: 1 - * '900': - * description: 태그의 양식이 올바르지 않습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 900 - * '902': - * description: 이미 존재하는 태그입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 902 - * '906': - * description: 디폴트 태그에는 병합할 수 없습니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 906 - * '910': - * description: 유효하지 않은 태그 id입니다. - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: integer - * example: 910 - * '905': - * description: DB 에러로 인한 업데이트 실패 - * content: - * application/json: - * schema: - * type: object - * properties: - * errorCode: - * type: number - * example: 500 - */ + /** + * @openapi + * /api/tags/{bookInfoId}/merge: + * patch: + * description: 태그를 병합한다. + * tags: + * - tags + * parameters: + * - in: path + * name: bookInfoId + * description: 병합할 책 정보의 id + * required: true + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * subTagIds: + * description: 병합될 서브 태그의 id 리스트 + * type: list + * required: true + * example: [1, 2, 3, 5, 10] + * superTagId: + * description: 슈퍼 태그의 id. null일 경우, 디폴트 태그로 병합됨을 의미한다. + * type: integer + * required: true + * example: 2 + * responses: + * '200': + * description: 슈퍼 태그 수정 성공. + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * description: 슈퍼 태그의 id + * type: integer + * example: 1 + * '900': + * description: 태그의 양식이 올바르지 않습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 900 + * '902': + * description: 이미 존재하는 태그입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 902 + * '906': + * description: 디폴트 태그에는 병합할 수 없습니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 906 + * '910': + * description: 유효하지 않은 태그 id입니다. + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: integer + * example: 910 + * '905': + * description: DB 에러로 인한 업데이트 실패 + * content: + * application/json: + * schema: + * type: object + * properties: + * errorCode: + * type: number + * example: 500 + */ .patch('/:bookInfoId/merge', authValidate(roleSet.librarian), mergeTags); router diff --git a/backend/src/v1/routes/users.routes.ts b/backend/src/v1/routes/users.routes.ts index b5e4efa2..53132d0e 100644 --- a/backend/src/v1/routes/users.routes.ts +++ b/backend/src/v1/routes/users.routes.ts @@ -1,9 +1,7 @@ import { Router } from 'express'; import { roleSet } from '~/v1/auth/auth.type'; import authValidate from '~/v1/auth/auth.validate'; -import { - create, getVersion, myupdate, search, update, -} from '~/v1/users/users.controller'; +import { create, getVersion, myupdate, search, update } from '~/v1/users/users.controller'; export const path = '/users'; export const router = Router(); @@ -31,7 +29,7 @@ export const router = Router(); * description: 한 페이지에 들어올 검색결과 수 * schema: * type: integer - * - in: query + * - in: query * name: id * description: 검색할 유저의 id * schema: @@ -358,10 +356,11 @@ export const router = Router(); * type: string * example: gshim.v1 */ -router.get('/search', search) +router + .get('/search', search) .post('/create', create) .patch('/update/:id', authValidate(roleSet.librarian), update) .patch('/myupdate', authValidate(roleSet.all), myupdate) .get('/EasterEgg', getVersion); -// .delete('/delete/:id', authValidate(roleSet.librarian), deleteUser); \ No newline at end of file +// .delete('/delete/:id', authValidate(roleSet.librarian), deleteUser); diff --git a/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts b/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts index a31bdad7..4dbe7150 100644 --- a/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts +++ b/backend/src/v1/search-keywords/booksInfoSearchKeywords.repository.ts @@ -1,10 +1,7 @@ import { QueryRunner, Repository } from 'typeorm'; import jipDataSource from '~/app-data-source'; import { BookInfo, BookInfoSearchKeywords } from '~/entity/entities'; -import { - disassembleHangul, - extractHangulInitials, -} from '../utils/processKeywords'; +import { disassembleHangul, extractHangulInitials } from '../utils/processKeywords'; import { UpdateBookInfo } from '../books/books.type'; import { FindBookInfoSearchKeyword } from './searchKeywords.type'; @@ -21,9 +18,7 @@ class BookInfoSearchKeywordRepository extends Repository } async createBookInfoSearchKeyword(bookInfo: BookInfo) { - const { - id, title, author, publisher, - } = bookInfo; + const { id, title, author, publisher } = bookInfo; const disassembledTitle = disassembleHangul(title); const titleInitials = extractHangulInitials(title); @@ -45,9 +40,7 @@ class BookInfoSearchKeywordRepository extends Repository } async updateBookInfoSearchKeyword(targetId: number, bookInfo: UpdateBookInfo) { - const { - id, title, author, publisher, - } = bookInfo; + const { id, title, author, publisher } = bookInfo; const disassembledTitle = disassembleHangul(title); const titleInitials = extractHangulInitials(title); diff --git a/backend/src/v1/search-keywords/searchKeywords.controller.ts b/backend/src/v1/search-keywords/searchKeywords.controller.ts index 2f231ce3..5684af04 100644 --- a/backend/src/v1/search-keywords/searchKeywords.controller.ts +++ b/backend/src/v1/search-keywords/searchKeywords.controller.ts @@ -21,16 +21,11 @@ export const getPopularSearchKeywords = async ( } if (error.message === 'DB error') { return next( - new ErrorResponse( - errorCode.QUERY_EXECUTION_FAILED, - status.INTERNAL_SERVER_ERROR, - ), + new ErrorResponse(errorCode.QUERY_EXECUTION_FAILED, status.INTERNAL_SERVER_ERROR), ); } logger.error(error); - return next( - new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR), - ); + return next(new ErrorResponse(errorCode.UNKNOWN_ERROR, status.INTERNAL_SERVER_ERROR)); } }; @@ -38,10 +33,8 @@ export const searchKeywordsAutocomplete = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { - let { - keyword, - } = req.query; +): Promise => { + let { keyword } = req.query; if (typeof keyword === 'string') { keyword = keyword.trim(); } diff --git a/backend/src/v1/search-keywords/searchKeywords.service.ts b/backend/src/v1/search-keywords/searchKeywords.service.ts index 7426f7d9..e933da84 100644 --- a/backend/src/v1/search-keywords/searchKeywords.service.ts +++ b/backend/src/v1/search-keywords/searchKeywords.service.ts @@ -7,11 +7,7 @@ import { disassembleHangul, removeSpecialCharacters, } from '~/v1/utils/processKeywords'; -import { - AutocompleteKeyword, - PopularSearchKeyword, - SearchKeyword, -} from './searchKeywords.type'; +import { AutocompleteKeyword, PopularSearchKeyword, SearchKeyword } from './searchKeywords.type'; import SearchKeywordsRepository from './searchKeywords.repository'; import SearchLogsRepository from './searchLogs.repository'; @@ -30,7 +26,7 @@ export const getPopularSearches = async ( SELECT keyword FROM search_logs LEFT JOIN search_keywords ON search_logs.search_keyword_id = search_keywords.id - WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 DAY - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY + WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 DAY - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY GROUP BY search_keywords.keyword HAVING COUNT(search_keywords.keyword) >= ? ORDER BY COUNT(search_keywords.keyword) DESC, MAX(search_logs.timestamp) DESC @@ -41,7 +37,7 @@ export const getPopularSearches = async ( SELECT keyword FROM search_logs LEFT JOIN search_keywords ON search_logs.search_keyword_id = search_keywords.id - WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 MONTH - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY + WHERE search_logs.timestamp BETWEEN NOW() - INTERVAL 1 MONTH - INTERVAL ? DAY AND NOW() - INTERVAL ? DAY GROUP BY search_keywords.keyword HAVING COUNT(search_keywords.keyword) >= ? ORDER BY COUNT(search_keywords.keyword) DESC, MAX(search_logs.timestamp) DESC @@ -78,10 +74,7 @@ export const getPopularSearches = async ( const updateLastPopular = (items: string[]) => { lastPopular = [...items]; - logger.debug( - `(${new Date().toLocaleString()}) Popular Search Keywords `, - lastPopular, - ); + logger.debug(`(${new Date().toLocaleString()}) Popular Search Keywords `, lastPopular); }; export const renewLastPopular = async () => { @@ -103,15 +96,13 @@ export const getPopularSearchKeywords = async () => { if (!lastPopular || lastPopular.length === 0) { updateLastPopular(popularSearchKeywords.map((item) => item.keyword)); } - const items: PopularSearchKeyword[] = popularSearchKeywords.map( - (item, index: number) => { - const preRanking = lastPopular.indexOf(item.keyword); - return { - searchKeyword: item.keyword, - rankingChange: preRanking === -1 ? null : preRanking - index, - }; - }, - ); + const items: PopularSearchKeyword[] = popularSearchKeywords.map((item, index: number) => { + const preRanking = lastPopular.indexOf(item.keyword); + return { + searchKeyword: item.keyword, + rankingChange: preRanking === -1 ? null : preRanking - index, + }; + }); return items; }; @@ -124,9 +115,7 @@ export const createSearchKeywordLog = async ( if (!keyword) return; const transactionQueryRunner = jipDataSource.createQueryRunner(); - const searchKeywordsRepository = new SearchKeywordsRepository( - transactionQueryRunner, - ); + const searchKeywordsRepository = new SearchKeywordsRepository(transactionQueryRunner); const searchLogsRepository = new SearchLogsRepository(transactionQueryRunner); try { diff --git a/backend/src/v1/slack/slack.controller.ts b/backend/src/v1/slack/slack.controller.ts index 84de2df6..a30f24c3 100644 --- a/backend/src/v1/slack/slack.controller.ts +++ b/backend/src/v1/slack/slack.controller.ts @@ -9,7 +9,7 @@ export const updateSlackList = async ( req: Request, res: Response, next: NextFunction, -) : Promise => { +): Promise => { try { await slack.updateSlackId(); res.status(204).send(); diff --git a/backend/src/v1/slack/slack.service.ts b/backend/src/v1/slack/slack.service.ts index 9936aa44..0430155a 100644 --- a/backend/src/v1/slack/slack.service.ts +++ b/backend/src/v1/slack/slack.service.ts @@ -8,17 +8,20 @@ import * as models from '../DTO/users.model'; const usersService = new UsersService(); -export const updateSlackIdUser = async (id: number, slackId: string) : Promise => { - const result : ResultSetHeader = await executeQuery(` +export const updateSlackIdUser = async (id: number, slackId: string): Promise => { + const result: ResultSetHeader = await executeQuery( + ` UPDATE user SET slack = ? WHERE id = ? - `, [slackId, id]); + `, + [slackId, id], + ); return result.affectedRows; }; -export const searchAuthenticatedUser = async () : Promise => { - const result : models.User[] = await executeQuery(` +export const searchAuthenticatedUser = async (): Promise => { + const result: models.User[] = await executeQuery(` SELECT * FROM user WHERE intraId IS NOT NULL AND (slack IS NULL OR slack = '') @@ -33,10 +36,10 @@ const userMap = new Map(); export const updateSlackId = async (): Promise => { let searchUsers: any[] = []; let cursor; - const authenticatedUser : models.User[] = await searchAuthenticatedUser(); + const authenticatedUser: models.User[] = await searchAuthenticatedUser(); if (authenticatedUser.length === 0) return; while (cursor === undefined || cursor !== '') { - const response = await web.users.list({ cursor, limit: 1000 }) as any; + const response = (await web.users.list({ cursor, limit: 1000 })) as any; searchUsers = searchUsers.concat(response.members); cursor = response.response_metadata.next_cursor; } @@ -59,14 +62,16 @@ export const updateSlackIdByUserId = async (userId: number): Promise => { } }; -export const findUser = (intraName: any) => (userMap.get(intraName)); +export const findUser = (intraName: any) => userMap.get(intraName); export const publishMessage = async (slackId: string, msg: string) => { - await web.chat.postMessage({ - token, - channel: slackId, - text: msg, - }).catch((e) => { - logger.error(e); - }); + await web.chat + .postMessage({ + token, + channel: slackId, + text: msg, + }) + .catch((e) => { + logger.error(e); + }); }; diff --git a/backend/src/v1/stocks/stocks.controller.ts b/backend/src/v1/stocks/stocks.controller.ts index 8cafd3dd..2f4a6502 100644 --- a/backend/src/v1/stocks/stocks.controller.ts +++ b/backend/src/v1/stocks/stocks.controller.ts @@ -1,6 +1,4 @@ -import { - NextFunction, Request, RequestHandler, Response, -} from 'express'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; import * as status from 'http-status'; import * as errorCode from '~/v1/utils/error/errorCode'; import ErrorResponse from '~/v1/utils/error/errorResponse'; diff --git a/backend/src/v1/stocks/stocks.repository.ts b/backend/src/v1/stocks/stocks.repository.ts index ec9fcbe6..5974de07 100644 --- a/backend/src/v1/stocks/stocks.repository.ts +++ b/backend/src/v1/stocks/stocks.repository.ts @@ -1,7 +1,4 @@ -import { - LessThan, - QueryRunner, Repository, -} from 'typeorm'; +import { LessThan, QueryRunner, Repository } from 'typeorm'; import { startOfDay, addDays } from 'date-fns'; import { Book, VStock } from '~/entity/entities'; import jipDataSource from '~/app-data-source'; @@ -14,36 +11,31 @@ class StocksRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(Book, entityManager); - this.vStock = new Repository( - VStock, - entityManager, - ); + this.vStock = new Repository(VStock, entityManager); } - async getAllStocksAndCount(limit:number, page:number) - : Promise<[VStock[], number]> { + async getAllStocksAndCount(limit: number, page: number): Promise<[VStock[], number]> { const today = startOfDay(new Date()); - const [items, totalItems] = await this.vStock - .findAndCount({ - where: { - updatedAt: LessThan(addDays(today, -15)), - }, - take: limit, - skip: limit * page, - }); + const [items, totalItems] = await this.vStock.findAndCount({ + where: { + updatedAt: LessThan(addDays(today, -15)), + }, + take: limit, + skip: limit * page, + }); return [items, totalItems]; } async getStockById(bookId: number) { - const stock = await this.vStock - .findOneBy({ bookId }); - if (stock === null) { throw new Error('701'); } + const stock = await this.vStock.findOneBy({ bookId }); + if (stock === null) { + throw new Error('701'); + } return stock; } async updateBook(bookId: number) { - await this - .update(bookId, { updatedAt: new Date() }); + await this.update(bookId, { updatedAt: new Date() }); } } export default StocksRepository; diff --git a/backend/src/v1/stocks/stocks.service.ts b/backend/src/v1/stocks/stocks.service.ts index 1b7e675f..36a00561 100644 --- a/backend/src/v1/stocks/stocks.service.ts +++ b/backend/src/v1/stocks/stocks.service.ts @@ -2,13 +2,10 @@ import jipDataSource from '~/app-data-source'; import { Meta } from '../DTO/common.interface'; import StocksRepository from './stocks.repository'; -export const getAllStocks = async ( - page: number, - limit: number, -) => { +export const getAllStocks = async (page: number, limit: number) => { const stocksRepo = new StocksRepository(); const [items, totalItems] = await stocksRepo.getAllStocksAndCount(limit, page); - const meta:Meta = { + const meta: Meta = { totalItems, itemCount: items.length, itemsPerPage: limit, @@ -18,9 +15,7 @@ export const getAllStocks = async ( return { items, meta }; }; -export const updateBook = async ( - bookId: number, -) => { +export const updateBook = async (bookId: number) => { const transaction = jipDataSource.createQueryRunner(); const stocksRepo = new StocksRepository(transaction); try { @@ -31,7 +26,7 @@ export const updateBook = async ( return stock; } catch (error: any) { await transaction.rollbackTransaction(); - throw (error); + throw error; } finally { await transaction.release(); } diff --git a/backend/src/v1/swagger/swagger.ts b/backend/src/v1/swagger/swagger.ts index e34ba4f2..7e2bbb0d 100644 --- a/backend/src/v1/swagger/swagger.ts +++ b/backend/src/v1/swagger/swagger.ts @@ -5,7 +5,7 @@ const swaggerOptions = { title: '42-jiphyoenjeon web service API', version: '0.1.0', description: - "42-jiphyeonjeon web service, that is, 42library's APIs with Express and documented with Swagger", + "42-jiphyeonjeon web service, that is, 42library's APIs with Express and documented with Swagger", license: { name: 'MIT', url: 'https://spdx.org/licenses/MIT.html', diff --git a/backend/src/v1/tags/tags.controller.ts b/backend/src/v1/tags/tags.controller.ts index e6594624..425cebc9 100644 --- a/backend/src/v1/tags/tags.controller.ts +++ b/backend/src/v1/tags/tags.controller.ts @@ -1,17 +1,11 @@ -import { - NextFunction, Request, Response, -} from 'express'; +import { NextFunction, Request, Response } from 'express'; import * as status from 'http-status'; import ErrorResponse from '~/v1/utils/error/errorResponse'; import * as parseCheck from '~/v1/utils/parseCheck'; import * as errorCode from '~/v1/utils/error/errorCode'; import TagsService from './tags.service'; -export const createDefaultTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createDefaultTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = req?.body?.bookInfoId; const content = req?.body?.content.trim(); @@ -21,7 +15,7 @@ export const createDefaultTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10)) === false) { + if ((await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10))) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } @@ -29,30 +23,27 @@ export const createDefaultTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.DUPLICATED_SUB_DEFAULT_TAGS, 400)); } - const defaultTagInsertion = await tagsService.createDefaultTags( - tokenId, - bookInfoId, - content, - ); + const defaultTagInsertion = await tagsService.createDefaultTags(tokenId, bookInfoId, content); await tagsService.releaseConnection(); return res.status(status.CREATED).send(defaultTagInsertion); }; -export const createSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const createSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = req?.body?.bookInfoId; const content = req?.body?.content.trim(); const tagsService = new TagsService(); const regex = /[^가-힣a-zA-Z0-9_]/g; - if (content === '' || content === 'default' || content.length > 42 || regex.test(content) === true) { + if ( + content === '' || + content === 'default' || + content.length > 42 || + regex.test(content) === true + ) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10)) === false) { + if ((await tagsService.isValidBookInfoId(parseInt(bookInfoId, 10))) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } @@ -65,11 +56,7 @@ export const createSuperTags = async ( return res.status(status.CREATED).send(superTagInsertion); }; -export const deleteSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const superTagId = Number(req?.params?.tagId); const tagsService = new TagsService(); @@ -82,11 +69,7 @@ export const deleteSuperTags = async ( return res.status(status.OK).send(); }; -export const deleteSubTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const deleteSubTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const subTagId = Number(req?.params?.tagId); const tagsService = new TagsService(); @@ -99,10 +82,7 @@ export const deleteSubTags = async ( return res.status(status.OK).send(); }; -export const searchSubDefaultTags = async ( - req: Request, - res: Response, -) => { +export const searchSubDefaultTags = async (req: Request, res: Response) => { const page: number = parseCheck.pageParse(parseInt(String(req?.query?.page), 10)); const limit: number = parseCheck.limitParse(parseInt(String(req?.query?.limit), 10)); const visibility: string = parseCheck.stringQueryParse(req?.query?.visibility); @@ -118,10 +98,7 @@ export const searchSubDefaultTags = async ( return res.status(status.OK).json(subDefaultTags); }; -export const searchSubTags = async ( - req: Request, - res: Response, -) => { +export const searchSubTags = async (req: Request, res: Response) => { const superTagId: number = parseInt(req.params.superTagId, 10); const tagsService = new TagsService(); const subTags = await tagsService.searchSubTags(superTagId); @@ -129,10 +106,7 @@ export const searchSubTags = async ( return res.status(status.OK).json(subTags); }; -export const searchSuperDefaultTags = async ( - req: Request, - res: Response, -) => { +export const searchSuperDefaultTags = async (req: Request, res: Response) => { const bookInfoId: number = parseInt(req.params.bookInfoId, 10); const tagsService = new TagsService(); const superDefaultTags = await tagsService.searchSuperDefaultTags(bookInfoId); @@ -140,11 +114,7 @@ export const searchSuperDefaultTags = async ( return res.status(status.OK).json(superDefaultTags); }; -export const mergeTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const mergeTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const bookInfoId = Number(req?.params?.bookInfoId); const superTagId = Number(req?.body?.superTagId); @@ -152,16 +122,15 @@ export const mergeTags = async ( const tagsService = new TagsService(); let returnSuperTagId = 0; - if (await tagsService.isValidBookInfoId(bookInfoId) === false) { + if ((await tagsService.isValidBookInfoId(bookInfoId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_BOOKINFO_ID, 400)); } - if (superTagId !== 0 - && await tagsService.isValidSuperTagId(superTagId, bookInfoId) === false) { + if (superTagId !== 0 && (await tagsService.isValidSuperTagId(superTagId, bookInfoId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } - if (await tagsService.isValidSubTagId(subTagIds) === false) { + if ((await tagsService.isValidSubTagId(subTagIds)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } @@ -180,25 +149,26 @@ export const mergeTags = async ( return res.status(status.OK).send({ id: returnSuperTagId }); }; -export const updateSuperTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateSuperTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const superTagId = parseInt(req?.body?.id, 10); const content = req?.body?.content; const tagsService = new TagsService(); const regex = /[^가-힣a-zA-Z0-9_]/g; - if (content === '' || content === 'default' || content.length > 42 || regex.test(content) === true) { + if ( + content === '' || + content === 'default' || + content.length > 42 || + regex.test(content) === true + ) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isExistingSuperTag(superTagId, content) === true) { + if ((await tagsService.isExistingSuperTag(superTagId, content)) === true) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.ALREADY_EXISTING_TAGS, 400)); } - if (await tagsService.isDefaultTag(superTagId) === true) { + if ((await tagsService.isDefaultTag(superTagId)) === true) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.DEFAULT_TAG_ID, 400)); } @@ -212,11 +182,7 @@ export const updateSuperTags = async ( return res.status(status.OK).send({ id: superTagId }); }; -export const updateSubTags = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const updateSubTags = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; const subTagId = parseInt(req?.body?.id, 10); const visibility = req?.body?.visibility; @@ -225,7 +191,7 @@ export const updateSubTags = async ( await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_INPUT_TAGS, 400)); } - if (await tagsService.isExistingSubTag(subTagId) === false) { + if ((await tagsService.isExistingSubTag(subTagId)) === false) { await tagsService.releaseConnection(); return next(new ErrorResponse(errorCode.INVALID_TAG_ID, 400)); } @@ -239,10 +205,7 @@ export const updateSubTags = async ( return res.status(status.OK).send({ id: subTagId }); }; -export const searchMainTags = async ( - req: Request, - res: Response, -) => { +export const searchMainTags = async (req: Request, res: Response) => { const limit: number = req.query.limit === undefined || null ? 100 : Number(req.query.limit); const tagsService = new TagsService(); const mainTags = await tagsService.searchMainTags(limit); diff --git a/backend/src/v1/tags/tags.repository.ts b/backend/src/v1/tags/tags.repository.ts index 39004f83..1ca59c02 100644 --- a/backend/src/v1/tags/tags.repository.ts +++ b/backend/src/v1/tags/tags.repository.ts @@ -3,7 +3,12 @@ import { In, QueryRunner, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import jipDataSource from '~/app-data-source'; import { - BookInfo, SubTag, SuperTag, User, VTagsSubDefault, VTagsSuperDefault, + BookInfo, + SubTag, + SuperTag, + User, + VTagsSubDefault, + VTagsSuperDefault, } from '~/entity/entities'; import { subDefaultTag, superDefaultTag } from '../DTO/tags.model'; @@ -17,14 +22,15 @@ export class SubTagRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(SubTag, entityManager); this.entityManager = entityManager; - this.vSubDefaultRepo = new Repository( - VTagsSubDefault, - entityManager, - ); + this.vSubDefaultRepo = new Repository(VTagsSubDefault, entityManager); } - async createDefaultTags(userId: number, bookInfoId: number, content: string, superTagId: number) - : Promise { + async createDefaultTags( + userId: number, + bookInfoId: number, + content: string, + superTagId: number, + ): Promise { const insertObject: QueryDeepPartialEntity = { superTagId, userId, @@ -42,11 +48,7 @@ export class SubTagRepository extends Repository { async getSubTags(conditions: object) { const subTags = await this.vSubDefaultRepo.find({ - select: [ - 'id', - 'content', - 'login', - ], + select: ['id', 'content', 'login'], where: conditions, }); return subTags; @@ -66,8 +68,7 @@ export class SubTagRepository extends Repository { ); } - async countSubTag(conditions: object) - : Promise { + async countSubTag(conditions: object): Promise { const count = await this.count({ where: conditions, }); @@ -75,10 +76,7 @@ export class SubTagRepository extends Repository { } async updateSubTags(userId: number, subTagId: number, isPublic: number) { - await this.update( - { id: subTagId }, - { isPublic, updateUserId: userId, updatedAt: new Date() }, - ); + await this.update({ id: subTagId }, { isPublic, updateUserId: userId, updatedAt: new Date() }); } } @@ -98,18 +96,9 @@ export class SuperTagRepository extends Repository { const entityManager = jipDataSource.createEntityManager(queryRunner); super(SuperTag, entityManager); this.entityManager = entityManager; - this.vSubDefaultRepo = new Repository( - VTagsSubDefault, - this.entityManager, - ); - this.userRepo = new Repository( - User, - this.entityManager, - ); - this.bookInfoRepo = new Repository( - BookInfo, - this.entityManager, - ); + this.vSubDefaultRepo = new Repository(VTagsSubDefault, this.entityManager); + this.userRepo = new Repository(User, this.entityManager); + this.bookInfoRepo = new Repository(BookInfo, this.entityManager); this.vSuperDefaultRepo = new Repository( VTagsSuperDefault, this.entityManager, @@ -126,22 +115,15 @@ export class SuperTagRepository extends Repository { async getSuperTags(conditions: object) { const superTags = await this.find({ - select: [ - 'id', - 'content', - 'bookInfoId', - ], + select: ['id', 'content', 'bookInfoId'], where: conditions, }); return superTags; } - async getDefaultTag(bookInfoId: number) - : Promise { + async getDefaultTag(bookInfoId: number): Promise { const defaultTag = await this.findOne({ - select: [ - 'id', - ], + select: ['id'], where: { bookInfoId, content: 'default', @@ -150,8 +132,7 @@ export class SuperTagRepository extends Repository { return defaultTag; } - async createSuperTag(content: string, bookInfoId: number, userId: number) - : Promise { + async createSuperTag(content: string, bookInfoId: number, userId: number): Promise { const insertObject: QueryDeepPartialEntity = { userId, bookInfoId, @@ -166,8 +147,11 @@ export class SuperTagRepository extends Repository { await this.update(superTagsId, { isDeleted: 1, updateUserId: deleteUser }); } - async getSubAndSuperTags(page: number, limit: number, conditions: Object) - : Promise<[subDefaultTag[], number]> { + async getSubAndSuperTags( + page: number, + limit: number, + conditions: Object, + ): Promise<[subDefaultTag[], number]> { const [items, count] = await this.vSubDefaultRepo.findAndCount({ select: [ 'bookInfoId', @@ -188,35 +172,35 @@ export class SuperTagRepository extends Repository { return [convertedItems, count]; } - async getSuperTagsWithSubCount(bookInfoId: number) - : Promise { + async getSuperTagsWithSubCount(bookInfoId: number): Promise { const superTags = await this.createQueryBuilder('sp') .select('sp.id', 'id') .addSelect('sp.content', 'content') .addSelect('NULL', 'login') - .addSelect((subQuery) => subQuery - .select('COUNT(sb.id)', 'count') - .from(SubTag, 'sb') - .where('sb.superTagId = sp.id AND sb.isDeleted IS FALSE AND sb.isPublic IS TRUE'), 'count') - .where('sp.bookInfoId = :bookInfoId AND sp.content != \'default\' AND sp.isDeleted IS FALSE', { bookInfoId }) + .addSelect( + (subQuery) => + subQuery + .select('COUNT(sb.id)', 'count') + .from(SubTag, 'sb') + .where('sb.superTagId = sp.id AND sb.isDeleted IS FALSE AND sb.isPublic IS TRUE'), + 'count', + ) + .where("sp.bookInfoId = :bookInfoId AND sp.content != 'default' AND sp.isDeleted IS FALSE", { + bookInfoId, + }) .getRawMany(); return superTags as superDefaultTag[]; } - async countSuperTag(conditions: object) - : Promise { + async countSuperTag(conditions: object): Promise { const count = await this.count({ where: conditions, }); return count; } - async updateSuperTags(updateUserId: number, superTagId: number, content: string) - : Promise { - await this.update( - { id: superTagId }, - { content, updateUserId, updatedAt: new Date() }, - ); + async updateSuperTags(updateUserId: number, superTagId: number, content: string): Promise { + await this.update({ id: superTagId }, { content, updateUserId, updatedAt: new Date() }); } async countBookInfoId(bookInfoId: number): Promise { diff --git a/backend/src/v1/tags/tags.service.ts b/backend/src/v1/tags/tags.service.ts index 928750e2..5b293d3f 100644 --- a/backend/src/v1/tags/tags.service.ts +++ b/backend/src/v1/tags/tags.service.ts @@ -7,11 +7,11 @@ import { SubTagRepository, SuperTagRepository } from './tags.repository'; import { superDefaultTag } from '../DTO/tags.model'; export class TagsService { - private readonly subTagRepository : SubTagRepository; + private readonly subTagRepository: SubTagRepository; - private readonly superTagRepository : SuperTagRepository; + private readonly superTagRepository: SuperTagRepository; - private readonly queryRunner : QueryRunner; + private readonly queryRunner: QueryRunner; constructor() { this.queryRunner = jipDataSource.createQueryRunner(); @@ -58,8 +58,12 @@ export class TagsService { return defaultTagsInsertion; } - async searchSubDefaultTags(page: number, limit: number, visibility: string, query: string) - : Promise { + async searchSubDefaultTags( + page: number, + limit: number, + visibility: string, + query: string, + ): Promise { const conditions: Array = []; const deleteAndVisibility: any = { isDeleted: 0, isPublic: null }; @@ -81,12 +85,14 @@ export class TagsService { limit, conditions, ); - const itemPerPage = (Number.isNaN(limit)) ? 10 : limit; + const itemPerPage = Number.isNaN(limit) ? 10 : limit; const meta = { totalItems: count, itemPerPage, - totalPages: parseInt(String(count / itemPerPage - + Number((count % itemPerPage !== 0) || !count)), 10), + totalPages: parseInt( + String(count / itemPerPage + Number(count % itemPerPage !== 0 || !count)), + 10, + ), firstPage: page === 0, finalPage: page === parseInt(String(count / itemPerPage), 10), currentPage: page, @@ -113,13 +119,11 @@ export class TagsService { })); const defaultTag = await this.superTagRepository.getDefaultTag(bookInfoId); if (defaultTag) { - const defaultTags = await this.subTagRepository.getSubTags( - { - superTagId: defaultTag.id, - isPublic: 1, - isDeleted: 0, - }, - ); + const defaultTags = await this.subTagRepository.getSubTags({ + superTagId: defaultTag.id, + isPublic: 1, + isDeleted: 0, + }); defaultTags.forEach((dt) => { superDefaultTags.push({ id: dt.id, @@ -186,12 +190,7 @@ export class TagsService { return subTagCount > 0; } - async mergeTags( - bookInfoId: number, - subTagIds: number[], - rawSuperTagId: number, - userId: number, - ) { + async mergeTags(bookInfoId: number, subTagIds: number[], rawSuperTagId: number, userId: number) { let superTagId = 0; try { @@ -200,8 +199,12 @@ export class TagsService { const defaultTag = await this.superTagRepository.getDefaultTag(bookInfoId); if (defaultTag === null) { superTagId = await this.superTagRepository.createSuperTag('default', bookInfoId, userId); - } else { superTagId = defaultTag.id; } - } else { superTagId = rawSuperTagId; } + } else { + superTagId = defaultTag.id; + } + } else { + superTagId = rawSuperTagId; + } await this.subTagRepository.mergeTags(subTagIds, superTagId, userId); await this.queryRunner.commitTransaction(); } catch (e) { @@ -216,9 +219,7 @@ export class TagsService { async isExistingSuperTag(superTagId: number, content: string): Promise { const superTag: SuperTag[] = await this.superTagRepository.getSuperTags({ id: superTagId }); const { bookInfoId } = superTag[0]; - const duplicates: number = await this.superTagRepository.countSuperTag( - { content, bookInfoId }, - ); + const duplicates: number = await this.superTagRepository.countSuperTag({ content, bookInfoId }); if (duplicates === 0) { return false; } @@ -265,7 +266,7 @@ export class TagsService { } async updateSubTags(userId: number, subTagId: number, visibility: string): Promise { - const isPublic = (visibility === 'public') ? 1 : 0; + const isPublic = visibility === 'public' ? 1 : 0; try { await this.queryRunner.startTransaction(); await this.subTagRepository.updateSubTags(userId, subTagId, isPublic); diff --git a/backend/src/v1/users/users.controller.spec.ts b/backend/src/v1/users/users.controller.spec.ts index bc984af8..3f964d57 100644 --- a/backend/src/v1/users/users.controller.spec.ts +++ b/backend/src/v1/users/users.controller.spec.ts @@ -3,18 +3,23 @@ import { searchSchema } from './users.types'; describe('searchSchema query', () => { test('regular query', () => { const data = { - id: 1, nicknameOrEmail: 'test', page: 1, limit: 1, + id: 1, + nicknameOrEmail: 'test', + page: 1, + limit: 1, }; expect(searchSchema.safeParse(data)).toEqual({ success: true, data }); }); test('default value for empty query', () => { - expect(searchSchema.safeParse({})) - .toEqual({ success: true, data: { page: 0, limit: 5 } }); + expect(searchSchema.safeParse({})).toEqual({ success: true, data: { page: 0, limit: 5 } }); }); test('id should be parseable to number', () => { const error = { - id: 'abcd', nicknameOrEmail: 'test', page: 1, limit: 1, + id: 'abcd', + nicknameOrEmail: 'test', + page: 1, + limit: 1, }; const parseResult = searchSchema.safeParse(error); diff --git a/backend/src/v1/users/users.controller.ts b/backend/src/v1/users/users.controller.ts index 0b5ffda0..1c47706f 100644 --- a/backend/src/v1/users/users.controller.ts +++ b/backend/src/v1/users/users.controller.ts @@ -11,39 +11,33 @@ import { searchSchema } from './users.types'; const usersService = new UsersService(); -export const search = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const search = async (req: Request, res: Response, next: NextFunction) => { const parsed = searchSchema.safeParse(req.query); if (!parsed.success) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } - const { - id, nicknameOrEmail, page, limit, - } = parsed.data; + const { id, nicknameOrEmail, page, limit } = parsed.data; let items; try { if (!nicknameOrEmail && !id) { items = await usersService.searchAllUsers(limit, page); } else if (nicknameOrEmail && !id) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), - )); + items = JSON.parse( + JSON.stringify( + await usersService.searchUserBynicknameOrEmail(nicknameOrEmail, limit, page), + ), + ); } else if (!nicknameOrEmail && id) { - items = JSON.parse(JSON.stringify( - await usersService.searchUserById(id), - )); + items = JSON.parse(JSON.stringify(await usersService.searchUserById(id))); } if (items) { - items.items = await Promise.all(items.items.map(async (data: User) => ({ - ...data, - lendings: - await usersService.userLendings(data.id), - reservations: - await usersService.userReservations(data.id), - }))); + items.items = await Promise.all( + items.items.map(async (data: User) => ({ + ...data, + lendings: await usersService.userLendings(data.id), + reservations: await usersService.userReservations(data.id), + })), + ); } return res.json(items); } catch (error: any) { @@ -72,9 +66,12 @@ export const create = async (req: Request, res: Response, next: NextFunction) => } try { pwSchema - .is().min(10) - .is().max(42) /* eslint-disable-next-line newline-per-chained-call */ - .has().digits(1) /* eslint-disable-next-line newline-per-chained-call */ + .is() + .min(10) + .is() + .max(42) /* eslint-disable-next-line newline-per-chained-call */ + .has() + .digits(1) /* eslint-disable-next-line newline-per-chained-call */ .symbols(1); if (!pwSchema.validate(String(password))) { return next(new ErrorResponse(errorCode.INVALIDATE_PASSWORD, status.BAD_REQUEST)); @@ -97,16 +94,13 @@ export const create = async (req: Request, res: Response, next: NextFunction) => return 0; }; -export const update = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const update = async (req: Request, res: Response, next: NextFunction) => { const { id } = req.params; - const { - nickname = '', intraId = 0, slack = '', role = -1, penaltyEndDate = '', - } = req.body; - if (!id || !(nickname !== '' || intraId !== 0 || slack !== '' || role !== -1 || penaltyEndDate !== '')) { + const { nickname = '', intraId = 0, slack = '', role = -1, penaltyEndDate = '' } = req.body; + if ( + !id || + !(nickname !== '' || intraId !== 0 || slack !== '' || role !== -1 || penaltyEndDate !== '') + ) { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } try { @@ -136,15 +130,9 @@ export const update = async ( return 0; }; -export const myupdate = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const myupdate = async (req: Request, res: Response, next: NextFunction) => { const { id: tokenId } = req.user as any; - const { - email = '', password = '0', - } = req.body; + const { email = '', password = '0' } = req.body; if (email === '' && password === '0') { return next(new ErrorResponse(errorCode.INVALID_INPUT, status.BAD_REQUEST)); } @@ -154,16 +142,21 @@ export const myupdate = async ( } else if (email === '' && password !== '0') { const pwSchema = new PasswordValidator(); pwSchema - .is().min(10) - .is().max(42) /* eslint-disable-next-line newline-per-chained-call */ - .has().lowercase() /* eslint-disable-next-line newline-per-chained-call */ - .has().digits(1) /* eslint-disable-next-line newline-per-chained-call */ + .is() + .min(10) + .is() + .max(42) /* eslint-disable-next-line newline-per-chained-call */ + .has() + .lowercase() /* eslint-disable-next-line newline-per-chained-call */ + .has() + .digits(1) /* eslint-disable-next-line newline-per-chained-call */ .symbols(1); if (!pwSchema.validate(password)) { return next(new ErrorResponse(errorCode.INVALIDATE_PASSWORD, status.BAD_REQUEST)); } await usersService.updateUserPassword(parseInt(tokenId, 10), bcrypt.hashSync(password, 10)); - } res.status(200).send('success'); + } + res.status(200).send('success'); } catch (error: any) { const errorNumber = parseInt(error.message, 10); if (errorNumber >= 200 && errorNumber < 300) { @@ -184,10 +177,7 @@ export const myupdate = async ( return 0; }; -export const getVersion = async ( - req: Request, - res: Response, -) => { +export const getVersion = async (req: Request, res: Response) => { res.status(200).send({ version: 'gshim.v1' }); return 0; }; diff --git a/backend/src/v1/users/users.repository.ts b/backend/src/v1/users/users.repository.ts index 310c330f..a210982d 100644 --- a/backend/src/v1/users/users.repository.ts +++ b/backend/src/v1/users/users.repository.ts @@ -3,7 +3,11 @@ import { Repository } from 'typeorm'; import { formatDate } from '~/v1/utils/dateFormat'; import jipDataSource from '~/app-data-source'; import { - VUserLending, VLendingForSearchUser, Reservation, UserReservation, User, + VUserLending, + VLendingForSearchUser, + Reservation, + UserReservation, + User, } from '~/entity/entities'; import * as models from '../DTO/users.model'; @@ -16,42 +20,26 @@ export default class UsersRepository extends Repository { private readonly userReservRepo: Repository; - constructor( - queryRunner?: QueryRunner, - ) { + constructor(queryRunner?: QueryRunner) { const qr = queryRunner; const manager = jipDataSource.createEntityManager(qr); super(User, manager); - this.userLendingRepo = new Repository( - VUserLending, - manager, - ); + this.userLendingRepo = new Repository(VUserLending, manager); this.lendingForSearchUserRepo = new Repository( VLendingForSearchUser, manager, ); - this.reservationsRepo = new Repository( - Reservation, - manager, - ); - this.userReservRepo = new Repository( - UserReservation, - manager, - ); + this.reservationsRepo = new Repository(Reservation, manager); + this.userReservRepo = new Repository(UserReservation, manager); } - async searchUserBy(conditions: {}, limit: number, page: number) - : Promise<[models.User[], number]> { + async searchUserBy( + conditions: {}, + limit: number, + page: number, + ): Promise<[models.User[], number]> { const [users, count] = await this.findAndCount({ - select: [ - 'id', - 'email', - 'nickname', - 'intraId', - 'slack', - 'penaltyEndDate', - 'role', - ], + select: ['id', 'email', 'nickname', 'intraId', 'slack', 'penaltyEndDate', 'role'], where: conditions, take: limit, skip: page * limit, @@ -68,19 +56,13 @@ export default class UsersRepository extends Repository { /** * @warning : use only password needed */ - async searchUserWithPasswordBy(conditions: {}, limit: number, page: number) - : Promise<[models.PrivateUser[], number]> { + async searchUserWithPasswordBy( + conditions: {}, + limit: number, + page: number, + ): Promise<[models.PrivateUser[], number]> { const [users, count] = await this.findAndCount({ - select: [ - 'id', - 'email', - 'nickname', - 'intraId', - 'slack', - 'penaltyEndDate', - 'role', - 'password', - ], + select: ['id', 'email', 'nickname', 'intraId', 'slack', 'penaltyEndDate', 'role', 'password'], where: conditions, take: limit, skip: page * limit, @@ -94,7 +76,7 @@ export default class UsersRepository extends Repository { return [customUsers, count]; } - async getLending(users: { userId: number; }[]) { + async getLending(users: { userId: number }[]) { if (users.length !== 0) return this.userLendingRepo.find({ where: users }); return this.userLendingRepo.find(); } @@ -109,11 +91,11 @@ export default class UsersRepository extends Repository { } async getUserLendings(userId: number) { - const userLendingList = await this.lendingForSearchUserRepo.find({ + const userLendingList = (await this.lendingForSearchUserRepo.find({ where: { userId, }, - }) as unknown as models.Lending[]; + })) as unknown as models.Lending[]; return userLendingList; } @@ -136,12 +118,8 @@ export default class UsersRepository extends Repository { }); } - async updateUser(id: number, values: {}) - : Promise { - const updatedUser = await this.update( - id, - values, - ) as unknown as models.User; + async updateUser(id: number, values: {}): Promise { + const updatedUser = (await this.update(id, values)) as unknown as models.User; return updatedUser; } } diff --git a/backend/src/v1/users/users.service.spec.ts b/backend/src/v1/users/users.service.spec.ts index 30b07530..5ad4c826 100644 --- a/backend/src/v1/users/users.service.spec.ts +++ b/backend/src/v1/users/users.service.spec.ts @@ -12,16 +12,15 @@ describe('UsersService', () => { jest.setTimeout(10 * 1000); let queryRunner: QueryRunner; beforeAll(async () => { - await jipDataSource.initialize().then( - () => { + await jipDataSource + .initialize() + .then(() => { logger.info('typeORM INIT SUCCESS'); logger.info(connectMode); - }, - ).catch( - (e) => { + }) + .catch((e) => { logger.error(`typeORM INIT FAILED : ${e.message}`); - }, - ); + }); // 트랜잭션 사전작업 queryRunner = jipDataSource.createQueryRunner(); await queryRunner.connect(); @@ -64,30 +63,8 @@ describe('UsersService', () => { // searchUserById it('searchUserById()', async () => { - expect(await usersService.searchUserById(1414)).toStrictEqual( - { - items: [ - { - id: 1414, - email: 'example_role1_7@gmail.com', - password: '4444', - nickname: 'hihi', - intraId: 44, - slack: 'dasdwqwe1132', - penaltyEndDay: new Date(Date.parse('2022-05-20 07:02:34')), - role: 1, - createdAt: new Date(Date.parse('2022-05-20 07:02:34.973193')), - updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), - }, - ], - }, - ); - }); - - // searchUserByIntraId - it('searchUserByIntraId()', async () => { - expect(await usersService.searchUserByIntraId(44)).toStrictEqual( - [ + expect(await usersService.searchUserById(1414)).toStrictEqual({ + items: [ { id: 1414, email: 'example_role1_7@gmail.com', @@ -101,7 +78,25 @@ describe('UsersService', () => { updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), }, ], - ); + }); + }); + + // searchUserByIntraId + it('searchUserByIntraId()', async () => { + expect(await usersService.searchUserByIntraId(44)).toStrictEqual([ + { + id: 1414, + email: 'example_role1_7@gmail.com', + password: '4444', + nickname: 'hihi', + intraId: 44, + slack: 'dasdwqwe1132', + penaltyEndDay: new Date(Date.parse('2022-05-20 07:02:34')), + role: 1, + createdAt: new Date(Date.parse('2022-05-20 07:02:34.973193')), + updatedAt: new Date(Date.parse('2022-05-20 16:13:39.314069')), + }, + ]); }); // searchAllUsers diff --git a/backend/src/v1/users/users.service.ts b/backend/src/v1/users/users.service.ts index b641e6a9..ee4bdba6 100644 --- a/backend/src/v1/users/users.service.ts +++ b/backend/src/v1/users/users.service.ts @@ -5,7 +5,7 @@ import * as types from '../DTO/common.interface'; import UsersRepository from './users.repository'; export default class UsersService { - private readonly usersRepository : UsersRepository; + private readonly usersRepository: UsersRepository; constructor() { this.usersRepository = new UsersRepository(); @@ -19,8 +19,9 @@ export default class UsersService { */ async withLendingInfo(users: models.User[]): Promise { const usersIdList = users.map((user) => ({ userId: user.id })); - const lending = await this.usersRepository - .getLending(usersIdList) as unknown as models.Lending[]; + const lending = (await this.usersRepository.getLending( + usersIdList, + )) as unknown as models.Lending[]; return users.map((user) => { const lendings = lending.filter((lend) => lend.userId === user.id); @@ -40,10 +41,11 @@ export default class UsersService { } async searchUserBynicknameOrEmail(nicknameOrEmail: string, limit: number, page: number) { - const [items, count] = await this.usersRepository.searchUserBy([ - { nickname: Like(`%${nicknameOrEmail}%`) }, - { email: Like(`%${nicknameOrEmail}`) }, - ], limit, page); + const [items, count] = await this.usersRepository.searchUserBy( + [{ nickname: Like(`%${nicknameOrEmail}%`) }, { email: Like(`%${nicknameOrEmail}`) }], + limit, + page, + ); const setItems = await this.withLendingInfo(items); const meta: types.Meta = { totalItems: count, @@ -67,7 +69,9 @@ export default class UsersService { } async searchUserWithPasswordByEmail(email: string) { - const items = (await this.usersRepository.searchUserWithPasswordBy({ email: Like(`%${email}%`) }, 0, 0))[0]; + const items = ( + await this.usersRepository.searchUserWithPasswordBy({ email: Like(`%${email}%`) }, 0, 0) + )[0]; return { items }; } @@ -98,7 +102,7 @@ export default class UsersService { return null; } - async updateUserEmail(id: number, email:string) { + async updateUserEmail(id: number, email: string) { const emailCount = (await this.usersRepository.searchUserBy({ email }, 0, 0))[1]; if (emailCount > 0) { throw new Error(errorCode.EMAIL_OVERLAP); @@ -118,16 +122,18 @@ export default class UsersService { role: number, penaltyEndDate: string, ) { - const nicknameCount = (await this.usersRepository - .searchUserBy({ nickname, id: Not(id) }, 0, 0))[1]; + const nicknameCount = ( + await this.usersRepository.searchUserBy({ nickname, id: Not(id) }, 0, 0) + )[1]; if (nicknameCount > 0) { throw new Error(errorCode.NICKNAME_OVERLAP); } if (!(role >= 0 && role <= 3)) { throw new Error(errorCode.INVALID_ROLE); } - const slackCount = (await this.usersRepository - .searchUserBy({ nickname, slack: Not(slack) }, 0, 0))[1]; + const slackCount = ( + await this.usersRepository.searchUserBy({ nickname, slack: Not(slack) }, 0, 0) + )[1]; if (slackCount > 0) { throw new Error(errorCode.SLACK_OVERLAP); } @@ -141,6 +147,6 @@ export default class UsersService { updateParam.penaltyEndDate = penaltyEndDate; } const updatedUser = this.usersRepository.updateUser(id, updateParam); - return (updatedUser); + return updatedUser; } } diff --git a/backend/src/v1/users/users.types.ts b/backend/src/v1/users/users.types.ts index 2c7b86f9..1ad3d274 100644 --- a/backend/src/v1/users/users.types.ts +++ b/backend/src/v1/users/users.types.ts @@ -7,6 +7,4 @@ export const searchSchema = z.object({ limit: z.coerce.number().min(1).default(5), }); -export const createSchema = z.object({ - -}); +export const createSchema = z.object({}); diff --git a/backend/src/v1/users/users.utils.ts b/backend/src/v1/users/users.utils.ts index 36d27fe8..79507cbb 100644 --- a/backend/src/v1/users/users.utils.ts +++ b/backend/src/v1/users/users.utils.ts @@ -1,6 +1,6 @@ /* eslint-disable import/prefer-default-export */ /* 추후 여러 utils 함수들이 추가될 거 생각해서, default export를 안 넣어둠 */ -export const isLibrian = (role : number):boolean => { +export const isLibrian = (role: number): boolean => { if (role === 2) return true; return false; }; diff --git a/backend/src/v1/utils/dateFormat.ts b/backend/src/v1/utils/dateFormat.ts index 5875c82a..b187e900 100644 --- a/backend/src/v1/utils/dateFormat.ts +++ b/backend/src/v1/utils/dateFormat.ts @@ -6,6 +6,8 @@ function leftPad(value: number) { } export const formatDate = (date: Date) => { - const formatted_date = `${date.getFullYear()}-${leftPad(date.getMonth() + 1)}-${leftPad(date.getDate())}`; + const formatted_date = `${date.getFullYear()}-${leftPad(date.getMonth() + 1)}-${leftPad( + date.getDate(), + )}`; return formatted_date; }; diff --git a/backend/src/v1/utils/error/errorCode.ts b/backend/src/v1/utils/error/errorCode.ts index 9cb9adce..773fdc9d 100644 --- a/backend/src/v1/utils/error/errorCode.ts +++ b/backend/src/v1/utils/error/errorCode.ts @@ -86,4 +86,5 @@ export const DUPLICATED_SUPER_TAGS = '908'; export const DUPLICATED_SUB_DEFAULT_TAGS = '909'; export const INVALID_TAG_ID = '910'; -export const CLIENT_AUTH_FAILED_ERROR_MESSAGE = 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'; +export const CLIENT_AUTH_FAILED_ERROR_MESSAGE = + 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'; diff --git a/backend/src/v1/utils/error/errorHandler.ts b/backend/src/v1/utils/error/errorHandler.ts index e9aca049..7becd989 100644 --- a/backend/src/v1/utils/error/errorHandler.ts +++ b/backend/src/v1/utils/error/errorHandler.ts @@ -21,7 +21,10 @@ export default function errorHandler( ); } else error = err as ErrorResponse; if (parseInt(error.errorCode, 10) === 42) { - res.status(error.status).json({ errorCode: parseInt(error.errorCode, 10), message: '42키값 업데이트가 필요합니다. 키값 업데이트까지는 일반 로그인을 이용해주세요.' }); + res.status(error.status).json({ + errorCode: parseInt(error.errorCode, 10), + message: '42키값 업데이트가 필요합니다. 키값 업데이트까지는 일반 로그인을 이용해주세요.', + }); } res.status(error.status).json({ errorCode: parseInt(error.errorCode, 10) }); } diff --git a/backend/src/v1/utils/isNullish.ts b/backend/src/v1/utils/isNullish.ts index f6ed0d4d..405e21c9 100644 --- a/backend/src/v1/utils/isNullish.ts +++ b/backend/src/v1/utils/isNullish.ts @@ -1,3 +1,3 @@ export default function isNullish(value: unknown) { - return (value === null || value === undefined); + return value === null || value === undefined; } diff --git a/backend/src/v1/utils/parseCheck.ts b/backend/src/v1/utils/parseCheck.ts index 596d08b3..23514262 100644 --- a/backend/src/v1/utils/parseCheck.ts +++ b/backend/src/v1/utils/parseCheck.ts @@ -1,32 +1,20 @@ -export const sortParse = ( - sort : any, -) : 'ASC' | 'DESC' => { +export const sortParse = (sort: any): 'ASC' | 'DESC' => { if (sort === 'asc' || sort === 'desc' || sort === 'ASC' || sort === 'DESC') { return sort.toUpperCase(); } return 'DESC'; }; -export const pageParse = ( - page : number, -) : number => (Number.isNaN(page) ? 0 : page); +export const pageParse = (page: number): number => (Number.isNaN(page) ? 0 : page); -export const limitParse = ( - limit : number, -) : number => (Number.isNaN(limit) ? 10 : limit); +export const limitParse = (limit: number): number => (Number.isNaN(limit) ? 10 : limit); -export const stringQueryParse = ( - stringQuery : any, -) : string => ((stringQuery === undefined || null) ? '' : stringQuery.trim()); +export const stringQueryParse = (stringQuery: any): string => + stringQuery === undefined || null ? '' : stringQuery.trim(); -export const booleanQueryParse = ( - booleanQuery : any, -) : boolean => (booleanQuery === 'true'); +export const booleanQueryParse = (booleanQuery: any): boolean => booleanQuery === 'true'; -export const disabledParse = ( - disabled : number, -) : number => (Number.isNaN(disabled) ? -1 : disabled); +export const disabledParse = (disabled: number): number => (Number.isNaN(disabled) ? -1 : disabled); -export const visibilityParse = ( - visibility : string, -) : string => ((visibility === undefined || null) ? '' : visibility); +export const visibilityParse = (visibility: string): string => + visibility === undefined || null ? '' : visibility; diff --git a/backend/src/v1/utils/types.ts b/backend/src/v1/utils/types.ts index 19157d71..fea013d4 100644 --- a/backend/src/v1/utils/types.ts +++ b/backend/src/v1/utils/types.ts @@ -1,5 +1,5 @@ import { RowDataPacket } from 'mysql2'; export type StringRows = RowDataPacket & { - str: string -} + str: string; +}; diff --git a/backend/src/v2/books/errors.ts b/backend/src/v2/books/errors.ts index fc7e2422..f307a577 100644 --- a/backend/src/v2/books/errors.ts +++ b/backend/src/v2/books/errors.ts @@ -1,23 +1,23 @@ export class PubdateFormatError extends Error { - declare readonly _tag: 'FormatError'; - - constructor(exp: string) { - super(`${exp}가 지정된 포맷과 일치하지 않습니다.`); - } + declare readonly _tag: 'FormatError'; + + constructor(exp: string) { + super(`${exp}가 지정된 포맷과 일치하지 않습니다.`); } +} export class IsbnNotFoundError extends Error { - declare readonly _tag: 'ISBN_NOT_FOUND'; + declare readonly _tag: 'ISBN_NOT_FOUND'; - constructor(exp: string) { - super(`국립중앙도서관 API에서 ISBN(${exp}) 검색이 실패하였습니다.`) - } + constructor(exp: string) { + super(`국립중앙도서관 API에서 ISBN(${exp}) 검색이 실패하였습니다.`); + } } export class NaverBookNotFound extends Error { - declare readonly _tag: 'NAVER_BOOK_NOT_FOUND'; + declare readonly _tag: 'NAVER_BOOK_NOT_FOUND'; - constructor(exp: string) { - super(`네이버 책검색 API에서 ISBN(${exp}) 검색이 실패하였습니다.`) - } -} \ No newline at end of file + constructor(exp: string) { + super(`네이버 책검색 API에서 ISBN(${exp}) 검색이 실패하였습니다.`); + } +} diff --git a/backend/src/v2/books/mod.ts b/backend/src/v2/books/mod.ts index 88eed967..dbc5f905 100644 --- a/backend/src/v2/books/mod.ts +++ b/backend/src/v2/books/mod.ts @@ -1,9 +1,26 @@ -import { contract } from "@jiphyeonjeon-42/contracts"; -import { initServer } from "@ts-rest/express"; -import { searchAllBooks, searchBookById, searchBookInfoById, searchBookInfoForCreate, searchBookInfosByTag, searchBookInfosSorted, updateBookDonator, updateBookOrBookInfo } from "./service"; -import { BookInfoNotFoundError, BookNotFoundError, bookInfoNotFound, bookNotFound, isbnNotFound, naverBookNotFound, pubdateFormatError } from "../shared"; -import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from "./errors"; -import authValidate from "~/v1/auth/auth.validate"; +import { contract } from '@jiphyeonjeon-42/contracts'; +import { initServer } from '@ts-rest/express'; +import { + searchAllBooks, + searchBookById, + searchBookInfoById, + searchBookInfoForCreate, + searchBookInfosByTag, + searchBookInfosSorted, + updateBookDonator, + updateBookOrBookInfo, +} from './service'; +import { + BookInfoNotFoundError, + BookNotFoundError, + bookInfoNotFound, + bookNotFound, + isbnNotFound, + naverBookNotFound, + pubdateFormatError, +} from '../shared'; +import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; +import authValidate from '~/v1/auth/auth.validate'; import { roleSet } from '~/v1/auth/auth.type'; const s = initServer(); diff --git a/backend/src/v2/books/repository.ts b/backend/src/v2/books/repository.ts index a2e723e0..b615f0ee 100644 --- a/backend/src/v2/books/repository.ts +++ b/backend/src/v2/books/repository.ts @@ -1,54 +1,59 @@ -import { db } from "~/kysely/mod.ts"; -import { sql } from "kysely"; +import { db } from '~/kysely/mod.ts'; +import { sql } from 'kysely'; -import jipDataSource from "~/app-data-source"; -import { VSearchBook, Book, BookInfo, VSearchBookByTag, User } from "~/entity/entities"; -import { Like } from "typeorm"; -import { dateAddDays, dateFormat } from "~/kysely/sqlDates"; +import jipDataSource from '~/app-data-source'; +import { VSearchBook, Book, BookInfo, VSearchBookByTag, User } from '~/entity/entities'; +import { Like } from 'typeorm'; +import { dateAddDays, dateFormat } from '~/kysely/sqlDates'; -export const vSearchBookRepo = jipDataSource.getRepository(VSearchBook) +export const vSearchBookRepo = jipDataSource.getRepository(VSearchBook); export const bookRepo = jipDataSource.getRepository(Book); export const bookInfoRepo = jipDataSource.getRepository(BookInfo); export const vSearchBookByTagRepo = jipDataSource.getRepository(VSearchBookByTag); export const userRepo = jipDataSource.getRepository(User); -export const getBookInfosByTag = async (whereQuery: object, sortQuery: object, page: number, limit: number) => { - return await vSearchBookByTagRepo.findAndCount({ - select: [ - 'id', - 'title', - 'author', - 'isbn', - 'image', - 'publishedAt', - 'createdAt', - 'updatedAt', - 'category', - 'superTagContent', - 'subTagContent', - 'lendingCnt' - ], - where: whereQuery, - take: limit, - skip: page * limit, - order: sortQuery - }); -} +export const getBookInfosByTag = async ( + whereQuery: object, + sortQuery: object, + page: number, + limit: number, +) => { + return await vSearchBookByTagRepo.findAndCount({ + select: [ + 'id', + 'title', + 'author', + 'isbn', + 'image', + 'publishedAt', + 'createdAt', + 'updatedAt', + 'category', + 'superTagContent', + 'subTagContent', + 'lendingCnt', + ], + where: whereQuery, + take: limit, + skip: page * limit, + order: sortQuery, + }); +}; const bookInfoBy = () => - db - .selectFrom('book_info') - .select([ - 'book_info.id', - 'book_info.title', - 'book_info.author', - 'book_info.publisher', - 'book_info.isbn', - 'book_info.image', - 'book_info.publishedAt', - 'book_info.createdAt', - 'book_info.updatedAt' - ]) + db + .selectFrom('book_info') + .select([ + 'book_info.id', + 'book_info.title', + 'book_info.author', + 'book_info.publisher', + 'book_info.isbn', + 'book_info.image', + 'book_info.publishedAt', + 'book_info.createdAt', + 'book_info.updatedAt', + ]); export const getBookInfosSorted = (limit: number) => bookInfoBy() @@ -58,141 +63,124 @@ export const getBookInfosSorted = (limit: number) => .select('category.name as category') .select(({ eb }) => eb.fn.count('lending.id').as('lendingCnt')) .limit(limit) - .groupBy('id') + .groupBy('id'); -export const searchBookInfoSpecById = async ( id: number ) => +export const searchBookInfoSpecById = async (id: number) => bookInfoBy() .select(({ selectFrom }) => [ selectFrom('category') - .select('name') - .whereRef('category.id', '=', 'book_info.categoryId') - .as('category'), + .select('name') + .whereRef('category.id', '=', 'book_info.categoryId') + .as('category'), ]) .where('id', '=', id) .executeTakeFirst(); -export const searchBooksByInfoId = async ( id: number ) => - db - .selectFrom('book') - .select([ - 'id', - 'callSign', - 'donator', - 'status' - ]) - .where('infoId', '=', id) - .execute(); - -export const getIsLendable = async (id: number) => -{ - const isLended = await db - .selectFrom('lending') - .where('bookId', '=', id) - .where('returnedAt', 'is', null) - .select('id') - .executeTakeFirst(); - - const book = await db - .selectFrom('book') - .where('id', '=', id) - .where('status', '=', 0) - .select('id') - .executeTakeFirst(); - - const isReserved = await db - .selectFrom('reservation') - .where('bookId', '=', id) - .where('status', '=', 0) - .select('id') - .executeTakeFirst(); - - return book !== undefined && isLended === undefined && isReserved !== undefined; -} - -export const getIsReserved = async (id: number) => -{ - const count = await db - .selectFrom('reservation') - .where('bookId', '=', id) - .where('status', '=', 0) - .select(({ eb }) => eb.fn.countAll().as('count')) - .executeTakeFirst(); - - if (Number(count?.count) > 0) - return true; - else - return false; -} +export const searchBooksByInfoId = async (id: number) => + db + .selectFrom('book') + .select(['id', 'callSign', 'donator', 'status']) + .where('infoId', '=', id) + .execute(); + +export const getIsLendable = async (id: number) => { + const isLended = await db + .selectFrom('lending') + .where('bookId', '=', id) + .where('returnedAt', 'is', null) + .select('id') + .executeTakeFirst(); + + const book = await db + .selectFrom('book') + .where('id', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + const isReserved = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select('id') + .executeTakeFirst(); + + return book !== undefined && isLended === undefined && isReserved !== undefined; +}; + +export const getIsReserved = async (id: number) => { + const count = await db + .selectFrom('reservation') + .where('bookId', '=', id) + .where('status', '=', 0) + .select(({ eb }) => eb.fn.countAll().as('count')) + .executeTakeFirst(); + + if (Number(count?.count) > 0) return true; + else return false; +}; export const getDuedate = async (id: number, interval = 14) => - db - .selectFrom('lending') - .where('bookId', '=', id) - .orderBy('createdAt', 'desc') - .limit(1) - .select(({ ref }) => { - const createdAt = ref('lending.createdAt'); - - return dateAddDays(createdAt, interval).as('dueDate'); - }) - .executeTakeFirst(); + db + .selectFrom('lending') + .where('bookId', '=', id) + .orderBy('createdAt', 'desc') + .limit(1) + .select(({ ref }) => { + const createdAt = ref('lending.createdAt'); + + return dateAddDays(createdAt, interval).as('dueDate'); + }) + .executeTakeFirst(); type SearchBookListArgs = { query: string; page: number; limit: number }; -export const searchBookListAndCount = async ({ - query, - page, - limit -}: SearchBookListArgs) => { - return await vSearchBookRepo.findAndCount({ - where: [ - { title: Like(`%${query}%`) }, - { author: Like(`%${query}%`) }, - { isbn: Like(`%${query}%`) }, - ], - take: limit, - skip: page * limit, - }); -} +export const searchBookListAndCount = async ({ query, page, limit }: SearchBookListArgs) => { + return await vSearchBookRepo.findAndCount({ + where: [ + { title: Like(`%${query}%`) }, + { author: Like(`%${query}%`) }, + { isbn: Like(`%${query}%`) }, + ], + take: limit, + skip: page * limit, + }); +}; type UpdateBookArgs = { - id: number, - callSign?: string | undefined, - status?: number | undefined, + id: number; + callSign?: string | undefined; + status?: number | undefined; +}; +export const updateBookById = async ({ id, callSign, status }: UpdateBookArgs) => { + await bookRepo.update(id, { callSign, status }); }; -export const updateBookById = async ({ - id, - callSign, - status, -}: UpdateBookArgs ) => { - await bookRepo.update(id, {callSign, status}); -} type UpdateBookInfoArgs = { - id: number, - title?: string | undefined, - author?: string | undefined, - publisher?: string | undefined, - publishedAt?: string | undefined, - image?: string | undefined, - categoryId?: number | undefined + id: number; + title?: string | undefined; + author?: string | undefined; + publisher?: string | undefined; + publishedAt?: string | undefined; + image?: string | undefined; + categoryId?: number | undefined; }; export const updateBookInfoById = async ({ - id, - title, - author, - publisher, - publishedAt, - image, - categoryId -}: UpdateBookInfoArgs ) => { - await bookInfoRepo.update(id, {title, author, publisher, publishedAt, image, categoryId}); -} - -type UpdateBookDonatorNameArgs = {bookId: number, donator: string, donatorId?: number | null}; + id, + title, + author, + publisher, + publishedAt, + image, + categoryId, +}: UpdateBookInfoArgs) => { + await bookInfoRepo.update(id, { title, author, publisher, publishedAt, image, categoryId }); +}; + +type UpdateBookDonatorNameArgs = { bookId: number; donator: string; donatorId?: number | null }; export const updateBookDonatorName = async ({ - bookId, - donator, - donatorId -}: UpdateBookDonatorNameArgs ) => { - await bookRepo.update(bookId, {donator, donatorId}); -} \ No newline at end of file + bookId, + donator, + donatorId, +}: UpdateBookDonatorNameArgs) => { + await bookRepo.update(bookId, { donator, donatorId }); +}; diff --git a/backend/src/v2/books/service.ts b/backend/src/v2/books/service.ts index 01b12741..5ee2f71c 100644 --- a/backend/src/v2/books/service.ts +++ b/backend/src/v2/books/service.ts @@ -1,313 +1,307 @@ -import { match } from "ts-pattern"; +import { match } from 'ts-pattern'; import { - searchBookListAndCount, - vSearchBookRepo, - updateBookById, - updateBookInfoById, - searchBookInfoSpecById, - searchBooksByInfoId, - getIsLendable, - getIsReserved, - getDuedate, - getBookInfosSorted, - getBookInfosByTag, - userRepo, - updateBookDonatorName} from "./repository"; -import { BookInfoNotFoundError, Meta, BookNotFoundError } from "../shared"; -import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from "./errors"; -import { dateNow, dateSubDays } from "~/kysely/sqlDates"; -import axios from "axios"; + searchBookListAndCount, + vSearchBookRepo, + updateBookById, + updateBookInfoById, + searchBookInfoSpecById, + searchBooksByInfoId, + getIsLendable, + getIsReserved, + getDuedate, + getBookInfosSorted, + getBookInfosByTag, + userRepo, + updateBookDonatorName, +} from './repository'; +import { BookInfoNotFoundError, Meta, BookNotFoundError } from '../shared'; +import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; +import { dateNow, dateSubDays } from '~/kysely/sqlDates'; +import axios from 'axios'; import { nationalIsbnApiKey, naverBookApiOption } from '~/config'; -type CategoryList = {name: string, count: number}; -type SearchBookInfosByTag = { query: string, sort: string, page: number, limit: number, category?: string | undefined }; +type CategoryList = { name: string; count: number }; +type SearchBookInfosByTag = { + query: string; + sort: string; + page: number; + limit: number; + category?: string | undefined; +}; export const searchBookInfosByTag = async ({ - query, - sort, - page, - limit, - category -}: SearchBookInfosByTag ) => { - let sortQuery = {}; - switch(sort) - { - case 'title': - sortQuery = { title: 'ASC' }; - break; - case 'popular': - sortQuery = { lendingCnt: 'DESC' }; - break; - default: - sortQuery = { createdAt: 'DESC' }; - } + query, + sort, + page, + limit, + category, +}: SearchBookInfosByTag) => { + let sortQuery = {}; + switch (sort) { + case 'title': + sortQuery = { title: 'ASC' }; + break; + case 'popular': + sortQuery = { lendingCnt: 'DESC' }; + break; + default: + sortQuery = { createdAt: 'DESC' }; + } - let whereQuery: Array = [ - { superTagContent: query }, - { subTagContent: query } - ]; + let whereQuery: Array = [{ superTagContent: query }, { subTagContent: query }]; - if (category) - { - whereQuery.push({ category }); - } + if (category) { + whereQuery.push({ category }); + } - const [bookInfoList, totalItems] = await getBookInfosByTag(whereQuery, sortQuery, page, limit); - let categoryList = new Array ; - bookInfoList.forEach((bookInfo) => { - const index = categoryList.findIndex((item) => bookInfo.category === item.name); - if (index === -1) - categoryList.push({name: bookInfo.category, count: 1}); - else - categoryList[index].count += 1; - }); - const meta = { - totalItems, - itemCount: bookInfoList.length, - itemsPerPage: limit, - totalPages: Math.ceil(bookInfoList.length / limit), - currentPage: page + 1 - } + const [bookInfoList, totalItems] = await getBookInfosByTag(whereQuery, sortQuery, page, limit); + let categoryList = new Array(); + bookInfoList.forEach((bookInfo) => { + const index = categoryList.findIndex((item) => bookInfo.category === item.name); + if (index === -1) categoryList.push({ name: bookInfo.category, count: 1 }); + else categoryList[index].count += 1; + }); + const meta = { + totalItems, + itemCount: bookInfoList.length, + itemsPerPage: limit, + totalPages: Math.ceil(bookInfoList.length / limit), + currentPage: page + 1, + }; - return { - items: bookInfoList, - categories: categoryList, - meta - }; -} + return { + items: bookInfoList, + categories: categoryList, + meta, + }; +}; -type SearchBookInfosSortedArgs = { sort: string, limit: number }; -export const searchBookInfosSorted = async ({ - sort, - limit, -}: SearchBookInfosSortedArgs ) => { - let items; - if (sort === 'popular') - { - items = await getBookInfosSorted(limit) - .where('lending.createdAt', '>=', dateSubDays(dateNow(), 42)) - .orderBy('lendingCnt', 'desc') - .orderBy('title', 'asc') - .execute(); - } - else { - items = await getBookInfosSorted(limit) - .orderBy('createdAt', 'desc') - .orderBy('title', 'asc') - .execute(); - } +type SearchBookInfosSortedArgs = { sort: string; limit: number }; +export const searchBookInfosSorted = async ({ sort, limit }: SearchBookInfosSortedArgs) => { + let items; + if (sort === 'popular') { + items = await getBookInfosSorted(limit) + .where('lending.createdAt', '>=', dateSubDays(dateNow(), 42)) + .orderBy('lendingCnt', 'desc') + .orderBy('title', 'asc') + .execute(); + } else { + items = await getBookInfosSorted(limit) + .orderBy('createdAt', 'desc') + .orderBy('title', 'asc') + .execute(); + } - return { items } as const -} + return { items } as const; +}; export const searchBookInfoById = async (id: number) => { - let bookSpec = await searchBookInfoSpecById(id); + let bookSpec = await searchBookInfoSpecById(id); - if (bookSpec === undefined) - return new BookInfoNotFoundError(id); + if (bookSpec === undefined) return new BookInfoNotFoundError(id); - if (bookSpec.publishedAt) - { - const date = new Date(bookSpec.publishedAt); - bookSpec.publishedAt = `${date.getFullYear()}년 ${date.getMonth() + 1}월`; - } + if (bookSpec.publishedAt) { + const date = new Date(bookSpec.publishedAt); + bookSpec.publishedAt = `${date.getFullYear()}년 ${date.getMonth() + 1}월`; + } - const eachbooks = await searchBooksByInfoId(id); + const eachbooks = await searchBooksByInfoId(id); - const books = await Promise.all( - eachbooks.map(async (eachBook) => { - const isLendable = await getIsLendable(eachBook.id); - const isReserved = await getIsReserved(eachBook.id); - let dueDate; + const books = await Promise.all( + eachbooks.map(async (eachBook) => { + const isLendable = await getIsLendable(eachBook.id); + const isReserved = await getIsReserved(eachBook.id); + let dueDate; - if (eachBook.status === 0 && isLendable === false) - { - dueDate = await getDuedate(eachBook.id); - dueDate = dueDate?.dueDate; - } - else - dueDate = '-'; + if (eachBook.status === 0 && isLendable === false) { + dueDate = await getDuedate(eachBook.id); + dueDate = dueDate?.dueDate; + } else dueDate = '-'; - return { - ...eachBook, - dueDate, - isLendable, - isReserved - } - }) - ); + return { + ...eachBook, + dueDate, + isLendable, + isReserved, + }; + }), + ); - return { - ...bookSpec, - books - } -} + return { + ...bookSpec, + books, + }; +}; -type SearchAllBooksArgs = { query?: string | undefined, page: number, limit: number }; -export const searchAllBooks = async ({ - query, - page, - limit -}: SearchAllBooksArgs) => { - const [BookList, totalItems] = await searchBookListAndCount({query: query ? query : '', page, limit}); +type SearchAllBooksArgs = { query?: string | undefined; page: number; limit: number }; +export const searchAllBooks = async ({ query, page, limit }: SearchAllBooksArgs) => { + const [BookList, totalItems] = await searchBookListAndCount({ + query: query ? query : '', + page, + limit, + }); - const meta: Meta = { - totalItems, - itemCount: BookList.length, - itemsPerPage: limit, - totalPages: Math.ceil(totalItems / limit), - currentPage: page + 1, - } - return {items: BookList, meta}; -} + const meta: Meta = { + totalItems, + itemCount: BookList.length, + itemsPerPage: limit, + totalPages: Math.ceil(totalItems / limit), + currentPage: page + 1, + }; + return { items: BookList, meta }; +}; type BookInfoForCreate = { - title: string, - author?: string | undefined - isbn: string, - category: string, - publisher: string, - pubdate: string, - image: string, -} + title: string; + author?: string | undefined; + isbn: string; + category: string; + publisher: string; + pubdate: string; + image: string; +}; const getInfoInNationalLibrary = async (isbn: string) => { - let bookInfo : BookInfoForCreate | undefined; - let searchResult; + let bookInfo: BookInfoForCreate | undefined; + let searchResult; - await axios - .get(`https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`) - .then((res) => { - searchResult = res.data.docs[0]; - const { - TITLE: title, SUBJECT: category, PUBLISHER: publisher, PUBLISH_PREDATE: pubdate, - } = searchResult; - const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice(-3)}/x${isbn}.jpg`; - bookInfo = { - title, image, category, isbn, publisher, pubdate, - }; - }) - .catch(() => { - console.log('Error'); - }) - return (bookInfo); -} + await axios + .get( + `https://www.nl.go.kr/seoji/SearchApi.do?cert_key=${nationalIsbnApiKey}&result_style=json&page_no=1&page_size=10&isbn=${isbn}`, + ) + .then((res) => { + searchResult = res.data.docs[0]; + const { + TITLE: title, + SUBJECT: category, + PUBLISHER: publisher, + PUBLISH_PREDATE: pubdate, + } = searchResult; + const image = `https://image.kyobobook.co.kr/images/book/xlarge/${isbn.slice( + -3, + )}/x${isbn}.jpg`; + bookInfo = { + title, + image, + category, + isbn, + publisher, + pubdate, + }; + }) + .catch(() => { + console.log('Error'); + }); + return bookInfo; +}; const getAuthorInNaver = async (isbn: string) => { - let author; + let author; - await axios - .get( - `https://openapi.naver.com/v1/search/book_adv?d_isbn=${isbn}`, - { - headers: { - 'X-Naver-Client-Id': `${naverBookApiOption.client}`, - 'X-Naver-Client-Secret': `${naverBookApiOption.secret}`, - }, - }, - ) - .then((res) => { - author = res.data.items[0].author; - }) - .catch(() => { - console.log('ERROR'); - }) - return (author); -} + await axios + .get(`https://openapi.naver.com/v1/search/book_adv?d_isbn=${isbn}`, { + headers: { + 'X-Naver-Client-Id': `${naverBookApiOption.client}`, + 'X-Naver-Client-Secret': `${naverBookApiOption.secret}`, + }, + }) + .then((res) => { + author = res.data.items[0].author; + }) + .catch(() => { + console.log('ERROR'); + }); + return author; +}; export const searchBookInfoForCreate = async (isbn: string) => { - let bookInfo = await getInfoInNationalLibrary(isbn); - if (bookInfo === undefined) - return new IsbnNotFoundError(isbn); + let bookInfo = await getInfoInNationalLibrary(isbn); + if (bookInfo === undefined) return new IsbnNotFoundError(isbn); - bookInfo.author = await getAuthorInNaver(isbn); - if (bookInfo.author === undefined) - return new NaverBookNotFound(isbn); + bookInfo.author = await getAuthorInNaver(isbn); + if (bookInfo.author === undefined) return new NaverBookNotFound(isbn); - return {bookInfo}; -} + return { bookInfo }; +}; type SearchBookByIdArgs = { id: number }; -export const searchBookById = async ({ - id, -}: SearchBookByIdArgs) => { - const book = await vSearchBookRepo.findOneBy({bookId: id}); +export const searchBookById = async ({ id }: SearchBookByIdArgs) => { + const book = await vSearchBookRepo.findOneBy({ bookId: id }); - return match(book) - .with(null, () => new BookNotFoundError(id)) - .otherwise(() => { - return { - id: book?.bookId, - ...book - }; - }); -} + return match(book) + .with(null, () => new BookNotFoundError(id)) + .otherwise(() => { + return { + id: book?.bookId, + ...book, + }; + }); +}; type UpdateBookArgs = { - bookId: number, - callSign?: string | undefined, - status?: number | undefined + bookId: number; + callSign?: string | undefined; + status?: number | undefined; }; const updateBook = async (book: UpdateBookArgs) => { - return await updateBookById({ id: book.bookId, callSign: book.callSign, status: book.status }); -} + return await updateBookById({ id: book.bookId, callSign: book.callSign, status: book.status }); +}; type UpdateBookInfoArgs = { - bookInfoId: number, - title?: string | undefined, - author?: string | undefined, - publisher?: string | undefined, - publishedAt?: string | undefined, - image?: string | undefined, - categoryId?: number | undefined, -} + bookInfoId: number; + title?: string | undefined; + author?: string | undefined; + publisher?: string | undefined; + publishedAt?: string | undefined; + image?: string | undefined; + categoryId?: number | undefined; +}; const pubdateFormatValidator = (pubdate: string) => { - const regexCondition = /^[0-9]{8}$/; - return regexCondition.test(pubdate) -} + const regexCondition = /^[0-9]{8}$/; + return regexCondition.test(pubdate); +}; const updateBookInfo = async (book: UpdateBookInfoArgs) => { - if (book.publishedAt && !pubdateFormatValidator(book.publishedAt)) - return new PubdateFormatError(book.publishedAt); - return await updateBookInfoById({ - id: book.bookInfoId, - title: book.title, - author: book.author, - publisher: book.publisher, - publishedAt: book.publishedAt, - image: book.image, - categoryId: book.categoryId - }); -} + if (book.publishedAt && !pubdateFormatValidator(book.publishedAt)) + return new PubdateFormatError(book.publishedAt); + return await updateBookInfoById({ + id: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId, + }); +}; -type UpdateBookOrBookInfoArgs = - Omit - & Omit - & { - bookId?: number | undefined, - bookInfoId?: number | undefined, +type UpdateBookOrBookInfoArgs = Omit & + Omit & { + bookId?: number | undefined; + bookInfoId?: number | undefined; + }; +export const updateBookOrBookInfo = async (book: UpdateBookOrBookInfoArgs) => { + if (book.bookId) + await updateBook({ + bookId: book.bookId, + callSign: book.callSign, + status: book.status, + }); + if (book.bookInfoId) + return await updateBookInfo({ + bookInfoId: book.bookInfoId, + title: book.title, + author: book.author, + publisher: book.publisher, + publishedAt: book.publishedAt, + image: book.image, + categoryId: book.categoryId, + }); }; -export const updateBookOrBookInfo = async ( book: UpdateBookOrBookInfoArgs ) => { - if (book.bookId) - await updateBook({ - bookId: book.bookId, - callSign: book.callSign, - status: book.status - }); - if (book.bookInfoId) - return await updateBookInfo({ - bookInfoId: book.bookInfoId, - title: book.title, - author: book.author, - publisher: book.publisher, - publishedAt: book.publishedAt, - image: book.image, - categoryId: book.categoryId - }); -} -type UpdateBookDonatorArgs = {bookId: number, nickname: string }; -export const updateBookDonator = async ({ - bookId, - nickname -}: UpdateBookDonatorArgs ) => { - const user = await userRepo.findOneBy({nickname}); - return await updateBookDonatorName({bookId, donator: nickname, donatorId: user ? user.id : null}); -} +type UpdateBookDonatorArgs = { bookId: number; nickname: string }; +export const updateBookDonator = async ({ bookId, nickname }: UpdateBookDonatorArgs) => { + const user = await userRepo.findOneBy({ nickname }); + return await updateBookDonatorName({ + bookId, + donator: nickname, + donatorId: user ? user.id : null, + }); +}; diff --git a/backend/src/v2/histories/repository.ts b/backend/src/v2/histories/repository.ts index 3a8f039f..9a861df8 100644 --- a/backend/src/v2/histories/repository.ts +++ b/backend/src/v2/histories/repository.ts @@ -38,12 +38,7 @@ type Args = { type?: 'title' | 'user' | 'callsign' | undefined; }; -export const getHistoriesByQuery = ({ - query, - type, - page, - limit, -}: Args & Offset) => +export const getHistoriesByQuery = ({ query, type, page, limit }: Args & Offset) => historiesRepo.findAndCount({ where: getSearchCondition({ query, type }), take: limit, @@ -56,11 +51,7 @@ type MyPageArgs = { type?: 'title' | 'callsign' | undefined; }; -export const getHistoriesByUser = ({ - login, - page, - limit, -}: MyPageArgs & Offset) => +export const getHistoriesByUser = ({ login, page, limit }: MyPageArgs & Offset) => historiesRepo.findAndCount({ where: { login }, take: limit, diff --git a/backend/src/v2/reviews/mod.ts b/backend/src/v2/reviews/mod.ts index 15f295f7..5639f7ff 100644 --- a/backend/src/v2/reviews/mod.ts +++ b/backend/src/v2/reviews/mod.ts @@ -9,14 +9,9 @@ import { getUser, reviewNotFound, } from '../shared/index.ts'; -import { - createReview, - removeReview, - toggleReviewVisibility, - updateReview, -} from './service.ts'; +import { createReview, removeReview, toggleReviewVisibility, updateReview } from './service.ts'; import { ReviewNotFoundError } from './errors.js'; -import { searchReviews } from './repository.ts' +import { searchReviews } from './repository.ts'; const s = initServer(); export const reviews = s.router(contract.reviews, { @@ -26,7 +21,7 @@ export const reviews = s.router(contract.reviews, { const body = await searchReviews(query); return { status: 200, body }; - } + }, }, post: { middleware: [authValidate(roleSet.all)], diff --git a/backend/src/v2/reviews/repository.ts b/backend/src/v2/reviews/repository.ts index 328995b2..90776403 100644 --- a/backend/src/v2/reviews/repository.ts +++ b/backend/src/v2/reviews/repository.ts @@ -40,16 +40,10 @@ const queryReviews = () => 'user.nickname', ]); -export const searchReviews = async ({ - search, - sort, - visibility, - page, - perPage, -}: SearchOption) => { +export const searchReviews = async ({ search, sort, visibility, page, perPage }: SearchOption) => { const searchQuery = queryReviews() - .$if(search !== undefined, qb => - qb.where(eb => + .$if(search !== undefined, (qb) => + qb.where((eb) => eb.or([ eb('user.nickname', 'like', `%${search}%`), eb('book_info.title', 'like', `%${search}%`), @@ -102,11 +96,7 @@ type ToggleVisibilityOption = { userId: number; disabled: SqlBool; }; -export const toggleVisibilityById = ({ - reviewsId, - userId, - disabled, -}: ToggleVisibilityOption) => +export const toggleVisibilityById = ({ reviewsId, userId, disabled }: ToggleVisibilityOption) => db .updateTable('reviews') .where('id', '=', reviewsId) @@ -119,11 +109,7 @@ type UpdateOption = { content: string; }; -export const updateReviewById = ({ - reviewsId, - userId, - content, -}: UpdateOption) => +export const updateReviewById = ({ reviewsId, userId, content }: UpdateOption) => db .updateTable('reviews') .where('id', '=', reviewsId) diff --git a/backend/src/v2/reviews/service.ts b/backend/src/v2/reviews/service.ts index 34cbb514..d7278a77 100644 --- a/backend/src/v2/reviews/service.ts +++ b/backend/src/v2/reviews/service.ts @@ -1,11 +1,7 @@ import { match } from 'ts-pattern'; import { BookInfoNotFoundError } from '~/v2/shared/errors'; -import { - ReviewDisabledError, - ReviewForbiddenAccessError, - ReviewNotFoundError, -} from './errors'; +import { ReviewDisabledError, ReviewForbiddenAccessError, ReviewNotFoundError } from './errors'; import { ParsedUser } from '~/v2/shared'; import { bookInfoExistsById, @@ -28,29 +24,18 @@ export const createReview = async (args: CreateArgs) => { type RemoveArgs = { reviewsId: number; deleter: ParsedUser }; export const removeReview = async ({ reviewsId, deleter }: RemoveArgs) => { const isAdmin = () => deleter.role === 'librarian'; - const doRemoveReview = () => - deleteReviewById({ reviewsId, deleteUserId: deleter.id }); + const doRemoveReview = () => deleteReviewById({ reviewsId, deleteUserId: deleter.id }); const review = await getReviewById(reviewsId); return match(review) - .with( - undefined, - { isDeleted: true }, - () => new ReviewNotFoundError(reviewsId), - ) + .with(undefined, { isDeleted: true }, () => new ReviewNotFoundError(reviewsId)) .when(isAdmin, doRemoveReview) .with({ userId: deleter.id }, doRemoveReview) - .otherwise( - () => new ReviewForbiddenAccessError({ userId: deleter.id, reviewsId }), - ); + .otherwise(() => new ReviewForbiddenAccessError({ userId: deleter.id, reviewsId })); }; type UpdateArgs = { reviewsId: number; userId: number; content: string }; -export const updateReview = async ({ - reviewsId, - userId, - content, -}: UpdateArgs) => { +export const updateReview = async ({ reviewsId, userId, content }: UpdateArgs) => { const review = await getReviewById(reviewsId); return await match(review) @@ -61,15 +46,10 @@ export const updateReview = async ({ }; type ToggleReviewArgs = { reviewsId: number; userId: number }; -export const toggleReviewVisibility = async ({ - reviewsId, - userId, -}: ToggleReviewArgs) => { +export const toggleReviewVisibility = async ({ reviewsId, userId }: ToggleReviewArgs) => { const review = await getReviewById(reviewsId); return await match(review) .with(undefined, () => new ReviewNotFoundError(reviewsId)) - .otherwise(({ disabled }) => - toggleVisibilityById({ reviewsId, userId, disabled }), - ); + .otherwise(({ disabled }) => toggleVisibilityById({ reviewsId, userId, disabled })); }; diff --git a/backend/src/v2/routes.ts b/backend/src/v2/routes.ts index 6be5946a..03992919 100644 --- a/backend/src/v2/routes.ts +++ b/backend/src/v2/routes.ts @@ -12,5 +12,5 @@ export default s.router(contract, { reviews, histories, stock, - books + books, }); diff --git a/backend/src/v2/shared/responses.ts b/backend/src/v2/shared/responses.ts index a36b2544..8122b3d4 100644 --- a/backend/src/v2/shared/responses.ts +++ b/backend/src/v2/shared/responses.ts @@ -1,4 +1,9 @@ -import { bookInfoNotFoundSchema, reviewNotFoundSchema, unauthorizedSchema, bookNotFoundSchema } from '@jiphyeonjeon-42/contracts'; +import { + bookInfoNotFoundSchema, + reviewNotFoundSchema, + unauthorizedSchema, + bookNotFoundSchema, +} from '@jiphyeonjeon-42/contracts'; import { z } from 'zod'; export const reviewNotFound = { @@ -37,22 +42,22 @@ export const pubdateFormatError = { status: 311, body: { code: 'PUBDATE_FORMAT_ERROR', - description: '입력한 pubdate가 알맞은 형식이 아님.' - } + description: '입력한 pubdate가 알맞은 형식이 아님.', + }, } as const; export const isbnNotFound = { status: 303, body: { code: 'ISBN_NOT_FOUND', - description: '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.' - } + description: '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.', + }, } as const; export const naverBookNotFound = { status: 310, body: { code: 'NAVER_BOOK_NOT_FOUND', - description: '네이버 책검색 API에서 ISBN 검색이 실패' - } -} as const; \ No newline at end of file + description: '네이버 책검색 API에서 ISBN 검색이 실패', + }, +} as const; diff --git a/backend/src/v2/stock/mod.ts b/backend/src/v2/stock/mod.ts index 800a9180..d42929e0 100644 --- a/backend/src/v2/stock/mod.ts +++ b/backend/src/v2/stock/mod.ts @@ -18,5 +18,5 @@ export const stock = s.router(contract.stock, { return bookNotFound; } return { status: 200, body: '재고 상태가 업데이트되었습니다.' } as const; - } + }, }); diff --git a/backend/src/v2/stock/repository.ts b/backend/src/v2/stock/repository.ts index bca83a14..c61491a0 100644 --- a/backend/src/v2/stock/repository.ts +++ b/backend/src/v2/stock/repository.ts @@ -8,11 +8,7 @@ export const bookRepo = jipDataSource.getRepository(Book); type SearchStockArgs = { page: number; limit: number; days: number }; -export const searchStockByUpdatedOlderThan = ({ - limit, - page, - days, -}: SearchStockArgs) => { +export const searchStockByUpdatedOlderThan = ({ limit, page, days }: SearchStockArgs) => { const today = startOfDay(new Date()); return stockRepo.findAndCount({ where: { updatedAt: LessThan(addDays(today, days * -1)) }, diff --git a/contracts/src/books/index.ts b/contracts/src/books/index.ts index a41e70d2..5226bffc 100644 --- a/contracts/src/books/index.ts +++ b/contracts/src/books/index.ts @@ -1,153 +1,159 @@ -import { initContract } from "@ts-rest/core"; +import { initContract } from '@ts-rest/core'; import { - searchAllBooksQuerySchema, - searchAllBooksResponseSchema, - searchBookByIdResponseSchema, - searchBookInfoCreateQuerySchema, - searchBookInfoCreateResponseSchema, - createBookBodySchema, - createBookResponseSchema, - categoryNotFoundSchema, - pubdateFormatErrorSchema, - insertionFailureSchema, - isbnNotFoundSchema, - naverBookNotFoundSchema, - updateBookBodySchema, - updateBookResponseSchema, - unknownPatchErrorSchema, - nonDataErrorSchema, - searchAllBookInfosQuerySchema, - searchBookInfosByTagQuerySchema, - searchBookInfosResponseSchema, - searchBookInfosSortedQuerySchema, - searchBookInfosSortedResponseSchema, - searchBookInfoByIdResponseSchema, - updateDonatorBodySchema, - updateDonatorResponseSchema, - searchBookInfoByIdPathSchema, - searchBookByIdParamSchema -} from "./schema"; -import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema, serverErrorSchema } from "../shared"; + searchAllBooksQuerySchema, + searchAllBooksResponseSchema, + searchBookByIdResponseSchema, + searchBookInfoCreateQuerySchema, + searchBookInfoCreateResponseSchema, + createBookBodySchema, + createBookResponseSchema, + categoryNotFoundSchema, + pubdateFormatErrorSchema, + insertionFailureSchema, + isbnNotFoundSchema, + naverBookNotFoundSchema, + updateBookBodySchema, + updateBookResponseSchema, + unknownPatchErrorSchema, + nonDataErrorSchema, + searchAllBookInfosQuerySchema, + searchBookInfosByTagQuerySchema, + searchBookInfosResponseSchema, + searchBookInfosSortedQuerySchema, + searchBookInfosSortedResponseSchema, + searchBookInfoByIdResponseSchema, + updateDonatorBodySchema, + updateDonatorResponseSchema, + searchBookInfoByIdPathSchema, + searchBookByIdParamSchema, +} from './schema'; +import { + badRequestSchema, + bookInfoNotFoundSchema, + bookNotFoundSchema, + serverErrorSchema, +} from '../shared'; const c = initContract(); export const booksContract = c.router( - { - // searchAllBookInfos: { - // method: 'GET', - // path: '/info/search', - // description: '책 정보(book_info)를 검색하여 가져온다.', - // query: searchAllBookInfosQuerySchema, - // responses: { - // 200: searchBookInfosResponseSchema, - // 400: badRequestSchema, - // 500: serverErrorSchema, - // }, - // }, - searchBookInfosByTag: { - method: 'GET', - path: '/info/tag', - description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.', - query: searchBookInfosByTagQuerySchema, - responses: { - 200: searchBookInfosResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfosSorted: { - method: 'GET', - path: '/info/sorted', - description: '책 정보를 기준에 따라 정렬한다. 정렬기준이 popular일 경우 당일으로부터 42일간 인기순으로 한다.', - query: searchBookInfosSortedQuerySchema, - responses: { - 200: searchBookInfosSortedResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoById: { - method: 'GET', - path: '/info/:id', - pathParams: searchBookInfoByIdPathSchema, - description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', - responses: { - 200: searchBookInfoByIdResponseSchema, - 404: bookInfoNotFoundSchema, - 500: serverErrorSchema - } - }, - searchAllBooks: { - method: 'GET', - path: '/search', - description: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', - query: searchAllBooksQuerySchema, - responses: { - 200: searchAllBooksResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoForCreate: { - method: 'GET', - path: '/create', - description: '책 생성을 위해 국립중앙도서관에서 ISBN으로 검색한 뒤에 책정보를 반환', - query: searchBookInfoCreateQuerySchema, - responses: { - 200: searchBookInfoCreateResponseSchema, - 303: isbnNotFoundSchema, - 310: naverBookNotFoundSchema, - 500: serverErrorSchema, - } - }, - searchBookById: { - method: 'GET', - path: '/:id', - description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', - pathParams: searchBookByIdParamSchema, - responses: { - 200: searchBookByIdResponseSchema, - 404: bookNotFoundSchema, - 500: serverErrorSchema, - } - }, - // createBook: { - // method: 'POST', - // path: '/create', - // description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', - // body: createBookBodySchema, - // responses: { - // 200: createBookResponseSchema, - // 308: insertionFailureSchema, - // 309: categoryNotFoundSchema, - // 311: pubdateFormatErrorSchema, - // 500: serverErrorSchema, - // }, - // }, - updateBook: { - method: 'PATCH', - path: '/update', - description: '책 정보를 수정합니다. book_info table or book table', - body: updateBookBodySchema, - responses: { - 204: updateBookResponseSchema, - 312: unknownPatchErrorSchema, - 313: nonDataErrorSchema, - 311: pubdateFormatErrorSchema, - 500: serverErrorSchema, - }, - }, - updateDonator: { - method: 'PATCH', - path: '/donator', - description: '기부자 정보를 수정합니다.', - body: updateDonatorBodySchema, - responses: { - 204: updateDonatorResponseSchema, - 404: bookNotFoundSchema, - 500: serverErrorSchema, - }, - }, - }, - { pathPrefix: '/books' }, -) + { + // searchAllBookInfos: { + // method: 'GET', + // path: '/info/search', + // description: '책 정보(book_info)를 검색하여 가져온다.', + // query: searchAllBookInfosQuerySchema, + // responses: { + // 200: searchBookInfosResponseSchema, + // 400: badRequestSchema, + // 500: serverErrorSchema, + // }, + // }, + searchBookInfosByTag: { + method: 'GET', + path: '/info/tag', + description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.', + query: searchBookInfosByTagQuerySchema, + responses: { + 200: searchBookInfosResponseSchema, + 400: badRequestSchema, + 500: serverErrorSchema, + }, + }, + searchBookInfosSorted: { + method: 'GET', + path: '/info/sorted', + description: + '책 정보를 기준에 따라 정렬한다. 정렬기준이 popular일 경우 당일으로부터 42일간 인기순으로 한다.', + query: searchBookInfosSortedQuerySchema, + responses: { + 200: searchBookInfosSortedResponseSchema, + 400: badRequestSchema, + 500: serverErrorSchema, + }, + }, + searchBookInfoById: { + method: 'GET', + path: '/info/:id', + pathParams: searchBookInfoByIdPathSchema, + description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + responses: { + 200: searchBookInfoByIdResponseSchema, + 404: bookInfoNotFoundSchema, + 500: serverErrorSchema, + }, + }, + searchAllBooks: { + method: 'GET', + path: '/search', + description: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', + query: searchAllBooksQuerySchema, + responses: { + 200: searchAllBooksResponseSchema, + 400: badRequestSchema, + 500: serverErrorSchema, + }, + }, + searchBookInfoForCreate: { + method: 'GET', + path: '/create', + description: '책 생성을 위해 국립중앙도서관에서 ISBN으로 검색한 뒤에 책정보를 반환', + query: searchBookInfoCreateQuerySchema, + responses: { + 200: searchBookInfoCreateResponseSchema, + 303: isbnNotFoundSchema, + 310: naverBookNotFoundSchema, + 500: serverErrorSchema, + }, + }, + searchBookById: { + method: 'GET', + path: '/:id', + description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + pathParams: searchBookByIdParamSchema, + responses: { + 200: searchBookByIdResponseSchema, + 404: bookNotFoundSchema, + 500: serverErrorSchema, + }, + }, + // createBook: { + // method: 'POST', + // path: '/create', + // description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', + // body: createBookBodySchema, + // responses: { + // 200: createBookResponseSchema, + // 308: insertionFailureSchema, + // 309: categoryNotFoundSchema, + // 311: pubdateFormatErrorSchema, + // 500: serverErrorSchema, + // }, + // }, + updateBook: { + method: 'PATCH', + path: '/update', + description: '책 정보를 수정합니다. book_info table or book table', + body: updateBookBodySchema, + responses: { + 204: updateBookResponseSchema, + 312: unknownPatchErrorSchema, + 313: nonDataErrorSchema, + 311: pubdateFormatErrorSchema, + 500: serverErrorSchema, + }, + }, + updateDonator: { + method: 'PATCH', + path: '/donator', + description: '기부자 정보를 수정합니다.', + body: updateDonatorBodySchema, + responses: { + 204: updateDonatorResponseSchema, + 404: bookNotFoundSchema, + 500: serverErrorSchema, + }, + }, + }, + { pathPrefix: '/books' }, +); diff --git a/contracts/src/books/schema.ts b/contracts/src/books/schema.ts index eb9d14b4..6eccc417 100644 --- a/contracts/src/books/schema.ts +++ b/contracts/src/books/schema.ts @@ -1,173 +1,186 @@ -import { metaSchema, positiveInt, mkErrorMessageSchema, statusSchema, metaPaginatedSchema, dateLike } from "../shared"; -import { z } from "../zodWithOpenapi"; +import { + metaSchema, + positiveInt, + mkErrorMessageSchema, + statusSchema, + metaPaginatedSchema, + dateLike, +} from '../shared'; +import { z } from '../zodWithOpenapi'; export const commonQuerySchema = z.object({ - query: z.string().optional(), - page: positiveInt.default(0).openapi({ example: 0 }), - limit: positiveInt.default(10).openapi({ example: 10 }), + query: z.string().optional(), + page: positiveInt.default(0).openapi({ example: 0 }), + limit: positiveInt.default(10).openapi({ example: 10 }), }); export const searchAllBookInfosQuerySchema = commonQuerySchema.extend({ - sort: z.enum(["new", "popular", "title"]).default('new'), - category: z.string().optional(), + sort: z.enum(['new', 'popular', 'title']).default('new'), + category: z.string().optional(), }); export const searchBookInfosByTagQuerySchema = commonQuerySchema.extend({ - query: z.string(), - sort: z.enum(["new", "popular", "title"]).default('new'), - category: z.string().optional(), + query: z.string(), + sort: z.enum(['new', 'popular', 'title']).default('new'), + category: z.string().optional(), }); export const searchBookInfosSortedQuerySchema = z.object({ - sort: z.enum(["new", "popular"]), - limit: positiveInt.default(10).openapi({ example: 10 }), + sort: z.enum(['new', 'popular']), + limit: positiveInt.default(10).openapi({ example: 10 }), }); export const searchBookInfoByIdPathSchema = z.object({ - id: positiveInt, + id: positiveInt, }); export const searchAllBooksQuerySchema = commonQuerySchema; export const searchBookInfoCreateQuerySchema = z.object({ - isbnQuery: z.string().openapi({ example: '9791191114225' }), + isbnQuery: z.string().openapi({ example: '9791191114225' }), }); export const createBookBodySchema = z.object({ - title: z.string(), - isbn: z.string(), - author: z.string(), - publisher: z.string(), - image: z.string(), - categoryId: z.string(), - pubdate: z.string(), - donator: z.string(), + title: z.string(), + isbn: z.string(), + author: z.string(), + publisher: z.string(), + image: z.string(), + categoryId: z.string(), + pubdate: z.string(), + donator: z.string(), }); export const searchBookByIdParamSchema = z.object({ - id: positiveInt, + id: positiveInt, }); export const updateBookBodySchema = z.object({ - bookInfoId: positiveInt.optional(), - title: z.string().optional(), - author: z.string().optional(), - publisher: z.string().optional(), - publishedAt: z.string().optional(), - image: z.string().optional(), - categoryId: positiveInt.optional(), - bookId: positiveInt.optional(), - callSign: z.string().optional(), - status: statusSchema.optional(), + bookInfoId: positiveInt.optional(), + title: z.string().optional(), + author: z.string().optional(), + publisher: z.string().optional(), + publishedAt: z.string().optional(), + image: z.string().optional(), + categoryId: positiveInt.optional(), + bookId: positiveInt.optional(), + callSign: z.string().optional(), + status: statusSchema.optional(), }); export const updateDonatorBodySchema = z.object({ - bookId: positiveInt, - nickname: z.string(), + bookId: positiveInt, + nickname: z.string(), }); export const bookInfoSchema = z.object({ - id: positiveInt, - title: z.string(), - author: z.string(), - publisher: z.string(), - isbn: z.string(), - image: z.string(), - category: z.string(), - publishedAt: z.string(), - createdAt: dateLike, - updatedAt: dateLike, + id: positiveInt, + title: z.string(), + author: z.string(), + publisher: z.string(), + isbn: z.string(), + image: z.string(), + category: z.string(), + publishedAt: z.string(), + createdAt: dateLike, + updatedAt: dateLike, }); export const searchBookInfosResponseSchema = metaPaginatedSchema( - bookInfoSchema - .extend({ - publishedAt: dateLike, - }) - .omit({ - publisher: true - }) - ) - .extend({ - categories: z.array( - z.object({ - name: z.string(), - count: positiveInt, - }), - ), + bookInfoSchema + .extend({ + publishedAt: dateLike, + }) + .omit({ + publisher: true, + }), +).extend({ + categories: z.array( + z.object({ + name: z.string(), + count: positiveInt, + }), + ), }); export const searchBookInfosSortedResponseSchema = z.object({ - items: z.array( - bookInfoSchema.extend({ - publishedAt: dateLike, - lendingCnt: positiveInt, - }), - ) + items: z.array( + bookInfoSchema.extend({ + publishedAt: dateLike, + lendingCnt: positiveInt, + }), + ), }); export const searchBookInfoByIdResponseSchema = bookInfoSchema.extend({ - books: z.array( - z.object({ - id: positiveInt, - callSign: z.string(), - donator: z.string(), - status: statusSchema, - dueDate: dateLike, - isLendable: positiveInt, - isReserved: positiveInt, - }), - ), -}); - -export const searchAllBooksResponseSchema = - metaPaginatedSchema( - z.object({ - bookId: positiveInt.openapi({ example: 1 }), - bookInfoId: positiveInt.openapi({ example: 1 }), - title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }), - author: z.string().openapi({ example: '드미트리 지노비에프' }), - donator: z.string().openapi({ example: 'mingkang' }), - publisher: z.string().openapi({ example: '길벗' }), - publishedAt: z.string().openapi({ example: '20170714' }), - isbn: z.string().openapi({ example: '9791160502152' }), - image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg' }), - status: statusSchema.openapi({ example: 3 }), - categoryId: positiveInt.openapi({ example: 8 }), - callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), - category: z.string().openapi({ example: '데이터 분석/AI/ML' }), - isLendable: positiveInt.openapi({ example: 0 }), - }) + books: z.array( + z.object({ + id: positiveInt, + callSign: z.string(), + donator: z.string(), + status: statusSchema, + dueDate: dateLike, + isLendable: positiveInt, + isReserved: positiveInt, + }), + ), +}); + +export const searchAllBooksResponseSchema = metaPaginatedSchema( + z.object({ + bookId: positiveInt.openapi({ example: 1 }), + bookInfoId: positiveInt.openapi({ example: 1 }), + title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }), + author: z.string().openapi({ example: '드미트리 지노비에프' }), + donator: z.string().openapi({ example: 'mingkang' }), + publisher: z.string().openapi({ example: '길벗' }), + publishedAt: z.string().openapi({ example: '20170714' }), + isbn: z.string().openapi({ example: '9791160502152' }), + image: z.string().openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg', + }), + status: statusSchema.openapi({ example: 3 }), + categoryId: positiveInt.openapi({ example: 8 }), + callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), + category: z.string().openapi({ example: '데이터 분석/AI/ML' }), + isLendable: positiveInt.openapi({ example: 0 }), + }), ); export const searchBookInfoCreateResponseSchema = z.object({ - bookInfo: z.object({ - title: z.string().openapi({ example: '작별인사' }), - image: z.string().openapi({ example: 'http://image.kyobobook.co.kr/images/book/xlarge/225/x9791191114225.jpg' }), - author: z.string().openapi({ example: '지은이: 김영하' }), - category: z.string().openapi({ example: '8' }), - isbn: z.string().openapi({ example: '9791191114225' }), - publisher: z.string().openapi({ example: '복복서가' }), - pubdate: z.string().openapi({ example: '20220502' }), - }), -}) + bookInfo: z.object({ + title: z.string().openapi({ example: '작별인사' }), + image: z.string().openapi({ + example: 'http://image.kyobobook.co.kr/images/book/xlarge/225/x9791191114225.jpg', + }), + author: z.string().openapi({ example: '지은이: 김영하' }), + category: z.string().openapi({ example: '8' }), + isbn: z.string().openapi({ example: '9791191114225' }), + publisher: z.string().openapi({ example: '복복서가' }), + pubdate: z.string().openapi({ example: '20220502' }), + }), +}); export const searchBookByIdResponseSchema = z.object({ - id: positiveInt.openapi({ example: 3 }), - bookId: positiveInt.openapi({ example: 3 }), - bookInfoId: positiveInt.openapi({ example: 2}), - title: z.string().openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }), - author: z.string().openapi({ example: '김선우' }), - donator: z.string().openapi({ example: 'mingkang' }), - publisher: z.string().openapi({ example: '한빛아카데미' }), - publishedAt: z.string().openapi({ example: '20130730' }), - isbn: z.string().openapi({ example: '9788998756444' }), - image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg' }), - status: statusSchema.openapi({ example: 0 }), - categoryId: positiveInt.openapi({ example: 2}), - callSign: z.string().openapi({ example: 'C5.13.v1.c2' }), - category: z.string().openapi({ example: '네트워크' }), - isLendable: positiveInt.openapi({ example: 1 }), + id: positiveInt.openapi({ example: 3 }), + bookId: positiveInt.openapi({ example: 3 }), + bookInfoId: positiveInt.openapi({ example: 2 }), + title: z + .string() + .openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }), + author: z.string().openapi({ example: '김선우' }), + donator: z.string().openapi({ example: 'mingkang' }), + publisher: z.string().openapi({ example: '한빛아카데미' }), + publishedAt: z.string().openapi({ example: '20130730' }), + isbn: z.string().openapi({ example: '9788998756444' }), + image: z.string().openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg', + }), + status: statusSchema.openapi({ example: 0 }), + categoryId: positiveInt.openapi({ example: 2 }), + callSign: z.string().openapi({ example: 'C5.13.v1.c2' }), + category: z.string().openapi({ example: '네트워크' }), + isLendable: positiveInt.openapi({ example: 1 }), }); export const updateBookResponseSchema = z.literal('책 정보가 수정되었습니다.'); @@ -175,19 +188,31 @@ export const updateBookResponseSchema = z.literal('책 정보가 수정되었습 export const updateDonatorResponseSchema = z.literal('기부자 정보가 수정되었습니다.'); export const createBookResponseSchema = z.object({ - callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), + callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), }); -export const isbnNotFoundSchema = mkErrorMessageSchema('ISBN_NOT_FOUND').describe('국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.'); +export const isbnNotFoundSchema = mkErrorMessageSchema('ISBN_NOT_FOUND').describe( + '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.', +); -export const naverBookNotFoundSchema = mkErrorMessageSchema('NAVER_BOOK_NOT_FOUND').describe('네이버 책검색 API에서 ISBN 검색이 실패'); +export const naverBookNotFoundSchema = mkErrorMessageSchema('NAVER_BOOK_NOT_FOUND').describe( + '네이버 책검색 API에서 ISBN 검색이 실패', +); -export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe('예상치 못한 에러로 책 정보 insert에 실패함.'); +export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe( + '예상치 못한 에러로 책 정보 insert에 실패함.', +); -export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe('보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음'); +export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe( + '보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음', +); -export const pubdateFormatErrorSchema = mkErrorMessageSchema('PUBDATE_FORMAT_ERROR').describe('입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"'); +export const pubdateFormatErrorSchema = mkErrorMessageSchema('PUBDATE_FORMAT_ERROR').describe( + '입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"', +); -export const unknownPatchErrorSchema = mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.'); +export const unknownPatchErrorSchema = + mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.'); -export const nonDataErrorSchema = mkErrorMessageSchema('NO_DATA_ERROR').describe('DATA가 적어도 한 개는 필요.'); +export const nonDataErrorSchema = + mkErrorMessageSchema('NO_DATA_ERROR').describe('DATA가 적어도 한 개는 필요.'); diff --git a/contracts/src/index.ts b/contracts/src/index.ts index 57546710..f2ec5fa9 100644 --- a/contracts/src/index.ts +++ b/contracts/src/index.ts @@ -23,7 +23,7 @@ export const contract = c.router( // TODO(@nyj001012): 태그 서비스 작성 // tags: tagContract, // TODO(@scarf005): 유저 서비스 작성 -// users: usersContract, + // users: usersContract, }, { pathPrefix: '/api/v2', diff --git a/contracts/src/reviews/index.ts b/contracts/src/reviews/index.ts index 18830248..5a8a8749 100644 --- a/contracts/src/reviews/index.ts +++ b/contracts/src/reviews/index.ts @@ -1,13 +1,20 @@ import { initContract } from '@ts-rest/core'; import { z } from 'zod'; -import { bookInfoIdSchema, bookInfoNotFoundSchema, metaPaginatedSchema, offsetPaginatedSchema, paginatedSearchSchema, visibility } from '../shared'; +import { + bookInfoIdSchema, + bookInfoNotFoundSchema, + metaPaginatedSchema, + offsetPaginatedSchema, + paginatedSearchSchema, + visibility, +} from '../shared'; import { contentSchema, mutationDescription, reviewIdPathSchema, reviewNotFoundSchema, } from './schema'; -import { reviewSchema } from './schema' +import { reviewSchema } from './schema'; export * from './schema'; @@ -25,7 +32,7 @@ export const reviewsContract = c.router( }), description: '전체 도서 리뷰 목록을 조회합니다.', responses: { - 200: metaPaginatedSchema(reviewSchema) + 200: metaPaginatedSchema(reviewSchema), }, }, post: { diff --git a/contracts/src/reviews/schema.ts b/contracts/src/reviews/schema.ts index 24d006f4..b140102c 100644 --- a/contracts/src/reviews/schema.ts +++ b/contracts/src/reviews/schema.ts @@ -16,15 +16,21 @@ export const reviewNotFoundSchema = export const mutationDescription = (action: '수정' | '삭제') => `리뷰를 ${action}합니다. 작성자 또는 관리자만 ${action} 가능합니다.`; -export const sqlBool = z.number().int().gte(0).lte(1).transform(x => Boolean(x)).or(z.boolean()); +export const sqlBool = z + .number() + .int() + .gte(0) + .lte(1) + .transform((x) => Boolean(x)) + .or(z.boolean()); export const reviewSchema = z.object({ id: z.number().int(), userId: z.number().int(), nickname: z.string().nullable(), bookInfoId: z.number().int(), - createdAt: z.date().transform(x => x.toISOString()), + createdAt: z.date().transform((x) => x.toISOString()), title: z.string().nullable(), content: z.string(), disabled: sqlBool, -}) +}); diff --git a/contracts/src/shared.ts b/contracts/src/shared.ts index 2f188541..d0f96669 100644 --- a/contracts/src/shared.ts +++ b/contracts/src/shared.ts @@ -2,16 +2,18 @@ import { z } from './zodWithOpenapi'; export const positiveInt = z.coerce.number().int().nonnegative(); -export const dateLike = z.union([z.date(), z.string()]).transform(String) +export const dateLike = z.union([z.date(), z.string()]).transform(String); export const bookInfoIdSchema = positiveInt.describe('개별 도서 ID'); export enum enumStatus { - "ok", "lost", "damaged", "designate" + 'ok', + 'lost', + 'damaged', + 'designate', } export const statusSchema = z.nativeEnum(enumStatus); - /** * 오류 메시지를 통일된 형식으로 보여주는 zod 스키마를 생성합니다. * @@ -28,16 +30,16 @@ export const statusSchema = z.nativeEnum(enumStatus); export const mkErrorMessageSchema = (code: T) => z.object({ code: z.literal(code) as z.ZodLiteral }); -export const unauthorizedSchema = mkErrorMessageSchema('UNAUTHORIZED').describe( - '권한이 없습니다.', -); +export const unauthorizedSchema = mkErrorMessageSchema('UNAUTHORIZED').describe('권한이 없습니다.'); export const bookNotFoundSchema = mkErrorMessageSchema('BOOK_NOT_FOUND').describe('해당 도서가 존재하지 않습니다'); -export const bookInfoNotFoundSchema = mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다'); +export const bookInfoNotFoundSchema = + mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다'); -export const serverErrorSchema = mkErrorMessageSchema('SERVER_ERROR').describe('서버에서 오류가 발생했습니다.'); +export const serverErrorSchema = + mkErrorMessageSchema('SERVER_ERROR').describe('서버에서 오류가 발생했습니다.'); export const badRequestSchema = mkErrorMessageSchema('BAD_REQUEST').describe('잘못된 요청입니다.'); @@ -72,8 +74,10 @@ export const offsetPaginatedSchema = >(itemSchema: T) = hasPrevPage: z.boolean().optional().describe('이전 페이지가 존재하는지 여부'), }); -export const visibility = z.enum([ 'all', 'public', 'hidden' ]).default('public').describe('공개 상태'); - +export const visibility = z + .enum(['all', 'public', 'hidden']) + .default('public') + .describe('공개 상태'); export const paginationQuerySchema = z.object({ page: positiveInt.default(1).optional().openapi({ example: 1 }), diff --git a/contracts/src/stock/index.ts b/contracts/src/stock/index.ts index 981c5d60..31944410 100644 --- a/contracts/src/stock/index.ts +++ b/contracts/src/stock/index.ts @@ -1,36 +1,36 @@ import { initContract } from '@ts-rest/core'; import { - stockGetQuerySchema, - stockGetResponseSchema, - stockPatchBodySchema, - stockPatchResponseSchema + stockGetQuerySchema, + stockGetResponseSchema, + stockPatchBodySchema, + stockPatchResponseSchema, } from './schema'; import { bookNotFoundSchema } from '../shared'; const c = initContract(); export const stockContract = c.router( - { - get: { - method: 'GET', - path: '/search', - description: '책 재고 정보를 검색해 온다.', - query: stockGetQuerySchema, - responses: { - 200: stockGetResponseSchema, - // 특정한 에러케이스가 생각나지 않습니다. - }, - }, - patch: { - method: 'PATCH', - path: '/update', - description: '책 재고를 확인하고 수정일시를 업데이트한다.', - body: stockPatchBodySchema, - responses: { - 200: stockPatchResponseSchema, - 404: bookNotFoundSchema, - }, - }, - }, - { pathPrefix: '/stock'}, -); \ No newline at end of file + { + get: { + method: 'GET', + path: '/search', + description: '책 재고 정보를 검색해 온다.', + query: stockGetQuerySchema, + responses: { + 200: stockGetResponseSchema, + // 특정한 에러케이스가 생각나지 않습니다. + }, + }, + patch: { + method: 'PATCH', + path: '/update', + description: '책 재고를 확인하고 수정일시를 업데이트한다.', + body: stockPatchBodySchema, + responses: { + 200: stockPatchResponseSchema, + 404: bookNotFoundSchema, + }, + }, + }, + { pathPrefix: '/stock' }, +); diff --git a/contracts/src/stock/schema.ts b/contracts/src/stock/schema.ts index 5aa14ce7..3a427006 100644 --- a/contracts/src/stock/schema.ts +++ b/contracts/src/stock/schema.ts @@ -4,34 +4,34 @@ import { z } from '../zodWithOpenapi'; export const bookIdSchema = positiveInt.describe('업데이트 할 도서 ID'); export const stockPatchBodySchema = z.object({ - id: bookIdSchema.openapi({ example: 0 }), + id: bookIdSchema.openapi({ example: 0 }), }); export const stockPatchResponseSchema = z.literal('재고 상태가 업데이트되었습니다.'); export const stockGetQuerySchema = z.object({ - page: positiveInt.default(0), - limit: positiveInt.default(10), + page: positiveInt.default(0), + limit: positiveInt.default(10), }); export const stockGetResponseSchema = z.object({ - items: z.array( - z.object({ - bookId: positiveInt, - bookInfoId: positiveInt, - title: z.string(), - author: z.string(), - donator: z.string(), - publisher: z.string(), - publishedAt: dateLike, - isbn: z.string(), - image: z.string(), - status: positiveInt, - categoryId: positiveInt, - callSign: z.string(), - category: z.string(), - updatedAt: dateLike, - }), - ), - meta: metaSchema, + items: z.array( + z.object({ + bookId: positiveInt, + bookInfoId: positiveInt, + title: z.string(), + author: z.string(), + donator: z.string(), + publisher: z.string(), + publishedAt: dateLike, + isbn: z.string(), + image: z.string(), + status: positiveInt, + categoryId: positiveInt, + callSign: z.string(), + category: z.string(), + updatedAt: dateLike, + }), + ), + meta: metaSchema, }); diff --git a/contracts/src/tags/index.ts b/contracts/src/tags/index.ts index 76c218f3..0c874bea 100644 --- a/contracts/src/tags/index.ts +++ b/contracts/src/tags/index.ts @@ -20,11 +20,7 @@ import { duplicateTagSchema, tagIdSchema, } from './schema'; -import { - bookInfoIdSchema, - bookInfoNotFoundSchema, - paginationQuerySchema, -} from '../shared'; +import { bookInfoIdSchema, bookInfoNotFoundSchema, paginationQuerySchema } from '../shared'; const c = initContract(); @@ -44,7 +40,8 @@ export const tagContract = c.router( method: 'GET', path: '/main', summary: '메인 페이지에서 사용할 태그 목록을 가져온다.', - description: '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 랜덤한 순서로 가져온다. 이는 메인 페이지에서 사용된다.', + description: + '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 랜덤한 순서로 가져온다. 이는 메인 페이지에서 사용된다.', query: paginationQuerySchema.omit({ page: true }), responses: { 200: superDefaultTagResponseSchema, @@ -54,7 +51,8 @@ export const tagContract = c.router( method: 'GET', path: '/{superTagId}/sub', summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 병합 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', + description: + 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 병합 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', pathParams: superTagIdQuerySchema, responses: { 200: subTagResponseSchema, @@ -64,7 +62,8 @@ export const tagContract = c.router( method: 'GET', path: '/manage/{superTagId}/sub', summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 관리 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', + description: + 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 관리 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', pathParams: superTagIdQuerySchema, responses: { 200: subTagResponseSchema, @@ -74,7 +73,8 @@ export const tagContract = c.router( method: 'GET', path: '/{bookInfoId}', summary: '도서에 등록된 슈퍼 태그, 디폴트 태그 목록을 가져온다.', - description: '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 가져온다. 이는 도서 상세 페이지 및 태그 병합 페이지에서 사용된다.', + description: + '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 가져온다. 이는 도서 상세 페이지 및 태그 병합 페이지에서 사용된다.', pathParams: bookInfoIdSchema, responses: { 200: tagsOfBookResponseSchema, diff --git a/contracts/src/tags/schema.ts b/contracts/src/tags/schema.ts index 5ed6db43..a3a43ce2 100644 --- a/contracts/src/tags/schema.ts +++ b/contracts/src/tags/schema.ts @@ -1,6 +1,4 @@ -import { - dateLike, metaSchema, mkErrorMessageSchema, positiveInt, -} from '../shared'; +import { dateLike, metaSchema, mkErrorMessageSchema, positiveInt } from '../shared'; import { z } from '../zodWithOpenapi'; export const subDefaultTagQuerySchema = z.object({ @@ -135,14 +133,15 @@ export const modifySuperTagBodySchema = z.object({ export const modifyTagResponseSchema = z.literal('success'); -export const incorrectTagFormatSchema = mkErrorMessageSchema('INCORRECT_TAG_FORMAT') - .describe('태그 형식이 올바르지 않습니다.'); +export const incorrectTagFormatSchema = + mkErrorMessageSchema('INCORRECT_TAG_FORMAT').describe('태그 형식이 올바르지 않습니다.'); -export const alreadyExistTagSchema = mkErrorMessageSchema('ALREADY_EXIST_TAG') - .describe('이미 존재하는 태그입니다.'); +export const alreadyExistTagSchema = + mkErrorMessageSchema('ALREADY_EXIST_TAG').describe('이미 존재하는 태그입니다.'); -export const defaultTagCannotBeModifiedSchema = mkErrorMessageSchema('DEFAULT_TAG_CANNOT_BE_MODIFIED') - .describe('디폴트 태그는 수정할 수 없습니다.'); +export const defaultTagCannotBeModifiedSchema = mkErrorMessageSchema( + 'DEFAULT_TAG_CANNOT_BE_MODIFIED', +).describe('디폴트 태그는 수정할 수 없습니다.'); export const modifySubTagBodySchema = z.object({ id: positiveInt.openapi({ @@ -159,8 +158,9 @@ export const modifySubTagBodySchema = z.object({ }), }); -export const NoAuthorityToModifyTagSchema = mkErrorMessageSchema('NO_AUTHORITY_TO_MODIFY_TAG') - .describe('태그를 수정할 권한이 없습니다.'); +export const NoAuthorityToModifyTagSchema = mkErrorMessageSchema( + 'NO_AUTHORITY_TO_MODIFY_TAG', +).describe('태그를 수정할 권한이 없습니다.'); export const mergeTagsBodySchema = z.object({ superTagId: positiveInt.nullable().openapi({ @@ -173,8 +173,8 @@ export const mergeTagsBodySchema = z.object({ }), }); -export const invalidTagIdSchema = mkErrorMessageSchema('INVALID_TAG_ID') - .describe('태그 id가 올바르지 않습니다.'); +export const invalidTagIdSchema = + mkErrorMessageSchema('INVALID_TAG_ID').describe('태그 id가 올바르지 않습니다.'); export const createTagBodySchema = z.object({ bookInfoId: positiveInt.openapi({ @@ -187,8 +187,8 @@ export const createTagBodySchema = z.object({ }), }); -export const duplicateTagSchema = mkErrorMessageSchema('DUPLICATE_TAG') - .describe('이미 존재하는 태그입니다.'); +export const duplicateTagSchema = + mkErrorMessageSchema('DUPLICATE_TAG').describe('이미 존재하는 태그입니다.'); export const tagIdSchema = z.object({ tagId: positiveInt.openapi({ diff --git a/contracts/src/users/schema.ts b/contracts/src/users/schema.ts index 2702de25..b343ad7e 100644 --- a/contracts/src/users/schema.ts +++ b/contracts/src/users/schema.ts @@ -8,29 +8,53 @@ export const searchUserSchema = z.object({ id: positiveInt.optional().describe('검색할 유저의 id'), }); -const reservationSchema = z.object({ - reservationId: positiveInt.describe('예약 번호').openapi({ example: 17 }), - reservedBookInfoId: positiveInt.describe('예약된 도서 번호').openapi({ example: 34 }), - endAt: z.coerce.string().nullable().describe('예약 만료 날짜').openapi({ example: '2023-08-16' }), - ranking: z.coerce.string().nullable().describe('예약 순위').openapi({ example: '1' }), - title: z.string().describe('예약된 도서 제목').openapi({ example: '생활코딩! Node.js 노드제이에스 프로그래밍(위키북스 러닝스쿨 시리즈)' }), - author: z.string().describe('예약된 도서 저자').openapi({ example: '이고잉' }), - image: z.string().describe('예약된 도서 이미지').openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/383/x9791158392383.jpg' }), - userId: positiveInt.describe('예약한 유저 번호').openapi({ example: 1547 }), -}).optional(); +const reservationSchema = z + .object({ + reservationId: positiveInt.describe('예약 번호').openapi({ example: 17 }), + reservedBookInfoId: positiveInt.describe('예약된 도서 번호').openapi({ example: 34 }), + endAt: z.coerce + .string() + .nullable() + .describe('예약 만료 날짜') + .openapi({ example: '2023-08-16' }), + ranking: z.coerce.string().nullable().describe('예약 순위').openapi({ example: '1' }), + title: z + .string() + .describe('예약된 도서 제목') + .openapi({ example: '생활코딩! Node.js 노드제이에스 프로그래밍(위키북스 러닝스쿨 시리즈)' }), + author: z.string().describe('예약된 도서 저자').openapi({ example: '이고잉' }), + image: z.string().describe('예약된 도서 이미지').openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/383/x9791158392383.jpg', + }), + userId: positiveInt.describe('예약한 유저 번호').openapi({ example: 1547 }), + }) + .optional(); -const lendingSchema = z.object({ - userId: positiveInt.describe('대출한 유저 번호').openapi({ example: 1547 }), - bookInfoId: positiveInt.describe('대출한 도서 info id').openapi({ example: 20 }), - lendDate: z.coerce.string().describe('대출 날짜').openapi({ example: '2023-08-08T20:20:55.000Z' }), - lendingCondition: z.string().describe('대출 상태').openapi({ example: '이상 없음' }), - image: z.string().describe('대출한 도서 이미지').openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/642/x9791185585642.jpg' }), - author: z.string().describe('대출한 도서 저자').openapi({ example: '어제이 애그러월, 조슈아 갠스, 아비 골드파브' }), - title: z.string().describe('대출한 도서 제목').openapi({ example: '예측 기계' }), - duedate: z.coerce.string().describe('반납 예정 날짜').openapi({ example: '2023-08-22T20:20:55.000Z' }), - overDueDay: positiveInt.describe('연체된 날 수').openapi({ example: 0 }), - reservedNum: z.string().describe('예약된 수').openapi({ example: '0' }), -}).optional(); +const lendingSchema = z + .object({ + userId: positiveInt.describe('대출한 유저 번호').openapi({ example: 1547 }), + bookInfoId: positiveInt.describe('대출한 도서 info id').openapi({ example: 20 }), + lendDate: z.coerce + .string() + .describe('대출 날짜') + .openapi({ example: '2023-08-08T20:20:55.000Z' }), + lendingCondition: z.string().describe('대출 상태').openapi({ example: '이상 없음' }), + image: z.string().describe('대출한 도서 이미지').openapi({ + example: 'https://image.kyobobook.co.kr/images/book/xlarge/642/x9791185585642.jpg', + }), + author: z + .string() + .describe('대출한 도서 저자') + .openapi({ example: '어제이 애그러월, 조슈아 갠스, 아비 골드파브' }), + title: z.string().describe('대출한 도서 제목').openapi({ example: '예측 기계' }), + duedate: z.coerce + .string() + .describe('반납 예정 날짜') + .openapi({ example: '2023-08-22T20:20:55.000Z' }), + overDueDay: positiveInt.describe('연체된 날 수').openapi({ example: 0 }), + reservedNum: z.string().describe('예약된 수').openapi({ example: '0' }), + }) + .optional(); const searchUserResponseItemSchema = z.object({ id: positiveInt.describe('유저 번호').openapi({ example: 1 }), @@ -38,8 +62,16 @@ const searchUserResponseItemSchema = z.object({ nickname: z.string().describe('닉네임').openapi({ example: 'kyungsle' }), intraId: positiveInt.describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), - penaltyEndDate: z.coerce.string().optional().describe('연체 패널티 끝나는 날짜').openapi({ example: '2022-05-22' }), - overDueDay: z.coerce.string().default('0').describe('현재 연체된 날 수').openapi({ example: '0' }), + penaltyEndDate: z.coerce + .string() + .optional() + .describe('연체 패널티 끝나는 날짜') + .openapi({ example: '2022-05-22' }), + overDueDay: z.coerce + .string() + .default('0') + .describe('현재 연체된 날 수') + .openapi({ example: '0' }), role: positiveInt.describe('유저 권한').openapi({ example: 2 }), reservations: z.array(reservationSchema).describe('해당 유저의 예약 정보'), lendings: z.array(lendingSchema).describe('해당 유저의 대출 정보'), @@ -66,11 +98,20 @@ export const updateUserSchema = z.object({ intraId: positiveInt.optional().describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().optional().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), role: positiveInt.optional().describe('유저 권한').openapi({ example: 2 }), - penaltyEndDate: z.coerce.string().optional().describe('연체 패널티 끝나는 날짜').openapi({ example: '2022-05-22' }), + penaltyEndDate: z.coerce + .string() + .optional() + .describe('연체 패널티 끝나는 날짜') + .openapi({ example: '2022-05-22' }), }); export const updatePrivateInfoSchema = z.object({ - email: z.string().email().optional().describe('이메일').openapi({ example: 'yena@student.42seoul.kr' }), + email: z + .string() + .email() + .optional() + .describe('이메일') + .openapi({ example: 'yena@student.42seoul.kr' }), password: z.string().optional().describe('패스워드').openapi({ example: 'KingGodMajesty42' }), }); diff --git a/package.json b/package.json index d47d4b24..0f5774cd 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ }, "dependencies": { "typescript": "5.1.6", - "zod": "^3.22.2", - "@ts-rest/core": "^3.28.0" + "@ts-rest/core": "^3.28.0", + "zod": "^3.22.2" }, "devDependencies": { "@types/node": "18.16.1", - "rome": "^12.1.3" + "eslint-config-prettier": "^9.0.0", + "rome": "^12.1.3", + "typescript": "5.1.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3abb761b..b45856f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@types/node': specifier: 18.16.1 version: 18.16.1 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.45.0) rome: specifier: ^12.1.3 version: 12.1.3 @@ -3045,6 +3048,15 @@ packages: semver: 6.3.0 dev: true + /eslint-config-prettier@9.0.0(eslint@8.45.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.45.0 + dev: true + /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: From c88ca8da4608489cc6c745a9fcd2c2af276dbe0a Mon Sep 17 00:00:00 2001 From: gilee Date: Fri, 8 Sep 2023 17:02:13 +0900 Subject: [PATCH 04/10] [fix] backend dockerfile error (#764) Co-authored-by: kylee --- Dockerfile | 6 ++++++ pnpm-lock.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 016bd4a0..56dbef19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,12 @@ FROM node:18-alpine as pnpm-installed # https://github.com/pnpm/pnpm/issues/4495#issuecomment-1317831712 ENV PNPM_HOME="/root/.local/share/pnpm" ENV PATH="${PATH}:${PNPM_HOME}" +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python +RUN python3 -m ensurepip +RUN pip3 install --no-cache --upgrade pip setuptools +RUN apk add --no-cache make +RUN apk add build-base RUN npm install --global pnpm RUN pnpm config set store-dir .pnpm-store RUN pnpm install --global node-pre-gyp diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b45856f0..b98bc8e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true From 15654412a5f9da964ad1f653d5650bdec8afd9fb Mon Sep 17 00:00:00 2001 From: scarf Date: Mon, 11 Sep 2023 09:49:20 +0900 Subject: [PATCH 05/10] fix: `positiveInt` -> `nonNegativeInt` (#766) --- contracts/src/books/schema.ts | 50 +++++++++++++++---------------- contracts/src/histories/schema.ts | 16 +++++----- contracts/src/likes/index.ts | 4 +-- contracts/src/likes/schema.ts | 4 +-- contracts/src/reviews/schema.ts | 4 +-- contracts/src/shared.ts | 12 ++++---- contracts/src/stock/schema.ts | 16 +++++----- contracts/src/tags/schema.ts | 32 ++++++++++---------- contracts/src/users/schema.ts | 32 ++++++++++---------- 9 files changed, 85 insertions(+), 85 deletions(-) diff --git a/contracts/src/books/schema.ts b/contracts/src/books/schema.ts index 6eccc417..abfa2895 100644 --- a/contracts/src/books/schema.ts +++ b/contracts/src/books/schema.ts @@ -1,6 +1,6 @@ import { metaSchema, - positiveInt, + nonNegativeInt, mkErrorMessageSchema, statusSchema, metaPaginatedSchema, @@ -10,8 +10,8 @@ import { z } from '../zodWithOpenapi'; export const commonQuerySchema = z.object({ query: z.string().optional(), - page: positiveInt.default(0).openapi({ example: 0 }), - limit: positiveInt.default(10).openapi({ example: 10 }), + page: nonNegativeInt.default(0).openapi({ example: 0 }), + limit: nonNegativeInt.default(10).openapi({ example: 10 }), }); export const searchAllBookInfosQuerySchema = commonQuerySchema.extend({ @@ -27,11 +27,11 @@ export const searchBookInfosByTagQuerySchema = commonQuerySchema.extend({ export const searchBookInfosSortedQuerySchema = z.object({ sort: z.enum(['new', 'popular']), - limit: positiveInt.default(10).openapi({ example: 10 }), + limit: nonNegativeInt.default(10).openapi({ example: 10 }), }); export const searchBookInfoByIdPathSchema = z.object({ - id: positiveInt, + id: nonNegativeInt, }); export const searchAllBooksQuerySchema = commonQuerySchema; @@ -52,29 +52,29 @@ export const createBookBodySchema = z.object({ }); export const searchBookByIdParamSchema = z.object({ - id: positiveInt, + id: nonNegativeInt, }); export const updateBookBodySchema = z.object({ - bookInfoId: positiveInt.optional(), + bookInfoId: nonNegativeInt.optional(), title: z.string().optional(), author: z.string().optional(), publisher: z.string().optional(), publishedAt: z.string().optional(), image: z.string().optional(), - categoryId: positiveInt.optional(), - bookId: positiveInt.optional(), + categoryId: nonNegativeInt.optional(), + bookId: nonNegativeInt.optional(), callSign: z.string().optional(), status: statusSchema.optional(), }); export const updateDonatorBodySchema = z.object({ - bookId: positiveInt, + bookId: nonNegativeInt, nickname: z.string(), }); export const bookInfoSchema = z.object({ - id: positiveInt, + id: nonNegativeInt, title: z.string(), author: z.string(), publisher: z.string(), @@ -98,7 +98,7 @@ export const searchBookInfosResponseSchema = metaPaginatedSchema( categories: z.array( z.object({ name: z.string(), - count: positiveInt, + count: nonNegativeInt, }), ), }); @@ -107,7 +107,7 @@ export const searchBookInfosSortedResponseSchema = z.object({ items: z.array( bookInfoSchema.extend({ publishedAt: dateLike, - lendingCnt: positiveInt, + lendingCnt: nonNegativeInt, }), ), }); @@ -115,21 +115,21 @@ export const searchBookInfosSortedResponseSchema = z.object({ export const searchBookInfoByIdResponseSchema = bookInfoSchema.extend({ books: z.array( z.object({ - id: positiveInt, + id: nonNegativeInt, callSign: z.string(), donator: z.string(), status: statusSchema, dueDate: dateLike, - isLendable: positiveInt, - isReserved: positiveInt, + isLendable: nonNegativeInt, + isReserved: nonNegativeInt, }), ), }); export const searchAllBooksResponseSchema = metaPaginatedSchema( z.object({ - bookId: positiveInt.openapi({ example: 1 }), - bookInfoId: positiveInt.openapi({ example: 1 }), + bookId: nonNegativeInt.openapi({ example: 1 }), + bookInfoId: nonNegativeInt.openapi({ example: 1 }), title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }), author: z.string().openapi({ example: '드미트리 지노비에프' }), donator: z.string().openapi({ example: 'mingkang' }), @@ -140,10 +140,10 @@ export const searchAllBooksResponseSchema = metaPaginatedSchema( example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg', }), status: statusSchema.openapi({ example: 3 }), - categoryId: positiveInt.openapi({ example: 8 }), + categoryId: nonNegativeInt.openapi({ example: 8 }), callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), category: z.string().openapi({ example: '데이터 분석/AI/ML' }), - isLendable: positiveInt.openapi({ example: 0 }), + isLendable: nonNegativeInt.openapi({ example: 0 }), }), ); @@ -162,9 +162,9 @@ export const searchBookInfoCreateResponseSchema = z.object({ }); export const searchBookByIdResponseSchema = z.object({ - id: positiveInt.openapi({ example: 3 }), - bookId: positiveInt.openapi({ example: 3 }), - bookInfoId: positiveInt.openapi({ example: 2 }), + id: nonNegativeInt.openapi({ example: 3 }), + bookId: nonNegativeInt.openapi({ example: 3 }), + bookInfoId: nonNegativeInt.openapi({ example: 2 }), title: z .string() .openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }), @@ -177,10 +177,10 @@ export const searchBookByIdResponseSchema = z.object({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg', }), status: statusSchema.openapi({ example: 0 }), - categoryId: positiveInt.openapi({ example: 2 }), + categoryId: nonNegativeInt.openapi({ example: 2 }), callSign: z.string().openapi({ example: 'C5.13.v1.c2' }), category: z.string().openapi({ example: '네트워크' }), - isLendable: positiveInt.openapi({ example: 1 }), + isLendable: nonNegativeInt.openapi({ example: 1 }), }); export const updateBookResponseSchema = z.literal('책 정보가 수정되었습니다.'); diff --git a/contracts/src/histories/schema.ts b/contracts/src/histories/schema.ts index a671cc38..5f645fbd 100644 --- a/contracts/src/histories/schema.ts +++ b/contracts/src/histories/schema.ts @@ -1,30 +1,30 @@ -import { dateLike, metaSchema, positiveInt } from '../shared'; +import { dateLike, metaSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; export const historiesGetMyQuerySchema = z.object({ query: z.string().optional(), - page: z.number().int().nonnegative().default(0), - limit: z.number().int().nonnegative().default(10), + page: nonNegativeInt.default(0), + limit: nonNegativeInt.default(10), }); export const historiesGetQuerySchema = z.object({ query: z.string().optional(), type: z.enum(['user', 'title', 'callsign']).optional(), - page: z.number().int().nonnegative().default(0), - limit: z.number().int().nonnegative().default(10), + page: nonNegativeInt.default(0), + limit: nonNegativeInt.default(10), }); export const historiesGetResponseSchema = z.object({ items: z.array( z.object({ - id: positiveInt, + id: nonNegativeInt, lendingCondition: z.string(), login: z.string(), returningCondition: z.string(), - penaltyDays: z.number().int().nonnegative(), + penaltyDays: nonNegativeInt, callSign: z.string(), title: z.string(), - bookInfoId: positiveInt, + bookInfoId: nonNegativeInt, image: z.string(), createdAt: dateLike, returnedAt: dateLike, diff --git a/contracts/src/likes/index.ts b/contracts/src/likes/index.ts index 5df56fa0..e247a504 100644 --- a/contracts/src/likes/index.ts +++ b/contracts/src/likes/index.ts @@ -1,5 +1,5 @@ import { initContract } from '@ts-rest/core'; -import { bookInfoIdSchema, bookInfoNotFoundSchema, positiveInt } from '../shared'; +import { bookInfoIdSchema, bookInfoNotFoundSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; import { likeNotFoundSchema, likeResponseSchema } from './schema'; @@ -15,7 +15,7 @@ export const likesContract = c.router( body: null, responses: { 200: z.object({ - userId: positiveInt, + userId: nonNegativeInt, bookInfoId: bookInfoIdSchema, }), }, diff --git a/contracts/src/likes/schema.ts b/contracts/src/likes/schema.ts index c5338101..e611f9a9 100644 --- a/contracts/src/likes/schema.ts +++ b/contracts/src/likes/schema.ts @@ -1,5 +1,5 @@ import z from 'zod'; -import { bookInfoIdSchema, positiveInt } from '../shared'; +import { bookInfoIdSchema, nonNegativeInt } from '../shared'; export const likeNotFoundSchema = z.object({ code: z.literal('LIKE_NOT_FOUND'), @@ -10,7 +10,7 @@ export const likeResponseSchema = z .object({ bookInfoId: bookInfoIdSchema, isLiked: z.boolean(), - likeNum: positiveInt, + likeNum: nonNegativeInt, }) .openapi({ examples: [ diff --git a/contracts/src/reviews/schema.ts b/contracts/src/reviews/schema.ts index b140102c..fcbdc82b 100644 --- a/contracts/src/reviews/schema.ts +++ b/contracts/src/reviews/schema.ts @@ -1,7 +1,7 @@ -import { mkErrorMessageSchema, positiveInt } from '../shared'; +import { mkErrorMessageSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; -export const reviewsIdSchema = positiveInt.describe('도서 리뷰 ID'); +export const reviewsIdSchema = nonNegativeInt.describe('도서 리뷰 ID'); export const contentSchema = z.object({ content: z.string().min(10).max(420).openapi({ example: '책 정말 재미있어요 10글자 넘었다' }), diff --git a/contracts/src/shared.ts b/contracts/src/shared.ts index d0f96669..dacd0031 100644 --- a/contracts/src/shared.ts +++ b/contracts/src/shared.ts @@ -1,10 +1,10 @@ import { z } from './zodWithOpenapi'; -export const positiveInt = z.coerce.number().int().nonnegative(); +export const nonNegativeInt = z.coerce.number().int().nonnegative(); export const dateLike = z.union([z.date(), z.string()]).transform(String); -export const bookInfoIdSchema = positiveInt.describe('개별 도서 ID'); +export const bookInfoIdSchema = nonNegativeInt.describe('개별 도서 ID'); export enum enumStatus { 'ok', @@ -46,8 +46,8 @@ export const badRequestSchema = mkErrorMessageSchema('BAD_REQUEST').describe(' export const forbiddenSchema = mkErrorMessageSchema('FORBIDDEN').describe('권한이 없습니다.'); export const metaSchema = z.object({ - totalItems: positiveInt.describe('전체 검색 결과 수 ').openapi({ example: 42 }), - totalPages: positiveInt.describe('전체 결과 페이지 수').openapi({ example: 5 }), + totalItems: nonNegativeInt.describe('전체 검색 결과 수 ').openapi({ example: 42 }), + totalPages: nonNegativeInt.describe('전체 결과 페이지 수').openapi({ example: 5 }), // itemCount: positiveInt.describe('현재 페이지의 검색 결과 수').openapi({ example: 3 }), // itemsPerPage: positiveInt.describe('한 페이지당 검색 결과 수').openapi({ example: 10 }), // currentPage: positiveInt.describe('현재 페이지').openapi({ example: 1 }), @@ -80,6 +80,6 @@ export const visibility = z .describe('공개 상태'); export const paginationQuerySchema = z.object({ - page: positiveInt.default(1).optional().openapi({ example: 1 }), - limit: positiveInt.default(10).optional().openapi({ example: 10 }), + page: nonNegativeInt.default(1).optional().openapi({ example: 1 }), + limit: nonNegativeInt.default(10).optional().openapi({ example: 10 }), }); diff --git a/contracts/src/stock/schema.ts b/contracts/src/stock/schema.ts index 3a427006..e4921cca 100644 --- a/contracts/src/stock/schema.ts +++ b/contracts/src/stock/schema.ts @@ -1,7 +1,7 @@ -import { dateLike, metaSchema, positiveInt } from '../shared'; +import { dateLike, metaSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; -export const bookIdSchema = positiveInt.describe('업데이트 할 도서 ID'); +export const bookIdSchema = nonNegativeInt.describe('업데이트 할 도서 ID'); export const stockPatchBodySchema = z.object({ id: bookIdSchema.openapi({ example: 0 }), @@ -10,15 +10,15 @@ export const stockPatchBodySchema = z.object({ export const stockPatchResponseSchema = z.literal('재고 상태가 업데이트되었습니다.'); export const stockGetQuerySchema = z.object({ - page: positiveInt.default(0), - limit: positiveInt.default(10), + page: nonNegativeInt.default(0), + limit: nonNegativeInt.default(10), }); export const stockGetResponseSchema = z.object({ items: z.array( z.object({ - bookId: positiveInt, - bookInfoId: positiveInt, + bookId: nonNegativeInt, + bookInfoId: nonNegativeInt, title: z.string(), author: z.string(), donator: z.string(), @@ -26,8 +26,8 @@ export const stockGetResponseSchema = z.object({ publishedAt: dateLike, isbn: z.string(), image: z.string(), - status: positiveInt, - categoryId: positiveInt, + status: nonNegativeInt, + categoryId: nonNegativeInt, callSign: z.string(), category: z.string(), updatedAt: dateLike, diff --git a/contracts/src/tags/schema.ts b/contracts/src/tags/schema.ts index a3a43ce2..a4699500 100644 --- a/contracts/src/tags/schema.ts +++ b/contracts/src/tags/schema.ts @@ -1,9 +1,9 @@ -import { dateLike, metaSchema, mkErrorMessageSchema, positiveInt } from '../shared'; +import { dateLike, metaSchema, mkErrorMessageSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; export const subDefaultTagQuerySchema = z.object({ - page: positiveInt.optional().default(0), - limit: positiveInt.optional().default(10), + page: nonNegativeInt.optional().default(0), + limit: nonNegativeInt.optional().default(10), visibility: z.enum(['public', 'private']).optional(), query: z.string().optional().openapi({ example: '개발자의 코드' }), }); @@ -11,7 +11,7 @@ export const subDefaultTagQuerySchema = z.object({ export const subDefaultTagResponseSchema = z.object({ items: z.array( z.object({ - bookInfoId: positiveInt.openapi({ + bookInfoId: nonNegativeInt.openapi({ description: '태그가 등록된 도서의 info id', example: 1, }), @@ -19,7 +19,7 @@ export const subDefaultTagResponseSchema = z.object({ description: '태그가 등록된 도서의 제목', example: '개발자의 코드', }), - id: positiveInt.openapi({ + id: nonNegativeInt.openapi({ description: '태그 고유 id', example: 1, }), @@ -59,7 +59,7 @@ export const superDefaultTagResponseSchema = z.object({ description: '태그 내용', example: '1서클_추천_책', }), - count: positiveInt.openapi({ + count: nonNegativeInt.openapi({ description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', example: 1, }), @@ -72,14 +72,14 @@ export const superDefaultTagResponseSchema = z.object({ }); export const superTagIdQuerySchema = z.object({ - superTagId: positiveInt.openapi({ + superTagId: nonNegativeInt.openapi({ description: '슈퍼 태그의 id', example: 1, }), }); export const subTagResponseSchema = z.object({ - id: positiveInt.openapi({ + id: nonNegativeInt.openapi({ description: '태그 고유 id', example: 1, }), @@ -96,7 +96,7 @@ export const subTagResponseSchema = z.object({ export const tagsOfBookResponseSchema = z.object({ items: z.array( z.object({ - id: positiveInt.openapi({ + id: nonNegativeInt.openapi({ description: '태그 고유 id', example: 1, }), @@ -112,7 +112,7 @@ export const tagsOfBookResponseSchema = z.object({ description: '태그의 타입. 슈퍼 태그는 super, 디폴트 태그는 default', example: 'super', }), - count: positiveInt.openapi({ + count: nonNegativeInt.openapi({ description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', example: 1, }), @@ -121,7 +121,7 @@ export const tagsOfBookResponseSchema = z.object({ }); export const modifySuperTagBodySchema = z.object({ - id: positiveInt.openapi({ + id: nonNegativeInt.openapi({ description: '수정할 슈퍼 태그의 id', example: 1, }), @@ -144,7 +144,7 @@ export const defaultTagCannotBeModifiedSchema = mkErrorMessageSchema( ).describe('디폴트 태그는 수정할 수 없습니다.'); export const modifySubTagBodySchema = z.object({ - id: positiveInt.openapi({ + id: nonNegativeInt.openapi({ description: '수정할 서브 태그의 id', example: 1, }), @@ -163,11 +163,11 @@ export const NoAuthorityToModifyTagSchema = mkErrorMessageSchema( ).describe('태그를 수정할 권한이 없습니다.'); export const mergeTagsBodySchema = z.object({ - superTagId: positiveInt.nullable().openapi({ + superTagId: nonNegativeInt.nullable().openapi({ description: '병합할 슈퍼 태그의 id. null이면 디폴트 태그로 병합됨을 의미한다.', example: 1, }), - subTagIds: z.array(positiveInt).openapi({ + subTagIds: z.array(nonNegativeInt).openapi({ description: '병합할 서브 태그의 id 목록', example: [1, 2, 3], }), @@ -177,7 +177,7 @@ export const invalidTagIdSchema = mkErrorMessageSchema('INVALID_TAG_ID').describe('태그 id가 올바르지 않습니다.'); export const createTagBodySchema = z.object({ - bookInfoId: positiveInt.openapi({ + bookInfoId: nonNegativeInt.openapi({ description: '태그를 등록할 도서의 info id', example: 1, }), @@ -191,7 +191,7 @@ export const duplicateTagSchema = mkErrorMessageSchema('DUPLICATE_TAG').describe('이미 존재하는 태그입니다.'); export const tagIdSchema = z.object({ - tagId: positiveInt.openapi({ + tagId: nonNegativeInt.openapi({ description: '태그의 id', example: 1, }), diff --git a/contracts/src/users/schema.ts b/contracts/src/users/schema.ts index b343ad7e..5108a14b 100644 --- a/contracts/src/users/schema.ts +++ b/contracts/src/users/schema.ts @@ -1,17 +1,17 @@ -import { metaSchema, mkErrorMessageSchema, positiveInt } from '../shared'; +import { metaSchema, mkErrorMessageSchema, nonNegativeInt } from '../shared'; import { z } from '../zodWithOpenapi'; export const searchUserSchema = z.object({ nicknameOrEmail: z.string().optional().describe('검색할 유저의 nickname or email'), - page: positiveInt.optional().default(0).describe('페이지'), - limit: positiveInt.optional().default(10).describe('한 페이지에 들어올 검색결과 수'), - id: positiveInt.optional().describe('검색할 유저의 id'), + page: nonNegativeInt.optional().default(0).describe('페이지'), + limit: nonNegativeInt.optional().default(10).describe('한 페이지에 들어올 검색결과 수'), + id: nonNegativeInt.optional().describe('검색할 유저의 id'), }); const reservationSchema = z .object({ - reservationId: positiveInt.describe('예약 번호').openapi({ example: 17 }), - reservedBookInfoId: positiveInt.describe('예약된 도서 번호').openapi({ example: 34 }), + reservationId: nonNegativeInt.describe('예약 번호').openapi({ example: 17 }), + reservedBookInfoId: nonNegativeInt.describe('예약된 도서 번호').openapi({ example: 34 }), endAt: z.coerce .string() .nullable() @@ -26,14 +26,14 @@ const reservationSchema = z image: z.string().describe('예약된 도서 이미지').openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/383/x9791158392383.jpg', }), - userId: positiveInt.describe('예약한 유저 번호').openapi({ example: 1547 }), + userId: nonNegativeInt.describe('예약한 유저 번호').openapi({ example: 1547 }), }) .optional(); const lendingSchema = z .object({ - userId: positiveInt.describe('대출한 유저 번호').openapi({ example: 1547 }), - bookInfoId: positiveInt.describe('대출한 도서 info id').openapi({ example: 20 }), + userId: nonNegativeInt.describe('대출한 유저 번호').openapi({ example: 1547 }), + bookInfoId: nonNegativeInt.describe('대출한 도서 info id').openapi({ example: 20 }), lendDate: z.coerce .string() .describe('대출 날짜') @@ -51,16 +51,16 @@ const lendingSchema = z .string() .describe('반납 예정 날짜') .openapi({ example: '2023-08-22T20:20:55.000Z' }), - overDueDay: positiveInt.describe('연체된 날 수').openapi({ example: 0 }), + overDueDay: nonNegativeInt.describe('연체된 날 수').openapi({ example: 0 }), reservedNum: z.string().describe('예약된 수').openapi({ example: '0' }), }) .optional(); const searchUserResponseItemSchema = z.object({ - id: positiveInt.describe('유저 번호').openapi({ example: 1 }), + id: nonNegativeInt.describe('유저 번호').openapi({ example: 1 }), email: z.string().email().describe('이메일').openapi({ example: 'kyungsle@gmail.com' }), nickname: z.string().describe('닉네임').openapi({ example: 'kyungsle' }), - intraId: positiveInt.describe('인트라 고유 번호').openapi({ example: '10068' }), + intraId: nonNegativeInt.describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), penaltyEndDate: z.coerce .string() @@ -72,7 +72,7 @@ const searchUserResponseItemSchema = z.object({ .default('0') .describe('현재 연체된 날 수') .openapi({ example: '0' }), - role: positiveInt.describe('유저 권한').openapi({ example: 2 }), + role: nonNegativeInt.describe('유저 권한').openapi({ example: 2 }), reservations: z.array(reservationSchema).describe('해당 유저의 예약 정보'), lendings: z.array(lendingSchema).describe('해당 유저의 대출 정보'), }); @@ -90,14 +90,14 @@ export const createUserSchema = z.object({ export const createUserResponseSchema = z.literal('유저 생성 성공!'); export const userIdSchema = z.object({ - id: positiveInt.describe('유저 id 값').openapi({ example: 1 }), + id: nonNegativeInt.describe('유저 id 값').openapi({ example: 1 }), }); export const updateUserSchema = z.object({ nickname: z.string().optional().describe('닉네임').openapi({ example: 'kyungsle' }), - intraId: positiveInt.optional().describe('인트라 고유 번호').openapi({ example: '10068' }), + intraId: nonNegativeInt.optional().describe('인트라 고유 번호').openapi({ example: '10068' }), slack: z.string().optional().describe('slack 멤버 Id').openapi({ example: 'U035MUEUGKW' }), - role: positiveInt.optional().describe('유저 권한').openapi({ example: 2 }), + role: nonNegativeInt.optional().describe('유저 권한').openapi({ example: 2 }), penaltyEndDate: z.coerce .string() .optional() From 6cbb823e990591f1ab6cd6c9947388c4afb9d66d Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 14 Sep 2023 16:05:56 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20v2=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=95=EB=A6=AC=20=EC=A0=81=EC=9A=A9=20(#771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: `/stock` 제거 https://github.com/jiphyeonjeon-42/backend/discussions/767#discussioncomment-6986111 * refactor: contracts에서 500번대 오류 제거 백엔드에서 복구 불가능한 오류일 시 반환하기 때문에, 프론트엔드에서 따로 처리하는 것이 좋을 것 같습니다. * refactor: `/history` -> `/lendings` https://github.com/jiphyeonjeon-42/backend/discussions/767#discussioncomment-6986282 Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> * refactor: `/users ` 정리 https://github.com/jiphyeonjeon-42/backend/discussions/767#discussioncomment-6986194 Co-authored-by: honeyl3ee * refactor: `/tag` 서비스 임시 제거 고도화를 하기 위해서는 내부 구현을 바꾸어야 하는 문제가 있어 우선순위를 낮추었습니다. 주석처리를 할까 고민했으나 제거 이전 커밋(15654412a5f9da964ad1f653d5650bdec8afd9fb)으로 체크아웃시 전체 코드를 확인 가능하기 때문에 복잡도 감소를 위해 코드를 제거하였습니다. * refactor: `/books` 경로 정리 https://github.com/jiphyeonjeon-42/backend/discussions/767#discussioncomment-6986195 https://github.com/jiphyeonjeon-42/backend/discussions/767#discussioncomment-6986483 Co-authored-by: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> * feat: swagger에서 1줄 요약 표시 --------- Co-authored-by: jwoo <74581396+Jiwon-Woo@users.noreply.github.com> Co-authored-by: honeyl3ee Co-authored-by: Jeong Jihwan <47599349+JeongJiHwan@users.noreply.github.com> --- backend/src/v2/books/mod.ts | 188 ++++++++--------- backend/src/v2/{histories => lendings}/mod.ts | 6 +- .../v2/{histories => lendings}/repository.ts | 0 backend/src/v2/routes.ts | 8 +- backend/src/v2/stock/mod.ts | 22 -- backend/src/v2/stock/repository.ts | 18 -- backend/src/v2/stock/service.ts | 41 ---- contracts/src/books/index.ts | 136 +++--------- contracts/src/books/mod.ts | 71 +++++++ contracts/src/books/schema.ts | 68 ------ contracts/src/index.ts | 11 +- .../{histories/index.ts => lendings/mod.ts} | 14 +- .../src/{histories => lendings}/schema.ts | 0 contracts/src/likes/index.ts | 7 +- contracts/src/reviews/index.ts | 6 +- contracts/src/shared.ts | 3 - contracts/src/stock/index.ts | 36 ---- contracts/src/stock/schema.ts | 37 ---- contracts/src/tags/index.ts | 166 --------------- contracts/src/tags/schema.ts | 198 ------------------ contracts/src/users/index.ts | 37 +--- contracts/src/users/schema.ts | 12 -- 22 files changed, 217 insertions(+), 868 deletions(-) rename backend/src/v2/{histories => lendings}/mod.ts (93%) rename backend/src/v2/{histories => lendings}/repository.ts (100%) delete mode 100644 backend/src/v2/stock/mod.ts delete mode 100644 backend/src/v2/stock/repository.ts delete mode 100644 backend/src/v2/stock/service.ts create mode 100644 contracts/src/books/mod.ts rename contracts/src/{histories/index.ts => lendings/mod.ts} (69%) rename contracts/src/{histories => lendings}/schema.ts (100%) delete mode 100644 contracts/src/stock/index.ts delete mode 100644 contracts/src/stock/schema.ts delete mode 100644 contracts/src/tags/index.ts delete mode 100644 contracts/src/tags/schema.ts diff --git a/backend/src/v2/books/mod.ts b/backend/src/v2/books/mod.ts index dbc5f905..a7cccbd3 100644 --- a/backend/src/v2/books/mod.ts +++ b/backend/src/v2/books/mod.ts @@ -1,112 +1,106 @@ -import { contract } from '@jiphyeonjeon-42/contracts'; -import { initServer } from '@ts-rest/express'; -import { - searchAllBooks, - searchBookById, - searchBookInfoById, - searchBookInfoForCreate, - searchBookInfosByTag, - searchBookInfosSorted, - updateBookDonator, - updateBookOrBookInfo, -} from './service'; -import { - BookInfoNotFoundError, - BookNotFoundError, - bookInfoNotFound, - bookNotFound, - isbnNotFound, - naverBookNotFound, - pubdateFormatError, -} from '../shared'; -import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; -import authValidate from '~/v1/auth/auth.validate'; -import { roleSet } from '~/v1/auth/auth.type'; +// import { contract } from '@jiphyeonjeon-42/contracts'; +// import { initServer } from '@ts-rest/express'; +// import { +// searchAllBooks, +// searchBookById, +// searchBookInfoById, +// searchBookInfoForCreate, +// searchBookInfosByTag, +// searchBookInfosSorted, +// updateBookDonator, +// updateBookOrBookInfo, +// } from './service'; +// import { +// BookInfoNotFoundError, +// BookNotFoundError, +// bookInfoNotFound, +// bookNotFound, +// isbnNotFound, +// naverBookNotFound, +// pubdateFormatError, +// } from '../shared'; +// import { IsbnNotFoundError, NaverBookNotFound, PubdateFormatError } from './errors'; +// import authValidate from '~/v1/auth/auth.validate'; +// import { roleSet } from '~/v1/auth/auth.type'; -const s = initServer(); -export const books = s.router(contract.books, { - // searchAllBookInfos: async ({ query }) => { - // const result = await searchAllBookInfos(query); +// const s = initServer(); +// export const books = s.router(contract.books, { +// // searchAllBookInfos: async ({ query }) => { +// // const result = await searchAllBookInfos(query); - // return { status: 200, body: result } as const; - // }, - // @ts-expect-error - searchBookInfosByTag: async ({ query }) => { - const result = await searchBookInfosByTag(query); +// // return { status: 200, body: result } as const; +// // }, +// searchBookInfosByTag: async ({ query }) => { +// const result = await searchBookInfosByTag(query); - return { status: 200, body: result } as const; - }, - // @ts-expect-error - searchBookInfosSorted: async ({ query }) => { - const result = await searchBookInfosSorted(query); +// return { status: 200, body: result } as const; +// }, +// searchBookInfosSorted: async ({ query }) => { +// const result = await searchBookInfosSorted(query); - return { status: 200, body: result } as const; - }, - // @ts-expect-error - searchBookInfoById: async ({ params: { id } }) => { - const result = await searchBookInfoById(id); +// return { status: 200, body: result } as const; +// }, +// searchBookInfoById: async ({ params: { id } }) => { +// const result = await searchBookInfoById(id); - if (result instanceof BookInfoNotFoundError) return bookInfoNotFound; +// if (result instanceof BookInfoNotFoundError) return bookInfoNotFound; - return { status: 200, body: result } as const; - }, - // @ts-expect-error - searchAllBooks: async ({ query }) => { - const result = await searchAllBooks(query); +// return { status: 200, body: result } as const; +// }, +// searchAllBooks: async ({ query }) => { +// const result = await searchAllBooks(query); - return { status: 200, body: result } as const; - }, - searchBookInfoForCreate: { - // middleware: [authValidate(roleSet.librarian)], - // @ts-expect-error - handler: async ({ query: { isbnQuery } }) => { - const result = await searchBookInfoForCreate(isbnQuery); +// return { status: 200, body: result } as const; +// }, +// searchBookInfoForCreate: { +// // middleware: [authValidate(roleSet.librarian)], +// handler: async ({ query: { isbnQuery } }) => { +// const result = await searchBookInfoForCreate(isbnQuery); - if (result instanceof IsbnNotFoundError) return isbnNotFound; +// if (result instanceof IsbnNotFoundError) return isbnNotFound; - if (result instanceof NaverBookNotFound) return naverBookNotFound; +// if (result instanceof NaverBookNotFound) return naverBookNotFound; - return { status: 200, body: result } as const; - }, - }, - // @ts-expect-error - searchBookById: async ({ params: { id } }) => { - const result = await searchBookById({ id }); +// return { status: 200, body: result } as const; +// }, +// }, +// searchBookById: async ({ params: { id } }) => { +// const result = await searchBookById({ id }); - if (result instanceof BookNotFoundError) { - return bookNotFound; - } +// if (result instanceof BookNotFoundError) { +// return bookNotFound; +// } - return { - status: 200, - body: result, - } as const; - }, - // createBook: { - // middleware: [authValidate(roleSet.librarian)], - // handler: async ({ body }) => { +// return { +// status: 200, +// body: result, +// } as const; +// }, +// // createBook: { +// // middleware: [authValidate(roleSet.librarian)], +// // handler: async ({ body }) => { - // } - // }, - updateBook: { - // middleware: [authValidate(roleSet.librarian)], - // @ts-expect-error - handler: async ({ body }) => { - const result = await updateBookOrBookInfo(body); +// // } +// // }, +// updateBook: { +// // middleware: [authValidate(roleSet.librarian)], +// // @ts-expect-error +// handler: async ({ body }) => { +// const result = await updateBookOrBookInfo(body); - if (result instanceof PubdateFormatError) { - return pubdateFormatError; - } - return { status: 200, body: '책 정보가 수정되었습니다.' } as const; - }, - }, - updateDonator: { - // middleware: [authValidate(roleSet.librarian)], - // @ts-expect-error - handler: async ({ body }) => { - const result = await updateBookDonator(body); +// if (result instanceof PubdateFormatError) { +// return pubdateFormatError; +// } +// return { status: 200, body: '책 정보가 수정되었습니다.' } as const; +// }, +// }, +// updateDonator: { +// // middleware: [authValidate(roleSet.librarian)], +// // @ts-expect-error +// handler: async ({ body }) => { +// const result = await updateBookDonator(body); - return { status: 200, body: '기부자 정보가 수정되었습니다.' } as const; - }, - }, -}); +// return { status: 200, body: '기부자 정보가 수정되었습니다.' } as const; +// }, +// }, +// }); diff --git a/backend/src/v2/histories/mod.ts b/backend/src/v2/lendings/mod.ts similarity index 93% rename from backend/src/v2/histories/mod.ts rename to backend/src/v2/lendings/mod.ts index 7d24dd45..6253a39d 100644 --- a/backend/src/v2/histories/mod.ts +++ b/backend/src/v2/lendings/mod.ts @@ -8,8 +8,8 @@ import { getHistoriesByQuery, getHistoriesByUser } from './repository'; import { getUser } from '../shared'; const s = initServer(); -export const histories = s.router(contract.histories, { - getMyHistories: { +export const lendings = s.router(contract.lendings, { + getMine: { middleware: [authValidate(roleSet.all)], handler: async ({ query, req: { user } }) => { const { nickname: login } = getUser.parse(user); @@ -27,7 +27,7 @@ export const histories = s.router(contract.histories, { }, }, - getAllHistories: { + get: { middleware: [authValidate(roleSet.librarian)], handler: async ({ query }) => { const [items, count] = await getHistoriesByQuery(query); diff --git a/backend/src/v2/histories/repository.ts b/backend/src/v2/lendings/repository.ts similarity index 100% rename from backend/src/v2/histories/repository.ts rename to backend/src/v2/lendings/repository.ts diff --git a/backend/src/v2/routes.ts b/backend/src/v2/routes.ts index 03992919..32f3d39c 100644 --- a/backend/src/v2/routes.ts +++ b/backend/src/v2/routes.ts @@ -3,14 +3,10 @@ import { contract } from '@jiphyeonjeon-42/contracts'; import { initServer } from '@ts-rest/express'; import { reviews } from './reviews/mod.ts'; -import { histories } from './histories/mod.ts'; -import { stock } from './stock/mod.ts'; -import { books } from './books/mod.ts'; +import { lendings } from './lendings/mod.ts'; const s = initServer(); export default s.router(contract, { reviews, - histories, - stock, - books, + lendings, }); diff --git a/backend/src/v2/stock/mod.ts b/backend/src/v2/stock/mod.ts deleted file mode 100644 index d42929e0..00000000 --- a/backend/src/v2/stock/mod.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { contract } from '@jiphyeonjeon-42/contracts'; -import { initServer } from '@ts-rest/express'; -import { searchStock, updateStock } from './service'; -import { BookNotFoundError, bookNotFound } from '../shared'; - -const s = initServer(); -export const stock = s.router(contract.stock, { - get: async ({ query }) => { - const result = await searchStock(query); - - return { status: 200, body: result } as const; - }, - - patch: async ({ body }) => { - const result = await updateStock(body); - - if (result instanceof BookNotFoundError) { - return bookNotFound; - } - return { status: 200, body: '재고 상태가 업데이트되었습니다.' } as const; - }, -}); diff --git a/backend/src/v2/stock/repository.ts b/backend/src/v2/stock/repository.ts deleted file mode 100644 index c61491a0..00000000 --- a/backend/src/v2/stock/repository.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { LessThan } from 'typeorm'; -import { startOfDay, addDays } from 'date-fns'; -import { Book, VStock } from '~/entity/entities'; -import jipDataSource from '~/app-data-source'; - -export const stockRepo = jipDataSource.getRepository(VStock); -export const bookRepo = jipDataSource.getRepository(Book); - -type SearchStockArgs = { page: number; limit: number; days: number }; - -export const searchStockByUpdatedOlderThan = ({ limit, page, days }: SearchStockArgs) => { - const today = startOfDay(new Date()); - return stockRepo.findAndCount({ - where: { updatedAt: LessThan(addDays(today, days * -1)) }, - take: limit, - skip: limit * page, - }); -}; diff --git a/backend/src/v2/stock/service.ts b/backend/src/v2/stock/service.ts deleted file mode 100644 index 048c5715..00000000 --- a/backend/src/v2/stock/service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { match } from 'ts-pattern'; - -import { VStock } from '~/entity/entities'; -import { type Repository } from 'typeorm'; - -import { Meta } from '~/v2/shared'; -import { BookNotFoundError } from '~/v2/shared/errors'; -import { searchStockByUpdatedOlderThan } from './repository'; -import { stockRepo } from './repository'; -import { bookRepo } from './repository'; - -type SearchArgs = { page: number; limit: number }; -export const searchStock = async ({ - limit, - page, -}: SearchArgs): Promise<{ items: VStock[]; meta: Meta }> => { - const [items, totalItems] = await searchStockByUpdatedOlderThan({ - limit, - page, - days: 15, - }); - - const meta: Meta = { - totalItems, - itemCount: items.length, - itemsPerPage: limit, - totalPages: Math.ceil(totalItems / limit), - currentPage: page + 1, - }; - - return { items, meta }; -}; - -type UpdateArgs = { id: number }; -export const updateStock = async ({ id }: UpdateArgs) => { - const stock = await stockRepo.findOneBy({ bookId: id }); - - return match(stock) - .with(null, () => new BookNotFoundError(id)) - .otherwise(() => bookRepo.update({ id }, { updatedAt: new Date() })); -}; diff --git a/contracts/src/books/index.ts b/contracts/src/books/index.ts index 5226bffc..712fec07 100644 --- a/contracts/src/books/index.ts +++ b/contracts/src/books/index.ts @@ -3,155 +3,67 @@ import { searchAllBooksQuerySchema, searchAllBooksResponseSchema, searchBookByIdResponseSchema, - searchBookInfoCreateQuerySchema, - searchBookInfoCreateResponseSchema, - createBookBodySchema, - createBookResponseSchema, - categoryNotFoundSchema, pubdateFormatErrorSchema, - insertionFailureSchema, - isbnNotFoundSchema, - naverBookNotFoundSchema, updateBookBodySchema, updateBookResponseSchema, unknownPatchErrorSchema, nonDataErrorSchema, - searchAllBookInfosQuerySchema, - searchBookInfosByTagQuerySchema, - searchBookInfosResponseSchema, - searchBookInfosSortedQuerySchema, - searchBookInfosSortedResponseSchema, searchBookInfoByIdResponseSchema, - updateDonatorBodySchema, - updateDonatorResponseSchema, searchBookInfoByIdPathSchema, searchBookByIdParamSchema, } from './schema'; -import { - badRequestSchema, - bookInfoNotFoundSchema, - bookNotFoundSchema, - serverErrorSchema, -} from '../shared'; +import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema } from '../shared'; const c = initContract(); -export const booksContract = c.router( +export const bookMetasContracts = c.router( { - // searchAllBookInfos: { - // method: 'GET', - // path: '/info/search', - // description: '책 정보(book_info)를 검색하여 가져온다.', - // query: searchAllBookInfosQuerySchema, - // responses: { - // 200: searchBookInfosResponseSchema, - // 400: badRequestSchema, - // 500: serverErrorSchema, - // }, - // }, - searchBookInfosByTag: { - method: 'GET', - path: '/info/tag', - description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.', - query: searchBookInfosByTagQuerySchema, - responses: { - 200: searchBookInfosResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfosSorted: { - method: 'GET', - path: '/info/sorted', - description: - '책 정보를 기준에 따라 정렬한다. 정렬기준이 popular일 경우 당일으로부터 42일간 인기순으로 한다.', - query: searchBookInfosSortedQuerySchema, - responses: { - 200: searchBookInfosSortedResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoById: { + getById: { method: 'GET', path: '/info/:id', pathParams: searchBookInfoByIdPathSchema, - description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + summary: '도서 정보를 조회합니다.', responses: { 200: searchBookInfoByIdResponseSchema, 404: bookInfoNotFoundSchema, - 500: serverErrorSchema, - }, - }, - searchAllBooks: { - method: 'GET', - path: '/search', - description: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', - query: searchAllBooksQuerySchema, - responses: { - 200: searchAllBooksResponseSchema, - 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - searchBookInfoForCreate: { - method: 'GET', - path: '/create', - description: '책 생성을 위해 국립중앙도서관에서 ISBN으로 검색한 뒤에 책정보를 반환', - query: searchBookInfoCreateQuerySchema, - responses: { - 200: searchBookInfoCreateResponseSchema, - 303: isbnNotFoundSchema, - 310: naverBookNotFoundSchema, - 500: serverErrorSchema, }, }, - searchBookById: { + }, + { pathPrefix: '/bookmetas' }, +); + +export const booksContract = c.router( + { + getById: { method: 'GET', path: '/:id', - description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + summary: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', pathParams: searchBookByIdParamSchema, responses: { 200: searchBookByIdResponseSchema, 404: bookNotFoundSchema, - 500: serverErrorSchema, }, }, - // createBook: { - // method: 'POST', - // path: '/create', - // description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.', - // body: createBookBodySchema, - // responses: { - // 200: createBookResponseSchema, - // 308: insertionFailureSchema, - // 309: categoryNotFoundSchema, - // 311: pubdateFormatErrorSchema, - // 500: serverErrorSchema, - // }, - // }, - updateBook: { + get: { + method: 'GET', + path: '/', + summary: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', + query: searchAllBooksQuerySchema, + responses: { + 200: searchAllBooksResponseSchema, + 400: badRequestSchema, + }, + }, + patch: { method: 'PATCH', path: '/update', - description: '책 정보를 수정합니다. book_info table or book table', + summary: '책 정보 하나를 수정합니다.', body: updateBookBodySchema, responses: { 204: updateBookResponseSchema, 312: unknownPatchErrorSchema, 313: nonDataErrorSchema, 311: pubdateFormatErrorSchema, - 500: serverErrorSchema, - }, - }, - updateDonator: { - method: 'PATCH', - path: '/donator', - description: '기부자 정보를 수정합니다.', - body: updateDonatorBodySchema, - responses: { - 204: updateDonatorResponseSchema, - 404: bookNotFoundSchema, - 500: serverErrorSchema, }, }, }, diff --git a/contracts/src/books/mod.ts b/contracts/src/books/mod.ts new file mode 100644 index 00000000..712fec07 --- /dev/null +++ b/contracts/src/books/mod.ts @@ -0,0 +1,71 @@ +import { initContract } from '@ts-rest/core'; +import { + searchAllBooksQuerySchema, + searchAllBooksResponseSchema, + searchBookByIdResponseSchema, + pubdateFormatErrorSchema, + updateBookBodySchema, + updateBookResponseSchema, + unknownPatchErrorSchema, + nonDataErrorSchema, + searchBookInfoByIdResponseSchema, + searchBookInfoByIdPathSchema, + searchBookByIdParamSchema, +} from './schema'; +import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema } from '../shared'; + +const c = initContract(); + +export const bookMetasContracts = c.router( + { + getById: { + method: 'GET', + path: '/info/:id', + pathParams: searchBookInfoByIdPathSchema, + summary: '도서 정보를 조회합니다.', + responses: { + 200: searchBookInfoByIdResponseSchema, + 404: bookInfoNotFoundSchema, + }, + }, + }, + { pathPrefix: '/bookmetas' }, +); + +export const booksContract = c.router( + { + getById: { + method: 'GET', + path: '/:id', + summary: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.', + pathParams: searchBookByIdParamSchema, + responses: { + 200: searchBookByIdResponseSchema, + 404: bookNotFoundSchema, + }, + }, + get: { + method: 'GET', + path: '/', + summary: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음', + query: searchAllBooksQuerySchema, + responses: { + 200: searchAllBooksResponseSchema, + 400: badRequestSchema, + }, + }, + patch: { + method: 'PATCH', + path: '/update', + summary: '책 정보 하나를 수정합니다.', + body: updateBookBodySchema, + responses: { + 204: updateBookResponseSchema, + 312: unknownPatchErrorSchema, + 313: nonDataErrorSchema, + 311: pubdateFormatErrorSchema, + }, + }, + }, + { pathPrefix: '/books' }, +); diff --git a/contracts/src/books/schema.ts b/contracts/src/books/schema.ts index abfa2895..1916aec6 100644 --- a/contracts/src/books/schema.ts +++ b/contracts/src/books/schema.ts @@ -19,27 +19,12 @@ export const searchAllBookInfosQuerySchema = commonQuerySchema.extend({ category: z.string().optional(), }); -export const searchBookInfosByTagQuerySchema = commonQuerySchema.extend({ - query: z.string(), - sort: z.enum(['new', 'popular', 'title']).default('new'), - category: z.string().optional(), -}); - -export const searchBookInfosSortedQuerySchema = z.object({ - sort: z.enum(['new', 'popular']), - limit: nonNegativeInt.default(10).openapi({ example: 10 }), -}); - export const searchBookInfoByIdPathSchema = z.object({ id: nonNegativeInt, }); export const searchAllBooksQuerySchema = commonQuerySchema; -export const searchBookInfoCreateQuerySchema = z.object({ - isbnQuery: z.string().openapi({ example: '9791191114225' }), -}); - export const createBookBodySchema = z.object({ title: z.string(), isbn: z.string(), @@ -68,10 +53,6 @@ export const updateBookBodySchema = z.object({ status: statusSchema.optional(), }); -export const updateDonatorBodySchema = z.object({ - bookId: nonNegativeInt, - nickname: z.string(), -}); export const bookInfoSchema = z.object({ id: nonNegativeInt, @@ -86,31 +67,6 @@ export const bookInfoSchema = z.object({ updatedAt: dateLike, }); -export const searchBookInfosResponseSchema = metaPaginatedSchema( - bookInfoSchema - .extend({ - publishedAt: dateLike, - }) - .omit({ - publisher: true, - }), -).extend({ - categories: z.array( - z.object({ - name: z.string(), - count: nonNegativeInt, - }), - ), -}); - -export const searchBookInfosSortedResponseSchema = z.object({ - items: z.array( - bookInfoSchema.extend({ - publishedAt: dateLike, - lendingCnt: nonNegativeInt, - }), - ), -}); export const searchBookInfoByIdResponseSchema = bookInfoSchema.extend({ books: z.array( @@ -147,20 +103,6 @@ export const searchAllBooksResponseSchema = metaPaginatedSchema( }), ); -export const searchBookInfoCreateResponseSchema = z.object({ - bookInfo: z.object({ - title: z.string().openapi({ example: '작별인사' }), - image: z.string().openapi({ - example: 'http://image.kyobobook.co.kr/images/book/xlarge/225/x9791191114225.jpg', - }), - author: z.string().openapi({ example: '지은이: 김영하' }), - category: z.string().openapi({ example: '8' }), - isbn: z.string().openapi({ example: '9791191114225' }), - publisher: z.string().openapi({ example: '복복서가' }), - pubdate: z.string().openapi({ example: '20220502' }), - }), -}); - export const searchBookByIdResponseSchema = z.object({ id: nonNegativeInt.openapi({ example: 3 }), bookId: nonNegativeInt.openapi({ example: 3 }), @@ -185,20 +127,10 @@ export const searchBookByIdResponseSchema = z.object({ export const updateBookResponseSchema = z.literal('책 정보가 수정되었습니다.'); -export const updateDonatorResponseSchema = z.literal('기부자 정보가 수정되었습니다.'); - export const createBookResponseSchema = z.object({ callSign: z.string().openapi({ example: 'K23.17.v1.c1' }), }); -export const isbnNotFoundSchema = mkErrorMessageSchema('ISBN_NOT_FOUND').describe( - '국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.', -); - -export const naverBookNotFoundSchema = mkErrorMessageSchema('NAVER_BOOK_NOT_FOUND').describe( - '네이버 책검색 API에서 ISBN 검색이 실패', -); - export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe( '예상치 못한 에러로 책 정보 insert에 실패함.', ); diff --git a/contracts/src/index.ts b/contracts/src/index.ts index f2ec5fa9..61c8b649 100644 --- a/contracts/src/index.ts +++ b/contracts/src/index.ts @@ -1,10 +1,8 @@ import { initContract } from '@ts-rest/core'; import { reviewsContract } from './reviews'; -import { historiesContract } from './histories'; +import { lendingsContract } from './lendings/mod'; import { usersContract } from './users'; import { likesContract } from './likes'; -import { stockContract } from './stock'; -import { tagContract } from './tags'; import { booksContract } from './books'; export * from './reviews'; @@ -17,11 +15,8 @@ export const contract = c.router( { // likes: likesContract, reviews: reviewsContract, - histories: historiesContract, - books: booksContract, - stock: stockContract, - // TODO(@nyj001012): 태그 서비스 작성 - // tags: tagContract, + lendings: lendingsContract, + // books: booksContract, // TODO(@scarf005): 유저 서비스 작성 // users: usersContract, }, diff --git a/contracts/src/histories/index.ts b/contracts/src/lendings/mod.ts similarity index 69% rename from contracts/src/histories/index.ts rename to contracts/src/lendings/mod.ts index 3726ef0f..c9fde889 100644 --- a/contracts/src/histories/index.ts +++ b/contracts/src/lendings/mod.ts @@ -11,21 +11,21 @@ export * from './schema'; // contract 를 생성할 때, router 함수를 사용하여 api 를 생성 const c = initContract(); -export const historiesContract = c.router({ - getMyHistories: { +export const lendingsContract = c.router({ + getMine: { method: 'GET', - path: '/mypage/histories', - description: '마이페이지에서 본인의 대출 기록을 가져온다.', + path: '/mypage/lendings', + summary: '내 대출 기록을 가져옵니다.', query: historiesGetMyQuerySchema, responses: { 200: historiesGetResponseSchema, 401: unauthorizedSchema, }, }, - getAllHistories: { + get: { method: 'GET', - path: '/histories', - description: '사서가 전체 대출 기록을 가져온다.', + path: '/lendings', + summary: '사서가 전체 대출 기록을 가져옵니다.', query: historiesGetQuerySchema, responses: { 200: historiesGetResponseSchema, diff --git a/contracts/src/histories/schema.ts b/contracts/src/lendings/schema.ts similarity index 100% rename from contracts/src/histories/schema.ts rename to contracts/src/lendings/schema.ts diff --git a/contracts/src/likes/index.ts b/contracts/src/likes/index.ts index e247a504..3335b466 100644 --- a/contracts/src/likes/index.ts +++ b/contracts/src/likes/index.ts @@ -10,7 +10,7 @@ export const likesContract = c.router( post: { method: 'POST', path: '/:bookInfoId/like', - description: '책에 좋아요를 누릅니다.', + summary: '책에 좋아요를 누릅니다.', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), body: null, responses: { @@ -23,8 +23,7 @@ export const likesContract = c.router( get: { method: 'GET', path: '/:bookInfoId/like', - summary: 'Like 정보를 가져온다.', - description: '사용자가 좋아요 버튼을 누르면 좋아요 개수를 가져온다.', + summary: '좋아요 개수를 가져옵니다.', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), responses: { 200: likeResponseSchema, @@ -34,7 +33,7 @@ export const likesContract = c.router( delete: { method: 'DELETE', path: '/:bookInfoId/like', - description: 'delete a like', + summary: '좋아요를 취소합니다', pathParams: z.object({ bookInfoId: bookInfoIdSchema }), body: null, responses: { diff --git a/contracts/src/reviews/index.ts b/contracts/src/reviews/index.ts index 5a8a8749..872cee7a 100644 --- a/contracts/src/reviews/index.ts +++ b/contracts/src/reviews/index.ts @@ -30,7 +30,7 @@ export const reviewsContract = c.router( search: z.string().optional().describe('도서 제목 또는 리뷰 작성자 닉네임'), visibility, }), - description: '전체 도서 리뷰 목록을 조회합니다.', + summary: '전체 도서 리뷰 목록을 조회합니다.', responses: { 200: metaPaginatedSchema(reviewSchema), }, @@ -39,7 +39,7 @@ export const reviewsContract = c.router( method: 'POST', path: '/', query: z.object({ bookInfoId: bookInfoIdSchema.openapi({ description: '도서 ID' }) }), - description: '책 리뷰를 작성합니다.', + summary: '책 리뷰를 작성합니다.', body: contentSchema, responses: { 201: z.literal('리뷰가 작성되었습니다.'), @@ -50,7 +50,7 @@ export const reviewsContract = c.router( method: 'PATCH', path: '/:reviewsId', pathParams: reviewIdPathSchema, - description: '책 리뷰의 비활성화 여부를 토글 방식으로 변환합니다.', + summary: '책 리뷰의 비활성화 여부를 토글 방식으로 변환합니다.', body: null, responses: { 200: z.literal('리뷰 공개 여부가 업데이트되었습니다.'), diff --git a/contracts/src/shared.ts b/contracts/src/shared.ts index dacd0031..edf2a796 100644 --- a/contracts/src/shared.ts +++ b/contracts/src/shared.ts @@ -38,9 +38,6 @@ export const bookNotFoundSchema = export const bookInfoNotFoundSchema = mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다'); -export const serverErrorSchema = - mkErrorMessageSchema('SERVER_ERROR').describe('서버에서 오류가 발생했습니다.'); - export const badRequestSchema = mkErrorMessageSchema('BAD_REQUEST').describe('잘못된 요청입니다.'); export const forbiddenSchema = mkErrorMessageSchema('FORBIDDEN').describe('권한이 없습니다.'); diff --git a/contracts/src/stock/index.ts b/contracts/src/stock/index.ts deleted file mode 100644 index 31944410..00000000 --- a/contracts/src/stock/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { initContract } from '@ts-rest/core'; -import { - stockGetQuerySchema, - stockGetResponseSchema, - stockPatchBodySchema, - stockPatchResponseSchema, -} from './schema'; -import { bookNotFoundSchema } from '../shared'; - -const c = initContract(); - -export const stockContract = c.router( - { - get: { - method: 'GET', - path: '/search', - description: '책 재고 정보를 검색해 온다.', - query: stockGetQuerySchema, - responses: { - 200: stockGetResponseSchema, - // 특정한 에러케이스가 생각나지 않습니다. - }, - }, - patch: { - method: 'PATCH', - path: '/update', - description: '책 재고를 확인하고 수정일시를 업데이트한다.', - body: stockPatchBodySchema, - responses: { - 200: stockPatchResponseSchema, - 404: bookNotFoundSchema, - }, - }, - }, - { pathPrefix: '/stock' }, -); diff --git a/contracts/src/stock/schema.ts b/contracts/src/stock/schema.ts deleted file mode 100644 index e4921cca..00000000 --- a/contracts/src/stock/schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { dateLike, metaSchema, nonNegativeInt } from '../shared'; -import { z } from '../zodWithOpenapi'; - -export const bookIdSchema = nonNegativeInt.describe('업데이트 할 도서 ID'); - -export const stockPatchBodySchema = z.object({ - id: bookIdSchema.openapi({ example: 0 }), -}); - -export const stockPatchResponseSchema = z.literal('재고 상태가 업데이트되었습니다.'); - -export const stockGetQuerySchema = z.object({ - page: nonNegativeInt.default(0), - limit: nonNegativeInt.default(10), -}); - -export const stockGetResponseSchema = z.object({ - items: z.array( - z.object({ - bookId: nonNegativeInt, - bookInfoId: nonNegativeInt, - title: z.string(), - author: z.string(), - donator: z.string(), - publisher: z.string(), - publishedAt: dateLike, - isbn: z.string(), - image: z.string(), - status: nonNegativeInt, - categoryId: nonNegativeInt, - callSign: z.string(), - category: z.string(), - updatedAt: dateLike, - }), - ), - meta: metaSchema, -}); diff --git a/contracts/src/tags/index.ts b/contracts/src/tags/index.ts deleted file mode 100644 index 0c874bea..00000000 --- a/contracts/src/tags/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { initContract } from '@ts-rest/core'; -import { z } from 'zod'; -import { - subDefaultTagQuerySchema, - subDefaultTagResponseSchema, - superDefaultTagResponseSchema, - superTagIdQuerySchema, - subTagResponseSchema, - tagsOfBookResponseSchema, - modifySuperTagBodySchema, - modifyTagResponseSchema, - incorrectTagFormatSchema, - alreadyExistTagSchema, - defaultTagCannotBeModifiedSchema, - modifySubTagBodySchema, - NoAuthorityToModifyTagSchema, - mergeTagsBodySchema, - invalidTagIdSchema, - createTagBodySchema, - duplicateTagSchema, - tagIdSchema, -} from './schema'; -import { bookInfoIdSchema, bookInfoNotFoundSchema, paginationQuerySchema } from '../shared'; - -const c = initContract(); - -export const tagContract = c.router( - { - getSubDefault: { - method: 'GET', - path: '', - summary: '서브/디폴트 태그 정보를 검색한다.', - description: '서브/디폴트 태그 정보를 검색한다. 이는 태그 관리 페이지에서 사용한다.', - query: subDefaultTagQuerySchema, - responses: { - 200: subDefaultTagResponseSchema, - }, - }, - getSuperDefaultForMain: { - method: 'GET', - path: '/main', - summary: '메인 페이지에서 사용할 태그 목록을 가져온다.', - description: - '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 랜덤한 순서로 가져온다. 이는 메인 페이지에서 사용된다.', - query: paginationQuerySchema.omit({ page: true }), - responses: { - 200: superDefaultTagResponseSchema, - }, - }, - getSubOfSuperTag: { - method: 'GET', - path: '/{superTagId}/sub', - summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: - 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 병합 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', - pathParams: superTagIdQuerySchema, - responses: { - 200: subTagResponseSchema, - }, - }, - getSubOfSuperTagForAdmin: { - method: 'GET', - path: '/manage/{superTagId}/sub', - summary: '슈퍼 태그에 속한 서브 태그 목록을 가져온다.', - description: - 'superTagId에 해당하는 슈퍼 태그에 속한 서브 태그 목록을 가져온다. 태그 관리 페이지에서 슈퍼 태그의 서브 태그를 가져올 때 사용한다.', - pathParams: superTagIdQuerySchema, - responses: { - 200: subTagResponseSchema, - }, - }, - getTagsOfBook: { - method: 'GET', - path: '/{bookInfoId}', - summary: '도서에 등록된 슈퍼 태그, 디폴트 태그 목록을 가져온다.', - description: - '슈퍼 태그(노출되는 태그), 디폴트 태그(노출되지 않고 분류되지 않은 태그)를 가져온다. 이는 도서 상세 페이지 및 태그 병합 페이지에서 사용된다.', - pathParams: bookInfoIdSchema, - responses: { - 200: tagsOfBookResponseSchema, - }, - }, - modifySuperTag: { - method: 'PATCH', - path: '/super', - description: '슈퍼 태그를 수정한다.', - body: modifySuperTagBodySchema, - responses: { - 204: z.null(), - 400: z.union([alreadyExistTagSchema, defaultTagCannotBeModifiedSchema]), - }, - }, - modifySubTag: { - method: 'PATCH', - path: '/sub', - description: '서브 태그를 수정한다.', - body: modifySubTagBodySchema, - responses: { - 200: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 901: NoAuthorityToModifyTagSchema, - }, - }, - mergeTags: { - method: 'PATCH', - path: '/{bookInfoId}/merge', - description: '태그를 병합한다.', - pathParams: bookInfoIdSchema, - body: mergeTagsBodySchema, - responses: { - 200: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 902: alreadyExistTagSchema, - 906: defaultTagCannotBeModifiedSchema, - 910: invalidTagIdSchema, - }, - }, - createDefaultTag: { - method: 'POST', - path: '/default', - description: '디폴트 태그를 생성한다. 태그 길이는 42자 이하여야 한다.', - body: createTagBodySchema, - responses: { - 201: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 907: bookInfoNotFoundSchema, - 909: duplicateTagSchema, - }, - }, - createSuperTag: { - method: 'POST', - path: '/super', - description: '슈퍼 태그를 생성한다. 태그 길이는 42자 이하여야 한다.', - body: createTagBodySchema, - responses: { - 201: modifyTagResponseSchema, - 900: incorrectTagFormatSchema, - 907: bookInfoNotFoundSchema, - 909: duplicateTagSchema, - }, - }, - deleteSubDefaultTag: { - method: 'DELETE', - path: '/sub/{tagId}', - description: '서브/디폴트 태그를 삭제한다.', - pathParams: tagIdSchema, - body: null, - responses: { - 200: modifyTagResponseSchema, - 910: invalidTagIdSchema, - }, - }, - deleteSuperTag: { - method: 'DELETE', - path: '/super/{tagId}', - description: '슈퍼 태그를 삭제한다.', - pathParams: tagIdSchema, - body: null, - responses: { - 200: modifyTagResponseSchema, - 910: invalidTagIdSchema, - }, - }, - }, - { pathPrefix: '/tags' }, -); diff --git a/contracts/src/tags/schema.ts b/contracts/src/tags/schema.ts deleted file mode 100644 index a4699500..00000000 --- a/contracts/src/tags/schema.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { dateLike, metaSchema, mkErrorMessageSchema, nonNegativeInt } from '../shared'; -import { z } from '../zodWithOpenapi'; - -export const subDefaultTagQuerySchema = z.object({ - page: nonNegativeInt.optional().default(0), - limit: nonNegativeInt.optional().default(10), - visibility: z.enum(['public', 'private']).optional(), - query: z.string().optional().openapi({ example: '개발자의 코드' }), -}); - -export const subDefaultTagResponseSchema = z.object({ - items: z.array( - z.object({ - bookInfoId: nonNegativeInt.openapi({ - description: '태그가 등록된 도서의 info id', - example: 1, - }), - title: z.string().openapi({ - description: '태그가 등록된 도서의 제목', - example: '개발자의 코드', - }), - id: nonNegativeInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - createdAt: dateLike.openapi({ - description: '태그가 등록된 시간', - example: '2023-04-12', - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '서브/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), - superContent: z.string().openapi({ - description: '슈퍼 태그의 내용', - example: '1서클_추천_책', - }), - visibility: z.enum(['public', 'private']).openapi({ - description: '태그의 공개 여부. 공개는 public, 비공개는 private', - example: 'private', - }), - meta: metaSchema, - }), - ), -}); - -export const superDefaultTagResponseSchema = z.object({ - items: z.array( - z.object({ - createdAt: dateLike.openapi({ - description: '태그 생성일', - example: '2023-04-12', - }), - content: z.string().openapi({ - description: '태그 내용', - example: '1서클_추천_책', - }), - count: nonNegativeInt.openapi({ - description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', - example: 1, - }), - type: z.enum(['super', 'default']).openapi({ - description: '태그의 타입. 슈퍼 태그는 super, 디폴트 태그는 default', - example: 'super', - }), - }), - ), -}); - -export const superTagIdQuerySchema = z.object({ - superTagId: nonNegativeInt.openapi({ - description: '슈퍼 태그의 id', - example: 1, - }), -}); - -export const subTagResponseSchema = z.object({ - id: nonNegativeInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '서브/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), -}); - -export const tagsOfBookResponseSchema = z.object({ - items: z.array( - z.object({ - id: nonNegativeInt.openapi({ - description: '태그 고유 id', - example: 1, - }), - login: z.string().openapi({ - description: '태그를 작성한 카뎃의 닉네임', - example: 'yena', - }), - content: z.string().openapi({ - description: '슈퍼/디폴트 태그의 내용', - example: 'yena가_추천하는', - }), - type: z.enum(['super', 'default']).openapi({ - description: '태그의 타입. 슈퍼 태그는 super, 디폴트 태그는 default', - example: 'super', - }), - count: nonNegativeInt.openapi({ - description: '슈퍼 태그에 속한 서브 태그의 개수. 디폴트 태그는 0', - example: 1, - }), - }), - ), -}); - -export const modifySuperTagBodySchema = z.object({ - id: nonNegativeInt.openapi({ - description: '수정할 슈퍼 태그의 id', - example: 1, - }), - content: z.string().openapi({ - description: '수정할 슈퍼 태그의 내용', - example: '1서클_추천_책', - }), -}); - -export const modifyTagResponseSchema = z.literal('success'); - -export const incorrectTagFormatSchema = - mkErrorMessageSchema('INCORRECT_TAG_FORMAT').describe('태그 형식이 올바르지 않습니다.'); - -export const alreadyExistTagSchema = - mkErrorMessageSchema('ALREADY_EXIST_TAG').describe('이미 존재하는 태그입니다.'); - -export const defaultTagCannotBeModifiedSchema = mkErrorMessageSchema( - 'DEFAULT_TAG_CANNOT_BE_MODIFIED', -).describe('디폴트 태그는 수정할 수 없습니다.'); - -export const modifySubTagBodySchema = z.object({ - id: nonNegativeInt.openapi({ - description: '수정할 서브 태그의 id', - example: 1, - }), - content: z.string().openapi({ - description: '수정할 서브 태그의 내용', - example: 'yena가_추천하는', - }), - visibility: z.enum(['public', 'private']).openapi({ - description: '태그의 공개 여부. 공개는 public, 비공개는 private', - example: 'private', - }), -}); - -export const NoAuthorityToModifyTagSchema = mkErrorMessageSchema( - 'NO_AUTHORITY_TO_MODIFY_TAG', -).describe('태그를 수정할 권한이 없습니다.'); - -export const mergeTagsBodySchema = z.object({ - superTagId: nonNegativeInt.nullable().openapi({ - description: '병합할 슈퍼 태그의 id. null이면 디폴트 태그로 병합됨을 의미한다.', - example: 1, - }), - subTagIds: z.array(nonNegativeInt).openapi({ - description: '병합할 서브 태그의 id 목록', - example: [1, 2, 3], - }), -}); - -export const invalidTagIdSchema = - mkErrorMessageSchema('INVALID_TAG_ID').describe('태그 id가 올바르지 않습니다.'); - -export const createTagBodySchema = z.object({ - bookInfoId: nonNegativeInt.openapi({ - description: '태그를 등록할 도서의 info id', - example: 1, - }), - content: z.string().openapi({ - description: '태그 내용', - example: 'yena가_추천하는', - }), -}); - -export const duplicateTagSchema = - mkErrorMessageSchema('DUPLICATE_TAG').describe('이미 존재하는 태그입니다.'); - -export const tagIdSchema = z.object({ - tagId: nonNegativeInt.openapi({ - description: '태그의 id', - example: 1, - }), -}); diff --git a/contracts/src/users/index.ts b/contracts/src/users/index.ts index 58a9379c..25287096 100644 --- a/contracts/src/users/index.ts +++ b/contracts/src/users/index.ts @@ -1,5 +1,5 @@ import { initContract } from '@ts-rest/core'; -import { badRequestSchema, serverErrorSchema, forbiddenSchema } from '../shared'; +import { badRequestSchema } from '../shared'; import { searchUserSchema, searchUserResponseSchema, @@ -7,8 +7,6 @@ import { createUserResponseSchema, userIdSchema, updateUserSchema, - updatePrivateInfoSchema, - updateUserResponseSchema, } from './schema'; export * from './schema'; @@ -17,50 +15,35 @@ const c = initContract(); export const usersContract = c.router( { - searchUser: { + get: { method: 'GET', - path: '/search', - description: '유저 정보를 검색해 온다. query가 null이면 모든 유저를 검색한다.', + path: '/', + summary: '유저 정보를 검색해 온다. query가 null이면 모든 유저를 검색한다.', query: searchUserSchema, responses: { 200: searchUserResponseSchema, 400: badRequestSchema, - 500: serverErrorSchema, }, }, - createUser: { + post: { method: 'POST', - path: '/create', - description: '유저를 생성한다.', + path: '/', + summary: '유저를 생성한다.', body: createUserSchema, responses: { 201: createUserResponseSchema, 400: badRequestSchema, - 500: serverErrorSchema, }, }, - updateUser: { + patch: { method: 'PATCH', - path: '/update/:id', - description: '유저 정보를 변경한다.', + path: '/:id', + summary: '유저 정보를 변경한다.', pathParams: userIdSchema, body: updateUserSchema, responses: { 200: updateUserSchema, 400: badRequestSchema, - 500: serverErrorSchema, - }, - }, - updatePrivateInfo: { - method: 'PATCH', - path: '/myupdate', - description: '유저의 정보를 변경한다.', - body: updatePrivateInfoSchema, - responses: { - 200: updateUserResponseSchema, - 400: badRequestSchema, - 403: forbiddenSchema, - 500: serverErrorSchema, }, }, }, diff --git a/contracts/src/users/schema.ts b/contracts/src/users/schema.ts index 5108a14b..073468cb 100644 --- a/contracts/src/users/schema.ts +++ b/contracts/src/users/schema.ts @@ -104,15 +104,3 @@ export const updateUserSchema = z.object({ .describe('연체 패널티 끝나는 날짜') .openapi({ example: '2022-05-22' }), }); - -export const updatePrivateInfoSchema = z.object({ - email: z - .string() - .email() - .optional() - .describe('이메일') - .openapi({ example: 'yena@student.42seoul.kr' }), - password: z.string().optional().describe('패스워드').openapi({ example: 'KingGodMajesty42' }), -}); - -export const updateUserResponseSchema = z.literal('유저 정보 변경 성공!'); From 1ac5b231c13ddf24b47d75d3358539f0f7b391a8 Mon Sep 17 00:00:00 2001 From: jimin Date: Thu, 21 Sep 2023 17:05:16 +0900 Subject: [PATCH 07/10] feat: add mydata service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 토큰에서 id 정보 찾아서 유저 정보 반환하는 controller --- backend/src/v1/users/users.controller.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/src/v1/users/users.controller.ts b/backend/src/v1/users/users.controller.ts index 0b5ffda0..dd53380f 100644 --- a/backend/src/v1/users/users.controller.ts +++ b/backend/src/v1/users/users.controller.ts @@ -184,6 +184,23 @@ export const myupdate = async ( return 0; }; +export const mydata = async ( + req: Request, + res: Response, +) => { + console.log(req.user); + const { id: tokenId } = req.user as any; + try { + const user = await usersService.searchUserById(parseInt(tokenId, 10)); + console.log(user); + if (!user || !user.items || !user.items[0]) return res.status(404).send('Not Found'); + return res.status(200).json(user.items[0]); + } catch (error: any) { + logger.error(error); + return res.status(500).send('Internal Server Error'); + } +}; + export const getVersion = async ( req: Request, res: Response, From 70943316a3035b2ac04f14e517ec2a35bfaa6a08 Mon Sep 17 00:00:00 2001 From: jimin Date: Thu, 21 Sep 2023 17:05:44 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20search=20?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20id=20=EA=B0=80=20undefined=20=20?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=ED=95=B8=EB=93=A4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/v1/users/users.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/v1/users/users.service.ts b/backend/src/v1/users/users.service.ts index b641e6a9..4e280514 100644 --- a/backend/src/v1/users/users.service.ts +++ b/backend/src/v1/users/users.service.ts @@ -56,6 +56,7 @@ export default class UsersService { } async searchUserById(id: number) { + if (!id) return null; let items = (await this.usersRepository.searchUserBy({ id }, 0, 0))[0]; items = await this.withLendingInfo(items); return { items }; From 1c8b49be8ed2e44fd75522b13c9be42cf1c9e812 Mon Sep 17 00:00:00 2001 From: jimin Date: Thu, 21 Sep 2023 17:07:23 +0900 Subject: [PATCH 09/10] feat: add swagger && /me endpoint && apply authValidate --- backend/src/v1/routes/users.routes.ts | 52 ++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/backend/src/v1/routes/users.routes.ts b/backend/src/v1/routes/users.routes.ts index b5e4efa2..7930156e 100644 --- a/backend/src/v1/routes/users.routes.ts +++ b/backend/src/v1/routes/users.routes.ts @@ -2,7 +2,7 @@ import { Router } from 'express'; import { roleSet } from '~/v1/auth/auth.type'; import authValidate from '~/v1/auth/auth.validate'; import { - create, getVersion, myupdate, search, update, + create, getVersion, myupdate, search, update, mydata, } from '~/v1/users/users.controller'; export const path = '/users'; @@ -358,10 +358,60 @@ export const router = Router(); * type: string * example: gshim.v1 */ + /** + * @openapi + * /api/users/me: + * get: + * description: 내 정보를 가져온다. + * tags: + * - users + * responses: + * '200': + * description: 내 정보를 반환한다. + * content: + * application/json: + * schema: + * properties: + * nickname: + * description: 에러코드 + * type: string + * example: jimin + * intraId: + * description: 인트라 ID + * type: string + * example: 10035 + * slack: + * description: slack 맴버 변수 + * type: string + * example: "U02LNNDRC9F" + * role: + * description: 유저의 권한 + * type: string + * example: 2 + * penaltyEbdDate: + * description: 패널티가 끝나는 날 + * type: string + * example: 2022-06-18 + * overDueDay: + * description: 현재 연체된 날수 + * type: string + * format: number + * example: 0 + * reservations: + * description: 해당 유저의 예약 정보 + * type: array + * example: [] + * lendings: + * description: 해당 유저의 대출 정보 + * type: array + * example: [] + */ +// TODO: search 에 authValildate(roleSet.librarian) 추가 router.get('/search', search) .post('/create', create) .patch('/update/:id', authValidate(roleSet.librarian), update) .patch('/myupdate', authValidate(roleSet.all), myupdate) + .get('/me', authValidate(roleSet.all), mydata) .get('/EasterEgg', getVersion); // .delete('/delete/:id', authValidate(roleSet.librarian), deleteUser); \ No newline at end of file From 5754befd96e48f600d4b36de1874e030e49cc498 Mon Sep 17 00:00:00 2001 From: jimin Date: Thu, 21 Sep 2023 17:30:45 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20searchUsersById=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=9D=B4=EC=A0=84=EA=B3=BC=20=EA=B0=99?= =?UTF-8?q?=EC=9D=B4=20=EB=A6=AC=ED=84=B4=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit searchUsersById 서비스 함수의 종속성이 생각보다 많음.controller 에서 items 의 length 를 확인하도록 --- backend/src/v1/users/users.controller.ts | 2 +- backend/src/v1/users/users.service.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/v1/users/users.controller.ts b/backend/src/v1/users/users.controller.ts index dd53380f..11374656 100644 --- a/backend/src/v1/users/users.controller.ts +++ b/backend/src/v1/users/users.controller.ts @@ -193,7 +193,7 @@ export const mydata = async ( try { const user = await usersService.searchUserById(parseInt(tokenId, 10)); console.log(user); - if (!user || !user.items || !user.items[0]) return res.status(404).send('Not Found'); + if (user.items.length === 0) return res.status(404).send('Not Found'); return res.status(200).json(user.items[0]); } catch (error: any) { logger.error(error); diff --git a/backend/src/v1/users/users.service.ts b/backend/src/v1/users/users.service.ts index 4e280514..b641e6a9 100644 --- a/backend/src/v1/users/users.service.ts +++ b/backend/src/v1/users/users.service.ts @@ -56,7 +56,6 @@ export default class UsersService { } async searchUserById(id: number) { - if (!id) return null; let items = (await this.usersRepository.searchUserBy({ id }, 0, 0))[0]; items = await this.withLendingInfo(items); return { items };