diff --git a/BE/src/config/query-database/admin-db-manager.service.ts b/BE/src/config/query-database/admin-db-manager.service.ts index afb0e289..10f190c2 100644 --- a/BE/src/config/query-database/admin-db-manager.service.ts +++ b/BE/src/config/query-database/admin-db-manager.service.ts @@ -13,8 +13,7 @@ export class AdminDBManager implements OnModuleInit { constructor(private readonly configService: ConfigService) {} async onModuleInit() { - // 서버 테스트를 위한 주석처리 - //this.createAdminConnection(); + this.createAdminConnection(); } private createAdminConnection() { diff --git a/BE/src/config/query-database/user-db-manager.service.ts b/BE/src/config/query-database/user-db-manager.service.ts index a66b6582..5a775c79 100644 --- a/BE/src/config/query-database/user-db-manager.service.ts +++ b/BE/src/config/query-database/user-db-manager.service.ts @@ -1,12 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { QueryResult } from 'mysql2/promise'; -import { ConfigService } from '@nestjs/config'; +import { Connection, QueryResult } from 'mysql2/promise'; @Injectable() export class UserDBManager { - constructor(private readonly configService: ConfigService) {} - async run(req: any, query: string): Promise { - const connection = await req.dbConnection; + async run(connection: Connection, query: string): Promise { const [result] = await connection.query(query); return result; } diff --git a/BE/src/query/query.controller.ts b/BE/src/query/query.controller.ts index 25db527a..c5c8d1d1 100644 --- a/BE/src/query/query.controller.ts +++ b/BE/src/query/query.controller.ts @@ -30,10 +30,15 @@ export class QueryController { @Post('/:shellId/execute') @UseGuards(ShellGuard) async executeQuery( - @Req() req: Request, + @Req() req: any, @Param('shellId') shellId: number, @Body() queryDto: QueryDto, ) { - return await this.queryService.execute(req, shellId, queryDto); + return await this.queryService.execute( + req.dbConnection, + req.sessionID, + shellId, + queryDto, + ); } } diff --git a/BE/src/query/query.service.ts b/BE/src/query/query.service.ts index 6d0557dc..b1d94b50 100644 --- a/BE/src/query/query.service.ts +++ b/BE/src/query/query.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { QueryDto } from './dto/query.dto'; import { QueryType } from '../common/enums/query-type.enum'; import { ShellService } from '../shell/shell.service'; -import { ResultSetHeader, RowDataPacket } from 'mysql2/promise'; +import { Connection, ResultSetHeader, RowDataPacket } from 'mysql2/promise'; import { Shell } from '../shell/shell.entity'; import { UserDBManager } from '../config/query-database/user-db-manager.service'; import { UsageService } from 'src/usage/usage.service'; @@ -17,12 +17,17 @@ export class QueryService { private readonly redisService: RedisService, ) {} - async execute(req: any, shellId: number, queryDto: QueryDto) { - this.redisService.setActiveUser(req.sessionID); + async execute( + connection: Connection, + sessionId: string, + shellId: number, + queryDto: QueryDto, + ) { + this.redisService.setActiveUser(sessionId); await this.shellService.findShellOrThrow(shellId); const baseUpdateData = { - sessionId: req.sessionID, + sessionId: sessionId, query: queryDto.query, queryType: this.detectQueryType(queryDto.query), }; @@ -35,7 +40,11 @@ export class QueryService { text: '지원하지 않는 쿼리입니다.', }); } - updateData = await this.processQuery(req, baseUpdateData, queryDto.query); + updateData = await this.processQuery( + connection, + baseUpdateData, + queryDto.query, + ); } catch (e) { const text = `ERROR ${e.errno || ''} (${e.sqlState || ''}): ${e.sqlMessage || ''}`; @@ -47,21 +56,19 @@ export class QueryService { }; return await this.shellService.replace(shellId, updateData); } - await this.usageService.updateRowCount(req); + await this.usageService.updateRowCount(sessionId); return await this.shellService.replace(shellId, updateData); } private async processQuery( - req: any, + connection: Connection, baseUpdateData: any, query: string, ): Promise> { const isResultTable = this.existResultTable(baseUpdateData.queryType); - const rows = await this.userDBManager.run(req, query); - const runTime = await this.measureQueryRunTime(req); - - // Update usage + const rows = await this.userDBManager.run(connection, query); + const runTime = await this.measureQueryRunTime(connection); let text: string; let resultTable: RowDataPacket[]; @@ -100,11 +107,12 @@ export class QueryService { return validTypes.includes(type); } - async measureQueryRunTime(req: any): Promise { + async measureQueryRunTime(connection: Connection): Promise { try { + const query = `SHOW PROFILES`; const rows = (await this.userDBManager.run( - req, - 'show profiles;', + connection, + query, )) as RowDataPacket[]; let lastQueryRunTime = rows[rows.length - 1]?.Duration; lastQueryRunTime = Math.round(lastQueryRunTime * 1000) / 1000 || 0; diff --git a/BE/src/record/record.controller.ts b/BE/src/record/record.controller.ts index 85bb77e2..d2c7e028 100644 --- a/BE/src/record/record.controller.ts +++ b/BE/src/record/record.controller.ts @@ -7,8 +7,8 @@ import { ResRecordDto } from './dto/res-record.dto'; import { ExecuteRecordSwagger } from '../config/swagger/record-swagger.decorator'; import { Serialize } from '../interceptors/serialize.interceptor'; import { UserDBConnectionInterceptor } from '../interceptors/user-db-connection.interceptor'; -import { Request } from 'express'; +@UseInterceptors(UserDBConnectionInterceptor) @ApiExtraModels(ResponseDto, ResRecordDto) @ApiTags('랜덤 데이터 생성 API') @Controller('api/record') @@ -20,10 +20,13 @@ export class RecordController { @Serialize(ResRecordDto) @Post() async insertRandomRecord( - @Req() req: Request, + @Req() req: any, @Body() randomRecordInsertDto: CreateRandomRecordDto, ) { - await this.recordService.validateDto(randomRecordInsertDto, req.sessionID); + await this.recordService.validateDto( + randomRecordInsertDto, + req.dbConnection, + ); return this.recordService.insertRandomRecord(req, randomRecordInsertDto); } } diff --git a/BE/src/record/record.service.ts b/BE/src/record/record.service.ts index f1eb98b8..d0826a8f 100644 --- a/BE/src/record/record.service.ts +++ b/BE/src/record/record.service.ts @@ -16,6 +16,7 @@ import { UsageService } from '../usage/usage.service'; import { FileService } from './file.service'; import { TableService } from '../table/table.service'; import { ResTableDto } from '../table/dto/res-table.dto'; +import { Connection } from "mysql2/promise"; @Injectable() export class RecordService { @@ -27,38 +28,37 @@ export class RecordService { async validateDto( createRandomRecordDto: CreateRandomRecordDto, - identify: string, + connection: Connection, ) { - const tableInfo: ResTableDto = (await this.tableService.find( - identify, - createRandomRecordDto.tableName, - )) as ResTableDto; - - if (!tableInfo?.tableName) - throw new BadRequestException( - `${createRandomRecordDto.tableName} 테이블이 존재하지 않습니다.`, - ); - - const baseColumns = tableInfo.columns; - const columnInfos: RandomColumnInfo[] = createRandomRecordDto.columns; - - columnInfos.forEach((columnInfo) => { - const targetName = columnInfo.name; - const targetDomain = columnInfo.type; - const baseColumn = baseColumns.find( - (column) => column.name === columnInfo.name, - ); - - if (!baseColumn) - throw new BadRequestException( - `${targetName} 컬럼이 ${createRandomRecordDto.tableName} 에 존재하지 않습니다.`, - ); - - if (!this.checkDomainAvailability(baseColumn.type, targetDomain)) - throw new BadRequestException( - `${targetName}(${baseColumn.type}) 컬럼에 ${targetDomain} 랜덤 값을 넣을 수 없습니다.`, - ); - }); + // const tableInfo: ResTableDto = (await this.tableService.find(connection + // createRandomRecordDto.tableName, + // )) + // + // if (!tableInfo?.tableName) + // throw new BadRequestException( + // `${createRandomRecordDto.tableName} 테이블이 존재하지 않습니다.`, + // ); + // + // const baseColumns = tableInfo.columns; + // const columnInfos: RandomColumnInfo[] = createRandomRecordDto.columns; + // + // columnInfos.forEach((columnInfo) => { + // const targetName = columnInfo.name; + // const targetDomain = columnInfo.type; + // const baseColumn = baseColumns.find( + // (column) => column.name === columnInfo.name, + // ); + // + // if (!baseColumn) + // throw new BadRequestException( + // `${targetName} 컬럼이 ${createRandomRecordDto.tableName} 에 존재하지 않습니다.`, + // ); + // + // if (!this.checkDomainAvailability(baseColumn.type, targetDomain)) + // throw new BadRequestException( + // `${targetName}(${baseColumn.type}) 컬럼에 ${targetDomain} 랜덤 값을 넣을 수 없습니다.`, + // ); + // }); } checkDomainAvailability(mysqlType: string, targetDomain: string) { diff --git a/BE/src/table/dto/res-table.dto.ts b/BE/src/table/dto/res-table.dto.ts index 362983e0..6e5f1687 100644 --- a/BE/src/table/dto/res-table.dto.ts +++ b/BE/src/table/dto/res-table.dto.ts @@ -7,8 +7,9 @@ export class ResTableDto { @Expose() columns: ColumnDto[]; - constructor(init?: Partial) { - Object.assign(this, init); + constructor(tableName: string, columns: ColumnDto[]) { + this.tableName = tableName; + this.columns = columns; } } @@ -25,9 +26,6 @@ export class ColumnDto { @Expose() FK: string; - @Expose() - IDX: boolean; - @Expose() UQ: boolean; diff --git a/BE/src/table/table.controller.ts b/BE/src/table/table.controller.ts index 82053060..d5b7842d 100644 --- a/BE/src/table/table.controller.ts +++ b/BE/src/table/table.controller.ts @@ -1,13 +1,17 @@ -import { Controller, Get, Param, Req } from '@nestjs/common'; +import { Controller, Get, Param, Req, UseInterceptors } from '@nestjs/common'; import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; -import { Request } from 'express'; import { TableService } from './table.service'; import { Serialize } from '../interceptors/serialize.interceptor'; import { ResTableDto } from './dto/res-table.dto'; import { ResTablesDto } from './dto/res-tables.dto'; -import { GetTableListSwagger, GetTableSwagger } from '../config/swagger/table-swagger.decorator'; +import { + GetTableListSwagger, + GetTableSwagger, +} from '../config/swagger/table-swagger.decorator'; import { ResponseDto } from '../common/response/response.dto'; +import { UserDBConnectionInterceptor } from '../interceptors/user-db-connection.interceptor'; +@UseInterceptors(UserDBConnectionInterceptor) @ApiExtraModels(ResponseDto, ResTablesDto, ResTableDto) @ApiTags('테이블 가져오기 API') @Controller('api') @@ -17,14 +21,14 @@ export class TableController { @GetTableListSwagger() @Serialize(ResTablesDto) @Get('/tables') - async findAll(@Req() req: Request) { - return await this.tableService.findAll(req.sessionID); + async findAll(@Req() req: any) { + return await this.tableService.findAll(req.dbConnection, req.sessionID); } @GetTableSwagger() @Serialize(ResTableDto) @Get('/tables/:tableName') - async find(@Req() req: Request, @Param('tableName') tableName: string) { - return await this.tableService.find(req.sessionID, tableName); + async find(@Req() req: any, @Param('tableName') tableName: string) { + return await this.tableService.find(req.dbConnection, tableName); } } diff --git a/BE/src/table/table.service.ts b/BE/src/table/table.service.ts index 8bc52b61..b6e1b3ed 100644 --- a/BE/src/table/table.service.ts +++ b/BE/src/table/table.service.ts @@ -1,136 +1,62 @@ import { Injectable } from '@nestjs/common'; +import { UserDBManager } from '../config/query-database/user-db-manager.service'; +import { Connection, RowDataPacket } from 'mysql2/promise'; import { ColumnDto, ResTableDto } from './dto/res-table.dto'; import { ResTablesDto } from './dto/res-tables.dto'; -import { AdminDBManager } from '../config/query-database/admin-db-manager.service'; @Injectable() export class TableService { - constructor(private readonly adminDBManager: AdminDBManager) {} - - async findAll(sessionId: string) { - const tables = await this.getTables(sessionId); - const columns = await this.getColumns(sessionId); - const foreignKeys = await this.getForeignKeys(sessionId); - const indexes = await this.getIndexes(sessionId); - - return new ResTablesDto( - this.mapTablesWithColumnsAndKeys(tables, columns, foreignKeys, indexes), - ); - } - - async find(sessionId: string, tableName: string): Promise { - const tables = await this.getTables(sessionId, tableName); - const columns = await this.getColumns(sessionId, tableName); - const foreignKeys = await this.getForeignKeys(sessionId, tableName); - const indexes = await this.getIndexes(sessionId); - - return ( - this.mapTablesWithColumnsAndKeys( - tables, - columns, - foreignKeys, - indexes, - )[0] || [] - ); + constructor(private readonly userDBManager: UserDBManager) {} + + async findAll(connection: Connection, sessionId: string) { + const tables = await this.getTables(connection, sessionId); + + const tableList: ResTableDto[] = []; + for (const tableName of tables) { + const columnDtos = await this.getColumns(connection, tableName); + const resTableDto = new ResTableDto(tableName, columnDtos); + tableList.push(resTableDto); + } + return new ResTablesDto(tableList); } - async getTables(identify: string, tableName?: string) { - const schema = identify.substring(0, 10); - const query = ` - SELECT TABLE_NAME - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = ? ${tableName ? 'AND TABLE_NAME = ?' : ''} - `; - const params = tableName ? [schema, tableName] : [schema]; - const [tables] = await this.adminDBManager.run(query, params); - return tables as any[]; + async find(connection: Connection, tableName: string) { + const columns = await this.getColumns(connection, tableName); + return new ResTableDto(tableName, columns); } - async getColumns(identify: string, tableName?: string) { - const schema = identify.substring(0, 10); - const query = ` - SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = ? ${tableName ? 'AND TABLE_NAME = ?' : ''} - ORDER BY ORDINAL_POSITION - `; - const params = tableName ? [schema, tableName] : [schema]; - const [columns] = await this.adminDBManager.run(query, params); - return columns as any[]; - } + async getTables( + connection: Connection, + sessionId: string, + ): Promise { + const query = `SHOW TABLES`; + const result = (await this.userDBManager.run( + connection, + query, + )) as RowDataPacket[]; - private async getForeignKeys(identify: string, tableName?: string) { - const schema = identify.substring(0, 10); - const query = ` - SELECT - TABLE_NAME, - COLUMN_NAME, - REFERENCED_TABLE_NAME, - REFERENCED_COLUMN_NAME - FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = ? - ${tableName ? 'AND TABLE_NAME = ?' : ''} - AND REFERENCED_TABLE_NAME IS NOT NULL - `; - const params = tableName ? [schema, tableName] : [schema]; - const [foreignKey] = await this.adminDBManager.run(query, params); - return foreignKey as any[]; - } + const schema = sessionId.substring(0, 10); + const key = `Tables_in_${schema}`; - private async getIndexes(identify: string, tableName?: string) { - const schema = identify.substring(0, 10); - const query = ` - SELECT TABLE_NAME, COLUMN_NAME, INDEX_NAME, NON_UNIQUE - FROM INFORMATION_SCHEMA.STATISTICS - WHERE TABLE_SCHEMA = ? ${tableName ? 'AND TABLE_NAME = ?' : ''} - `; - const params = tableName ? [schema, tableName] : [schema]; - const [indexes] = await this.adminDBManager.run(query, params); - return indexes as any[]; + return result.map((row) => row[key]); } - private mapTablesWithColumnsAndKeys( - tables: any[], - columns: any[], - foreignKeys: any[], - indexes: any[], - ): ResTableDto[] { - return tables.map((table) => { - const tableColumns = columns.filter( - (col) => col.TABLE_NAME === table.TABLE_NAME, - ); - - const columnDtos = tableColumns.map((col) => { - const fk = foreignKeys.find( - (key) => - key.TABLE_NAME === col.TABLE_NAME && - key.COLUMN_NAME === col.COLUMN_NAME, - ); - - const hasIndex = indexes.some( - (idx) => - idx.TABLE_NAME === col.TABLE_NAME && - idx.COLUMN_NAME === col.COLUMN_NAME, - ); - - return new ColumnDto({ - name: col.COLUMN_NAME, - type: col.COLUMN_TYPE, - PK: col.COLUMN_KEY === 'PRI', - FK: fk - ? `${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME}` - : null, - UQ: col.COLUMN_KEY === 'UNI', - AI: col.EXTRA.includes('auto_increment'), - NN: col.IS_NULLABLE === 'NO', - IDX: hasIndex, - }); - }); - - return new ResTableDto({ - tableName: table.TABLE_NAME || null, - columns: columnDtos || null, - }); - }); + async getColumns(connection: Connection, tableName: string) { + const query = `SHOW FULL COLUMNS FROM ${tableName}`; + const result = (await this.userDBManager.run( + connection, + query, + )) as RowDataPacket[]; + return result.map( + (row) => + new ColumnDto({ + name: row.Field, + type: row.Type, + PK: row.Key === 'PRI', + UQ: row.Key === 'UNI', + AI: row.Extra.includes('auto_increment'), + NN: row.Null !== 'YES', + }), + ); } } diff --git a/BE/src/usage/usage.service.ts b/BE/src/usage/usage.service.ts index 0d2d834d..c34e4ad2 100644 --- a/BE/src/usage/usage.service.ts +++ b/BE/src/usage/usage.service.ts @@ -22,23 +22,23 @@ export class UsageService { } public async updateRowCount(req: any) { - const tableList: string[] = ( - await this.tableService.getTables(req.sessionID) - ).map((table) => table.TABLE_NAME); - if (tableList.length === 0) { - await this.redisService.setRowCount(req.sessionID, 0); - return { - currentUsage: 0, - availUsage: this.MAX_ROW_COUNT, - }; - } - const query = this.createSumQuery(req, tableList); - const result = await this.userDBManager.run(req, query); - const rowCount = parseInt(result[0].total_rows, 10); - - if (rowCount > this.MAX_ROW_COUNT) throw new DataLimitExceedException(); - - await this.redisService.setRowCount(req.sessionID, rowCount); + // const tableList: string[] = ( + // await this.tableService.getTables(req.sessionID) + // ).map((table) => table.TABLE_NAME); + // if (tableList.length === 0) { + // await this.redisService.setRowCount(req.sessionID, 0); + // return { + // currentUsage: 0, + // availUsage: this.MAX_ROW_COUNT, + // }; + // } + // const query = this.createSumQuery(req, tableList); + // const result = await this.userDBManager.run(req, query); + // const rowCount = parseInt(result[0].total_rows, 10); + // + // if (rowCount > this.MAX_ROW_COUNT) throw new DataLimitExceedException(); + // + // await this.redisService.setRowCount(req.sessionID, rowCount); } private createSumQuery(req: any, tableNameList: string[]): string { diff --git a/FE/package-lock.json b/FE/package-lock.json index 7aa0e368..2988eb5c 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -48,6 +48,7 @@ "devDependencies": { "@eslint/js": "^9.11.1", "@types/axios": "^0.14.4", + "@types/node": "^22.10.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-query": "^1.2.9", @@ -2211,6 +2212,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -8241,6 +8252,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/FE/package.json b/FE/package.json index e08541f5..2f031640 100644 --- a/FE/package.json +++ b/FE/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@eslint/js": "^9.11.1", "@types/axios": "^0.14.4", + "@types/node": "^22.10.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-query": "^1.2.9", diff --git a/FE/src/types/interfaces.ts b/FE/src/types/interfaces.ts index 40a361f6..96e37768 100644 --- a/FE/src/types/interfaces.ts +++ b/FE/src/types/interfaces.ts @@ -26,7 +26,6 @@ export interface TableColumnType { type: string FK: string | null PK: boolean - IDX: boolean UQ: boolean AI: boolean NN: boolean