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: