diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index b3b072f..0000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: autofix.ci # needed to securely identify the workflow - -on: - pull_request: - push: - branches: ["main"] - -defaults: - run: - working-directory: backend - -permissions: - contents: read - -jobs: - autofix: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: pnpm 설치 - uses: pnpm/action-setup@v4 - with: - version: 9 - package_json_file: "backend/package.json" - - - name: Node.js 22 설치 - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: "pnpm" - cache-dependency-path: backend/pnpm-lock.yaml - - - name: 의존성 설치 - run: pnpm install --frozen-lockfile - - - name: 포매팅 - run: | - pnpm format - pnpm lint - - - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index afaefe3..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI -on: - push: - branches: [main] - pull_request: - branches: [main] - -defaults: - run: - working-directory: backend - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20, 22] - steps: - - uses: actions/checkout@v4 - - - name: pnpm 설치 - uses: pnpm/action-setup@v4 - with: - version: 9 - package_json_file: "backend/package.json" - - - name: Node.js ${{ matrix.node-version }} 설치 - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "pnpm" - cache-dependency-path: backend/pnpm-lock.yaml - - - name: 의존성 설치 - run: pnpm install --frozen-lockfile - - - name: 테스트 - run: | - pnpm lint:ci - pnpm typecheck - pnpm test:cov - - - name: Codecov에 커버리지 결과 배포 - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e1e1c63..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} diff --git a/backend/package.json b/backend/package.json index 92e92a7..b7a67b7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,9 @@ "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.4.0", + "@nestjs/typeorm": "^10.0.2", + "dotenv": "^16.4.5", + "mysql2": "^3.11.0", "nestjs-zod": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 01d648c..1867c84 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -31,7 +31,16 @@ importers: version: 7.8.1 typeorm: specifier: ^0.3.20 - version: 0.3.20(ts-node@10.9.2(@types/node@20.16.3)(typescript@5.5.4)) + version: 0.3.20(mysql2@3.11.0)(ts-node@10.9.2) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.4.1)(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + mysql2: + specifier: ^3.11.0 + version: 3.11.0 devDependencies: '@nestjs/cli': specifier: ^10.0.0 @@ -3060,7 +3069,7 @@ snapshots: '@babel/traverse': 7.25.6 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3228,7 +3237,7 @@ snapshots: '@babel/parser': 7.25.6 '@babel/template': 7.25.0 '@babel/types': 7.25.6 - debug: 4.3.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3258,7 +3267,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.6 + debug: 4.3.7 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -3274,7 +3283,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -3305,7 +3314,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -3318,7 +3327,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 @@ -3350,7 +3359,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -3368,7 +3377,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.16.3 + '@types/node': 20.16.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -3390,7 +3399,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -3460,7 +3469,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.16.3 + '@types/node': 20.16.5 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -3584,7 +3593,7 @@ snapshots: comment-json: 4.2.3 jsonc-parser: 3.3.1 pluralize: 8.0.0 - typescript: 5.5.4 + typescript: 5.6.2 transitivePeerDependencies: - chokidar @@ -3689,7 +3698,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 20.16.3 + '@types/node': 20.16.5 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -3744,7 +3753,7 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.16.3 + '@types/node': 20.16.5 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -3753,7 +3762,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.16.3 + '@types/node': 20.16.5 form-data: 4.0.0 '@types/supertest@6.0.2': @@ -3770,11 +3779,11 @@ snapshots: '@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.3.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/type-utils': 8.3.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.3.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.3.0 + '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.5.0 + '@typescript-eslint/type-utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.5.0 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -3787,11 +3796,11 @@ snapshots: '@typescript-eslint/parser@8.3.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.3.0 - debug: 4.3.6 + '@typescript-eslint/scope-manager': 8.5.0 + '@typescript-eslint/types': 8.5.0 + '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.5.0 + debug: 4.3.7 eslint: 8.57.0 optionalDependencies: typescript: 5.5.4 @@ -3819,9 +3828,9 @@ snapshots: '@typescript-eslint/typescript-estree@8.3.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/visitor-keys': 8.3.0 - debug: 4.3.6 + '@typescript-eslint/types': 8.5.0 + '@typescript-eslint/visitor-keys': 8.5.0 + debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -3835,9 +3844,9 @@ snapshots: '@typescript-eslint/utils@8.3.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.5.0 + '@typescript-eslint/types': 8.5.0 + '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) eslint: 8.57.0 transitivePeerDependencies: - supports-color @@ -3845,7 +3854,7 @@ snapshots: '@typescript-eslint/visitor-keys@8.3.0': dependencies: - '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/types': 8.5.0 eslint-visitor-keys: 3.4.3 '@ungap/structured-clone@1.2.0': {} @@ -4123,8 +4132,8 @@ snapshots: browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001655 - electron-to-chromium: 1.5.13 + caniuse-lite: 1.0.30001660 + electron-to-chromium: 1.5.18 node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) @@ -4482,7 +4491,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.3.7 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -4966,7 +4975,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.6 + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -5004,7 +5013,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -5098,7 +5107,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -5108,7 +5117,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.16.3 + '@types/node': 20.16.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -5147,7 +5156,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -5182,7 +5191,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -5210,9 +5219,9 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 - cjs-module-lexer: 1.4.0 + cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 glob: 7.2.3 graceful-fs: 4.2.11 @@ -5256,7 +5265,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -5275,7 +5284,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.3 + '@types/node': 20.16.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -5284,13 +5293,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.16.3 + '@types/node': 20.16.5 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 20.16.3 + '@types/node': 20.16.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -5916,7 +5925,7 @@ snapshots: strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 + ansi-regex: 6.1.0 strip-bom@3.0.0: {} @@ -5930,7 +5939,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.6 + debug: 4.3.7 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -5978,7 +5987,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 - terser: 5.31.6 + terser: 5.32.0 webpack: 5.94.0 terser@5.31.6: @@ -6039,7 +6048,7 @@ snapshots: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.5.4 + typescript: 5.6.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.25.2 @@ -6054,7 +6063,7 @@ snapshots: micromatch: 4.0.8 semver: 7.6.3 source-map: 0.7.4 - typescript: 5.5.4 + typescript: 5.6.2 webpack: 5.94.0 ts-node@10.9.2(@types/node@20.16.3)(typescript@5.5.4): @@ -6064,14 +6073,14 @@ snapshots: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.16.3 + '@types/node': 20.16.5 acorn: 8.12.1 - acorn-walk: 8.3.3 + acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.4 + typescript: 5.6.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -6116,10 +6125,11 @@ snapshots: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.13 - debug: 4.3.6 + debug: 4.3.7 dotenv: 16.4.5 - glob: 10.4.2 + glob: 10.4.5 mkdirp: 2.1.6 + mysql2: 3.11.0 reflect-metadata: 0.2.2 sha.js: 2.4.11 tslib: 2.7.0 diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts index 6d6bf97..d22f389 100644 --- a/backend/src/app.controller.spec.ts +++ b/backend/src/app.controller.spec.ts @@ -19,10 +19,4 @@ describe('AppController', () => { expect(appController.getHello()).toBe('Hello World!'); }); }); - - describe('hello/:name', () => { - it('should return "Hello Nest!"', () => { - expect(appController.getHelloWithName('Nest')).toBe('Hello Nest!'); - }); - }); }); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts index dfdff83..cce879e 100644 --- a/backend/src/app.controller.ts +++ b/backend/src/app.controller.ts @@ -1,6 +1,5 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @Controller() export class AppController { @@ -10,15 +9,4 @@ export class AppController { getHello(): string { return this.appService.getHello(); } - - @Get('hello/:name') - @ApiOperation({ - summary: '인사말을 반환합니다.', - description: '이름을 입력하면 그 이름을 포함한 인사말을 반환합니다.', - }) - @ApiParam({ name: 'name', description: '이름', example: 'Nest' }) - @ApiResponse({ status: 200, description: '성공', example: 'Hello Nest!' }) - getHelloWithName(@Param('name') name: string): string { - return this.appService.getHello(name); - } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index fc1eaed..4fc6281 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,12 +1,27 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; // Add this line import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { HistoriesController } from './controller/histories.controller'; -import { HistoriesService } from './service/histories.service'; +import { HistoriesModule } from './repository/module/histories.module'; + +import * as dotenv from 'dotenv'; +dotenv.config(); @Module({ - imports: [], - controllers: [AppController, HistoriesController], - providers: [AppService, HistoriesService], + imports: [ + TypeOrmModule.forRoot({ + type: 'mysql', + host: process.env.RDS_HOSTNAME, + port: 3306, + username: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, + database: process.env.RDS_DB_NAME, + entities: [__dirname + '/**/*{.ts,.js}'], + synchronize: false, + }), + HistoriesModule, + ], + controllers: [AppController], + providers: [AppService], }) export class AppModule {} diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts index 0329b72..927d7cc 100644 --- a/backend/src/app.service.ts +++ b/backend/src/app.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - getHello(name?: string): string { - return `Hello ${name ?? 'World'}!`; + getHello(): string { + return 'Hello World!'; } } diff --git a/backend/src/controller/histories.controller.ts b/backend/src/controller/histories.controller.ts index e726b18..109df36 100644 --- a/backend/src/controller/histories.controller.ts +++ b/backend/src/controller/histories.controller.ts @@ -6,7 +6,7 @@ export class HistoriesController { constructor(private historiesService: HistoriesService) {} @Get() - getHistories(): string { + getHistories() { return this.historiesService.getHistories(); } } diff --git a/backend/src/database.module.ts b/backend/src/database.module.ts new file mode 100644 index 0000000..857bc80 --- /dev/null +++ b/backend/src/database.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { databaseProviders } from './database.providers'; + +@Module({ + providers: [...databaseProviders], + exports: [...databaseProviders], +}) +export class DatabaseModule {} diff --git a/backend/src/database.providers.ts b/backend/src/database.providers.ts new file mode 100644 index 0000000..8f0f1fc --- /dev/null +++ b/backend/src/database.providers.ts @@ -0,0 +1,21 @@ +import { DataSource } from 'typeorm'; + +export const databaseProviders = [ + { + provide: 'DATA_SOURCE', + useFactory: async () => { + const dataSource = new DataSource({ + type: 'mysql', + host: process.env.RDS_HOSTNAME, + port: 3306, + username: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, + database: process.env.RDS_DB_NAME, + entities: [__dirname + './entity/entities/*.entity{.ts,.js}'], + synchronize: false, + }); + + return dataSource.initialize(); + }, + }, +]; diff --git a/backend/src/entities/Book.ts b/backend/src/entities/Book.ts new file mode 100644 index 0000000..38a7a48 --- /dev/null +++ b/backend/src/entities/Book.ts @@ -0,0 +1,67 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { BookInfo } from './BookInfo'; +import { User } from './User'; +import { Lending } from './Lending'; +import { Reservation } from './Reservation'; + +@Index('FK_donator_id_from_user', ['donatorId'], {}) +@Entity('book') +export class Book { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id?: number; + + @Column('varchar', { name: 'donator', nullable: true, length: 255 }) + donator: string | null; + + @Column('varchar', { name: 'callSign', length: 255 }) + callSign: string; + + @Column('int', { name: 'status' }) + status: number; + + @Column('datetime', { + name: 'createdAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + createdAt?: Date; + + @Column('int') + infoId: number; + + @Column('datetime', { + name: 'updatedAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + updatedAt?: Date; + + @Column('int', { name: 'donatorId', nullable: true }) + donatorId: number | null; + + @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.books, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'infoId', referencedColumnName: 'id' }]) + info?: BookInfo; + + @ManyToOne(() => User, (user) => user.books, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'donatorId', referencedColumnName: 'id' }]) + donator2?: User; + + @OneToMany(() => Lending, (lending) => lending.book) + lendings?: Lending[]; + + @OneToMany(() => Reservation, (reservation) => reservation.book) + reservations?: Reservation[]; +} diff --git a/backend/src/entities/BookInfo.ts b/backend/src/entities/BookInfo.ts new file mode 100644 index 0000000..a0c19bc --- /dev/null +++ b/backend/src/entities/BookInfo.ts @@ -0,0 +1,82 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Book } from './Book'; +import { Category } from './Category'; +import { Likes } from './Likes'; +import { Reservation } from './Reservation'; +import { Reviews } from './Reviews'; +import { SuperTag } from './SuperTag'; +import { BookInfoSearchKeywords } from './BookInfoSearchKeywords'; + +@Index('categoryId', ['categoryId'], {}) +@Entity('book_info') +export class BookInfo { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id?: number; + + @Column('varchar', { name: 'title', length: 255 }) + title?: string; + + @Column('varchar', { name: 'author', length: 255 }) + author?: string; + + @Column('varchar', { name: 'publisher', length: 255 }) + publisher?: string; + + @Column('varchar', { name: 'isbn', nullable: true, length: 255 }) + isbn?: string | null; + + @Column('varchar', { name: 'image', nullable: true, length: 255 }) + image?: string | null; + + @Column('date', { name: 'publishedAt', nullable: true }) + publishedAt?: string | null; + + @Column('datetime', { + name: 'createdAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + createdAt?: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + updatedAt?: Date; + + @Column('int', { name: 'categoryId' }) + categoryId?: number; + + @OneToMany(() => Book, (book) => book.info) + books?: Book[]; + + @ManyToOne(() => Category, (category) => category.bookInfos, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'categoryId', referencedColumnName: 'id' }]) + category?: Category; + + @OneToMany(() => Likes, (likes) => likes.bookInfo) + likes?: Likes[]; + + @OneToMany(() => Reservation, (reservation) => reservation.bookInfo) + reservations?: Reservation[]; + + @OneToMany(() => Reviews, (reviews) => reviews.bookInfo) + reviews?: Reviews[]; + + @OneToMany(() => SuperTag, (superTags) => superTags.userId) + superTags?: SuperTag[]; + + @OneToOne(() => BookInfoSearchKeywords, (bookInfoSearchKeyword) => bookInfoSearchKeyword.bookInfo) + bookInfoSearchKeyword?: BookInfoSearchKeywords; +} diff --git a/backend/src/entities/BookInfoSearchKeywords.ts b/backend/src/entities/BookInfoSearchKeywords.ts new file mode 100644 index 0000000..a51b03e --- /dev/null +++ b/backend/src/entities/BookInfoSearchKeywords.ts @@ -0,0 +1,34 @@ +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; + + @Column('varchar', { name: 'disassembled_title', length: 255 }) + disassembledTitle?: string; + + @Column('varchar', { name: 'disassembled_author', length: 255 }) + disassembledAuthor?: string; + + @Column('varchar', { name: 'disassembled_publisher', length: 255 }) + disassembledPublisher?: string; + + @Column('varchar', { name: 'title_initials', length: 255 }) + titleInitials?: string; + + @Column('varchar', { name: 'author_initials', length: 255 }) + authorInitials?: string; + + @Column('varchar', { name: 'publisher_initials', length: 255 }) + publisherInitials?: string; + + @Column('int', { name: 'book_info_id' }) + bookInfoId?: number; + + @OneToOne(() => BookInfo, (bookInfo) => bookInfo.id) + @JoinColumn([{ name: 'book_info_id', referencedColumnName: 'id' }]) + bookInfo?: BookInfo; +} diff --git a/backend/src/entities/Category.ts b/backend/src/entities/Category.ts new file mode 100644 index 0000000..cd6cca5 --- /dev/null +++ b/backend/src/entities/Category.ts @@ -0,0 +1,16 @@ +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { BookInfo } from './BookInfo'; + +@Index('id', ['id'], { unique: true }) +@Index('name', ['name'], { unique: true }) +@Entity('category', { schema: '42library' }) +export class Category { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('varchar', { name: 'name', unique: true, length: 255 }) + name: string; + + @OneToMany(() => BookInfo, (bookInfo) => bookInfo.category) + bookInfos: BookInfo[]; +} diff --git a/backend/src/entities/Lending.ts b/backend/src/entities/Lending.ts new file mode 100644 index 0000000..4a321f4 --- /dev/null +++ b/backend/src/entities/Lending.ts @@ -0,0 +1,76 @@ +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'], {}) +@Entity('lending', { schema: '42library' }) +export class Lending { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('int', { name: 'lendingLibrarianId' }) + lendingLibrarianId: number; + + @Column('varchar', { name: 'lendingCondition', length: 255 }) + lendingCondition: string; + + @Column('int', { name: 'returningLibrarianId', nullable: true }) + returningLibrarianId: number | null; + + @Column('varchar', { + name: 'returningCondition', + nullable: true, + length: 255, + }) + returningCondition: string | null; + + @Column('datetime', { name: 'returnedAt', nullable: true }) + returnedAt: Date | null; + + @Column('timestamp', { + name: 'createdAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + createdAt: Date; + + @Column('timestamp', { + name: 'updatedAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + updatedAt: Date; + + @ManyToOne(() => Book, (book) => book.lendings, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) + book: Book; + + @Column({ name: 'bookId', type: 'int' }) + bookId: number; + + @ManyToOne(() => User, (user) => user.lendings, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) + user: User; + + @Column({ name: 'userId', type: 'int' }) + userId: number; + + @ManyToOne(() => User, (user) => user.lendings2, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'lendingLibrarianId', referencedColumnName: 'id' }]) + lendingLibrarian: User; + + @ManyToOne(() => User, (user) => user.lendings3, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'returningLibrarianId', referencedColumnName: 'id' }]) + returningLibrarian: User; +} diff --git a/backend/src/entities/Likes.ts b/backend/src/entities/Likes.ts new file mode 100644 index 0000000..86a147c --- /dev/null +++ b/backend/src/entities/Likes.ts @@ -0,0 +1,34 @@ +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'], {}) +@Entity('likes', { schema: '42library' }) +export class Likes { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('int', { name: 'userId' }) + userId: number; + + @Column('int', { name: 'bookInfoId' }) + bookInfoId: number; + + @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) + isDeleted: boolean; + + @ManyToOne(() => User, (user) => user.likes, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) + user: User; + + @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.likes, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) + bookInfo: BookInfo; +} diff --git a/backend/src/entities/Reservation.ts b/backend/src/entities/Reservation.ts new file mode 100644 index 0000000..d829214 --- /dev/null +++ b/backend/src/entities/Reservation.ts @@ -0,0 +1,59 @@ +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'], {}) +@Entity('reservation') +export class Reservation { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('datetime', { name: 'endAt', nullable: true }) + endAt: Date | null; + + @Column('datetime', { + name: 'createdAt', + default: () => 'CURRENT_TIMESTAMP(6)', + }) + createdAt: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => 'CURRENT_TIMESTAMP(6)', + }) + updatedAt: Date; + + @Column('int', { name: 'status', default: () => '0' }) + status: number; + + @Column('int', { name: 'bookInfoId' }) + bookInfoId: 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; + + @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reservations, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) + bookInfo: BookInfo; + + @ManyToOne(() => Book, (book) => book.reservations, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookId', referencedColumnName: 'id' }]) + book: Book; + + @Column('int', { name: 'bookId', nullable: true }) + bookId: number | null; +} diff --git a/backend/src/entities/Reviews.ts b/backend/src/entities/Reviews.ts new file mode 100644 index 0000000..62b1e75 --- /dev/null +++ b/backend/src/entities/Reviews.ts @@ -0,0 +1,61 @@ +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'], {}) +@Entity('reviews') +export class Reviews { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('int', { name: 'userId' }) + userId: number; + + @Column('int', { name: 'bookInfoId' }) + bookInfoId: number; + + @Column('datetime', { + name: 'createdAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + createdAt: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => "'CURRENT_TIMESTAMP(6)'", + }) + updatedAt: Date; + + @Column('int', { name: 'updateUserId' }) + updateUserId: number; + + @Column('tinyint', { name: 'isDeleted', width: 1, default: () => "'0'" }) + isDeleted: boolean; + + @Column('int', { name: 'deleteUserId', nullable: true }) + deleteUserId: number | null; + + @Column('text', { name: 'content' }) + content: string; + + @Column('tinyint', { name: 'disabled', width: 1, default: () => "'0'" }) + disabled: boolean; + + @Column('int', { name: 'disabledUserId', nullable: true }) + disabledUserId: number | null; + + @ManyToOne(() => User, (user) => user.reviews, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) + user: User; + + @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.reviews, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) + bookInfo: BookInfo; +} diff --git a/backend/src/entities/SearchKeywords.ts b/backend/src/entities/SearchKeywords.ts new file mode 100644 index 0000000..b7407de --- /dev/null +++ b/backend/src/entities/SearchKeywords.ts @@ -0,0 +1,20 @@ +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { SearchLogs } from './SearchLogs'; + +@Entity('search_keywords') +export class SearchKeywords { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id?: number; + + @Column('varchar', { name: 'keyword', length: 255 }) + keyword?: string; + + @Column('varchar', { name: 'disassembled_keyword', length: 255 }) + disassembledKeyword?: string; + + @Column('varchar', { name: 'initial_consonants', length: 255 }) + initialConsonants?: string; + + @OneToMany(() => SearchLogs, (searchLogs) => searchLogs.searchKeyword) + searchLogs?: SearchLogs[]; +} diff --git a/backend/src/entities/SearchLogs.ts b/backend/src/entities/SearchLogs.ts new file mode 100644 index 0000000..35249e8 --- /dev/null +++ b/backend/src/entities/SearchLogs.ts @@ -0,0 +1,22 @@ +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; + + @Column('int', { name: 'search_keyword_id' }) + searchKeywordId?: number; + + @Column('varchar', { name: 'timestamp', length: 255 }) + timestamp?: string; + + @ManyToOne(() => SearchKeywords, (SearchKeyword) => SearchKeyword.id, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'search_keyword_id', referencedColumnName: 'id' }]) + searchKeyword?: SearchKeywords; +} diff --git a/backend/src/entities/SubTag.ts b/backend/src/entities/SubTag.ts new file mode 100644 index 0000000..2124b39 --- /dev/null +++ b/backend/src/entities/SubTag.ts @@ -0,0 +1,58 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { User } from './User'; +import { SuperTag } from './SuperTag'; + +@Index('userId', ['userId'], {}) +@Index('superTagId', ['superTagId'], {}) +@Entity('sub_tag', { schema: 'jip_dev' }) +export class SubTag { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('int', { name: 'userId' }) + userId: number; + + @Column('int', { name: 'superTagId' }) + superTagId: number; + + @Column('datetime', { + name: 'createdAt', + default: () => 'current_timestamp(6)', + }) + createdAt: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => 'current_timestamp(6)', + }) + updatedAt: Date; + + @Column('tinyint', { name: 'isDeleted', default: () => '0' }) + isDeleted: number; + + @Column('int', { name: 'updateUserId' }) + updateUserId: number; + + @Column('varchar', { name: 'content', length: 42 }) + content: string; + + @Column('tinyint', { name: 'isPublic' }) + isPublic: number; + + @ManyToOne(() => User, (user) => user.subTag, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) + user: User; + + @ManyToOne(() => SuperTag, (superTag) => superTag.subTags, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'superTagId', referencedColumnName: 'id' }]) + superTag: SuperTag; + + @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) + bookInfoId: number; +} diff --git a/backend/src/entities/SuperTag.ts b/backend/src/entities/SuperTag.ts new file mode 100644 index 0000000..72a579e --- /dev/null +++ b/backend/src/entities/SuperTag.ts @@ -0,0 +1,64 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { SubTag } from './SubTag'; +import { User } from './User'; +import { BookInfo } from './BookInfo'; + +@Index('userId', ['userId'], {}) +@Index('bookInfoId', ['bookInfoId'], {}) +@Entity('super_tag', { schema: 'jip_dev' }) +export class SuperTag { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('int', { name: 'userId' }) + userId: number; + + @Column('int', { name: 'bookInfoId' }) + bookInfoId: number; + + @Column('datetime', { + name: 'createdAt', + default: () => 'current_timestamp(6)', + }) + createdAt: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => 'current_timestamp(6)', + }) + updatedAt: Date; + + @Column('tinyint', { name: 'isDeleted', default: () => '0' }) + isDeleted: number; + + @Column('int', { name: 'updateUserId' }) + updateUserId: number; + + @Column('varchar', { name: 'content', length: 42 }) + content: string; + + @OneToMany(() => SubTag, (subTag) => subTag.superTag) + subTags: SubTag[]; + + @ManyToOne(() => User, (user) => user.superTags, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }]) + user: User; + + @ManyToOne(() => BookInfo, (bookInfo) => bookInfo.superTags, { + onDelete: 'NO ACTION', + onUpdate: 'NO ACTION', + }) + @JoinColumn([{ name: 'bookInfoId', referencedColumnName: 'id' }]) + bookInfo: BookInfo; +} diff --git a/backend/src/entities/User.ts b/backend/src/entities/User.ts new file mode 100644 index 0000000..6d7bf39 --- /dev/null +++ b/backend/src/entities/User.ts @@ -0,0 +1,85 @@ +import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Book } from './Book'; +import { Lending } from './Lending'; +import { Likes } from './Likes'; +import { Reservation } from './Reservation'; +import { Reviews } from './Reviews'; +import { SubTag } from './SubTag'; +import { SuperTag } from './SuperTag'; + +@Index('email', ['email'], { unique: true }) +@Index('intraId', ['intraId'], { unique: true }) +@Index('slack', ['slack'], { unique: true }) +@Entity('user') +export class User { + @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) + id: number; + + @Column('varchar', { name: 'email', unique: true, length: 255 }) + email: string; + + @Column('varchar', { name: 'password', length: 255, select: false }) + password: string; + + @Column('varchar', { name: 'nickname', nullable: true, length: 255 }) + nickname: string | null; + + @Column('int', { name: 'intraId', nullable: true, unique: true }) + intraId: number | null; + + @Column('varchar', { + name: 'slack', + nullable: true, + unique: true, + length: 255, + }) + slack: string | null; + + @Column('datetime', { + name: 'penaltyEndDate', + default: () => 'CURRENT_TIMESTAMP', + }) + penaltyEndDate: Date; + + @Column('tinyint', { name: 'role', default: () => '0' }) + role: number; + + @Column('datetime', { + name: 'createdAt', + default: () => 'CURRENT_TIMESTAMP(6)', + }) + createdAt: Date; + + @Column('datetime', { + name: 'updatedAt', + default: () => 'CURRENT_TIMESTAMP(6)', + }) + updatedAt: Date; + + @OneToMany(() => Book, (book) => book.donator2) + books: Book[]; + + @OneToMany(() => Lending, (lending) => lending.user) + lendings: Lending[]; + + @OneToMany(() => Lending, (lending) => lending.lendingLibrarian) + lendings2: Lending[]; + + @OneToMany(() => Lending, (lending) => lending.returningLibrarian) + lendings3: Lending[]; + + @OneToMany(() => Likes, (likes) => likes.user) + likes: Likes[]; + + @OneToMany(() => Reservation, (reservation) => reservation.user) + reservations: Reservation[]; + + @OneToMany(() => Reviews, (reviews) => reviews.user) + reviews: Reviews[]; + + @OneToMany(() => SubTag, (subtag) => subtag.userId) + subTag: SubTag[]; + + @OneToMany(() => SuperTag, (superTags) => superTags.userId) + superTags: SuperTag[]; +} diff --git a/backend/src/entities/UserReservation.ts b/backend/src/entities/UserReservation.ts new file mode 100644 index 0000000..64e9905 --- /dev/null +++ b/backend/src/entities/UserReservation.ts @@ -0,0 +1,55 @@ +import { ViewEntity, ViewColumn, DataSource } from 'typeorm'; +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(*) + 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'), +}) +export class UserReservation { + @ViewColumn() + reservationId: number; + + @ViewColumn() + reservedBookInfoId: number; + + @ViewColumn() + reservationDate: Date; + + @ViewColumn() + endAt: Date; + + @ViewColumn() + ranking: number; + + @ViewColumn() + title: string; + + @ViewColumn() + author: string; + + @ViewColumn() + image: string; + + @ViewColumn() + userId: number; +} diff --git a/backend/src/entities/VHistories.ts b/backend/src/entities/VHistories.ts new file mode 100644 index 0000000..a7c5e4e --- /dev/null +++ b/backend/src/entities/VHistories.ts @@ -0,0 +1,84 @@ +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( + ` + 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'), +}) +export class VHistories { + @ViewColumn() + id: number; + + @ViewColumn() + lendingCondition: string; + + @ViewColumn() + login: string; + + @ViewColumn() + returningCondition: string; + + @ViewColumn() + penaltyDays: number; + + @ViewColumn() + callSign: string; + + @ViewColumn() + title: string; + + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + image: string; + + @ViewColumn() + createdAt: Date; + + @ViewColumn() + returnedAt: Date; + + @ViewColumn() + updatedAt: Date; + + @ViewColumn() + dueDate: Date; + + @ViewColumn() + lendingLibrarianNickName: string; + + @ViewColumn() + returningLibrarianNickname: string; +} diff --git a/backend/src/entities/VLending.ts b/backend/src/entities/VLending.ts new file mode 100644 index 0000000..71f086c --- /dev/null +++ b/backend/src/entities/VLending.ts @@ -0,0 +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'), +}) +export class VLending { + @ViewColumn() + id: number; + + @ViewColumn() + lendingCondition: string; + + @ViewColumn() + login: string; + + @ViewColumn() + penaltyDays: number; + + @ViewColumn() + bookId: number; + + @ViewColumn() + callSign: string; + + @ViewColumn() + title: string; + + @ViewColumn() + image: string; + + @ViewColumn() + createdAt: Date; + + @ViewColumn() + returnedAt: Date; + + @ViewColumn() + dueDate: Date; +} diff --git a/backend/src/entities/VLendingForSearchUser.ts b/backend/src/entities/VLendingForSearchUser.ts new file mode 100644 index 0000000..6690c25 --- /dev/null +++ b/backend/src/entities/VLendingForSearchUser.ts @@ -0,0 +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'), +}) +export class VLendingForSearchUser { + @ViewColumn() + userId: number; + + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + lendDate: Date; + + @ViewColumn() + lendingCondition: string; + + @ViewColumn() + image: string; + + @ViewColumn() + author: string; + + @ViewColumn() + title: string; + + @ViewColumn() + duedate: Date; + + @ViewColumn() + overDueDay: Date; + + @ViewColumn() + reservedNum: number; +} diff --git a/backend/src/entities/VSearchBook.ts b/backend/src/entities/VSearchBook.ts new file mode 100644 index 0000000..f1d82dd --- /dev/null +++ b/backend/src/entities/VSearchBook.ts @@ -0,0 +1,78 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { BookInfo } from './BookInfo'; +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'), +}) +export class VSearchBook { + @ViewColumn() + bookId: number; + + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + title: string; + + @ViewColumn() + author: string; + + @ViewColumn() + donator: string; + + @ViewColumn() + publisher: string; + + @ViewColumn() + publishedAt: string; + + @ViewColumn() + isbn: string; + + @ViewColumn() + image: string; + + @ViewColumn() + status: number; + + @ViewColumn() + categoryId: string; + + @ViewColumn() + callSign: string; + + @ViewColumn() + category: string; + + @ViewColumn() + isLendable: boolean; +} diff --git a/backend/src/entities/VSearchBookByTag.ts b/backend/src/entities/VSearchBookByTag.ts new file mode 100644 index 0000000..5d74c98 --- /dev/null +++ b/backend/src/entities/VSearchBookByTag.ts @@ -0,0 +1,75 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { BookInfo } from './BookInfo'; +import { Category } from './Category'; +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'), +}) +export class VSearchBookByTag { + @ViewColumn() + id: number; + + @ViewColumn() + title: string; + + @ViewColumn() + author: string; + + @ViewColumn() + isbn: number; + + @ViewColumn() + image: string; + + @ViewColumn() + publishedAt: string; + + @ViewColumn() + createdAt: string; + + @ViewColumn() + updatedAt: string; + + @ViewColumn() + category: string; + + @ViewColumn() + superTagContent: string; + + @ViewColumn() + subTagContent: string; + + @ViewColumn() + lendingCnt: number; +} + +export default VSearchBookByTag; diff --git a/backend/src/entities/VStock.ts b/backend/src/entities/VStock.ts new file mode 100644 index 0000000..4b81cd1 --- /dev/null +++ b/backend/src/entities/VStock.ts @@ -0,0 +1,76 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { BookInfo } from './BookInfo'; +import { Book } from './Book'; +import { Category } from './Category'; +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'), +}) +export class VStock { + @ViewColumn() + bookId: number; + + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + title: string; + + @ViewColumn() + author: string; + + @ViewColumn() + donator: string; + + @ViewColumn() + publisher: string; + + @ViewColumn() + publishedAt: string; + + @ViewColumn() + isbn: string; + + @ViewColumn() + image: string; + + @ViewColumn() + status: number; + + @ViewColumn() + categoryId: number; + + @ViewColumn() + callSign: string; + + @ViewColumn() + category: string; + + @ViewColumn() + updatedAt: Date; +} diff --git a/backend/src/entities/VTagsSubDefault.ts b/backend/src/entities/VTagsSubDefault.ts new file mode 100644 index 0000000..ca3ad9d --- /dev/null +++ b/backend/src/entities/VTagsSubDefault.ts @@ -0,0 +1,59 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { BookInfo } from './BookInfo'; +import { SuperTag } from './SuperTag'; +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'), +}) +export class VTagsSubDefault { + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + title: string; + + @ViewColumn() + id: number; + + @ViewColumn() + createdAt: string; + + @ViewColumn() + login: string; + + @ViewColumn() + content: string; + + @ViewColumn() + superTagId: number; + + @ViewColumn() + superContent: string; + + @ViewColumn() + isPublic: boolean; + + @ViewColumn() + isDeleted: boolean; + + @ViewColumn() + visibility: string; +} diff --git a/backend/src/entities/VTagsSuperDefault.ts b/backend/src/entities/VTagsSuperDefault.ts new file mode 100644 index 0000000..d5f5255 --- /dev/null +++ b/backend/src/entities/VTagsSuperDefault.ts @@ -0,0 +1,63 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { ObjectLiteral, SelectQueryBuilder } from 'typeorm/browser'; + +@ViewEntity('v_tags_super_default', { + expression: ` + SELECT + sp1.content AS content, + ( + SELECT + COUNT(sb1.id) + FROM + sub_tag sb1 + WHERE + sb1.superTagId = sp1.id + AND sb1.isDeleted IS FALSE + AND sb1.isPublic IS TRUE) AS COUNT, + 'super' AS type, + DATE_FORMAT(sp1.createdAt, '%Y-%m-%d' + ) AS createdAt + FROM + super_tag sp1 + WHERE + sp1.isDeleted IS FALSE + AND sp1.content <> 'default' + UNION + SELECT + sb2.content AS content, + 0 AS count, + 'default' AS type, + DATE_FORMAT(sb2.createdAt, '%Y-%m-%d') AS createdAt + FROM + ( + sub_tag sb2 + JOIN super_tag sp2 + ON sb2.superTagId = sp2.id + ) + WHERE + sp2.content = 'default' + AND sb2.isDeleted IS FALSE + AND sb2.isPublic IS TRUE + AND NOT EXISTS( + SELECT + 1 + FROM super_tag sp3 + WHERE sp3.content = sb2.content + LIMIT 1 + ) + ORDER BY RAND() + `, +}) +export class VTagsSuperDefault { + @ViewColumn() + content: string; + + @ViewColumn() + count: number; + + @ViewColumn() + type: string; + + @ViewColumn() + createdAt: string; +} diff --git a/backend/src/entities/VUserLending.ts b/backend/src/entities/VUserLending.ts new file mode 100644 index 0000000..39f6046 --- /dev/null +++ b/backend/src/entities/VUserLending.ts @@ -0,0 +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'), +}) +export class VUserLending { + @ViewColumn() + userId: number; + + @ViewColumn() + lendDate: Date; + + @ViewColumn() + lendingCondition: string; + + @ViewColumn() + bookInfoId: number; + + @ViewColumn() + title: string; + + @ViewColumn() + duedate: Date; + + @ViewColumn() + image: string; + + @ViewColumn() + overDueDay: number; +} diff --git a/backend/src/entities/index.ts b/backend/src/entities/index.ts new file mode 100644 index 0000000..e08aff7 --- /dev/null +++ b/backend/src/entities/index.ts @@ -0,0 +1,23 @@ +export * from './Book'; +export * from './BookInfo'; +export * from './BookInfoSearchKeywords'; +export * from './Category'; +export * from './Lending'; +export * from './Likes'; +export * from './Reservation'; +export * from './Reviews'; +export * from './SearchKeywords'; +export * from './SearchLogs'; +export * from './SubTag'; +export * from './SuperTag'; +export * from './User'; +export * from './UserReservation'; +export * from './VHistories'; +export * from './VLending'; +export * from './VLendingForSearchUser'; +export * from './VSearchBook'; +export * from './VStock'; +export * from './VTagsSubDefault'; +export * from './VTagsSuperDefault'; +export * from './VUserLending'; +export * from './VSearchBookByTag'; diff --git a/backend/src/main.ts b/backend/src/main.ts index e1eafb4..13cad38 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,25 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { patchNestJsSwagger } from 'nestjs-zod'; async function bootstrap() { const app = await NestFactory.create(AppModule); - - const config = new DocumentBuilder() - .setTitle('집현전 백엔드 6차') - .setDescription('집현전 백엔드 6차 API 명세') - .setVersion('0.0.1') - .setExternalDoc('JSON 명세', 'json') - .build(); - - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document, { - jsonDocumentUrl: 'api/json', - }); - await app.listen(3000); } - -patchNestJsSwagger(); bootstrap(); diff --git a/backend/src/repository/module/histories.module.ts b/backend/src/repository/module/histories.module.ts new file mode 100644 index 0000000..9362380 --- /dev/null +++ b/backend/src/repository/module/histories.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { HistoriesService } from 'src/service/histories.service'; +import { historiesProviders } from '../providers/histories.providers'; // Import the 'historiesProviders' variable from the correct file +import { TypeOrmModule } from '@nestjs/typeorm'; +import { VHistories } from 'src/entities/VHistories'; +import { HistoriesController } from 'src/controller/histories.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([VHistories])], + providers: [...historiesProviders, HistoriesService], + controllers: [HistoriesController], +}) +export class HistoriesModule {} diff --git a/backend/src/repository/providers/histories.providers.ts b/backend/src/repository/providers/histories.providers.ts new file mode 100644 index 0000000..eef7610 --- /dev/null +++ b/backend/src/repository/providers/histories.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { VHistories } from 'src/entities'; + +export const historiesProviders = [ + { + provide: 'VHISTORIES_REPOSITORY', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(VHistories), + inject: [DataSource], + }, +]; diff --git a/backend/src/service/histories.service.ts b/backend/src/service/histories.service.ts index c8e4840..6a9fd95 100644 --- a/backend/src/service/histories.service.ts +++ b/backend/src/service/histories.service.ts @@ -1,9 +1,16 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { VHistories } from 'src/entities'; +import { Repository } from 'typeorm'; @Injectable() export class HistoriesService { - constructor() {} - getHistories(): string { - return 'Histories'; + constructor( + @InjectRepository(VHistories) + private historiesRepository: Repository, + ) {} + + async getHistories() { + return await this.historiesRepository.find(); } } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index a1c778d..1cd0a92 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -16,6 +16,7 @@ "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "types": ["node"], } }