diff --git a/BE/package-lock.json b/BE/package-lock.json index 93c317de..f703fe1d 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -34,7 +34,8 @@ "typeorm": "^0.3.20", "uuid": "^11.0.2", "winston": "^3.17.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -13019,6 +13020,14 @@ "dependencies": { "safe-buffer": "~5.2.0" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/BE/package.json b/BE/package.json index c9101be2..6942e8c8 100644 --- a/BE/package.json +++ b/BE/package.json @@ -45,7 +45,8 @@ "typeorm": "^0.3.20", "uuid": "^11.0.2", "winston": "^3.17.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/BE/src/common/exception/exception.handler.ts b/BE/src/common/exception/exception.handler.ts index 6eff3822..d5184295 100644 --- a/BE/src/common/exception/exception.handler.ts +++ b/BE/src/common/exception/exception.handler.ts @@ -11,7 +11,6 @@ export class ExceptionHandler implements ExceptionFilter { catch(error: Error, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - const status = error instanceof HttpException ? error.getStatus() diff --git a/BE/src/interceptors/serialize.interceptor.ts b/BE/src/interceptors/serialize.interceptor.ts index 302acb82..ecdace1e 100644 --- a/BE/src/interceptors/serialize.interceptor.ts +++ b/BE/src/interceptors/serialize.interceptor.ts @@ -7,6 +7,8 @@ import { import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { plainToInstance } from 'class-transformer'; +import { catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; interface ClassConstructor { new (...args: any[]): T; @@ -23,6 +25,9 @@ class SerializeInterceptor implements NestInterceptor { next: CallHandler, ): Observable { return next.handle().pipe( + catchError((error) => { + return throwError(() => error); + }), map((inputData: any) => { let data; if (inputData instanceof Array) { @@ -51,6 +56,7 @@ class SerializeInterceptor implements NestInterceptor { } removeNullProperties(obj: any): any { + if (!obj) return; return Object.fromEntries( // eslint-disable-next-line @typescript-eslint/no-unused-vars Object.entries(obj).filter(([_, value]) => value !== null), diff --git a/BE/src/interceptors/user-db-connection.interceptor.ts b/BE/src/interceptors/user-db-connection.interceptor.ts index ec818270..5f5ae2d2 100644 --- a/BE/src/interceptors/user-db-connection.interceptor.ts +++ b/BE/src/interceptors/user-db-connection.interceptor.ts @@ -50,9 +50,8 @@ export class UserDBConnectionInterceptor implements NestInterceptor { await request.dbConnection.commit(); }), catchError(async (err) => { - if (err instanceof DataLimitExceedException) { + if (err instanceof DataLimitExceedException) await request.dbConnection.rollback(); - } throw err; }), finalize(async () => await request.dbConnection.end()), diff --git a/BE/src/record/constant/random-record.constant.ts b/BE/src/record/constant/random-record.constant.ts index 653c366e..854cf74d 100644 --- a/BE/src/record/constant/random-record.constant.ts +++ b/BE/src/record/constant/random-record.constant.ts @@ -30,3 +30,34 @@ export const TypeToConstructor = { sex: SexGenerator, boolean: BooleanGenerator, }; + +export const DomainToTypes = { + name: 'string', + country: 'string', + city: 'string', + email: 'string', + phone: 'string', + sex: 'string', + boolean: 'number', + number: 'number', + enum: 'string', +}; + +export const mysqlToJsType = (mysqlType: string): string => { + const baseType = mysqlType.split('(')[0]; + const typeMap: Record = { + VARCHAR: 'string', + CHAR: 'string', + TEXT: 'string', + INT: 'number', + TINYINT: 'number', + BIGINT: 'number', + FLOAT: 'number', + DOUBLE: 'number', + DECIMAL: 'number', + DATETIME: 'string', + DATE: 'string', + TIMESTAMP: 'string', + }; + return typeMap[baseType.toUpperCase()] || 'unknown'; +}; diff --git a/BE/src/record/random-column.entity.ts b/BE/src/record/random-column.entity.ts index f50caf63..93756099 100644 --- a/BE/src/record/random-column.entity.ts +++ b/BE/src/record/random-column.entity.ts @@ -1,7 +1,7 @@ import { Domains } from './dto/create-random-record.dto'; import { RandomValueGenerator } from './domain'; -export interface RandomColumnEntity { +export interface RandomColumnModel { name: string; type: Domains; generator: RandomValueGenerator; diff --git a/BE/src/record/record.controller.ts b/BE/src/record/record.controller.ts index fba1d500..85bb77e2 100644 --- a/BE/src/record/record.controller.ts +++ b/BE/src/record/record.controller.ts @@ -19,10 +19,11 @@ export class RecordController { @ExecuteRecordSwagger() @Serialize(ResRecordDto) @Post() - insertRandomRecord( + async insertRandomRecord( @Req() req: Request, @Body() randomRecordInsertDto: CreateRandomRecordDto, ) { + await this.recordService.validateDto(randomRecordInsertDto, req.sessionID); return this.recordService.insertRandomRecord(req, randomRecordInsertDto); } } diff --git a/BE/src/record/record.module.ts b/BE/src/record/record.module.ts index 9f030b83..86843dce 100644 --- a/BE/src/record/record.module.ts +++ b/BE/src/record/record.module.ts @@ -4,9 +4,10 @@ import { RecordService } from './record.service'; import { RedisModule } from '../config/redis/redis.module'; import { QueryDBModule } from '../config/query-database/query-db.moudle'; import { UsageModule } from '../usage/usage.module'; +import { TableModule } from 'src/table/table.module'; @Module({ - imports: [RedisModule, QueryDBModule, UsageModule], + imports: [RedisModule, QueryDBModule, UsageModule, TableModule], controllers: [RecordController], providers: [RecordService], }) diff --git a/BE/src/record/record.service.ts b/BE/src/record/record.service.ts index ce34fea8..efc52bdc 100644 --- a/BE/src/record/record.service.ts +++ b/BE/src/record/record.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Injectable, InternalServerErrorException, OnModuleInit, @@ -8,7 +9,7 @@ import { CreateRandomRecordDto, RandomColumnInfo, } from './dto/create-random-record.dto'; -import { RandomColumnEntity } from './random-column.entity'; +import { RandomColumnModel } from './random-column.entity'; import fs from 'fs/promises'; import crypto from 'crypto'; import path from 'path'; @@ -22,12 +23,19 @@ import { } from './constant/random-record.constant'; import { UserDBManager } from '../config/query-database/user-db-manager.service'; import { UsageService } from '../usage/usage.service'; +import { TableService } from 'src/table/table.service'; +import { ResTableDto } from 'src/table/dto/res-table.dto'; +import { + DomainToTypes, + mysqlToJsType, +} from './constant/random-record.constant'; @Injectable() export class RecordService implements OnModuleInit { constructor( private readonly userDBManager: UserDBManager, private readonly usageService: UsageService, + private readonly tableService: TableService, ) {} async onModuleInit() { @@ -42,11 +50,53 @@ export class RecordService implements OnModuleInit { } } + async validateDto( + createRandomRecordDto: CreateRandomRecordDto, + identify: string, + ) { + 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} 랜덤 값을 넣을 수 없습니다.`, + ); + }); + } + + checkDomainAvailability(mysqlType: string, targetDomain: string) { + const baseType = mysqlToJsType(mysqlType); + const targetType = DomainToTypes[targetDomain]; + if (baseType === 'number' && targetType === 'string') return false; + return true; + } async insertRandomRecord( req: any, createRandomRecordDto: CreateRandomRecordDto, ): Promise { - const columnEntities: RandomColumnEntity[] = + const columnEntities: RandomColumnModel[] = createRandomRecordDto.columns.map((column) => this.toEntity(column)); const columnNames = columnEntities.map((column) => column.name); @@ -72,7 +122,7 @@ export class RecordService implements OnModuleInit { }); } - private toEntity(randomColumnInfo: RandomColumnInfo): RandomColumnEntity { + private toEntity(randomColumnInfo: RandomColumnInfo): RandomColumnModel { let generator: RandomValueGenerator; if (generalDomain.includes(randomColumnInfo.type)) generator = new TypeToConstructor[randomColumnInfo.type](); @@ -93,7 +143,7 @@ export class RecordService implements OnModuleInit { } private async generateCsvFile( - columnEntities: RandomColumnEntity[], + columnEntities: RandomColumnModel[], rows: number, ): Promise { const randomString = crypto.randomBytes(10).toString('hex'); @@ -126,12 +176,12 @@ export class RecordService implements OnModuleInit { ); } - private generateCsvHeader(columnEntities: RandomColumnEntity[]): string { + private generateCsvHeader(columnEntities: RandomColumnModel[]): string { return columnEntities.map((column) => column.name).join(', ') + '\n'; } private generateCsvData( - columnEntities: RandomColumnEntity[], + columnEntities: RandomColumnModel[], rows: number, ): string { let data = columnEntities.map((column) => diff --git a/BE/src/table/table.service.ts b/BE/src/table/table.service.ts index 5bee02de..2520c864 100644 --- a/BE/src/table/table.service.ts +++ b/BE/src/table/table.service.ts @@ -18,7 +18,7 @@ export class TableService { ); } - async find(sessionId: string, tableName: string) { + 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); @@ -45,7 +45,7 @@ export class TableService { return tables as any[]; } - private async getColumns(schema: string, tableName?: string) { + async getColumns(schema: string, tableName?: string) { const query = ` SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS diff --git a/FE/src/components/LeftSidebar.tsx b/FE/src/components/LeftSidebar.tsx index 24d82f09..98e12d7e 100644 --- a/FE/src/components/LeftSidebar.tsx +++ b/FE/src/components/LeftSidebar.tsx @@ -14,27 +14,28 @@ import { } from '@/components/ui/sidebar' import logo from '@/assets/logo.svg' -import { TableType } from '@/types/interfaces' import TableTool from '@/components/TableTool' import RecordTool from '@/components/RecordTool' import ExampleQueryTool from '@/components/ExampleQueryTool' import { ErrorBoundary } from 'react-error-boundary' import SidebarErrorPage from '@/pages/SideBarErrorPage' import useToastErrorHandler from '@/hooks/error/toastErrorHandler' +import LoadingPage from '@/pages/LoadingPage' +import { useTables } from '@/hooks/query/useTableQuery' type LeftSidebarProps = React.ComponentProps & { activeItem: (typeof MENU)[0] setActiveItem: React.Dispatch> - tables: TableType[] } export default function LeftSidebar({ activeItem, setActiveItem, - tables, ...props }: LeftSidebarProps) { const menu = MENU.slice(0, -1) + + const tables = useTables() const { setOpen, toggleSidebar } = useSidebar() const handleError = useToastErrorHandler() @@ -101,39 +102,43 @@ export default function LeftSidebar({ - - - - {activeItem.title === MENU_TITLE.TABLE && ( - window.location.reload()} - onError={handleError} - > - - - )} - {activeItem.title === MENU_TITLE.RECORD && ( - window.location.reload()} - onError={handleError} - > - - - )} - {activeItem.title === MENU_TITLE.TESTQUERY && ( - window.location.reload()} - onError={handleError} - > - - - )} - - - + {tables.isLoading ? ( + + ) : ( + + + + {activeItem.title === MENU_TITLE.TABLE && ( + window.location.reload()} + onError={handleError} + > + + + )} + {activeItem.title === MENU_TITLE.RECORD && ( + window.location.reload()} + onError={handleError} + > + + + )} + {activeItem.title === MENU_TITLE.TESTQUERY && ( + window.location.reload()} + onError={handleError} + > + + + )} + + + + )} ) diff --git a/FE/src/components/MainContent.tsx b/FE/src/components/MainContent.tsx index cae543c2..4cbc7a32 100644 --- a/FE/src/components/MainContent.tsx +++ b/FE/src/components/MainContent.tsx @@ -4,6 +4,7 @@ import Shell from '@/components/common/shell/Shell' import { useShells } from '@/hooks/query/useShellQuery' import useUsages from '@/hooks/query/useUsageQuery' import useShellHandlers from '@/hooks/useShellHandler' +import LoadingPage from '@/pages/LoadingPage' import { ErrorBoundary } from 'react-error-boundary' import ShellErrorFallback from './common/shell/ShellErrorFallback' @@ -34,17 +35,21 @@ export default function MainContent() { /> -
- {shells.data?.map((shell) => ( - window.location.reload()} - > - - - ))} -
+ {shells.isLoading ? ( + + ) : ( +
+ {shells.data?.map((shell) => ( + window.location.reload()} + > + + + ))} +
+ )} ) } diff --git a/FE/src/components/RightSidebar.tsx b/FE/src/components/RightSidebar.tsx index 764a460d..9e583449 100644 --- a/FE/src/components/RightSidebar.tsx +++ b/FE/src/components/RightSidebar.tsx @@ -1,5 +1,5 @@ import { MENU_TITLE } from '@/constants/constants' -import { TableType } from '@/types/interfaces' +import { useTables } from '@/hooks/query/useTableQuery' import { Sidebar, SidebarContent, @@ -7,20 +7,16 @@ import { SidebarGroup, SidebarGroupContent, } from '@/components/ui/sidebar' +import LoadingPage from '@/pages/LoadingPage' import ViewTable from './ViewTable' -export default function RightSidebar({ - tables = [], - ...props -}: { - tables: TableType[] -}) { +export default function RightSidebar() { + const tables = useTables() + return ( @@ -30,13 +26,17 @@ export default function RightSidebar({ - - - - - - - + {tables.isLoading ? ( + + ) : ( + + + + + + + + )} ) } diff --git a/FE/src/components/common/ResultTable.tsx b/FE/src/components/common/ResultTable.tsx index 34b6cb4c..5972ef3d 100644 --- a/FE/src/components/common/ResultTable.tsx +++ b/FE/src/components/common/ResultTable.tsx @@ -7,7 +7,7 @@ import { TableHead, TableHeader, TableRow, -} from '../ui/table' +} from '@/components/ui/table' function ResultTable>({ data, diff --git a/FE/src/components/common/shell/Shell.tsx b/FE/src/components/common/shell/Shell.tsx index d2d391e5..5aa3ab21 100644 --- a/FE/src/components/common/shell/Shell.tsx +++ b/FE/src/components/common/shell/Shell.tsx @@ -24,6 +24,7 @@ export default function Shell({ shell }: ShellProps) { const LINE_HEIGHT = 1.2 const prevQueryRef = useRef(query ?? '') + const [isExecuting, setIsExecuting] = useState(false) const [focused, setFocused] = useState(true) const [inputValue, setInputValue] = useState(query ?? '') const [editorHeight, setEditorHeight] = useState(LINE_HEIGHT) @@ -69,10 +70,13 @@ export default function Shell({ shell }: ShellProps) { const handleClick = async () => { if (!id || !shell) throw new Error(`Invalid shell or id, ${id}`) + setIsExecuting(true) try { await executeShell({ ...shell, query }) } catch (error) { throw new Error(`Failed to execute shell with id, ${id}`) + } finally { + setIsExecuting(false) } try { @@ -107,15 +111,21 @@ export default function Shell({ shell }: ShellProps) {
diff --git a/FE/src/pages/LoadingPage.tsx b/FE/src/pages/LoadingPage.tsx new file mode 100644 index 00000000..063ef0e9 --- /dev/null +++ b/FE/src/pages/LoadingPage.tsx @@ -0,0 +1,10 @@ +export default function LoadingPage() { + return ( +
+
+ +
+

Loading...

+
+ ) +} diff --git a/FE/src/pages/MainPage.tsx b/FE/src/pages/MainPage.tsx index f01fb7d7..46dae3fb 100644 --- a/FE/src/pages/MainPage.tsx +++ b/FE/src/pages/MainPage.tsx @@ -6,13 +6,11 @@ import RightSidebar from '@/components/RightSidebar' import { Toaster } from '@/components/ui/toaster' import MainContent from '@/components/MainContent' -import { useTables } from '@/hooks/query/useTableQuery' import { ErrorBoundary } from 'react-error-boundary' import MainErrorPage from './MainErrorPage' export default function MainPage() { const [activeItem, setActiveItem] = useState(MENU[0]) - const tables = useTables() return ( window.location.reload()} > - + - + diff --git a/FE/tailwind.config.js b/FE/tailwind.config.js index ed6f0772..398f3177 100644 --- a/FE/tailwind.config.js +++ b/FE/tailwind.config.js @@ -69,6 +69,9 @@ export default { border: 'hsl(var(--sidebar-border))', ring: 'hsl(var(--sidebar-ring))', }, + animation: { + 'spin-slow': 'spin 3s linear infinite', + }, }, }, }, diff --git a/README.md b/README.md index 0785d26f..dae302dd 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Query를 연습하고 싶은데, DB 환경세팅이 너무 어렵고 많은 시
## 핵심경험 (FE) -> [FE 프로젝트 핵심 경험](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98-%E2%80%90-%EA%B9%80%EB%8B%A4%EC%98%81(FE)) : 상세한 프로젝트 핵심 경험은 해당 링크에서 확인하실 수 있습니다. +[FE 프로젝트 핵심 경험(김다영)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98-%E2%80%90-%EA%B9%80%EB%8B%A4%EC%98%81(FE)) ### **핵심 경험 요약** - `1주차`: 서비스 설계, 작업 관리(플래닝 포커), GitHub 협업 전략. @@ -87,55 +87,72 @@ Query를 연습하고 싶은데, DB 환경세팅이 너무 어렵고 많은 시 - `6주차`: 코드 리팩토링 및 최적화. ### 주요 트러블슈팅 -> [성급한 추상화 (feat. useCustomMutation)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%84%B1%EA%B8%89%ED%95%9C-%EC%B6%94%EC%83%81%ED%99%94-(feat.-useCustomMutation)) +[성급한 추상화 (feat. useCustomMutation)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%84%B1%EA%B8%89%ED%95%9C-%EC%B6%94%EC%83%81%ED%99%94-(feat.-useCustomMutation)) -- 커스텀 훅(useCustomMutation) 작성 후 범용성과 유연성 부족으로 불편함 발생 -- 추상화는 반복된 코드 속 공통점을 충분히 파악한 뒤 이점이 있는 경우 진행해야한다는 교훈을 얻음 +> 커스텀 훅(useCustomMutation) 작성 후 범용성과 유연성 부족으로 불편함 발생 +> 추상화는 반복된 코드 속 공통점을 충분히 파악한 뒤 이점이 있는 경우 진행해야한다는 교훈을 얻음 - -> [상태는 대체 어디에…](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%83%81%ED%83%9C%EB%8A%94-%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%94%94%EC%97%90%E2%80%A6) -- 부모에서 포커스 상태를 관리하자 자식 컴포넌트 전체가 리렌더링되어 깜빡이는 현상 발생 -- 상태는 최대한 사용하는 컴포넌트에 두어 불필요한 리렌더링을 방지해야한다는 교훈을 얻음 +[상태는 대체 어디에…](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%83%81%ED%83%9C%EB%8A%94-%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%94%94%EC%97%90%E2%80%A6) + +> 부모에서 포커스 상태를 관리하자 자식 컴포넌트 전체가 리렌더링되어 깜빡이는 현상 발생 +> 상태는 최대한 사용하는 컴포넌트에 두어 불필요한 리렌더링을 방지해야한다는 교훈을 얻음 -> [React의 고유한 key](https://github.com/boostcampwm-2024/web36-QLab/wiki/React%EC%9D%98-%EA%B3%A0%EC%9C%A0%ED%95%9C-key) +[React의 고유한 key](https://github.com/boostcampwm-2024/web36-QLab/wiki/React%EC%9D%98-%EA%B3%A0%EC%9C%A0%ED%95%9C-key) -- new Date().getTime()으로 생성한 키를 사용해 React의 key 경고 발생. -- 서버에서 생성된 ID 사용. 서버 ID가 없을 경우, UUID와 객체 문자열 변환을 조합해 고유 키 생성. +> new Date().getTime()으로 생성한 키를 사용해 React의 key 경고 발생. +> 서버에서 생성된 ID 사용. 서버 ID가 없을 경우, UUID와 객체 문자열 변환을 조합해 고유 키 생성.

## 핵심경험 (BE) -> [BE 프로젝트 핵심 경험(성유진)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98-%E2%80%90-%EC%84%B1%EC%9C%A0%EC%A7%84(BE)) +[BE 프로젝트 핵심 경험(성유진)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98-%E2%80%90-%EC%84%B1%EC%9C%A0%EC%A7%84(BE)) -> [BE 프로젝트 핵심 경험(오민택)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98%E2%80%90%EC%98%A4%EB%AF%BC%ED%83%9D(BE)) +[BE 프로젝트 핵심 경험(오민택)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98%E2%80%90%EC%98%A4%EB%AF%BC%ED%83%9D(BE)) -> [BE 프로젝트 핵심 경험(장승훈)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98%E2%80%90%EC%9E%A5%EC%8A%B9%ED%9B%88(BE)) +[BE 프로젝트 핵심 경험(장승훈)](https://github.com/boostcampwm-2024/web36-QLab/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B2%BD%ED%97%98%E2%80%90%EC%9E%A5%EC%8A%B9%ED%9B%88(BE)) -### 주요 기능 구현 방법 및 트러블 슈팅 -> [사용자의 쿼리 실행 환경은 어떻게 관리되나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EC%BF%BC%EB%A6%AC-%EC%8B%A4%ED%96%89%ED%99%98%EA%B2%BD%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%EB%90%98%EB%82%98%EC%9A%94%3F) +### 주요 기능 구현 방법 +[사용자의 쿼리 실행 환경은 어떻게 관리되나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EC%BF%BC%EB%A6%AC-%EC%8B%A4%ED%96%89%ED%99%98%EA%B2%BD%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%EB%90%98%EB%82%98%EC%9A%94%3F) -- 세션을 통한 DB Connection 관리하는 법 -- 응답되는 쿼리 실행 시간 측정 방법 +> 세션을 통한 DB Connection 관리하는 법 +> 응답되는 쿼리 실행 시간 측정 방법 -> [유저의 무한한 데이터삽입은 어떻게 제한시키나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9C%A0%EC%A0%80%EC%9D%98-%EB%AC%B4%ED%95%9C%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%82%BD%EC%9E%85%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%9C%ED%95%9C%EC%8B%9C%ED%82%A4%EB%82%98%EC%9A%94%3F) +[유저의 무한한 데이터삽입은 어떻게 제한시키나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9C%A0%EC%A0%80%EC%9D%98-%EB%AC%B4%ED%95%9C%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%82%BD%EC%9E%85%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%9C%ED%95%9C%EC%8B%9C%ED%82%A4%EB%82%98%EC%9A%94%3F) -- 한정된 스토리지 속 유저 데이터 삽입 제한 방법 -- Redis pub/sub 이용하기 +> 한정된 스토리지 속 유저 데이터 삽입 제한 방법 +> Redis pub/sub 이용하기 -> [대용량 랜덤 데이터는 어떻게 삽입되나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%9E%9C%EB%8D%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%BD%EC%9E%85%EB%90%98%EB%82%98%EC%9A%94%3F) +[대용량 랜덤 데이터는 어떻게 삽입되나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%9E%9C%EB%8D%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%BD%EC%9E%85%EB%90%98%EB%82%98%EC%9A%94%3F) + +> 대용량 데이터를 빠르게 삽입하는 방법 + + + +### 트러블 슈팅 + +[인프라는 어떻게 관리 했나요?](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9D%B8%ED%94%84%EB%9D%BC%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%96%88%EB%82%98%EC%9A%94%3F) + +> 원활한 서비스를 위한 인프라 관리방법 + +[유저에게 더 좋은 쿼리 실행 환경을 제공할 수 있도록 with 부하테스트](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9C%A0%EC%A0%80%EC%97%90%EA%B2%8C-%EB%8D%94-%EC%A2%8B%EC%9D%80-%EC%BF%BC%EB%A6%AC-%EC%8B%A4%ED%96%89-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8F%84%EB%A1%9D-with-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8) + +> 제한된 서버 환경에서도 최대한 독립적인 실행 환경을 제공하기 위한 개선 과정 + +[인터셉터로 관심사 분리하기](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0%EB%A1%9C-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0) + +> 인터셉터를 이용하여 유저 DB 커넥션 관리 코드 개선 -- 대용량 데이터를 빠르게 삽입하는 방법 +[세션을 관리하기 위해서 Redis 활용하기](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%84%B8%EC%85%98%EC%9D%84-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%B4%EC%84%9C-Redis-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0) -> [유저에게 더 좋은 쿼리 실행 환경을 제공할 수 있도록 with 부하테스트](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9C%A0%EC%A0%80%EC%97%90%EA%B2%8C-%EB%8D%94-%EC%A2%8B%EC%9D%80-%EC%BF%BC%EB%A6%AC-%EC%8B%A4%ED%96%89-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8F%84%EB%A1%9D-with-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8) +> Redis를 이용하여 체계적으로 세션 관리하기 -- 제한된 서버 환경에서도 최대한 독립적인 실행 환경을 제공하기 위한 개선 과정 +[jest로 도전한 NestJS 인터셉터 통합테스트](https://github.com/boostcampwm-2024/web36-QLab/wiki/jest%EB%A1%9C-%EB%8F%84%EC%A0%84%ED%95%9C-NestJS-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0-%ED%86%B5%ED%95%A9%ED%85%8C%EC%8A%A4%ED%8A%B8) -> [인터셉터로 관심사 분리하기](https://github.com/boostcampwm-2024/web36-QLab/wiki/%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0%EB%A1%9C-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0) +> 의미있게 통합테스트 작성하기 -- 인터셉터를 이용하여 유저 DB 커넥션 관리 코드 개선 ## 기술 스택 diff --git a/package-lock.json b/package-lock.json index 43532bf2..118e8c2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "ace-builds": "^1.36.5", - "react-ace": "^13.0.0" + "react-ace": "^13.0.0", + "zod": "^3.23.8" } }, "node_modules/ace-builds": { @@ -121,6 +122,14 @@ "dependencies": { "loose-envify": "^1.1.0" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c734e1d7..7728f512 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "license": "ISC", "dependencies": { "ace-builds": "^1.36.5", - "react-ace": "^13.0.0" + "react-ace": "^13.0.0", + "zod": "^3.23.8" } } diff --git a/schemas/index.ts b/schemas/index.ts new file mode 100644 index 00000000..413445f6 --- /dev/null +++ b/schemas/index.ts @@ -0,0 +1,14 @@ +import { QueryDto, QueryDtoSchema } from "./query"; +import { RandomColumnInfoSchema, RandomColumnInfo, CreateRandomRecordDto, CreateRandomRecordDtoSchema } from "./record"; +import { UpdateShellDto, UpdateShellDtoSchema } from "./shells"; + +export { + QueryDto, + QueryDtoSchema, + CreateRandomRecordDto, + CreateRandomRecordDtoSchema, + RandomColumnInfo, + RandomColumnInfoSchema, + UpdateShellDto, + UpdateShellDtoSchema, +}; diff --git a/schemas/query.ts b/schemas/query.ts new file mode 100644 index 00000000..c1a64b07 --- /dev/null +++ b/schemas/query.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +// QueryDto 스키마 +export const QueryDtoSchema = z.object({ + query: z.string(), +}); + +// 타입 추론 +export type QueryDto = z.infer; diff --git a/schemas/record.ts b/schemas/record.ts new file mode 100644 index 00000000..d1db536a --- /dev/null +++ b/schemas/record.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +// Domains Enum 정의 +const Domains = z.enum(["name", "country", "city", "email", "phone", "sex", "boolean", "number", "enum"]); +type Domains = z.infer; + +// RandomColumnInfo 스키마 +export const RandomColumnInfoSchema = z + .object({ + name: z.string().trim().min(1, "Column Name is required"), + type: Domains, + blank: z.number().int().min(0).max(100, "Blank must be between 0 and 100"), + min: z.number().optional(), + max: z.number().optional(), + enum: z + .array(z.string()) + .max(10, "Enum array can have at most 10 items") + .optional() + .refine((val) => val === undefined || Array.isArray(val), { + message: "Enum must be an array of strings or undefined", + }), + }) + .superRefine((data, ctx) => { + if (data.type === "number") { + if (data.min === undefined) { + ctx.addIssue({ + path: ["min"], + message: "min must be defined when type is 'number'", + code: "custom", + }); + } + if (data.max === undefined) { + ctx.addIssue({ + path: ["max"], + message: "max must be defined when type is 'number'", + code: "custom", + }); + } + if (data.min !== undefined && data.max !== undefined && data.min > data.max) { + ctx.addIssue({ + path: ["min"], + message: "min must be less than or equal to max", + code: "custom", + }); + } + } + + if (data.type === "enum" && data.enum === undefined) { + ctx.addIssue({ + path: ["enum"], + message: "enum must be defined when type is 'enum'", + code: "custom", + }); + } + }); + +// CreateRandomRecordDto 스키마 +export const CreateRandomRecordDtoSchema = z.object({ + tableName: z.string().trim().min(1, "Table name is required"), + columns: z.array(RandomColumnInfoSchema), + count: z.number().int().min(1, "Count must be at least 1").max(100000, "Count cannot exceed 100000"), +}); + +export type RandomColumnInfo = z.infer; +export type CreateRandomRecordDto = z.infer; diff --git a/schemas/shells.ts b/schemas/shells.ts new file mode 100644 index 00000000..97c3f9c6 --- /dev/null +++ b/schemas/shells.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +// UpdateShellDto 스키마 +export const UpdateShellDtoSchema = z.object({ + query: z.string().trim().min(1, "Query must be a non-empty string"), +}); + +// 타입 추론 +export type UpdateShellDto = z.infer;