From 6e813090371da596165d7567ccfa9045eff6da9f Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sat, 2 May 2026 16:32:12 +0900 Subject: [PATCH 01/29] =?UTF-8?q?=F0=9F=94=A5=20remove::=20WarmupModule,?= =?UTF-8?q?=20WarmupService=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/api/app/package.json | 1 - .../src/api/internal/warmup/warmup.module.ts | 11 -- .../src/api/internal/warmup/warmup.service.ts | 120 ------------------ 3 files changed, 132 deletions(-) delete mode 100644 services/api/app/src/api/internal/warmup/warmup.module.ts delete mode 100644 services/api/app/src/api/internal/warmup/warmup.service.ts diff --git a/services/api/app/package.json b/services/api/app/package.json index 4552b12..3d6710f 100644 --- a/services/api/app/package.json +++ b/services/api/app/package.json @@ -34,7 +34,6 @@ "@nestjs/config": "^3.2.3", "@nestjs/core": "^9.4.3", "@nestjs/platform-express": "^9.0.0", - "@nestjs/schedule": "4.1.2", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "@sentry/browser": "^10.43.0", diff --git a/services/api/app/src/api/internal/warmup/warmup.module.ts b/services/api/app/src/api/internal/warmup/warmup.module.ts deleted file mode 100644 index d408dbb..0000000 --- a/services/api/app/src/api/internal/warmup/warmup.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { CafeteriasModule } from 'src/api/public/cafeterias/cafeterias.module'; -import { CafeteriasRepositoryModule } from 'src/type-orm/entities/cafeterias/cafeterias-repository.module'; -import { WarmupService } from './warmup.service'; - -@Module({ - imports: [ScheduleModule.forRoot(), CafeteriasModule, CafeteriasRepositoryModule], - providers: [WarmupService], -}) -export class WarmupModule {} diff --git a/services/api/app/src/api/internal/warmup/warmup.service.ts b/services/api/app/src/api/internal/warmup/warmup.service.ts deleted file mode 100644 index f74e8ff..0000000 --- a/services/api/app/src/api/internal/warmup/warmup.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; -import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/utils/time'; -import { CafeteriasService } from 'src/api/public/cafeterias/cafeterias.service'; -import { CafeteriasRepository } from 'src/type-orm/entities/cafeterias/cafeterias.repository'; - -@Injectable() -export class WarmupService implements OnApplicationBootstrap { - private readonly logger = new Logger(WarmupService.name); - - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - private readonly cafeteriasService: CafeteriasService, - private readonly cafeteriasRepository: CafeteriasRepository, - ) {} - - onApplicationBootstrap() { - this.scheduledWarmup(); - } - - @Cron(CronExpression.EVERY_5_MINUTES) - async scheduledWarmup() { - try { - await this.runWarmup(); - } catch (err: unknown) { - this.logger.warn( - `scheduled warmup failed: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - private async runWarmup(): Promise { - const start = performance.now(); - - const [dbMs, cpuMs] = await Promise.all([this.pingDatabase(), this.exerciseCpuHotPaths()]); - - const cacheMs = await this.prewarmDietCache(); - - const totalMs = +(performance.now() - start).toFixed(2); - this.logger.log( - `warmup ok — total=${totalMs}ms db=${dbMs}ms cpu=${cpuMs}ms cache=${cacheMs}ms`, - ); - - return { ok: true, totalMs, dbMs, cpuMs, cacheMs }; - } - - // ── private helpers ───────────────────────────────────────────────────────── - - private async pingDatabase(): Promise { - const t = performance.now(); - await this.dataSource.query('SELECT 1'); - return +(performance.now() - t).toFixed(2); - } - - /** - * Idle 이후 첫 요청에서 병목이 발생하는 V8 JIT 핫패스를 미리 실행. - * - * 대상: - * - Date 산술 / KST 변환 (getTodayOrTomorrow, getDietTime) - * - JSON.parse / JSON.stringify - * - 문자열 포맷팅 (toISOString, slice) - * - 간단한 배열 연산 - */ - private exerciseCpuHotPaths(): number { - const t = performance.now(); - - // 1. 날짜 계산 (cafeterias.service와 동일한 코드 경로) - const date = getTodayOrTomorrow(); - getDietTime(date); - date.toISOString().slice(0, 10); - - // 2. JSON 직렬화 / 역직렬화 - const sample = { - id: 1, - name: 'warmup', - date: date.toISOString(), - items: Array.from({ length: 10 }, (_, i) => ({ id: i, value: `item-${i}` })), - }; - JSON.parse(JSON.stringify(sample)); - - // 3. 문자열 / 배열 조작 - const tags = ['아침', '점심', '저녁']; - tags.map(t => t.repeat(3)).join(','); - - return +(performance.now() - t).toFixed(2); - } - - /** - * 모든 식당의 현재 식단을 캐시에 pre-population. - * warmup 주기(5분)마다 실행되므로, TTL(10분)과 함께 캐시가 항상 warm 상태로 유지됨. - */ - private async prewarmDietCache(): Promise { - const t = performance.now(); - - try { - const cafeterias = await this.cafeteriasRepository.findCafeteriasByCampusId(); - await Promise.allSettled( - cafeterias.map(cafeteria => this.cafeteriasService.getCafeteriaDiet(cafeteria.id)), - ); - this.logger.log(`diet cache prewarmed — count=${cafeterias.length}`); - } catch (err: unknown) { - this.logger.warn( - `diet cache prewarm failed: ${err instanceof Error ? err.message : String(err)}`, - ); - } - - return +(performance.now() - t).toFixed(2); - } -} - -export interface WarmupResult { - ok: boolean; - totalMs: number; - dbMs: number; - cpuMs: number; - cacheMs: number; -} From 44c7db0764eeb9302ea1caa8bfc301b985e65076 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sat, 2 May 2026 16:33:04 +0900 Subject: [PATCH 02/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EC=8B=9D=EB=8B=B9=20=EC=8B=9D=EB=8B=A8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WarmupModule 제거 - CafeteriaMessageFactory에서 diets를 menuGroups로 변경하여 카테고리별로 식단 정보를 그룹화 - CafeteriasService에서 getCafeteriaDiet 메서드 리팩토링, 식단 정보를 카테고리별로 반환하도록 수정 - 관련 DTO 및 테스트 코드 업데이트 --- .../cafeterias/cafeteria-message.factory.ts | 23 ++-- .../cafeterias-native.controller.ts | 30 +----- .../cafeterias/cafeterias.controller.ts | 5 +- .../cafeterias/cafeterias.service.spec.ts | 95 ++++++++++------ .../public/cafeterias/cafeterias.service.ts | 101 ++++++------------ .../list-cafeteria-diet-request.dto.ts | 6 ++ .../dtos/results/cafeteria-diet-result.dto.ts | 7 +- services/api/app/src/app.module.ts | 2 - 8 files changed, 127 insertions(+), 142 deletions(-) diff --git a/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts b/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts index ed57e58..2036223 100644 --- a/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts +++ b/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts @@ -52,7 +52,7 @@ export class CafeteriaMessageFactory { } public createCafeteriaDietListCard(result: CafeteriaDietResult): SkillTemplate { - const { cafeteria, date, time, diets } = result; + const { cafeteria, date, time, menuGroups } = result; const title = `🍱 ${cafeteria.name}(${cafeteria.campus.name.slice(0, 2)})`; const year = date.getFullYear(); const month = date.getMonth() + 1; @@ -60,22 +60,13 @@ export class CafeteriaMessageFactory { let description = `${year}-${month}-${day}(${getDayWeek(date)}) ${time} 메뉴\n\n`; - if (diets.length > 0) { - // dishCategory 또는 dishType 별로 그룹화 - const grouped: Record = {}; - diets.forEach(diet => { - const type = diet.dishCategory || diet.dishType || ''; - if (!grouped[type]) grouped[type] = []; - grouped[type].push(diet.dishName); - }); - - // 출력 - description += Object.entries(grouped) - .map(([type, names]) => { - if (type) { - return `[${type}]\n${names.join('\n')}`; + if (menuGroups.length > 0) { + description += menuGroups + .map(({ category, items }) => { + if (category) { + return `[${category}]\n${items.join('\n')}`; } else { - return names.join('\n'); + return items.join('\n'); } }) .join('\n\n'); diff --git a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts b/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts index 8276915..a883daf 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts @@ -2,12 +2,10 @@ import { Controller, Get, Param, ParseIntPipe, Query, UseGuards } from '@nestjs/ import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { NativeResponseDto } from 'src/api/common/dtos/native-response.dto'; import { JwtAuthGuard } from 'src/api/public/users/guards/jwt-auth.guard'; +import { getDietTime } from 'src/api/public/cafeterias/utils/time'; import { GetCafeteriaDietQueryDto } from './dtos/requests/get-cafeteria-diet-query.dto'; import { GetCafeteriasQueryDto } from './dtos/requests/get-cafeterias-query.dto'; -import { - CafeteriaDietResponseDto, - MenuCategoryDto, -} from './dtos/responses/cafeteria-diet-response.dto'; +import { CafeteriaDietResponseDto } from './dtos/responses/cafeteria-diet-response.dto'; import { CafeteriaResponseDto } from './dtos/responses/cafeteria-response.dto'; import { CafeteriasService } from './cafeterias.service'; @@ -43,26 +41,8 @@ export class CafeteriasNativeController { @Query() query: GetCafeteriaDietQueryDto, ): Promise> { const date = query.date ? new Date(query.date) : new Date(); - const result = await this.cafeteriasService.getCafeteriaDietByDate( - cafeteriaId, - date, - query.time, - ); - - const grouped = new Map(); - for (const diet of result.diets) { - const key = diet.dishCategory || diet.dishType || ''; - let items = grouped.get(key); - if (!items) { - items = []; - grouped.set(key, items); - } - items.push(diet.dishName); - } - const menus: MenuCategoryDto[] = Array.from(grouped.entries()).map(([category, items]) => ({ - category, - items, - })); + const time = query.time ?? getDietTime(date); + const result = await this.cafeteriasService.getCafeteriaDiet(cafeteriaId, date, time); const data: CafeteriaDietResponseDto = { cafeteria: { @@ -77,7 +57,7 @@ export class CafeteriasNativeController { }, date: result.date.toISOString().slice(0, 10), time: result.time, - menus, + menus: result.menuGroups, }; return new NativeResponseDto(data); diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts b/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts index 19af107..362015e 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts @@ -14,6 +14,7 @@ import { FetchCurrentUser } from 'src/api/public/users/decorators/fetch-current- import { BlockId } from 'src/api/common/utils/constants'; import { User } from 'src/type-orm/entities/users/users.entity'; import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; +import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/utils/time'; import { CafeteriasService } from './cafeterias.service'; @ApiTags('cafeterias') @@ -60,7 +61,9 @@ export class CafeteriasController { @ClientExtra(ListCafeteriaDietExtraRequestDto) extra: ListCafeteriaDietExtraRequestDto, ) { - const { cafeteriaId, date, time } = extra; + const { cafeteriaId } = extra; + const date = getTodayOrTomorrow(extra.date); + const time = extra.time ?? getDietTime(date); const result = await this.cafeteriasService.getCafeteriaDiet(cafeteriaId, date, time); const template = this.cafeteriaMessageFactory.createCafeteriaDietListCard(result); return new ResponseDTO(template); diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts b/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts index d81a10c..35a63e4 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts @@ -82,6 +82,7 @@ describe('CafeteriasService', () => { describe('getCafeteriaDiet', () => { it('식당 정보와 식단 목록을 포함한 결과를 반환한다', async () => { + const date = new Date('2026-04-21T00:00:00.000Z'); const cafeteria = makeCafeteria({ id: 5 }); const diets: CafeteriaDiet[] = [ { @@ -95,7 +96,7 @@ describe('CafeteriasService', () => { cafeteriasRepository.findCafeteriaById.mockResolvedValue(cafeteria); cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue(diets); - const result = await service.getCafeteriaDiet(5, '오늘', '점심'); + const result = await service.getCafeteriaDiet(5, date, '점심'); expect(result.cafeteria).toEqual({ id: 5, @@ -107,61 +108,93 @@ describe('CafeteriasService', () => { thumbnailUrl: 'https://example.com/campus.jpg', }, }); - expect(result.diets).toEqual([ + expect(result.menuGroups).toEqual([ { - dishName: '김치찌개', - dishCategory: '한식', - dishType: '국', + category: '한식', + items: ['김치찌개'], }, ]); expect(result.time).toBe('점심'); - expect(result.date).toBeInstanceOf(Date); - }); - - it('dietTime이 없으면 현재 시각 기반으로 자동 결정된다', async () => { - const cafeteria = makeCafeteria(); - cafeteriasRepository.findCafeteriaById.mockResolvedValue(cafeteria); - cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue([]); - - const result = await service.getCafeteriaDiet(1, '오늘', undefined); - - expect(['아침', '점심', '저녁']).toContain(result.time); + expect(result.date).toBe(date); + expect(cafeteriasRepository.findCafeteriaDietsByCafeteriaId).toHaveBeenCalledWith( + 5, + date, + '점심', + ); }); - it('dietDate가 "내일"이면 내일 날짜로 조회한다', async () => { + it('식단을 카테고리 우선순위에 따라 그룹화한다', async () => { + const date = new Date('2026-04-21T00:00:00.000Z'); const cafeteria = makeCafeteria(); + const diets: CafeteriaDiet[] = [ + { + id: 1, + cafeteriaId: 1, + dishName: '김치찌개', + dishCategory: '한식', + dishType: '국', + } as any, + { + id: 2, + cafeteriaId: 1, + dishName: '된장국', + dishCategory: null, + dishType: '국', + } as any, + { + id: 3, + cafeteriaId: 1, + dishName: '잡곡밥', + dishCategory: null, + dishType: null, + } as any, + { + id: 4, + cafeteriaId: 1, + dishName: '제육볶음', + dishCategory: '한식', + dishType: '볶음', + } as any, + ]; cafeteriasRepository.findCafeteriaById.mockResolvedValue(cafeteria); - cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue([]); - - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); + cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue(diets); - const result = await service.getCafeteriaDiet(1, '내일', '아침'); + const result = await service.getCafeteriaDiet(1, date, '점심'); - expect(result.date.getDate()).toBe(tomorrow.getDate()); + expect(result.menuGroups).toEqual([ + { + category: '한식', + items: ['김치찌개', '제육볶음'], + }, + { + category: '국', + items: ['된장국'], + }, + { + category: '', + items: ['잡곡밥'], + }, + ]); }); it('식당을 찾을 수 없으면 NotFoundException을 던진다', async () => { + const date = new Date('2026-04-21T00:00:00.000Z'); cafeteriasRepository.findCafeteriaById.mockResolvedValue(null); cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue([]); - await expect(service.getCafeteriaDiet(999, '오늘', '점심')).rejects.toThrow( - NotFoundException, - ); + await expect(service.getCafeteriaDiet(999, date, '점심')).rejects.toThrow(NotFoundException); }); it('cache miss이면 DB를 조회하고 결과를 캐시에 저장한다', async () => { + const date = new Date('2026-04-21T00:00:00.000Z'); cacheManager.get.mockResolvedValue(null); cafeteriasRepository.findCafeteriaById.mockResolvedValue(makeCafeteria()); cafeteriasRepository.findCafeteriaDietsByCafeteriaId.mockResolvedValue([]); - const result = await service.getCafeteriaDiet(1, '오늘', '점심'); + const result = await service.getCafeteriaDiet(1, date, '점심'); expect(cafeteriasRepository.findCafeteriaById).toHaveBeenCalledWith(1); - expect(cacheManager.set).toHaveBeenCalledWith( - expect.stringMatching(/^diet:1:\d{4}-\d{2}-\d{2}:점심$/), - result, - ); + expect(cacheManager.set).toHaveBeenCalledWith('diet:1:2026-04-21:점심', result); }); }); }); diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts b/services/api/app/src/api/public/cafeterias/cafeterias.service.ts index 8b4553a..78b4b35 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.service.ts @@ -1,19 +1,17 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; +import { DietTime } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; import { - DietDate, - DietTime, -} from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { CafeteriaDietResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto'; + CafeteriaDietItemResult, + CafeteriaDietResult, + CafeteriaMenuGroupResult, +} from 'src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto'; import { CafeteriaListResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto'; -import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/utils/time'; import { CafeteriasRepository } from 'src/type-orm/entities/cafeterias/cafeterias.repository'; @Injectable() export class CafeteriasService { - readonly logger = new Logger(CafeteriasService.name); - constructor( private readonly cafeteriasRepository: CafeteriasRepository, @Inject(CACHE_MANAGER) readonly cacheManager: Cache, @@ -41,67 +39,20 @@ export class CafeteriasService { }; } - /** - * 식당 식단 정보 조회 (날짜를 직접 받는 Native REST용) - */ - public async getCafeteriaDietByDate( - cafeteriaId: number, - date: Date, - time?: DietTime, - ): Promise { - const resolvedTime = time ?? getDietTime(date); - - const cafeteria = await this.cafeteriasRepository.findCafeteriaById(cafeteriaId); - - if (!cafeteria) { - throw new NotFoundException(`식당(${cafeteriaId}) 정보를 찾을 수 없습니다.`); - } - - const diets = await this.cafeteriasRepository.findCafeteriaDietsByCafeteriaId( - cafeteriaId, - date, - resolvedTime, - ); - - return { - cafeteria: { - id: cafeteria.id, - name: cafeteria.name, - thumbnailUrl: cafeteria.thumbnailUrl, - campus: { - id: cafeteria.campus.id, - name: cafeteria.campus.name, - thumbnailUrl: cafeteria.campus.thumbnailUrl, - }, - }, - diets: diets.map(diet => ({ - dishCategory: diet.dishCategory, - dishType: diet.dishType, - dishName: diet.dishName, - })), - date, - time: resolvedTime, - }; - } - /** * 식당 식단 정보 조회 */ @CacheKey({ - key: ([cafeteriaId, dietDate, dietTime]) => { - const date = getTodayOrTomorrow(dietDate as DietDate | undefined); - const time = (dietTime as DietTime | undefined) ?? getDietTime(date); - return `diet:${cafeteriaId as number}:${date.toISOString().slice(0, 10)}:${time}`; - }, + key: ([cafeteriaId, date, time]) => + `diet:${cafeteriaId as number}:${(date as Date).toISOString().slice(0, 10)}:${ + time as DietTime + }`, }) public async getCafeteriaDiet( cafeteriaId: number, - dietDate?: DietDate, - dietTime?: DietTime, + date: Date, + time: DietTime, ): Promise { - const date = getTodayOrTomorrow(dietDate); - const time = dietTime ?? getDietTime(date); - const cafeteria = await this.cafeteriasRepository.findCafeteriaById(cafeteriaId); if (!cafeteria) { @@ -114,6 +65,12 @@ export class CafeteriasService { time, ); + const dietResults = diets.map(diet => ({ + dishCategory: diet.dishCategory, + dishType: diet.dishType, + dishName: diet.dishName, + })); + return { cafeteria: { id: cafeteria.id, @@ -125,13 +82,25 @@ export class CafeteriasService { thumbnailUrl: cafeteria.campus.thumbnailUrl, }, }, - diets: diets.map(diet => ({ - dishCategory: diet.dishCategory, - dishType: diet.dishType, - dishName: diet.dishName, - })), + menuGroups: this.groupDietsByCategory(dietResults), date, time, }; } + + private groupDietsByCategory(diets: CafeteriaDietItemResult[]): CafeteriaMenuGroupResult[] { + const grouped = new Map(); + + for (const diet of diets) { + const category = diet.dishCategory || diet.dishType || ''; + const items = grouped.get(category) ?? []; + items.push(diet.dishName); + grouped.set(category, items); + } + + return Array.from(grouped.entries()).map(([category, items]) => ({ + category, + items, + })); + } } diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts b/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts index f06143f..44233bd 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts +++ b/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts @@ -1,10 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; import { ClientExtraDto } from 'src/api/common/dtos/requests'; export type DietDate = '오늘' | '내일'; export type DietTime = '아침' | '점심' | '저녁'; export class ListCafeteriaDietExtraRequestDto extends ClientExtraDto { + @ApiProperty({ description: '식당 ID', example: 1 }) cafeteriaId: number; + + @ApiProperty({ description: '날짜', example: '오늘' }) date: DietDate; + + @ApiProperty({ description: '식사 시간', example: '아침' }) time?: DietTime; } diff --git a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts b/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts index e561a61..148f673 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts +++ b/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts @@ -7,9 +7,14 @@ export interface CafeteriaDietItemResult { dishName: string; } +export interface CafeteriaMenuGroupResult { + category: string; + items: string[]; +} + export interface CafeteriaDietResult { cafeteria: CafeteriaItemResult; - diets: CafeteriaDietItemResult[]; + menuGroups: CafeteriaMenuGroupResult[]; date: Date; time: DietTime; } diff --git a/services/api/app/src/app.module.ts b/services/api/app/src/app.module.ts index 6751634..94301a5 100644 --- a/services/api/app/src/app.module.ts +++ b/services/api/app/src/app.module.ts @@ -22,7 +22,6 @@ import { SchedulesModule } from './api/public/schedules/schedules.module'; import { ShuttlesModule } from './api/public/shuttles/shuttles.module'; import { MetricsInterceptor } from './api/common/interceptors/metrics.interceptor'; import { HealthModule } from './api/internal/health/health.module'; -import { WarmupModule } from './api/internal/warmup/warmup.module'; @Module({ imports: [ @@ -44,7 +43,6 @@ import { WarmupModule } from './api/internal/warmup/warmup.module'; SchedulesModule, ShuttlesModule, HealthModule, - WarmupModule, ], providers: [ { From 5de2f21abb13b932ba8735b1a361dac50655930d Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sat, 2 May 2026 16:39:49 +0900 Subject: [PATCH 03/29] =?UTF-8?q?=F0=9F=94=A5=20remove:=20@nestjs/schedule?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/api/app/pnpm-lock.yaml | 41 --------------------------------- 1 file changed, 41 deletions(-) diff --git a/services/api/app/pnpm-lock.yaml b/services/api/app/pnpm-lock.yaml index 64b204e..596f933 100644 --- a/services/api/app/pnpm-lock.yaml +++ b/services/api/app/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: '@nestjs/platform-express': specifier: ^9.0.0 version: 9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3) - '@nestjs/schedule': - specifier: 4.1.2 - version: 4.1.2(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/swagger': specifier: ^7.4.0 version: 7.4.2(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) @@ -688,12 +685,6 @@ packages: '@nestjs/common': ^9.0.0 '@nestjs/core': ^9.0.0 - '@nestjs/schedule@4.1.2': - resolution: {integrity: sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==} - peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 - '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 - '@nestjs/schematics@9.2.0': resolution: {integrity: sha512-wHpNJDPzM6XtZUOB3gW0J6mkFCSJilzCM3XrHI1o0C8vZmFE1snbmkIXNyoi1eV0Nxh1BMymcgz5vIMJgQtTqw==} peerDependencies: @@ -1248,9 +1239,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -1980,9 +1968,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron@3.2.1: - resolution: {integrity: sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==} - cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -3069,10 +3054,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - luxon@3.5.0: - resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} - engines: {node: '>=12'} - macos-release@2.5.1: resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} engines: {node: '>=6'} @@ -4343,10 +4324,6 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true - uuid@11.0.3: - resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} - hasBin: true - uuid@11.1.1: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true @@ -5286,13 +5263,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/schedule@4.1.2(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))': - dependencies: - '@nestjs/common': 9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - cron: 3.2.1 - uuid: 11.0.3 - '@nestjs/schematics@9.2.0(chokidar@3.5.3)(typescript@4.9.5)': dependencies: '@angular-devkit/core': 16.0.1(chokidar@3.5.3) @@ -6027,8 +5997,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/luxon@3.4.2': {} - '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -6858,11 +6826,6 @@ snapshots: create-require@1.1.1: {} - cron@3.2.1: - dependencies: - '@types/luxon': 3.4.2 - luxon: 3.5.0 - cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -8203,8 +8166,6 @@ snapshots: dependencies: yallist: 3.1.1 - luxon@3.5.0: {} - macos-release@2.5.1: {} magic-string@0.30.0: @@ -9387,8 +9348,6 @@ snapshots: uuid@10.0.0: {} - uuid@11.0.3: {} - uuid@11.1.1: {} uuid@9.0.1: {} From c764807a66531f8a6b17662c49a5303cc798a104 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sat, 2 May 2026 21:34:33 +0900 Subject: [PATCH 04/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EB=B3=84=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...24\352\265\254\354\202\254\355\225\255.md" | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 "docs/\353\217\204\353\251\224\354\235\270\353\263\204 \354\232\224\352\265\254\354\202\254\355\225\255.md" diff --git "a/docs/\353\217\204\353\251\224\354\235\270\353\263\204 \354\232\224\352\265\254\354\202\254\355\225\255.md" "b/docs/\353\217\204\353\251\224\354\235\270\353\263\204 \354\232\224\352\265\254\354\202\254\355\225\255.md" new file mode 100644 index 0000000..36ff980 --- /dev/null +++ "b/docs/\353\217\204\353\251\224\354\235\270\353\263\204 \354\232\224\352\265\254\354\202\254\355\225\255.md" @@ -0,0 +1,249 @@ +# 도메인별 요구사항 + +## 기준 + +- 원본 시나리오: [유저 시나리오.md](./유저%20시나리오.md) +- 목적: 사용자 시나리오를 바탕으로 도메인 모델을 도출하기 위한 요구사항을 도메인별로 정리한다. +- 범위: 카카오톡 챗봇과 모바일 앱에서 제공하는 학교 생활 정보 조회 기능 + +--- + +## 1. 사용자 설정 도메인 + +### 도메인 책임 + +사용자 설정 도메인은 사용자의 식별 정보와 개인화 기준이 되는 캠퍼스, 학과 정보를 관리한다. 다른 도메인은 사용자의 캠퍼스 또는 학과 설정을 조회 조건으로 사용할 수 있다. + +### 주요 개념 후보 + +| 개념 | 설명 | +| --- | --- | +| 사용자 | 커넥트 지누를 통해 교내 정보를 조회하는 주체 | +| 사용자 프로필 | 사용자에게 저장된 캠퍼스와 학과 정보 | +| 캠퍼스 | 식당, 학과, 셔틀 등 학교 생활 정보의 기준이 되는 캠퍼스 | +| 단과대학 | 학과를 그룹화하는 상위 조직 | +| 학과 | 사용자 개인화와 학과 공지사항 조회의 기준이 되는 조직 | + +### 기능 요구사항 + +- 시스템은 사용자가 챗봇을 사용할 때 사용자를 식별할 수 있어야 한다. +- 시스템은 사용자별로 캠퍼스와 학과 정보를 저장할 수 있어야 한다. +- 시스템은 사용자의 저장된 캠퍼스와 학과 정보를 조회할 수 있어야 한다. +- 저장된 캠퍼스와 학과가 없는 사용자는 미설정 상태로 처리해야 한다. +- 사용자는 캠퍼스, 단과대학, 학과 순서로 자신의 학과 정보를 등록하거나 변경할 수 있어야 한다. +- 사용자가 캠퍼스를 선택하면 시스템은 해당 캠퍼스의 단과대학 목록을 제공해야 한다. +- 사용자가 단과대학을 선택하면 시스템은 해당 단과대학의 학과 목록을 제공해야 한다. +- 학과 저장이 완료되면 시스템은 선택한 캠퍼스와 학과를 사용자 프로필에 반영해야 한다. + +### 정책 및 제약 + +- 단과대학 목록은 한 화면에 최대 5개씩 제공한다. +- 학과 목록은 한 화면에 최대 5개씩 제공한다. +- 목록이 5개를 초과하면 이전/다음 탐색을 통해 나머지 항목을 확인할 수 있어야 한다. +- 학과 공지사항 조회는 사용자 학과 정보가 등록되어 있어야 사용할 수 있다. +- 학식 조회는 사용자가 직접 캠퍼스를 선택하지 않은 경우 저장된 캠퍼스를 기본 조회 조건으로 사용할 수 있다. + +### 예외 요구사항 + +- 사용자 처리 중 예상하지 못한 문제가 발생하면 오류 메시지를 제공해야 한다. +- 선택한 캠퍼스, 단과대학, 학과 정보를 찾을 수 없으면 오류 상태로 처리해야 한다. +- 저장된 프로필 정보가 없으면 캠퍼스와 학과를 미설정 상태로 표시해야 한다. + +--- + +## 2. 공지사항 도메인 + +### 도메인 책임 + +공지사항 도메인은 학교 전체 공지사항과 사용자의 학과 공지사항을 최신순으로 제공한다. 사용자는 공지 목록에서 원문으로 이동할 수 있어야 한다. + +### 주요 개념 후보 + +| 개념 | 설명 | +| --- | --- | +| 공지사항 | 학교 또는 학과에서 게시한 안내 글 | +| 학교 공지사항 | 학교 전체 기준으로 제공되는 공지사항 | +| 학과 공지사항 | 특정 학과 또는 관련 상위 학과 기준으로 제공되는 공지사항 | +| 공지 카테고리 | 학교 공지사항을 분류하는 기준 | +| 공지 분류 | 학과 공지사항을 묶어 보여주는 분류 | +| 원문 링크 | 사용자가 공지사항 상세 내용을 확인할 수 있는 외부 또는 내부 경로 | + +### 기능 요구사항 + +- 시스템은 학교 전체 공지사항을 주요 카테고리별로 조회할 수 있어야 한다. +- 시스템은 학교 공지사항을 최신순으로 제공해야 한다. +- 시스템은 사용자의 학과 정보를 기준으로 학과 공지사항을 조회할 수 있어야 한다. +- 학과 공지사항 조회 시 필요한 경우 관련 상위 학과의 공지사항도 함께 조회해야 한다. +- 공지사항 목록에는 사용자가 원문을 확인할 수 있는 링크 또는 식별자가 포함되어야 한다. +- 챗봇과 모바일 앱은 공지사항을 사용자가 탐색하기 쉬운 카드 또는 목록 형태로 제공해야 한다. + +### 정책 및 제약 + +- 학교 공지사항은 `기관`, `채용`, `장학`, `외부기관 행사`, `학사` 카테고리를 제공한다. +- 학교 공지사항은 각 카테고리마다 최신 공지사항을 최대 4개씩 제공한다. +- 학교 공지사항은 전체 최대 20개까지 제공한다. +- 학과 공지사항은 공지 분류별 최신 글을 최대 4개씩 제공한다. +- 관련 상위 학과 공지사항을 함께 제공하더라도 분류별 최대 4개 기준을 유지한다. +- 학과 공지사항은 사용자 프로필에 학과가 등록되어 있을 때 조회할 수 있다. + +### 예외 요구사항 + +- 학교 공지사항이 없으면 현재 등록된 공지사항이 없음을 안내해야 한다. +- 학과 공지사항 조회 시 사용자 학과 정보가 없으면 학과 등록을 먼저 안내해야 한다. +- 학과 공지사항이 없으면 현재 등록된 공지사항이 없음을 안내해야 한다. + +--- + +## 3. 학식 도메인 + +### 도메인 책임 + +학식 도메인은 캠퍼스별 식당 목록과 식당별 식단을 제공한다. 사용자는 캠퍼스, 식당, 날짜, 끼니를 기준으로 메뉴를 조회할 수 있다. + +### 주요 개념 후보 + +| 개념 | 설명 | +| --- | --- | +| 캠퍼스 | 식당 목록을 구분하는 기준 | +| 식당 | 특정 캠퍼스에 속한 학식 제공 장소 | +| 식단 | 특정 식당, 날짜, 끼니에 제공되는 메뉴 정보 | +| 끼니 | 아침, 점심, 저녁 중 하나의 식사 구분 | +| 메뉴 분류 | 식단 메뉴를 묶어 보여주는 분류 | +| 조회 조건 | 캠퍼스, 식당, 날짜, 끼니 조합 | + +### 기능 요구사항 + +- 시스템은 캠퍼스 목록을 제공할 수 있어야 한다. +- 시스템은 캠퍼스별 식당 목록을 제공할 수 있어야 한다. +- 사용자가 챗봇에서 학식을 조회하면 시스템은 선택한 캠퍼스 또는 저장된 캠퍼스를 기준으로 식당 목록을 제공해야 한다. +- 사용자는 식당을 선택한 뒤 날짜와 끼니를 선택해 메뉴를 조회할 수 있어야 한다. +- 시스템은 특정 식당의 특정 날짜, 특정 끼니에 해당하는 식단을 조회할 수 있어야 한다. +- 끼니가 직접 선택되지 않으면 시스템은 현재 시간을 기준으로 끼니를 자동 선택해야 한다. +- 날짜가 직접 선택되지 않으면 시스템은 현재 시간을 기준으로 오늘 또는 내일을 자동 선택해야 한다. +- 모바일 앱은 캠퍼스, 식당, 날짜, 끼니 선택 상태에 맞는 메뉴를 조회할 수 있어야 한다. +- 메뉴는 분류별로 묶어 제공해야 한다. + +### 정책 및 제약 + +- 챗봇에서는 `오늘 아침`, `오늘 점심`, `오늘 저녁`, `내일 아침`, `내일 점심`, `내일 저녁` 6개 빠른 선택지를 제공한다. +- 모바일 앱에서는 오늘부터 7일간의 날짜를 선택할 수 있다. +- 끼니는 아침, 점심, 저녁 중 하나이다. +- 끼니 자동 선택 기준은 다음과 같다. + - 19:00부터 다음 날 09:29까지는 아침으로 본다. + - 09:30부터 13:29까지는 점심으로 본다. + - 13:30부터 18:59까지는 저녁으로 본다. +- 챗봇에서 날짜를 직접 선택하지 않은 경우 19:00 전에는 오늘, 19:00 이후에는 내일 메뉴를 보여준다. +- 챗봇의 자동 선택 기준은 한국 시간 기준으로 판단한다. +- 모바일 앱의 자동 선택 기준은 휴대폰에 설정된 현재 시간 기준으로 판단한다. + +### 예외 요구사항 + +- 선택한 캠퍼스도 저장된 캠퍼스도 없으면 캠퍼스 선택을 안내해야 한다. +- 해당 캠퍼스에 등록된 식당이 없으면 식당 정보가 없음을 안내해야 한다. +- 선택한 식당 정보를 찾을 수 없으면 오류 상태로 처리해야 한다. +- 선택한 날짜와 끼니에 등록된 메뉴가 없으면 메뉴가 없음을 안내해야 한다. +- 모바일 앱에서 캠퍼스, 식당, 메뉴 정보를 불러오지 못하면 각각의 실패 메시지를 제공해야 한다. + +--- + +## 4. 학사일정 도메인 + +### 도메인 책임 + +학사일정 도메인은 사용자가 선택한 월 또는 현재 월의 학사일정을 제공한다. + +### 주요 개념 후보 + +| 개념 | 설명 | +| --- | --- | +| 학사일정 | 학교의 학사 관련 일정 | +| 조회 월 | 학사일정을 조회하는 월 단위 조건 | +| 일정 항목 | 특정 기간 또는 날짜에 해당하는 학사 이벤트 | + +### 기능 요구사항 + +- 시스템은 특정 월의 학사일정을 조회할 수 있어야 한다. +- 사용자가 월을 선택하면 시스템은 해당 월의 학사일정을 제공해야 한다. +- 사용자가 월을 선택하지 않으면 시스템은 현재 월의 학사일정을 제공해야 한다. +- 챗봇은 조회된 학사일정을 사용자가 읽을 수 있는 형태로 제공해야 한다. + +### 정책 및 제약 + +- 조회 월은 1부터 12 사이의 값이어야 한다. +- 월 선택이 없는 경우 현재 월을 기본 조회 조건으로 사용한다. + +### 예외 요구사항 + +- 올바르지 않은 월을 선택하면 올바른 월 입력을 안내해야 한다. +- 해당 월에 등록된 학사일정이 없으면 일정이 없음을 안내해야 한다. + +--- + +## 5. 셔틀버스 도메인 + +### 도메인 책임 + +셔틀버스 도메인은 제공 중인 셔틀 노선과 노선별 시간표를 관리한다. 모바일 앱에서는 현재 시간 기준 다음 버스를 계산해 강조해서 제공한다. + +### 주요 개념 후보 + +| 개념 | 설명 | +| --- | --- | +| 셔틀 노선 | 출발 캠퍼스와 도착 캠퍼스를 가지는 운행 경로 | +| 셔틀 시간표 | 특정 노선의 운행 시간 목록 | +| 운행 구간 | 시간표를 묶어 보여주는 구간 | +| 운행 시간 | 셔틀이 특정 구간에서 출발하거나 도착하는 시간 | +| 다음 버스 | 현재 시간 이후 가장 가까운 운행 | +| 운행 예외 | 금요일 미운행 등 특정 조건에서 운행하지 않는 시간 | + +### 기능 요구사항 + +- 시스템은 등록된 셔틀 노선 목록을 제공할 수 있어야 한다. +- 사용자는 셔틀 노선 목록에서 원하는 노선을 선택할 수 있어야 한다. +- 시스템은 선택한 노선의 시간표를 제공할 수 있어야 한다. +- 챗봇은 선택한 노선의 시간표를 구간별로 묶어 제공해야 한다. +- 모바일 앱은 셔틀 노선 목록을 제공하고 첫 번째 노선을 기본으로 선택해야 한다. +- 모바일 앱은 사용자가 노선을 변경하면 해당 노선의 시간표를 다시 보여줘야 한다. +- 모바일 앱은 현재 시간 기준으로 가장 가까운 다음 버스를 계산해야 한다. +- 모바일 앱은 이미 지난 시간, 다음 버스 시간, 앞으로 남은 시간을 구분해 보여줘야 한다. +- 모바일 앱은 사용자가 갱신 버튼을 누르면 시간표를 즉시 새로 불러올 수 있어야 한다. +- 모바일 앱은 일정 시간마다 시간표를 새로 확인해 최신 상태를 유지해야 한다. + +### 정책 및 제약 + +- 현재 제공 노선은 `가좌 → 칠암 캠퍼스`, `칠암 → 가좌 캠퍼스` 2개이다. +- 시간표는 구간별로 묶어 보여준다. +- 금요일에 운행하지 않는 시간은 별도 표시로 구분한다. +- 다음 버스는 현재 시간 이후 가장 가까운 1개만 강조한다. +- 모바일 앱은 시간표를 1분마다 자동으로 새로 확인한다. + +### 예외 요구사항 + +- 등록된 노선이 없으면 노선 정보가 없음을 안내해야 한다. +- 선택한 노선 정보를 찾을 수 없으면 오류 상태로 처리해야 한다. +- 모바일 앱에서 노선 정보를 불러오지 못하면 노선 정보 조회 실패 메시지를 제공해야 한다. +- 모바일 앱에서 시간표를 불러오지 못하면 시간표 조회 실패 메시지를 제공해야 한다. +- 오늘 남은 운행이 없으면 운행이 종료되었음을 안내해야 한다. + +--- + +## 6. 도메인 간 관계 후보 + +| 관계 | 설명 | +| --- | --- | +| 사용자 프로필 - 캠퍼스 | 사용자는 개인화 기준으로 캠퍼스를 가질 수 있다. | +| 사용자 프로필 - 학과 | 사용자는 학과 공지사항 조회 기준으로 학과를 가질 수 있다. | +| 캠퍼스 - 단과대학 | 캠퍼스는 여러 단과대학을 가질 수 있다. | +| 단과대학 - 학과 | 단과대학은 여러 학과를 가질 수 있다. | +| 캠퍼스 - 식당 | 캠퍼스는 여러 식당을 가질 수 있다. | +| 식당 - 식단 | 식당은 날짜와 끼니별 식단을 가질 수 있다. | +| 학과 - 학과 공지사항 | 학과는 여러 공지사항의 조회 기준이 될 수 있다. | +| 셔틀 노선 - 셔틀 시간표 | 셔틀 노선은 여러 운행 시간을 가진다. | + +## 7. 공통 요구사항 후보 + +- 모든 조회 기능은 데이터가 없는 상태와 시스템 오류 상태를 구분해야 한다. +- 챗봇과 모바일 앱은 같은 도메인 규칙을 공유하되, 표시 방식과 기본 선택 정책은 채널 특성에 맞게 다를 수 있다. +- 시간 기반 자동 선택은 기준 시간대와 경계 시간을 명확히 정의해야 한다. +- 외부 원천 데이터의 부재, 변경, 삭제에 대비해 찾을 수 없는 정보에 대한 예외 처리가 필요하다. +- 도메인 모델은 조회 조건, 표시 정책, 예외 메시지를 분리해 변경 가능성을 낮춰야 한다. From ae4281dc40f6a26ec7a007b7e5f974dd37c1c9e6 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sun, 3 May 2026 16:48:51 +0900 Subject: [PATCH 05/29] =?UTF-8?q?=F0=9F=94=A5=20remove:=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EB=8A=94=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/api/app/package.json | 13 - services/api/app/pnpm-lock.yaml | 1909 +---------------- .../api/public/users/users.service.spec.ts | 6 - .../app/src/api/public/users/users.service.ts | 2 - services/api/app/src/app.module.ts | 18 +- services/api/app/src/main.ts | 2 - .../api/app/src/type-orm/database.module.ts | 9 - 7 files changed, 14 insertions(+), 1945 deletions(-) diff --git a/services/api/app/package.json b/services/api/app/package.json index 3d6710f..8b6fa83 100644 --- a/services/api/app/package.json +++ b/services/api/app/package.json @@ -28,7 +28,6 @@ "db:seed": "psql postgresql://postgres:postgres@localhost:54322/postgres -f supabase/seeds/seed.sql" }, "dependencies": { - "@nestjs/axios": "^3.1.3", "@nestjs/cache-manager": "^2.3.0", "@nestjs/common": "^9.4.3", "@nestjs/config": "^3.2.3", @@ -36,35 +35,25 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", - "@sentry/browser": "^10.43.0", "@sentry/nestjs": "^10.43.0", "@sentry/node": "^10.43.0", - "@sentry/node-native": "^10.43.0", - "@sentry/profiling-node": "^10.43.0", - "@supabase/supabase-js": "^2.45.1", "@willsoto/nestjs-prometheus": "^6.0.2", - "axios": "^1.7.9", "cache-manager": "^5.7.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", - "dotenv": "^16.4.5", "joi": "^17.13.3", "nest-winston": "^1.10.2", "pg": "^8.12.0", - "postgres": "^3.4.4", "prom-client": "^15.1.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", "typeorm": "^0.3.20", - "typeorm-transactional": "^0.5.0", "winston": "^3.17.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.2.0", "@nestjs/testing": "^9.0.0", - "@snaplet/copycat": "^5.1.0", - "@snaplet/seed": "0.98.0", "@types/express": "^4.17.13", "@types/jest": "29.5.1", "@types/node": "18.16.12", @@ -77,11 +66,9 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "29.5.0", "prettier": "^2.3.2", - "source-map-support": "^0.5.20", "supabase": "^2.78.1", "supertest": "^6.1.3", "ts-jest": "29.1.0", - "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "4.2.0", "typescript": "=5.5.3" diff --git a/services/api/app/pnpm-lock.yaml b/services/api/app/pnpm-lock.yaml index 596f933..5299bdd 100644 --- a/services/api/app/pnpm-lock.yaml +++ b/services/api/app/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@nestjs/axios': - specifier: ^3.1.3 - version: 3.1.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.15.2)(rxjs@7.8.2) '@nestjs/cache-manager': specifier: ^2.3.0 version: 2.3.0(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(cache-manager@5.7.6)(rxjs@7.8.2) @@ -32,30 +29,15 @@ importers: '@nestjs/typeorm': specifier: ^10.0.2 version: 10.0.2(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3))) - '@sentry/browser': - specifier: ^10.43.0 - version: 10.51.0 '@sentry/nestjs': specifier: ^10.43.0 version: 10.51.0(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@sentry/node': specifier: ^10.43.0 version: 10.51.0 - '@sentry/node-native': - specifier: ^10.43.0 - version: 10.51.0 - '@sentry/profiling-node': - specifier: ^10.43.0 - version: 10.51.0 - '@supabase/supabase-js': - specifier: ^2.45.1 - version: 2.105.1 '@willsoto/nestjs-prometheus': specifier: ^6.0.2 version: 6.1.0(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(prom-client@15.1.3) - axios: - specifier: ^1.7.9 - version: 1.15.2 cache-manager: specifier: ^5.7.6 version: 5.7.6 @@ -65,9 +47,6 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.4 - dotenv: - specifier: ^16.4.5 - version: 16.6.1 joi: specifier: ^17.13.3 version: 17.13.3 @@ -77,9 +56,6 @@ importers: pg: specifier: ^8.12.0 version: 8.20.0 - postgres: - specifier: ^3.4.4 - version: 3.4.9 prom-client: specifier: ^15.1.3 version: 15.1.3 @@ -92,9 +68,6 @@ importers: typeorm: specifier: ^0.3.20 version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3)) - typeorm-transactional: - specifier: ^0.5.0 - version: 0.5.0(reflect-metadata@0.1.14)(typeorm@0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3))) winston: specifier: ^3.17.0 version: 3.19.0 @@ -108,12 +81,6 @@ importers: '@nestjs/testing': specifier: ^9.0.0 version: 9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3)) - '@snaplet/copycat': - specifier: ^5.1.0 - version: 5.1.1 - '@snaplet/seed': - specifier: 0.98.0 - version: 0.98.0(@snaplet/copycat@5.1.1)(@types/pg@8.20.0)(openai@4.104.0(ws@8.20.0)(zod@3.25.76))(pg@8.20.0)(postgres@3.4.9)(ws@8.20.0) '@types/express': specifier: ^4.17.13 version: 4.17.25 @@ -150,9 +117,6 @@ importers: prettier: specifier: ^2.3.2 version: 2.8.8 - source-map-support: - specifier: ^0.5.20 - version: 0.5.21 supabase: specifier: ^2.78.1 version: 2.98.0 @@ -162,9 +126,6 @@ importers: ts-jest: specifier: 29.1.0 version: 29.1.0(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.5.0(@types/node@18.16.12)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3)))(typescript@5.5.3) - ts-loader: - specifier: ^9.2.3 - version: 9.5.7(typescript@5.5.3)(webpack@5.82.1) ts-node: specifier: ^10.0.0 version: 10.9.2(@types/node@18.16.12)(typescript@5.5.3) @@ -393,18 +354,11 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@faker-js/faker@8.4.1': - resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} - '@fastify/otel@0.18.0': resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@glideapps/ts-necessities@2.2.3': - resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==} - '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -424,66 +378,6 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead - '@inquirer/checkbox@2.5.0': - resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==} - engines: {node: '>=18'} - - '@inquirer/confirm@3.2.0': - resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} - engines: {node: '>=18'} - - '@inquirer/core@9.2.1': - resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} - engines: {node: '>=18'} - - '@inquirer/editor@2.2.0': - resolution: {integrity: sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==} - engines: {node: '>=18'} - - '@inquirer/expand@2.3.0': - resolution: {integrity: sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==} - engines: {node: '>=18'} - - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} - - '@inquirer/input@2.3.0': - resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==} - engines: {node: '>=18'} - - '@inquirer/number@1.1.0': - resolution: {integrity: sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==} - engines: {node: '>=18'} - - '@inquirer/password@2.2.0': - resolution: {integrity: sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==} - engines: {node: '>=18'} - - '@inquirer/prompts@5.5.0': - resolution: {integrity: sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==} - engines: {node: '>=18'} - - '@inquirer/rawlist@2.3.0': - resolution: {integrity: sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==} - engines: {node: '>=18'} - - '@inquirer/search@1.1.0': - resolution: {integrity: sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==} - engines: {node: '>=18'} - - '@inquirer/select@2.5.0': - resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==} - engines: {node: '>=18'} - - '@inquirer/type@1.5.5': - resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} - engines: {node: '>=18'} - - '@inquirer/type@2.0.0': - resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} - engines: {node: '>=18'} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -588,18 +482,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@langchain/core@0.2.36': - resolution: {integrity: sha512-qHLvScqERDeH7y2cLuJaSAlMwg3f/3Oc9nayRSXRU2UuaK/SOhI42cxiPLj1FnuHJSmN0rBQFkrLx02gI4mcVg==} - engines: {node: '>=18'} - - '@langchain/groq@0.0.15': - resolution: {integrity: sha512-JN6dlPNHZEFKc8JjQzO+H7IqCpcE3s/1nYnb+sHxpc22Amr3yxXgp6CvanLhmD+R8Wj1AE1vG7AkzjM8MLtv+A==} - engines: {node: '>=18'} - - '@langchain/openai@0.2.11': - resolution: {integrity: sha512-Pu8+WfJojCgSf0bAsXb4AjqvcDyAWyoEB1AoCRNACgEnBWZuitz3hLwCo9I+6hAbeg3QJ37g82yKcmvKAg1feg==} - engines: {node: '>=18'} - '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -607,13 +489,6 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@nestjs/axios@3.1.3': - resolution: {integrity: sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ==} - peerDependencies: - '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - axios: ^1.3.1 - rxjs: ^6.0.0 || ^7.0.0 - '@nestjs/cache-manager@2.3.0': resolution: {integrity: sha512-pxeBp9w/s99HaW2+pezM1P3fLiWmUEnTUoUMLa9UYViCtjj0E0A19W/vaT5JFACCzFIeNrwH4/16jkpAhQ25Vw==} peerDependencies: @@ -955,75 +830,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/debug@5.14.0-dev.34': - resolution: {integrity: sha512-mc4Ue07QjYcb4yV0ZXap2AJBLlBAk0owO3fHKWovQA9Ig2XXlxlAUesk9RxPYKj9zIpDZXYMPUC3iKIdUi5SUA==} - - '@prisma/engines-version@5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361': - resolution: {integrity: sha512-XkTJYtdOIrJkJv/tzXzsaUsfyvp82IWSPx4DlR52G0cyKoqT6lC55daIdsnuEoKPM2jPcL6P7dJENYBMGHQLEg==} - - '@prisma/engines@5.14.0-dev.34': - resolution: {integrity: sha512-RWkQHOPxSfy0ANoE0hhrDTf7SuNACILx/LTM1LINlWSYG+Ev/do+5RFbrCv6liCxi1fRZuuhtTux9sH56o01cQ==} - - '@prisma/fetch-engine@5.14.0-dev.34': - resolution: {integrity: sha512-Ieqp/Zfq7KaZWndJAq2K0Z5r77DBPyvXlKXbztXnyvoQhce+9QTkjwJ8U3dOHUwSwNqIb6TY7j1dal3epSUZkg==} - - '@prisma/generator-helper@5.14.0-dev.34': - resolution: {integrity: sha512-AsY7piYVHtaGf/TjSoK2j7pZmG+xX/Mqv/VQMNJmfJDEGAnt1fXg6e6veSGLm/SqxA3JJhVCaX3XUHYDeXnsOg==} - - '@prisma/get-platform@5.14.0-dev.34': - resolution: {integrity: sha512-JlzzUMQKsj1cFMXiGMkqrdP7dl3OZtZQapEeCAoH42J6GCrEuV+qNhTOlkywyNuFDj+j1VjfE7p9HRFO1+kiiw==} - '@prisma/instrumentation@7.6.0': resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==} peerDependencies: '@opentelemetry/api': ^1.8 - '@prisma/internals@5.14.0-dev.34': - resolution: {integrity: sha512-FKToi0h7DFkSZ+eAo737RisLAlRrHq2VPRnm53aVe7LH1J4qwVhl7U+Gy9CsifUgi5VDX311M2W5hyaRcBs46A==} - - '@prisma/prisma-schema-wasm@5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361': - resolution: {integrity: sha512-lMNW0WEI+eP5gPn+blBj2yK2znvQlWQbbcOdbqR6PmOOMZRPXbfoC1LgxFn0QrZalJ1csJSFPjmQiYcrv9/39w==} - - '@prisma/schema-files-loader@5.14.0-dev.34': - resolution: {integrity: sha512-oO0dMzBJbNN3OwcNpRpKO6iq/rqWg02OKBeUI+Qy3Cwrqo5SlKO+DeolkUnx2PPWiHitDX/8UkGRBkMRG0HI9g==} - - '@sagold/json-pointer@5.1.2': - resolution: {integrity: sha512-+wAhJZBXa6MNxRScg6tkqEbChEHMgVZAhTHVJ60Y7sbtXtu9XA49KfUkdWlS2x78D6H9nryiKePiYozumauPfA==} - - '@sagold/json-query@6.2.0': - resolution: {integrity: sha512-7bOIdUE6eHeoWtFm8TvHQHfTVSZuCs+3RpOKmZCDBIOrxpvF/rNFTeuvIyjHva/RR0yVS3kQtr+9TW72LQEZjA==} - - '@scaleleap/pg-format@1.0.0': - resolution: {integrity: sha512-gFkcYMnpeylF2OJ30FsDBjwICB9JTiZ5i3guPwdiBDrJFwIKr+Zk6jwI8Mg22a4FwXn5ezd5cHEFMKqBqBz4RQ==} - - '@sentry-internal/browser-utils@10.51.0': - resolution: {integrity: sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==} - engines: {node: '>=18'} - - '@sentry-internal/feedback@10.51.0': - resolution: {integrity: sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==} - engines: {node: '>=18'} - - '@sentry-internal/node-cpu-profiler@2.2.0': - resolution: {integrity: sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ==} - engines: {node: '>=18'} - - '@sentry-internal/node-native-stacktrace@0.4.1': - resolution: {integrity: sha512-D/Tp25ELxDZPj5LD74v3KdOLQFwYtaPZVPI2id+cZ89Cch9Ndd2LV0XbgTzyJFDlSMh0h0hOnmcmjp7uA8b5ZA==} - engines: {node: '>=18'} - - '@sentry-internal/replay-canvas@10.51.0': - resolution: {integrity: sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==} - engines: {node: '>=18'} - - '@sentry-internal/replay@10.51.0': - resolution: {integrity: sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==} - engines: {node: '>=18'} - - '@sentry/browser@10.51.0': - resolution: {integrity: sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==} - engines: {node: '>=18'} - '@sentry/core@10.51.0': resolution: {integrity: sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==} engines: {node: '>=18'} @@ -1059,10 +870,6 @@ packages: '@opentelemetry/semantic-conventions': optional: true - '@sentry/node-native@10.51.0': - resolution: {integrity: sha512-nydVfaS+3wCIAAtBDmV6zGNgwxesApJ0G+a1Bctbx7UN2I4LRw4ohaG7+iKMLSWeMKKgWLINpCfjVoNkNBH9gw==} - engines: {node: '>=18'} - '@sentry/node@10.51.0': resolution: {integrity: sha512-2yZLRZwS1dKG8/4eOTpGSo/gO/EgmT9aPj6lAzUkRa7bZCTTdW4BraaHU0leX5T94909Qfhbr3W5AVTfDOCKiQ==} engines: {node: '>=18'} @@ -1076,11 +883,6 @@ packages: '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 '@opentelemetry/semantic-conventions': ^1.39.0 - '@sentry/profiling-node@10.51.0': - resolution: {integrity: sha512-hfolAU9VfesPf0O0pXTNR9z7Cj0U6PtPnTpZg4RAEYYa73TdXqSJAYGLIiZt1F0YfGk0lIIPJTOsVTM095zt1w==} - engines: {node: '>=18'} - hasBin: true - '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -1099,74 +901,12 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@snaplet/copycat@5.1.1': - resolution: {integrity: sha512-sRxkzKIYJ6Q7hL4c8m+2u7oS/jE0FW/24HUa4a23IAuiopNc/nf5bm1Lecyra2xMVcCWrucE40AlQLnNNCO+iw==} - - '@snaplet/seed@0.98.0': - resolution: {integrity: sha512-qLh7e596mKbY3DZW1CryhYyZdFJ/irAU760t00ac1VHsx/1Brfu3jpc6Bq69XBRZ6I3/nqPDlRckVP//bpFUMQ==} - engines: {node: '>=18.5.0'} - hasBin: true - peerDependencies: - '@prisma/client': '>=5' - '@snaplet/copycat': '>=2' - '@types/better-sqlite3': '*' - '@types/pg': '*' - better-sqlite3: '>=9' - mysql2: '>=3' - pg: '>=8' - postgres: '>=3' - peerDependenciesMeta: - '@prisma/client': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - better-sqlite3: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} - '@supabase/auth-js@2.105.1': - resolution: {integrity: sha512-zc4s8Xg4truwE1Q4Q8M8oUVDARMd05pKh73NyQsMbYU1HDdDN2iiKzena/yu+yJze3WrD4c092FdckPiK1rLQw==} - engines: {node: '>=20.0.0'} - - '@supabase/functions-js@2.105.1': - resolution: {integrity: sha512-dTk1e7oE51VGc1lS2S0J0NLo0Wp4JYChj74ArJKbIWgoWuFwO0wcJYjeyOV3AAEpKst8/LQWUZOUKO1tRXBrpA==} - engines: {node: '>=20.0.0'} - - '@supabase/phoenix@0.4.1': - resolution: {integrity: sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==} - - '@supabase/postgrest-js@2.105.1': - resolution: {integrity: sha512-6SbtsoWC55xfsm7gbfLqvF+yIwTQEbjt+jFGf4klDpwSnUy17Hv5x0Dq52oqwTQlw6Ta0h1D5gTP0/pApqNojA==} - engines: {node: '>=20.0.0'} - - '@supabase/realtime-js@2.105.1': - resolution: {integrity: sha512-3X3cUEl5cJ4lRQHr1hXHx0b98OaL97RRO2vrRZ98FD91JV/MquZHhrGJSv/+IkOnjF6E2e0RUOxE8P3Zi035ow==} - engines: {node: '>=20.0.0'} - - '@supabase/storage-js@2.105.1': - resolution: {integrity: sha512-owfdCNH5ikXXDusjzsgU6LavEBqGUoueOnL/9XIucld70/WJ/rbqp89K//c9QPICDNuegsmpoeasydDAiucLKQ==} - engines: {node: '>=20.0.0'} - - '@supabase/supabase-js@2.105.1': - resolution: {integrity: sha512-4gn6HmsAkCCVU7p8JmgKGhHJ5Btod4ZzSp8qKZf4JHaTxbhaIK86/usHzeLxWv7EJJDhBmILDmJOSOf9iF4CLA==} - engines: {node: '>=20.0.0'} - - '@total-typescript/ts-reset@0.5.1': - resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==} - '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -1194,9 +934,6 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/cls-hooked@4.3.9': - resolution: {integrity: sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==} - '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -1245,21 +982,12 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mute-stream@0.0.4': - resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node@18.16.12': resolution: {integrity: sha512-tIRrjbY9C277MOfP8M3zjMIhtMlUJ6YVqkGgLjz+74jVsdf4/UjC6Hku4+1N0BS0qyC0JAS6tJLUk9H6JUKviQ==} - '@types/node@22.19.17': - resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} - '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1278,9 +1006,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -1305,21 +1030,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/urijs@1.19.26': - resolution: {integrity: sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==} - - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} - '@types/wrap-ansi@3.0.0': - resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} - - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1444,10 +1157,6 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1481,10 +1190,6 @@ packages: resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} engines: {node: '>= 20'} - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1520,14 +1225,6 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - ansi-escapes@5.0.0: - resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} - engines: {node: '>=12'} - - ansi-escapes@6.2.1: - resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==} - engines: {node: '>=14.16'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1566,19 +1263,12 @@ packages: arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-differ@4.0.0: - resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -1586,17 +1276,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array-union@3.0.1: - resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} - engines: {node: '>=12'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - async-hook-jl@1.7.6: - resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==} - engines: {node: ^4.7 || >=6.9 || >=7.3} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1607,9 +1289,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.15.2: - resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} - babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1642,14 +1321,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base-64@0.1.0: - resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.24: - resolution: {integrity: sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==} + baseline-browser-mapping@2.10.25: + resolution: {integrity: sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==} engines: {node: '>=6.0.0'} hasBin: true @@ -1689,9 +1365,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-or-node@2.1.1: - resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1721,14 +1394,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - c12@1.11.2: - resolution: {integrity: sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==} - peerDependencies: - magicast: ^0.3.4 - peerDependenciesMeta: - magicast: - optional: true - cache-manager@5.7.6: resolution: {integrity: sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==} engines: {node: '>= 18'} @@ -1764,13 +1429,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - change-case@5.4.4: - resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -1778,21 +1436,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1805,9 +1452,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -1824,10 +1468,6 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -1840,10 +1480,6 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1852,10 +1488,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - cls-hooked@4.2.2: - resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==} - engines: {node: ^4.7 || >=6.9 || >=7.3 || >=8.2.1} - cmd-shim@8.0.0: resolution: {integrity: sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -1867,9 +1499,6 @@ packages: collect-v8-coverage@1.0.3: resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} - collection-utils@1.0.1: - resolution: {integrity: sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1897,10 +1526,6 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -1918,16 +1543,9 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1968,16 +1586,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-fetch@4.1.0: - resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -2002,13 +1614,6 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -2031,9 +1636,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2042,17 +1644,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2068,16 +1663,10 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} - digest-fetch@1.3.0: - resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discontinuous-range@1.0.0: - resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -2101,26 +1690,16 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ebnf@1.9.1: - resolution: {integrity: sha512-uW2UKSsuty9ANJ3YByIQE4ANkD8nqUPO7r6Fwcc1ADKPe9FRdcPpMl3VEput4JSvKBJ4J86npIC2MLP0pYkCuw==} - hasBin: true - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.348: - resolution: {integrity: sha512-QC2X59nRlycQQMc4ZXjSVBX+tSgJfgRtcrYHbIZLgOV2dCvefoQGegLR7lLXKgpPpSuVmJU19LMzGrSa2C7k3Q==} - - emitter-listener@1.1.2: - resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==} + electron-to-chromium@1.5.349: + resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2250,13 +1829,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -2272,14 +1844,6 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - exit-hook@4.0.0: - resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==} - engines: {node: '>=18'} - exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -2296,9 +1860,6 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - fast-copy@3.0.2: - resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2334,9 +1895,6 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - fictional@2.1.1: - resolution: {integrity: sha512-lHrMISII22AXlro16kjq0tTEr+LWWQwPVT3h3H8Xxd1pDr3sTI+ZxNOpp/SQm1wrY1jr5A7taOxJAOXvnLdjVQ==} - figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2361,10 +1919,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - find-up@7.0.0: - resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} - engines: {node: '>=18'} - flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2375,18 +1929,6 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - fnv-plus@1.3.1: - resolution: {integrity: sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw==} - - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2402,17 +1944,10 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2435,18 +1970,6 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} - engines: {node: '>=14.14'} - - fs-extra@11.3.4: - resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} - engines: {node: '>=14.14'} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} @@ -2469,10 +1992,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2493,14 +2012,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - - giget@1.2.5: - resolution: {integrity: sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==} - hasBin: true - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2544,9 +2055,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - groq-sdk@0.3.3: - resolution: {integrity: sha512-wdOeZ2QymPjjP3tmFpUAnfMisoLbt7xF2MfpROeFAngcqWbfTyB9j9pMWSEAMF/E4gZx8f2Y+5zswO0q92CSxA==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2585,17 +2093,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - - iceberg-js@0.8.1: - resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} - engines: {node: '>=20.0.0'} - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2627,10 +2124,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflection@3.0.2: - resolution: {integrity: sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==} - engines: {node: '>=18.0.0'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2661,9 +2154,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -2692,10 +2182,6 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2708,10 +2194,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -2720,17 +2202,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - - is-url@1.2.4: - resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} - isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -2771,9 +2242,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - javascript-stringify@2.1.0: - resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} - jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2907,19 +2375,9 @@ packages: node-notifier: optional: true - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - - js-tiktoken@1.0.21: - resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2946,9 +2404,6 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-library@9.3.5: - resolution: {integrity: sha512-5eBDx7cbfs+RjylsVO+N36b0GOPtv78rfqgf2uON+uaHUIC62h63Y8pkV2ovKbaL4ZpQcHp21968x5nx/dFwqQ==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2976,21 +2431,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - langsmith@0.1.68: - resolution: {integrity: sha512-otmiysWtVAqzMx3CJ4PrtUBhWRG5Co8Z4o7hSZENPjlit9/j3/vm3TSvbaxpDYakZxtMjhkcJTqrdYFipISEiQ==} - peerDependencies: - openai: '*' - peerDependenciesMeta: - openai: - optional: true - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3017,10 +2460,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -3040,10 +2479,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - logform@2.7.0: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} @@ -3076,9 +2511,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -3127,14 +2559,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -3153,26 +2577,14 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -3181,20 +2593,9 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - moo@0.5.3: - resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -3206,28 +2607,12 @@ packages: engines: {node: '>= 6.0.0'} deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. - multimatch@7.0.0: - resolution: {integrity: sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==} - engines: {node: '>=18'} - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nearley@2.20.1: - resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} - hasBin: true - negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -3241,10 +2626,6 @@ packages: '@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 winston: ^3.0.0 - node-abi@3.89.0: - resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} - engines: {node: '>=10'} - node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -3256,9 +2637,6 @@ packages: node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3290,15 +2668,6 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - nypm@0.5.4: - resolution: {integrity: sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==} - engines: {node: ^14.16.0 || >=16.10.0} - hasBin: true - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3307,9 +2676,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - ohash@1.1.6: - resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3324,38 +2690,14 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - - openai@4.104.0: - resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.23.8 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - ora@8.2.0: - resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} - engines: {node: '>=18'} - os-name@4.0.1: resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} engines: {node: '>=10'} @@ -3364,10 +2706,6 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3376,10 +2714,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3388,22 +2722,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3411,12 +2729,6 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3433,10 +2745,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3445,10 +2753,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3469,15 +2773,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -3527,9 +2822,6 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -3554,10 +2846,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres@3.4.9: - resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} - engines: {node: '>=12'} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3582,10 +2870,6 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - prom-client@15.1.3: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} @@ -3602,10 +2886,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -3627,16 +2907,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quicktype-core@23.0.149: - resolution: {integrity: sha512-P6orZe46XwDcl17MdJc1SLgAornP3XzEHYE25vhS2DWG5t0mszS9oSS5BiFir/XnBv2Ak0P70Zz5m7C2WhLjWw==} - - railroad-diagrams@1.0.0: - resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} - - randexp@0.4.6: - resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} - engines: {node: '>=0.12'} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3649,9 +2919,6 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - rc9@2.1.2: - resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3666,10 +2933,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readable-stream@4.5.2: - resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3684,9 +2947,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - remeda@1.61.0: - resolution: {integrity: sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3724,18 +2984,6 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - - ret@0.1.15: - resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} - engines: {node: '>=0.12'} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3784,10 +3032,6 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3830,9 +3074,6 @@ packages: engines: {node: '>=4'} hasBin: true - shimmer@1.2.1: - resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -3856,9 +3097,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - siphash@1.2.0: - resolution: {integrity: sha512-zGo/O5A0Nr4oSteEAMlhemqQpCBbVTRaTjUQdO+QFUqe1iofq/NNPe2W1RxJreh89fIk6NhQcNi41UeTGCvr+g==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3866,10 +3104,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - smtp-address-parser@1.0.10: - resolution: {integrity: sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==} - engines: {node: '>=0.10'} - source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3884,10 +3118,6 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3899,13 +3129,6 @@ packages: resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} engines: {node: '>=14'} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - - stack-chain@1.3.7: - resolution: {integrity: sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==} - stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -3917,18 +3140,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -3941,10 +3156,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -3971,10 +3182,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4002,10 +3209,6 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-hyperlinks@2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -4021,11 +3224,6 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - tar@7.5.13: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} @@ -4033,10 +3231,6 @@ packages: tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} - terminal-link@3.0.0: - resolution: {integrity: sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==} - engines: {node: '>=12'} - terser-webpack-plugin@5.5.0: resolution: {integrity: sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==} engines: {node: '>= 10.13.0'} @@ -4071,12 +3265,6 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4134,13 +3322,6 @@ packages: esbuild: optional: true - ts-loader@9.5.7: - resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} - engines: {node: '>=12.0.0'} - peerDependencies: - typescript: '*' - webpack: ^5.0.0 - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -4185,10 +3366,6 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -4200,13 +3377,6 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm-transactional@0.5.0: - resolution: {integrity: sha512-53/CwnXpOIJnWU3oVCNbhHB95FwciKSGbY+m/Hw4e2dBM2c4toiOHwf4pmk83Ne7guznmDgVr/5IUfbp+JTPCg==} - engines: {node: '>=12.0.0'} - peerDependencies: - reflect-metadata: '>= 0.1.12' - typeorm: '>= 0.2.8' - typeorm@0.3.28: resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} engines: {node: '>=16.13.0'} @@ -4272,26 +3442,10 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unicode-properties@1.4.1: - resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} - - unicode-trie@2.0.0: - resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4309,9 +3463,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - urijs@1.19.11: - resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4319,11 +3470,6 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - uuid@11.1.1: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true @@ -4340,9 +3486,6 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - valid-url@1.0.9: - resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} - validator@13.15.35: resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} @@ -4365,10 +3508,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4418,13 +3557,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4444,18 +3576,6 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4467,9 +3587,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4478,11 +3595,6 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} - engines: {node: '>= 14.6'} - hasBin: true - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4499,22 +3611,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - snapshots: '@angular-devkit/core@16.0.1(chokidar@3.5.3)': @@ -4775,8 +3871,6 @@ snapshots: '@eslint/js@8.57.1': {} - '@faker-js/faker@8.4.1': {} - '@fastify/otel@0.18.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -4787,8 +3881,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@glideapps/ts-necessities@2.2.3': {} - '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -4807,106 +3899,6 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@inquirer/checkbox@2.5.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 1.5.5 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.3 - - '@inquirer/confirm@3.2.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - - '@inquirer/core@9.2.1': - dependencies: - '@inquirer/figures': 1.0.15 - '@inquirer/type': 2.0.0 - '@types/mute-stream': 0.0.4 - '@types/node': 22.19.17 - '@types/wrap-ansi': 3.0.0 - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 1.0.0 - signal-exit: 4.1.0 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - - '@inquirer/editor@2.2.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - external-editor: 3.1.0 - - '@inquirer/expand@2.3.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - yoctocolors-cjs: 2.1.3 - - '@inquirer/figures@1.0.15': {} - - '@inquirer/input@2.3.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - - '@inquirer/number@1.1.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - - '@inquirer/password@2.2.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - ansi-escapes: 4.3.2 - - '@inquirer/prompts@5.5.0': - dependencies: - '@inquirer/checkbox': 2.5.0 - '@inquirer/confirm': 3.2.0 - '@inquirer/editor': 2.2.0 - '@inquirer/expand': 2.3.0 - '@inquirer/input': 2.3.0 - '@inquirer/number': 1.1.0 - '@inquirer/password': 2.2.0 - '@inquirer/rawlist': 2.3.0 - '@inquirer/search': 1.1.0 - '@inquirer/select': 2.5.0 - - '@inquirer/rawlist@2.3.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 1.5.5 - yoctocolors-cjs: 2.1.3 - - '@inquirer/search@1.1.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 1.5.5 - yoctocolors-cjs: 2.1.3 - - '@inquirer/select@2.5.0': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 1.5.5 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.3 - - '@inquirer/type@1.5.5': - dependencies: - mute-stream: 1.0.0 - - '@inquirer/type@2.0.0': - dependencies: - mute-stream: 1.0.0 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5121,55 +4113,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@langchain/core@0.2.36(openai@4.104.0(ws@8.20.0)(zod@3.25.76))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.21 - langsmith: 0.1.68(openai@4.104.0(ws@8.20.0)(zod@3.25.76)) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - openai - - '@langchain/groq@0.0.15(openai@4.104.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0)': - dependencies: - '@langchain/core': 0.2.36(openai@4.104.0(ws@8.20.0)(zod@3.25.76)) - '@langchain/openai': 0.2.11(ws@8.20.0) - groq-sdk: 0.3.3 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - encoding - - openai - - ws - - '@langchain/openai@0.2.11(ws@8.20.0)': - dependencies: - '@langchain/core': 0.2.36(openai@4.104.0(ws@8.20.0)(zod@3.25.76)) - js-tiktoken: 1.0.21 - openai: 4.104.0(ws@8.20.0)(zod@3.25.76) - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - encoding - - ws - '@lukeed/csprng@1.1.0': {} '@microsoft/tsdoc@0.15.1': {} - '@nestjs/axios@3.1.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.15.2)(rxjs@7.8.2)': - dependencies: - '@nestjs/common': 9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - axios: 1.15.2 - rxjs: 7.8.2 - '@nestjs/cache-manager@2.3.0(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))(cache-manager@5.7.6)(rxjs@7.8.2)': dependencies: '@nestjs/common': 9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -5598,31 +4545,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/debug@5.14.0-dev.34': {} - - '@prisma/engines-version@5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361': {} - - '@prisma/engines@5.14.0-dev.34': - dependencies: - '@prisma/debug': 5.14.0-dev.34 - '@prisma/engines-version': 5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361 - '@prisma/fetch-engine': 5.14.0-dev.34 - '@prisma/get-platform': 5.14.0-dev.34 - - '@prisma/fetch-engine@5.14.0-dev.34': - dependencies: - '@prisma/debug': 5.14.0-dev.34 - '@prisma/engines-version': 5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361 - '@prisma/get-platform': 5.14.0-dev.34 - - '@prisma/generator-helper@5.14.0-dev.34': - dependencies: - '@prisma/debug': 5.14.0-dev.34 - - '@prisma/get-platform@5.14.0-dev.34': - dependencies: - '@prisma/debug': 5.14.0-dev.34 - '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5630,69 +4552,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/internals@5.14.0-dev.34': - dependencies: - '@prisma/debug': 5.14.0-dev.34 - '@prisma/engines': 5.14.0-dev.34 - '@prisma/fetch-engine': 5.14.0-dev.34 - '@prisma/generator-helper': 5.14.0-dev.34 - '@prisma/get-platform': 5.14.0-dev.34 - '@prisma/prisma-schema-wasm': 5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361 - '@prisma/schema-files-loader': 5.14.0-dev.34 - arg: 5.0.2 - prompts: 2.4.2 - - '@prisma/prisma-schema-wasm@5.14.0-6.264f24ce0b2f544ff968ff76bfaa999de1161361': {} - - '@prisma/schema-files-loader@5.14.0-dev.34': - dependencies: - fs-extra: 11.1.1 - - '@sagold/json-pointer@5.1.2': {} - - '@sagold/json-query@6.2.0': - dependencies: - '@sagold/json-pointer': 5.1.2 - ebnf: 1.9.1 - - '@scaleleap/pg-format@1.0.0': {} - - '@sentry-internal/browser-utils@10.51.0': - dependencies: - '@sentry/core': 10.51.0 - - '@sentry-internal/feedback@10.51.0': - dependencies: - '@sentry/core': 10.51.0 - - '@sentry-internal/node-cpu-profiler@2.2.0': - dependencies: - detect-libc: 2.1.2 - node-abi: 3.89.0 - - '@sentry-internal/node-native-stacktrace@0.4.1': - dependencies: - detect-libc: 2.1.2 - node-abi: 3.89.0 - - '@sentry-internal/replay-canvas@10.51.0': - dependencies: - '@sentry-internal/replay': 10.51.0 - '@sentry/core': 10.51.0 - - '@sentry-internal/replay@10.51.0': - dependencies: - '@sentry-internal/browser-utils': 10.51.0 - '@sentry/core': 10.51.0 - - '@sentry/browser@10.51.0': - dependencies: - '@sentry-internal/browser-utils': 10.51.0 - '@sentry-internal/feedback': 10.51.0 - '@sentry-internal/replay': 10.51.0 - '@sentry-internal/replay-canvas': 10.51.0 - '@sentry/core': 10.51.0 - '@sentry/core@10.51.0': {} '@sentry/nestjs@10.51.0(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3(@nestjs/common@9.4.3(cache-manager@5.7.6)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@9.4.3)(reflect-metadata@0.1.14)(rxjs@7.8.2))': @@ -5722,15 +4581,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/node-native@10.51.0': - dependencies: - '@sentry-internal/node-native-stacktrace': 0.4.1 - '@sentry/core': 10.51.0 - '@sentry/node': 10.51.0 - transitivePeerDependencies: - - '@opentelemetry/exporter-trace-otlp-http' - - supports-color - '@sentry/node@10.51.0': dependencies: '@fastify/otel': 0.18.0(@opentelemetry/api@1.9.1) @@ -5776,15 +4626,6 @@ snapshots: '@opentelemetry/semantic-conventions': 1.40.0 '@sentry/core': 10.51.0 - '@sentry/profiling-node@10.51.0': - dependencies: - '@sentry-internal/node-cpu-profiler': 2.2.0 - '@sentry/core': 10.51.0 - '@sentry/node': 10.51.0 - transitivePeerDependencies: - - '@opentelemetry/exporter-trace-otlp-http' - - supports-color - '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -5803,58 +4644,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@snaplet/copycat@5.1.1': - dependencies: - '@faker-js/faker': 8.4.1 - fictional: 2.1.1 - string-argv: 0.3.2 - uuid: 9.0.1 - - '@snaplet/seed@0.98.0(@snaplet/copycat@5.1.1)(@types/pg@8.20.0)(openai@4.104.0(ws@8.20.0)(zod@3.25.76))(pg@8.20.0)(postgres@3.4.9)(ws@8.20.0)': - dependencies: - '@inquirer/prompts': 5.5.0 - '@langchain/core': 0.2.36(openai@4.104.0(ws@8.20.0)(zod@3.25.76)) - '@langchain/groq': 0.0.15(openai@4.104.0(ws@8.20.0)(zod@3.25.76))(ws@8.20.0) - '@langchain/openai': 0.2.11(ws@8.20.0) - '@prisma/generator-helper': 5.14.0-dev.34 - '@prisma/internals': 5.14.0-dev.34 - '@scaleleap/pg-format': 1.0.0 - '@snaplet/copycat': 5.1.1 - '@total-typescript/ts-reset': 0.5.1 - ansi-escapes: 6.2.1 - c12: 1.11.2 - change-case: 5.4.4 - debug: 4.4.3 - dedent: 1.7.2 - execa: 8.0.1 - exit-hook: 4.0.0 - find-up: 7.0.0 - fs-extra: 11.3.4 - inflection: 3.0.2 - javascript-stringify: 2.1.0 - json-schema-library: 9.3.5 - kleur: 4.1.5 - multimatch: 7.0.0 - ora: 8.2.0 - quicktype-core: 23.0.149 - remeda: 1.61.0 - sqlstring: 2.3.3 - terminal-link: 3.0.0 - uuid: 9.0.1 - yargs: 17.7.2 - zod: 3.25.76 - optionalDependencies: - '@types/pg': 8.20.0 - pg: 8.20.0 - postgres: 3.4.9 - transitivePeerDependencies: - - babel-plugin-macros - - encoding - - magicast - - openai - - supports-color - - ws - '@so-ric/colorspace@1.1.6': dependencies: color: 5.0.3 @@ -5862,48 +4651,6 @@ snapshots: '@sqltools/formatter@1.2.5': {} - '@supabase/auth-js@2.105.1': - dependencies: - tslib: 2.8.1 - - '@supabase/functions-js@2.105.1': - dependencies: - tslib: 2.8.1 - - '@supabase/phoenix@0.4.1': {} - - '@supabase/postgrest-js@2.105.1': - dependencies: - tslib: 2.8.1 - - '@supabase/realtime-js@2.105.1': - dependencies: - '@supabase/phoenix': 0.4.1 - '@types/ws': 8.18.1 - tslib: 2.8.1 - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@supabase/storage-js@2.105.1': - dependencies: - iceberg-js: 0.8.1 - tslib: 2.8.1 - - '@supabase/supabase-js@2.105.1': - dependencies: - '@supabase/auth-js': 2.105.1 - '@supabase/functions-js': 2.105.1 - '@supabase/postgrest-js': 2.105.1 - '@supabase/realtime-js': 2.105.1 - '@supabase/storage-js': 2.105.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@total-typescript/ts-reset@0.5.1': {} - '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -5938,10 +4685,6 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 18.16.12 - '@types/cls-hooked@4.3.9': - dependencies: - '@types/node': 18.16.12 - '@types/connect@3.4.38': dependencies: '@types/node': 18.16.12 @@ -6001,25 +4744,12 @@ snapshots: '@types/mime@1.3.5': {} - '@types/mute-stream@0.0.4': - dependencies: - '@types/node': 18.16.12 - '@types/mysql@2.15.27': dependencies: '@types/node': 18.16.12 - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 18.16.12 - form-data: 4.0.5 - '@types/node@18.16.12': {} - '@types/node@22.19.17': - dependencies: - undici-types: 6.21.0 - '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.7': @@ -6042,8 +4772,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/retry@0.12.0': {} - '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 @@ -6078,18 +4806,8 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@types/urijs@1.19.26': {} - - '@types/uuid@10.0.0': {} - '@types/validator@13.15.10': {} - '@types/wrap-ansi@3.0.0': {} - - '@types/ws@8.18.1': - dependencies: - '@types/node': 18.16.12 - '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -6264,10 +4982,6 @@ snapshots: '@xtuc/long@4.2.2': {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6293,10 +5007,6 @@ snapshots: agent-base@9.0.0: {} - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -6341,12 +5051,6 @@ snapshots: dependencies: type-fest: 0.21.3 - ansi-escapes@5.0.0: - dependencies: - type-fest: 1.4.0 - - ansi-escapes@6.2.1: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -6372,28 +5076,18 @@ snapshots: arg@4.1.3: {} - arg@5.0.2: {} - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} - array-differ@4.0.0: {} - array-flatten@1.1.1: {} array-union@2.1.0: {} - array-union@3.0.1: {} - asap@2.0.6: {} - async-hook-jl@1.7.6: - dependencies: - stack-chain: 1.3.7 - async@3.2.6: {} asynckit@0.4.0: {} @@ -6402,14 +5096,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.15.2: - dependencies: - follow-redirects: 1.16.0 - form-data: 4.0.5 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - babel-jest@29.7.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -6469,11 +5155,9 @@ snapshots: balanced-match@4.0.4: {} - base-64@0.1.0: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.10.24: {} + baseline-browser-mapping@2.10.25: {} bin-links@6.0.0: dependencies: @@ -6544,13 +5228,11 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-or-node@2.1.1: {} - browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.24 + baseline-browser-mapping: 2.10.25 caniuse-lite: 1.0.30001791 - electron-to-chromium: 1.5.348 + electron-to-chromium: 1.5.349 node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -6580,21 +5262,6 @@ snapshots: bytes@3.1.2: {} - c12@1.11.2: - dependencies: - chokidar: 3.6.0 - confbox: 0.1.8 - defu: 6.1.7 - dotenv: 16.6.1 - giget: 1.2.5 - jiti: 1.21.7 - mlly: 1.8.2 - ohash: 1.1.6 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.3.1 - rc9: 2.1.2 - cache-manager@5.7.6: dependencies: eventemitter3: 5.0.4 @@ -6632,16 +5299,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} - - change-case@5.4.4: {} - char-regex@1.0.2: {} chardet@0.7.0: {} - charenc@0.0.2: {} - chokidar@3.5.3: dependencies: anymatch: 3.1.3 @@ -6654,30 +5315,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chownr@2.0.0: {} - chownr@3.0.0: {} chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} - citty@0.1.6: - dependencies: - consola: 3.4.2 - cjs-module-lexer@1.4.3: {} cjs-module-lexer@2.2.0: {} @@ -6694,10 +5337,6 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - cli-spinners@2.9.2: {} cli-table3@0.6.3: @@ -6708,8 +5347,6 @@ snapshots: cli-width@3.0.0: {} - cli-width@4.1.0: {} - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6718,20 +5355,12 @@ snapshots: clone@1.0.4: {} - cls-hooked@4.2.2: - dependencies: - async-hook-jl: 1.7.6 - emitter-listener: 1.1.2 - semver: 5.7.2 - cmd-shim@8.0.0: {} co@4.6.0: {} collect-v8-coverage@1.0.3: {} - collection-utils@1.0.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -6757,8 +5386,6 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@10.0.1: {} - commander@2.20.3: {} commander@4.1.1: {} @@ -6774,12 +5401,8 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 - confbox@0.1.8: {} - consola@2.15.3: {} - consola@3.4.2: {} - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -6826,20 +5449,12 @@ snapshots: create-require@1.1.1: {} - cross-fetch@4.1.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - crypt@0.0.2: {} - data-uri-to-buffer@4.0.1: {} dayjs@1.11.20: {} @@ -6852,10 +5467,6 @@ snapshots: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - - decimal.js@10.6.0: {} - dedent@1.7.2: {} deep-is@0.1.4: {} @@ -6872,18 +5483,12 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - defu@6.1.7: {} - delayed-stream@1.0.0: {} depd@2.0.0: {} - destr@2.0.5: {} - destroy@1.2.0: {} - detect-libc@2.1.2: {} - detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -6895,17 +5500,10 @@ snapshots: diff@4.0.4: {} - digest-fetch@1.3.0: - dependencies: - base-64: 0.1.0 - md5: 2.3.0 - dir-glob@3.0.1: dependencies: path-type: 4.0.0 - discontinuous-range@1.0.0: {} - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -6924,20 +5522,12 @@ snapshots: eastasianwidth@0.2.0: {} - ebnf@1.9.1: {} - ee-first@1.1.1: {} - electron-to-chromium@1.5.348: {} - - emitter-listener@1.1.2: - dependencies: - shimmer: 1.2.1 + electron-to-chromium@1.5.349: {} emittery@0.13.1: {} - emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7077,10 +5667,6 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: {} - - eventemitter3@4.0.7: {} - eventemitter3@5.0.4: {} events@3.3.0: {} @@ -7109,20 +5695,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@8.0.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - - exit-hook@4.0.0: {} - exit@0.1.2: {} expect@29.7.0: @@ -7175,8 +5747,6 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - fast-copy@3.0.2: {} - fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -7212,13 +5782,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - fictional@2.1.1: - dependencies: - decimal.js: 10.6.0 - fast-json-stable-stringify: 2.1.0 - fnv-plus: 1.3.1 - siphash: 1.2.0 - figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -7253,12 +5816,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - find-up@7.0.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - unicorn-magic: 0.1.0 - flat-cache@3.2.0: dependencies: flatted: 3.4.2 @@ -7269,10 +5826,6 @@ snapshots: fn.name@1.1.0: {} - fnv-plus@1.3.1: {} - - follow-redirects@1.16.0: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -7299,8 +5852,6 @@ snapshots: typescript: 4.9.5 webpack: 5.82.1 - form-data-encoder@1.7.2: {} - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -7309,11 +5860,6 @@ snapshots: hasown: 2.0.3 mime-types: 2.1.35 - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -7337,22 +5883,6 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 - fs-extra@11.1.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - - fs-extra@11.3.4: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -7366,8 +5896,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7394,18 +5922,6 @@ snapshots: get-stream@6.0.1: {} - get-stream@8.0.1: {} - - giget@1.2.5: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - defu: 6.1.7 - node-fetch-native: 1.6.7 - nypm: 0.5.4 - pathe: 2.0.3 - tar: 6.2.1 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7460,20 +5976,6 @@ snapshots: graphemer@1.4.0: {} - groq-sdk@0.3.3: - dependencies: - '@types/node': 18.16.12 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - digest-fetch: 1.3.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - web-streams-polyfill: 3.3.3 - transitivePeerDependencies: - - encoding - has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -7511,14 +6013,6 @@ snapshots: human-signals@2.1.0: {} - human-signals@5.0.0: {} - - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - - iceberg-js@0.8.1: {} - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -7553,8 +6047,6 @@ snapshots: imurmurhash@0.1.4: {} - inflection@3.0.2: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -7608,8 +6100,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-buffer@1.1.6: {} - is-callable@1.2.7: {} is-core-module@2.16.1: @@ -7628,28 +6118,18 @@ snapshots: is-interactive@1.0.0: {} - is-interactive@2.0.0: {} - is-number@7.0.0: {} is-path-inside@3.0.3: {} is-stream@2.0.1: {} - is-stream@3.0.0: {} - is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.20 is-unicode-supported@0.1.0: {} - is-unicode-supported@1.3.0: {} - - is-unicode-supported@2.1.0: {} - - is-url@1.2.4: {} - isarray@1.0.0: {} isarray@2.0.5: {} @@ -7705,8 +6185,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - javascript-stringify@2.1.0: {} - jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -8022,8 +6500,6 @@ snapshots: - supports-color - ts-node - jiti@1.21.7: {} - joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -8032,12 +6508,6 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - js-base64@3.7.8: {} - - js-tiktoken@1.0.21: - dependencies: - base64-js: 1.5.1 - js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -8059,16 +6529,6 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - json-schema-library@9.3.5: - dependencies: - '@sagold/json-pointer': 5.1.2 - '@sagold/json-query': 6.2.0 - deepmerge: 4.3.1 - fast-copy: 3.0.2 - fast-deep-equal: 3.1.3 - smtp-address-parser: 1.0.10 - valid-url: 1.0.9 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8091,21 +6551,8 @@ snapshots: kleur@3.0.3: {} - kleur@4.1.5: {} - kuler@2.0.0: {} - langsmith@0.1.68(openai@4.104.0(ws@8.20.0)(zod@3.25.76)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.7.4 - uuid: 10.0.0 - optionalDependencies: - openai: 4.104.0(ws@8.20.0)(zod@3.25.76) - leven@3.1.0: {} levn@0.4.1: @@ -8127,10 +6574,6 @@ snapshots: dependencies: p-locate: 5.0.0 - locate-path@7.2.0: - dependencies: - p-locate: 6.0.0 - lodash.clonedeep@4.5.0: {} lodash.memoize@4.1.2: {} @@ -8146,11 +6589,6 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-symbols@6.0.0: - dependencies: - chalk: 5.6.2 - is-unicode-supported: 1.3.0 - logform@2.7.0: dependencies: '@colors/colors': 1.6.0 @@ -8184,12 +6622,6 @@ snapshots: math-intrinsics@1.1.0: {} - md5@2.3.0: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - media-typer@0.3.0: {} memfs@3.5.3: @@ -8221,10 +6653,6 @@ snapshots: mimic-fn@2.1.0: {} - mimic-fn@4.0.0: {} - - mimic-function@5.0.1: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -8243,21 +6671,10 @@ snapshots: minimist@1.2.8: {} - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - minipass@4.2.8: {} - minipass@5.0.0: {} - minipass@7.1.3: {} - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - minizlib@3.1.0: dependencies: minipass: 7.1.3 @@ -8266,19 +6683,8 @@ snapshots: dependencies: minimist: 1.2.8 - mkdirp@1.0.4: {} - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - module-details-from-path@1.0.4: {} - moo@0.5.3: {} - ms@2.0.0: {} ms@2.1.3: {} @@ -8293,27 +6699,10 @@ snapshots: type-is: 1.6.18 xtend: 4.0.2 - multimatch@7.0.0: - dependencies: - array-differ: 4.0.0 - array-union: 3.0.1 - minimatch: 9.0.9 - - mustache@4.2.0: {} - mute-stream@0.0.8: {} - mute-stream@1.0.0: {} - natural-compare@1.4.0: {} - nearley@2.20.1: - dependencies: - commander: 2.20.3 - moo: 0.5.3 - railroad-diagrams: 1.0.0 - randexp: 0.4.6 - negotiator@0.6.3: {} neo-async@2.6.2: {} @@ -8324,10 +6713,6 @@ snapshots: fast-safe-stringify: 2.1.1 winston: 3.19.0 - node-abi@3.89.0: - dependencies: - semver: 7.7.4 - node-abort-controller@3.1.1: {} node-domexception@1.0.0: {} @@ -8336,8 +6721,6 @@ snapshots: dependencies: lodash: 4.18.1 - node-fetch-native@1.6.7: {} - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -8360,25 +6743,10 @@ snapshots: dependencies: path-key: 3.1.1 - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - nypm@0.5.4: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - pathe: 2.0.3 - pkg-types: 1.3.1 - tinyexec: 0.3.2 - ufo: 1.6.4 - object-assign@4.1.1: {} object-inspect@1.13.4: {} - ohash@1.1.6: {} - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8395,29 +6763,6 @@ snapshots: dependencies: mimic-fn: 2.1.0 - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - - openai@4.104.0(ws@8.20.0)(zod@3.25.76): - dependencies: - '@types/node': 18.16.12 - '@types/node-fetch': 2.6.13 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - optionalDependencies: - ws: 8.20.0 - zod: 3.25.76 - transitivePeerDependencies: - - encoding - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8439,18 +6784,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - ora@8.2.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.2.0 - os-name@4.0.1: dependencies: macos-release: 2.5.1 @@ -8458,8 +6791,6 @@ snapshots: os-tmpdir@1.0.2: {} - p-finally@1.0.0: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -8468,10 +6799,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@4.0.0: - dependencies: - yocto-queue: 1.2.2 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -8480,32 +6807,10 @@ snapshots: dependencies: p-limit: 3.1.0 - p-locate@6.0.0: - dependencies: - p-limit: 4.0.0 - - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} - pako@0.2.9: {} - - pako@1.0.11: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8521,14 +6826,10 @@ snapshots: path-exists@4.0.0: {} - path-exists@5.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} path-scurry@1.11.1: @@ -8544,12 +6845,6 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.2: {} - - pathe@2.0.3: {} - - perfect-debounce@1.0.0: {} - pg-cloudflare@1.3.0: optional: true @@ -8595,12 +6890,6 @@ snapshots: dependencies: find-up: 4.1.0 - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} @@ -8615,8 +6904,6 @@ snapshots: dependencies: xtend: 4.0.2 - postgres@3.4.9: {} - prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -8635,8 +6922,6 @@ snapshots: process-nextick-args@2.0.1: {} - process@0.11.10: {} - prom-client@15.1.3: dependencies: '@opentelemetry/api': 1.9.1 @@ -8654,8 +6939,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@2.1.0: {} - pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -8675,33 +6958,6 @@ snapshots: queue-microtask@1.2.3: {} - quicktype-core@23.0.149: - dependencies: - '@glideapps/ts-necessities': 2.2.3 - '@types/urijs': 1.19.26 - browser-or-node: 2.1.1 - collection-utils: 1.0.1 - cross-fetch: 4.1.0 - is-url: 1.2.4 - js-base64: 3.7.8 - lodash: 4.18.1 - pako: 1.0.11 - pluralize: 8.0.0 - readable-stream: 4.5.2 - unicode-properties: 1.4.1 - urijs: 1.19.11 - wordwrap: 1.0.0 - yaml: 2.8.3 - transitivePeerDependencies: - - encoding - - railroad-diagrams@1.0.0: {} - - randexp@0.4.6: - dependencies: - discontinuous-range: 1.0.0 - ret: 0.1.15 - range-parser@1.2.1: {} raw-body@2.5.1: @@ -8718,11 +6974,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - rc9@2.1.2: - dependencies: - defu: 6.1.7 - destr: 2.0.5 - react-is@18.3.1: {} read-cmd-shim@6.0.0: {} @@ -8743,14 +6994,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readable-stream@4.5.2: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -8763,8 +7006,6 @@ snapshots: reflect-metadata@0.2.2: {} - remeda@1.61.0: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -8798,15 +7039,6 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - - ret@0.1.15: {} - - retry@0.13.1: {} - reusify@1.1.0: {} rimraf@3.0.2: @@ -8852,8 +7084,6 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) - semver@5.7.2: {} - semver@6.3.1: {} semver@7.7.4: {} @@ -8914,8 +7144,6 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 - shimmer@1.2.1: {} - side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -8948,16 +7176,10 @@ snapshots: signal-exit@4.1.0: {} - siphash@1.2.0: {} - sisteransi@1.0.5: {} slash@3.0.0: {} - smtp-address-parser@1.0.10: - dependencies: - nearley: 2.20.1 - source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -8972,18 +7194,12 @@ snapshots: source-map@0.7.4: {} - source-map@0.7.6: {} - split2@4.2.0: {} sprintf-js@1.0.3: {} sql-highlight@6.1.0: {} - sqlstring@2.3.3: {} - - stack-chain@1.3.7: {} - stack-trace@0.0.10: {} stack-utils@2.0.6: @@ -8992,12 +7208,8 @@ snapshots: statuses@2.0.1: {} - stdin-discarder@0.2.2: {} - streamsearch@1.1.0: {} - string-argv@0.3.2: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -9015,12 +7227,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.5.0 - strip-ansi: 7.2.0 - string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -9043,8 +7249,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-final-newline@3.0.0: {} - strip-json-comments@3.1.1: {} supabase@2.98.0: @@ -9086,11 +7290,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-hyperlinks@2.3.0: - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - supports-preserve-symlinks-flag@1.0.0: {} swagger-ui-dist@5.17.14: {} @@ -9099,15 +7298,6 @@ snapshots: tapable@2.3.3: {} - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -9120,11 +7310,6 @@ snapshots: dependencies: bintrees: 1.0.2 - terminal-link@3.0.0: - dependencies: - ansi-escapes: 5.0.0 - supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.5.0(webpack@5.82.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -9152,10 +7337,6 @@ snapshots: through@2.3.8: {} - tiny-inflate@1.0.3: {} - - tinyexec@0.3.2: {} - tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -9201,16 +7382,6 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - ts-loader@9.5.7(typescript@5.5.3)(webpack@5.82.1): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.21.0 - micromatch: 4.0.8 - semver: 7.7.4 - source-map: 0.7.6 - typescript: 5.5.3 - webpack: 5.82.1 - ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -9255,8 +7426,6 @@ snapshots: type-fest@0.21.3: {} - type-fest@1.4.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -9270,14 +7439,6 @@ snapshots: typedarray@0.0.6: {} - typeorm-transactional@0.5.0(reflect-metadata@0.1.14)(typeorm@0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3))): - dependencies: - '@types/cls-hooked': 4.3.9 - cls-hooked: 4.2.2 - reflect-metadata: 0.1.14 - semver: 7.7.4 - typeorm: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3)) - typeorm@0.3.28(pg@8.20.0)(ts-node@10.9.2(@types/node@18.16.12)(typescript@5.5.3)): dependencies: '@sqltools/formatter': 1.2.5 @@ -9306,26 +7467,10 @@ snapshots: typescript@5.5.3: {} - ufo@1.6.4: {} - uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 - undici-types@6.21.0: {} - - unicode-properties@1.4.1: - dependencies: - base64-js: 1.5.1 - unicode-trie: 2.0.0 - - unicode-trie@2.0.0: - dependencies: - pako: 0.2.9 - tiny-inflate: 1.0.3 - - unicorn-magic@0.1.0: {} - universalify@2.0.1: {} unpipe@1.0.0: {} @@ -9340,14 +7485,10 @@ snapshots: dependencies: punycode: 2.3.1 - urijs@1.19.11: {} - util-deprecate@1.0.2: {} utils-merge@1.0.1: {} - uuid@10.0.0: {} - uuid@11.1.1: {} uuid@9.0.1: {} @@ -9360,8 +7501,6 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - valid-url@1.0.9: {} - validator@13.15.35: {} vary@1.1.2: {} @@ -9381,8 +7520,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - web-streams-polyfill@4.0.0-beta.3: {} - webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: {} @@ -9465,14 +7602,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrap@1.0.0: {} - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -9496,22 +7625,16 @@ snapshots: dependencies: signal-exit: 4.1.0 - ws@8.20.0: {} - xtend@4.0.2: {} y18n@5.0.8: {} yallist@3.1.1: {} - yallist@4.0.0: {} - yallist@5.0.0: {} yaml@1.10.3: {} - yaml@2.8.3: {} - yargs-parser@21.1.1: {} yargs@17.7.2: @@ -9527,13 +7650,3 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} - - yocto-queue@1.2.2: {} - - yoctocolors-cjs@2.1.3: {} - - zod-to-json-schema@3.25.2(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} diff --git a/services/api/app/src/api/public/users/users.service.spec.ts b/services/api/app/src/api/public/users/users.service.spec.ts index 11999b3..0fa881e 100644 --- a/services/api/app/src/api/public/users/users.service.spec.ts +++ b/services/api/app/src/api/public/users/users.service.spec.ts @@ -1,11 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; - -jest.mock('typeorm-transactional', () => ({ - initializeTransactionalContext: jest.fn(), - Transactional: () => (_target: any, _key: string, descriptor: PropertyDescriptor) => descriptor, -})); - import { User } from 'src/type-orm/entities/users/users.entity'; import { UsersRepository } from 'src/type-orm/entities/users/users.repository'; diff --git a/services/api/app/src/api/public/users/users.service.ts b/services/api/app/src/api/public/users/users.service.ts index 465e5f9..c749d15 100644 --- a/services/api/app/src/api/public/users/users.service.ts +++ b/services/api/app/src/api/public/users/users.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { UpsertDepartmentResult } from 'src/api/public/users/dtos/results/upsert-department-result.dto'; import { UserProfileResult } from 'src/api/public/users/dtos/results/user-profile-result.dto'; -import { Transactional } from 'typeorm-transactional'; import { User } from '../../../type-orm/entities/users/users.entity'; import { UsersRepository } from '../../../type-orm/entities/users/users.repository'; @@ -28,7 +27,6 @@ export class UsersService { }; } - @Transactional() public async upsert( userId: string, campusId: number, diff --git a/services/api/app/src/app.module.ts b/services/api/app/src/app.module.ts index 94301a5..f308ea4 100644 --- a/services/api/app/src/app.module.ts +++ b/services/api/app/src/app.module.ts @@ -1,15 +1,13 @@ -import { HttpModule } from '@nestjs/axios'; import { CacheModule } from '@nestjs/cache-manager'; -import { Module, OnApplicationBootstrap } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { APP_FILTER, APP_INTERCEPTOR, HttpAdapterHost } from '@nestjs/core'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; import { makeCounterProvider, makeHistogramProvider, PrometheusModule, } from '@willsoto/nestjs-prometheus'; -import { Server } from 'node:http'; import { LoggerModule } from 'src/api/internal/logger/logger.module'; import { CafeteriasModule } from 'src/api/public/cafeterias/cafeterias.module'; import { CampusesModule } from './api/public/campuses/campuses.module'; @@ -27,7 +25,6 @@ import { HealthModule } from './api/internal/health/health.module'; imports: [ SentryModule.forRoot(), LoggerModule, - HttpModule, CacheModule.register({ isGlobal: true, ttl: 60 * 60 * 1000 }), PrometheusModule.register(), DatabaseModule, @@ -71,13 +68,4 @@ import { HealthModule } from './api/internal/health/health.module'; }), ], }) -export class AppModule implements OnApplicationBootstrap { - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - onApplicationBootstrap() { - const server = this.httpAdapterHost.httpAdapter.getHttpServer() as Server; - - server.keepAliveTimeout = 61 * 1000; - server.headersTimeout = 65 * 1000; - } -} +export class AppModule {} diff --git a/services/api/app/src/main.ts b/services/api/app/src/main.ts index 0466cab..dd581f6 100644 --- a/services/api/app/src/main.ts +++ b/services/api/app/src/main.ts @@ -2,7 +2,6 @@ import { ValidationPipe } from '@nestjs/common'; import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { initializeTransactionalContext } from 'typeorm-transactional'; import { HttpExceptionFilter } from './api/common/filters/http-exception.filter'; import { SentryInterceptor } from './api/common/interceptors/sentry.interceptor'; import { AppModule } from './app.module'; @@ -10,7 +9,6 @@ import './instrument'; import { SentryFilter } from './api/common/filters/sentry.filter'; async function bootstrap() { - initializeTransactionalContext(); const app = await NestFactory.create(AppModule, { bufferLogs: true, }); diff --git a/services/api/app/src/type-orm/database.module.ts b/services/api/app/src/type-orm/database.module.ts index 782a350..02f0431 100644 --- a/services/api/app/src/type-orm/database.module.ts +++ b/services/api/app/src/type-orm/database.module.ts @@ -2,8 +2,6 @@ import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import * as path from 'path'; -import { DataSource } from 'typeorm'; -import { addTransactionalDataSource, getDataSourceByName } from 'typeorm-transactional'; @Module({ imports: [ @@ -32,13 +30,6 @@ import { addTransactionalDataSource, getDataSourceByName } from 'typeorm-transac statement_timeout: 1800, // 스테이트먼트 타임아웃 시간 (default: 0ms, 무제한) }, }), - async dataSourceFactory(options) { - if (!options) throw new Error('Invalid options passed'); - - return ( - getDataSourceByName('default') || addTransactionalDataSource(new DataSource(options)) - ); - }, }), ], controllers: [], From d75dd698233c35e570ffb62bb295e221f3a3ca9c Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sun, 3 May 2026 17:43:38 +0900 Subject: [PATCH 06/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Cafeteri?= =?UTF-8?q?aDietQueryDto=20=ED=95=84=EB=93=9C=20=ED=95=84=EC=88=98?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - date 및 time 필드를 필수로 변경하여 유효성 검증 강화 - CafeteriasNativeController에서 getCafeteriaDiet 호출 시 time 파라미터 처리 방식 수정 --- mobile/services/cafeteriaApi.ts | 5 ++--- .../cafeterias-native.controller.ts | 6 ++---- .../requests/get-cafeteria-diet-query.dto.ts | 20 +++++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/mobile/services/cafeteriaApi.ts b/mobile/services/cafeteriaApi.ts index 56075c4..7a84677 100644 --- a/mobile/services/cafeteriaApi.ts +++ b/mobile/services/cafeteriaApi.ts @@ -57,9 +57,8 @@ export function getCafeterias(campusId: number): Promise { export function getCafeteriaDiet( cafeteriaId: number, date: string, - time?: DietTimeLabel, + time: DietTimeLabel, ): Promise { - const params = new URLSearchParams({ date }); - if (time) params.set('time', time); + const params = new URLSearchParams({ date, time }); return request(`/cafeterias/${cafeteriaId}/diet?${params.toString()}`); } diff --git a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts b/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts index a883daf..b149cf9 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts @@ -2,7 +2,6 @@ import { Controller, Get, Param, ParseIntPipe, Query, UseGuards } from '@nestjs/ import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { NativeResponseDto } from 'src/api/common/dtos/native-response.dto'; import { JwtAuthGuard } from 'src/api/public/users/guards/jwt-auth.guard'; -import { getDietTime } from 'src/api/public/cafeterias/utils/time'; import { GetCafeteriaDietQueryDto } from './dtos/requests/get-cafeteria-diet-query.dto'; import { GetCafeteriasQueryDto } from './dtos/requests/get-cafeterias-query.dto'; import { CafeteriaDietResponseDto } from './dtos/responses/cafeteria-diet-response.dto'; @@ -40,9 +39,8 @@ export class CafeteriasNativeController { @Param('cafeteriaId', ParseIntPipe) cafeteriaId: number, @Query() query: GetCafeteriaDietQueryDto, ): Promise> { - const date = query.date ? new Date(query.date) : new Date(); - const time = query.time ?? getDietTime(date); - const result = await this.cafeteriasService.getCafeteriaDiet(cafeteriaId, date, time); + const date = new Date(query.date); + const result = await this.cafeteriasService.getCafeteriaDiet(cafeteriaId, date, query.time); const data: CafeteriaDietResponseDto = { cafeteria: { diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts b/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts index a421e76..ea0926f 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts +++ b/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts @@ -1,21 +1,21 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsIn, IsISO8601, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsIn, IsISO8601 } from 'class-validator'; import { DietTime } from './list-cafeteria-diet-request.dto'; export class GetCafeteriaDietQueryDto { - @IsOptional() + @IsDefined({ message: 'date는 필수입니다.' }) @IsISO8601({ strict: true }, { message: 'date는 유효한 날짜여야 합니다.' }) - @ApiPropertyOptional({ - description: '날짜 (YYYY-MM-DD). 생략 시 오늘 날짜.', + @ApiProperty({ + description: '날짜 (YYYY-MM-DD).', example: '2026-04-21', }) - date?: string; + date: string; - @IsOptional() + @IsDefined({ message: 'time은 필수입니다.' }) @IsIn(['아침', '점심', '저녁']) - @ApiPropertyOptional({ - description: '식사 시간. 생략 시 현재 시간 기준 자동 결정.', + @ApiProperty({ + description: '식사 시간.', enum: ['아침', '점심', '저녁'], }) - time?: DietTime; + time: DietTime; } From dfc7931b0d2705a13c05087353482e0a1db9a78e Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Mon, 4 May 2026 21:25:17 +0900 Subject: [PATCH 07/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20README.md=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EA=B3=BC=EC=A0=95=EA=B3=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1ed11a..df277ce 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ 제한된 기간과 팀 상황 안에서 왜 그렇게 설계했는지를 남기기 위해 작성했습니다. -각 의사결정은 `문제 → 선택 → 근거 → 결과/트레이드오프 → 느낀 점` 순서로 정리했습니다. +각 의사결정은 `문제 → 원인 → 해결 → 결과/트레이드오프 → 느낀 점` 순서로 정리했습니다. ### 1. 데이터 모델링 @@ -136,6 +136,107 @@ - 특정 학과나 사용자보다 학교 전체에서 공통으로 사용하는 정보에 가까워 독립 테이블로 관리했습니다. - 일정 유형을 저장해 특정 유형의 일정 필터링(예: 대학생, 대학원생 등)이 가능하도록 했습니다. +### 2. 제한된 리소스 환경에서의 인스턴스 분리 + +**문제** + +- 서비스 초기에 GCP 프리티어 단일 인스턴스에서 API 서버, 스크래핑 작업, 모니터링을 함께 운영했습니다. +- 스크래핑 작업이나 메트릭 수집 시 API 응답이 눈에 띄게 느려지는 문제가 발생했습니다. + +**원인** + +- 배치 작업이 CPU와 메모리를 많이 사용하면서 API 서버가 사용할 자원이 부족해졌습니다. +- 메모리가 부족해지면서 스왑 메모리를 사용하게 되었고, 이로 인해 디스크 I/O가 증가했습니다. +- 여러 작업이 동시에 실행되면서 컨텍스트 스위칭이 증가해 전체적인 성능이 저하되었습니다. + +**해결** + +- 문제를 해결하기 위해 역할에 따라 인스턴스를 분리하는 구조로 개선했습니다. + 1. API 서버 전용 인스턴스 + 2. 스크래핑 작업 전용 인스턴스 + 3. 모니터링 전용 인스턴스 +- 구글 계정 2개를 사용해 프리티어 인스턴스를 추가로 확보하여, 비용을 늘리지 않으면서도 작업을 나눠서 실행할 수 있었습니다. +- 스크래핑 작업은 Github Actions Runner를 활용하여 주기적으로 실행되도록 구성했습니다. + +**결과** + +- 리소스 경쟁이 줄어들면서 전체 시스템의 성능이 개선되었습니다. +- 서비스 간 장애 전파 가능성을 줄였습니다. +- 수직 확장(서버 스펙 증가)이나 수평 확장(인스턴스 추가)을 하지 않고도 문제를 해결할 수 있었습니다. +- 결과적으로 성능 개선과 비용 절감을 동시에 달성할 수 있었습니다. + +**트레이드오프** + +- 배포 대상과 운영 지점이 늘어나 환경 변수, 네트워크, 모니터링 설정을 더 꼼꼼히 관리해야 했습니다. + +**느낀 점** + +- 인프라 자원은 정말 비싸다는 것을 느꼈습니다. +- 프리티어 자원을 최대한으로 활용하면 비용 지출 없이도 충분히 서비스를 운영할 수 있다는 것을 배웠습니다. + +### 3. 교내 데이터 수집 방식 분리 + +**문제** + +- 교내 페이지를 HTTP 요청 기반으로 수집했을 때 응답은 정상이어도 실제 데이터가 비어 있는 경우가 있었습니다. +- 동일한 페이지를 브라우저에서는 정상적으로 확인할 수 있었지만, 코드로 수집할 경우 데이터가 누락되는 문제가 발생했습니다. + +**원인** + +- 일부 페이지는 서버에서 완성된 HTML을 반환하는 SSR 방식이 아니라, JavaScript 실행 이후 데이터가 렌더링되는 CSR 구조였습니다. +- 따라서 단순 HTTP 요청으로는 초기 HTML만 받아오게 되어 실제 데이터가 포함되지 않았습니다. +- 브라우저에서는 JavaScript가 실행되면서 추가 API 요청을 통해 데이터를 가져오고 화면에 렌더링되기 때문에 차이가 발생했습니다. + +**해결** + +- 페이지 렌더링 방식에 따라 수집 방식을 분리했습니다. + 1. SSR 페이지 → HTTP 요청 기반 수집 유지 + 2. CSR 페이지 → Selenium 기반 브라우저 렌더링 수집 적용 +- 모든 페이지를 Selenium으로 처리하는 대신, 필요한 경우에만 선택적으로 적용하도록 설계했습니다. +- 이를 위해 페이지 구조를 분석하여 CSR 여부를 판단하는 기준을 정리하고, 수집 로직을 분기 처리했습니다. + +**결과** + +- 데이터가 비어 오는 문제를 해결하고 수집 정확도를 크게 개선할 수 있었습니다. +- 필요한 페이지에만 Selenium을 적용하여 실행 시간과 리소스 사용량을 최소화할 수 있었습니다. + +**느낀 점** + +- 데이터를 잘 수집하기 위해서는 웹 통신 과정에 대한 이해가 꼭 필요하다는 것을 느꼈습니다. +- 특히 네트워크 탭을 통해 실제 API 요청 흐름을 분석하면서 문제를 해결할 수 있었던 경험이 인상 깊었습니다. +- 이 경험을 통해 기능 구현 전에 동작 원리를 먼저 이해하려는 습관이 중요하다는 것을 배웠습니다. + +### 4. 수집 지연 감지를 위한 모니터링 + +**문제** + +- 학식, 셔틀버스, 공지사항과 같은 데이터 수집이 지연되더라도 즉시 인지하기 어려웠습니다. +- 사용자가 오래된 데이터를 확인하기 전까지는 문제가 드러나지 않아 대응이 늦어지는 상황이 발생했습니다. + +**원인** + +- 기존에는 서버가 살아 있는지만 확인하고 있었습니다. +- 하지만 실제로 중요한 "데이터가 언제 마지막으로 갱신되었는지"는 확인하지 않고 있었습니다. +- 이로 인해 수집이 실패하거나 지연되더라도 바로 알 수 없는 구조였습니다. + +**해결** + +- Prometheus, Grafana, Sentry를 활용해 모니터링 환경을 구성했습니다. +- 데이터 수집 지연 감지는 PostgreSQL에 저장된 마지막 수집 시간과 현재 시간의 차이를 확인하는 쿼리로 체크했습니다. +- 해당 값을 Grafana 대시보드로 시각화하여, 데이터가 얼마나 지연되고 있는지 한눈에 확인할 수 있도록 했습니다. + +**결과** + +- 실제로 셔틀버스 데이터가 약 156분 지연된 문제를 대시보드를 통해 빠르게 확인할 수 있었습니다. +- 원인을 확인한 결과 Github Actions의 cron 설정 문제였고, 실행 시간을 조정하여 해결했습니다. +- 이후에는 데이터 수집 지연을 빠르게 인지하고 대응할 수 있게 되었습니다. +- 다만 데이터 구조가 바뀔 때마다 모니터링 쿼리와 대시보드를 함께 수정해야 하는 번거로움이 있었습니다. + +**느낀 점** + +- [모니터링을 공부](https://dongho-blog.kro.kr/posts/3145ca9b-265e-8028-9953-f659ea1a9c67)하면서 사용자가 알기 전에 문제를 발견할 수 있도록 하는 것이 서비스 운영에서 매우 중요하다고 느꼈습니다. +- 특히 p95 응답 속도, 에러율 같은 지표를 통해 사용자 경험에 직접적인 영향을 주는 요소를 기준으로 서비스를 바라봐야 한다는 것을 배웠습니다. + --- ## 🔷 팀원 소개 From b1beaf4614e2523f95f2f133e5885048a09fce80 Mon Sep 17 00:00:00 2001 From: Dongho Jang Date: Mon, 4 May 2026 21:37:30 +0900 Subject: [PATCH 08/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20README.md=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df277ce..535167a 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,16 @@ --- -## 설계 과정과 이유 +## 🔷 사용자 요청 처리 흐름 + +image + +- 카카오 챗봇은 사용자의 발화를 미리 정의된 의도와 매칭하고, 각 의도에 연결된 API를 호출하는 구조입니다. +- 예를 들어 "내일 기숙사 식당 점심 알려줘" 와 같은 자연어 요청이 들어오면, NLU가 식단 조회 의도로 분류 후 연결된 식단 API를 호출해 결과를 반환합니다. + +--- + +## 🔷 설계 과정과 이유 제한된 기간과 팀 상황 안에서 왜 그렇게 설계했는지를 남기기 위해 작성했습니다. @@ -136,6 +145,8 @@ - 특정 학과나 사용자보다 학교 전체에서 공통으로 사용하는 정보에 가까워 독립 테이블로 관리했습니다. - 일정 유형을 저장해 특정 유형의 일정 필터링(예: 대학생, 대학원생 등)이 가능하도록 했습니다. +

+ ### 2. 제한된 리소스 환경에서의 인스턴스 분리 **문제** @@ -149,6 +160,8 @@ - 메모리가 부족해지면서 스왑 메모리를 사용하게 되었고, 이로 인해 디스크 I/O가 증가했습니다. - 여러 작업이 동시에 실행되면서 컨텍스트 스위칭이 증가해 전체적인 성능이 저하되었습니다. +image + **해결** - 문제를 해결하기 위해 역할에 따라 인스턴스를 분리하는 구조로 개선했습니다. @@ -158,6 +171,8 @@ - 구글 계정 2개를 사용해 프리티어 인스턴스를 추가로 확보하여, 비용을 늘리지 않으면서도 작업을 나눠서 실행할 수 있었습니다. - 스크래핑 작업은 Github Actions Runner를 활용하여 주기적으로 실행되도록 구성했습니다. +image + **결과** - 리소스 경쟁이 줄어들면서 전체 시스템의 성능이 개선되었습니다. @@ -174,6 +189,8 @@ - 인프라 자원은 정말 비싸다는 것을 느꼈습니다. - 프리티어 자원을 최대한으로 활용하면 비용 지출 없이도 충분히 서비스를 운영할 수 있다는 것을 배웠습니다. +

+ ### 3. 교내 데이터 수집 방식 분리 **문제** @@ -187,6 +204,19 @@ - 따라서 단순 HTTP 요청으로는 초기 HTML만 받아오게 되어 실제 데이터가 포함되지 않았습니다. - 브라우저에서는 JavaScript가 실행되면서 추가 API 요청을 통해 데이터를 가져오고 화면에 렌더링되기 때문에 차이가 발생했습니다. + + + + + +
+
+ SSR +
+
+ CSR +
+ **해결** - 페이지 렌더링 방식에 따라 수집 방식을 분리했습니다. @@ -195,6 +225,10 @@ - 모든 페이지를 Selenium으로 처리하는 대신, 필요한 경우에만 선택적으로 적용하도록 설계했습니다. - 이를 위해 페이지 구조를 분석하여 CSR 여부를 판단하는 기준을 정리하고, 수집 로직을 분기 처리했습니다. +image + +
+ **결과** - 데이터가 비어 오는 문제를 해결하고 수집 정확도를 크게 개선할 수 있었습니다. @@ -206,6 +240,8 @@ - 특히 네트워크 탭을 통해 실제 API 요청 흐름을 분석하면서 문제를 해결할 수 있었던 경험이 인상 깊었습니다. - 이 경험을 통해 기능 구현 전에 동작 원리를 먼저 이해하려는 습관이 중요하다는 것을 배웠습니다. +

+ ### 4. 수집 지연 감지를 위한 모니터링 **문제** @@ -225,6 +261,8 @@ - 데이터 수집 지연 감지는 PostgreSQL에 저장된 마지막 수집 시간과 현재 시간의 차이를 확인하는 쿼리로 체크했습니다. - 해당 값을 Grafana 대시보드로 시각화하여, 데이터가 얼마나 지연되고 있는지 한눈에 확인할 수 있도록 했습니다. +image + **결과** - 실제로 셔틀버스 데이터가 약 156분 지연된 문제를 대시보드를 통해 빠르게 확인할 수 있었습니다. @@ -232,6 +270,8 @@ - 이후에는 데이터 수집 지연을 빠르게 인지하고 대응할 수 있게 되었습니다. - 다만 데이터 구조가 바뀔 때마다 모니터링 쿼리와 대시보드를 함께 수정해야 하는 번거로움이 있었습니다. +image + **느낀 점** - [모니터링을 공부](https://dongho-blog.kro.kr/posts/3145ca9b-265e-8028-9953-f659ea1a9c67)하면서 사용자가 알기 전에 문제를 발견할 수 있도록 하는 것이 서비스 운영에서 매우 중요하다고 느꼈습니다. From 2a2f0a97bcb4f98e890933b4c4b79f2f86535afe Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 11 Jun 2026 17:37:47 +0900 Subject: [PATCH 09/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20TypeORM?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=95=88=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/cafeterias/cafeterias.controller.ts | 2 +- .../api/public/cafeterias/cafeterias.module.ts | 14 +++++++++++--- .../public}/cafeterias/cafeterias.repository.ts | 4 ++-- .../public/cafeterias/cafeterias.service.spec.ts | 6 +++--- .../api/public/cafeterias/cafeterias.service.ts | 2 +- .../entities}/cafeteria-diet.entity.ts | 2 +- .../cafeterias/entities}/cafeteria.entity.ts | 4 ++-- .../src/api/public/campuses/campuses.module.ts | 4 ++-- .../public}/campuses/campuses.repository.ts | 2 +- .../src/api/public/campuses/campuses.service.ts | 2 +- .../public/campuses/entities}/campus.entity.ts | 4 ++-- .../src/api/public/colleges/colleges.module.ts | 4 ++-- .../public}/colleges/colleges.repository.ts | 2 +- .../src/api/public/colleges/colleges.service.ts | 2 +- .../public/colleges/entities}/college.entity.ts | 2 +- .../api/public/departments/departments.module.ts | 4 ++-- .../departments/departments.repository.ts | 2 +- .../public/departments/departments.service.ts | 2 +- .../departments/entities}/department.entity.ts | 4 ++-- .../notices/entities}/notice-category.entity.ts | 2 +- .../public/notices/entities}/notice.entity.ts | 0 .../notices/notice-categories.repository.ts | 2 +- .../src/api/public/notices/notices.controller.ts | 2 +- .../app/src/api/public/notices/notices.module.ts | 16 +++++++++++++--- .../public}/notices/notices.repository.ts | 2 +- .../api/public/notices/notices.service.spec.ts | 10 +++++----- .../src/api/public/notices/notices.service.ts | 10 +++++----- .../schedules}/academic-calendars.repository.ts | 2 +- .../entities}/academic-calendar.entity.ts | 0 .../src/api/public/schedules/schedules.module.ts | 13 ++++++++++--- .../api/public/schedules/schedules.service.ts | 2 +- .../entities}/shuttle-timetable.entity.ts | 0 .../shuttles}/shuttle-timetable.repository.ts | 2 +- .../src/api/public/shuttles/shuttles.module.ts | 7 +++---- .../src/api/public/shuttles/shuttles.service.ts | 2 +- .../public/users/entities}/users.entity.ts | 4 ++-- .../app/src/api/public/users/users.controller.ts | 2 +- .../api/app/src/api/public/users/users.module.ts | 13 +++---------- .../public}/users/users.repository.ts | 2 +- .../src/api/public/users/users.service.spec.ts | 4 ++-- .../app/src/api/public/users/users.service.ts | 4 ++-- services/api/app/src/app.module.ts | 4 ++-- .../logger/logger.module.ts | 0 .../type-orm/database.module.ts | 0 .../academic-calendars-repository.module.ts | 11 ----------- .../cafeterias/cafeterias-repository.module.ts | 12 ------------ .../notices/notices-repository.module.ts | 13 ------------- 47 files changed, 95 insertions(+), 114 deletions(-) rename services/api/app/src/{type-orm/entities => api/public}/cafeterias/cafeterias.repository.ts (90%) rename services/api/app/src/{type-orm/entities/cafeterias => api/public/cafeterias/entities}/cafeteria-diet.entity.ts (88%) rename services/api/app/src/{type-orm/entities/cafeterias => api/public/cafeterias/entities}/cafeteria.entity.ts (77%) rename services/api/app/src/{type-orm/entities => api/public}/campuses/campuses.repository.ts (90%) rename services/api/app/src/{type-orm/entities/campuses => api/public/campuses/entities}/campus.entity.ts (74%) rename services/api/app/src/{type-orm/entities => api/public}/colleges/colleges.repository.ts (92%) rename services/api/app/src/{type-orm/entities/colleges => api/public/colleges/entities}/college.entity.ts (84%) rename services/api/app/src/{type-orm/entities => api/public}/departments/departments.repository.ts (90%) rename services/api/app/src/{type-orm/entities/departments => api/public/departments/entities}/department.entity.ts (83%) rename services/api/app/src/{type-orm/entities/notices => api/public/notices/entities}/notice-category.entity.ts (90%) rename services/api/app/src/{type-orm/entities/notices => api/public/notices/entities}/notice.entity.ts (100%) rename services/api/app/src/{type-orm/entities => api/public}/notices/notice-categories.repository.ts (96%) rename services/api/app/src/{type-orm/entities => api/public}/notices/notices.repository.ts (96%) rename services/api/app/src/{type-orm/entities/academic-calendars => api/public/schedules}/academic-calendars.repository.ts (95%) rename services/api/app/src/{type-orm/entities/academic-calendars => api/public/schedules/entities}/academic-calendar.entity.ts (100%) rename services/api/app/src/{type-orm/entities/shuttle-timetables => api/public/shuttles/entities}/shuttle-timetable.entity.ts (100%) rename services/api/app/src/{type-orm/entities/shuttle-timetables => api/public/shuttles}/shuttle-timetable.repository.ts (89%) rename services/api/app/src/{type-orm/entities/users => api/public/users/entities}/users.entity.ts (71%) rename services/api/app/src/{type-orm/entities => api/public}/users/users.repository.ts (94%) rename services/api/app/src/{api/internal => infrastructure}/logger/logger.module.ts (100%) rename services/api/app/src/{ => infrastructure}/type-orm/database.module.ts (100%) delete mode 100644 services/api/app/src/type-orm/entities/academic-calendars/academic-calendars-repository.module.ts delete mode 100644 services/api/app/src/type-orm/entities/cafeterias/cafeterias-repository.module.ts delete mode 100644 services/api/app/src/type-orm/entities/notices/notices-repository.module.ts diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts b/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts index 362015e..2215936 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts @@ -12,7 +12,7 @@ import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.fac import { CurrentUser } from 'src/api/public/users/decorators/current-user.decorator'; import { FetchCurrentUser } from 'src/api/public/users/decorators/fetch-current-user.decorator'; import { BlockId } from 'src/api/common/utils/constants'; -import { User } from 'src/type-orm/entities/users/users.entity'; +import { User } from 'src/api/public/users/entities/users.entity'; import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/utils/time'; import { CafeteriasService } from './cafeterias.service'; diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts index fa6fd16..a8c67e7 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts @@ -1,16 +1,24 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { CafeteriaMessageFactory } from 'src/api/public/cafeterias/cafeteria-message.factory'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; import { CampusesModule } from 'src/api/public/campuses/campuses.module'; import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.factory'; -import { CafeteriasRepositoryModule } from 'src/type-orm/entities/cafeterias/cafeterias-repository.module'; import { CafeteriasNativeController } from './cafeterias-native.controller'; import { CafeteriasController } from './cafeterias.controller'; import { CafeteriasService } from './cafeterias.service'; @Module({ - imports: [CampusesModule, CafeteriasRepositoryModule], + imports: [CampusesModule, TypeOrmModule.forFeature([Cafeteria, CafeteriaDiet])], controllers: [CafeteriasController, CafeteriasNativeController], - providers: [CafeteriasService, CafeteriaMessageFactory, CampusMessageFactory], + providers: [ + CafeteriasService, + CafeteriasRepository, + CafeteriaMessageFactory, + CampusMessageFactory, + ], exports: [CafeteriasService], }) export class CafeteriasModule {} diff --git a/services/api/app/src/type-orm/entities/cafeterias/cafeterias.repository.ts b/services/api/app/src/api/public/cafeterias/cafeterias.repository.ts similarity index 90% rename from services/api/app/src/type-orm/entities/cafeterias/cafeterias.repository.ts rename to services/api/app/src/api/public/cafeterias/cafeterias.repository.ts index bc33000..fe9d6b3 100644 --- a/services/api/app/src/type-orm/entities/cafeterias/cafeterias.repository.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DietTime } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { CafeteriaDiet } from 'src/type-orm/entities/cafeterias/cafeteria-diet.entity'; -import { Cafeteria } from 'src/type-orm/entities/cafeterias/cafeteria.entity'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts b/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts index 35a63e4..732c187 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts @@ -1,9 +1,9 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { CafeteriaDiet } from 'src/type-orm/entities/cafeterias/cafeteria-diet.entity'; -import { Cafeteria } from 'src/type-orm/entities/cafeterias/cafeteria.entity'; -import { CafeteriasRepository } from 'src/type-orm/entities/cafeterias/cafeterias.repository'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; import { CafeteriasService } from './cafeterias.service'; const makeCafeteria = (overrides: Partial = {}): Cafeteria => diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts b/services/api/app/src/api/public/cafeterias/cafeterias.service.ts index 78b4b35..2cf4e8f 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.service.ts @@ -8,7 +8,7 @@ import { CafeteriaMenuGroupResult, } from 'src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto'; import { CafeteriaListResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto'; -import { CafeteriasRepository } from 'src/type-orm/entities/cafeterias/cafeterias.repository'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; @Injectable() export class CafeteriasService { diff --git a/services/api/app/src/type-orm/entities/cafeterias/cafeteria-diet.entity.ts b/services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts similarity index 88% rename from services/api/app/src/type-orm/entities/cafeterias/cafeteria-diet.entity.ts rename to services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts index 58e719e..0e0c681 100644 --- a/services/api/app/src/type-orm/entities/cafeterias/cafeteria-diet.entity.ts +++ b/services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts @@ -1,4 +1,4 @@ -import { Cafeteria } from 'src/type-orm/entities/cafeterias/cafeteria.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; @Entity('cafeteria_diet') diff --git a/services/api/app/src/type-orm/entities/cafeterias/cafeteria.entity.ts b/services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts similarity index 77% rename from services/api/app/src/type-orm/entities/cafeterias/cafeteria.entity.ts rename to services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts index bdd49e3..33d5859 100644 --- a/services/api/app/src/type-orm/entities/cafeterias/cafeteria.entity.ts +++ b/services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts @@ -1,5 +1,5 @@ -import { CafeteriaDiet } from 'src/type-orm/entities/cafeterias/cafeteria-diet.entity'; -import { Campus } from 'src/type-orm/entities/campuses/campus.entity'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; +import { Campus } from 'src/api/public/campuses/entities/campus.entity'; import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; @Entity('cafeteria') diff --git a/services/api/app/src/api/public/campuses/campuses.module.ts b/services/api/app/src/api/public/campuses/campuses.module.ts index b622bd2..d1eb525 100644 --- a/services/api/app/src/api/public/campuses/campuses.module.ts +++ b/services/api/app/src/api/public/campuses/campuses.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Campus } from 'src/type-orm/entities/campuses/campus.entity'; -import { CampusesRepository } from 'src/type-orm/entities/campuses/campuses.repository'; +import { Campus } from 'src/api/public/campuses/entities/campus.entity'; +import { CampusesRepository } from 'src/api/public/campuses/campuses.repository'; import { CampusesNativeController } from './campuses-native.controller'; import { CampusesService } from './campuses.service'; diff --git a/services/api/app/src/type-orm/entities/campuses/campuses.repository.ts b/services/api/app/src/api/public/campuses/campuses.repository.ts similarity index 90% rename from services/api/app/src/type-orm/entities/campuses/campuses.repository.ts rename to services/api/app/src/api/public/campuses/campuses.repository.ts index 86a8260..6eaac0b 100644 --- a/services/api/app/src/type-orm/entities/campuses/campuses.repository.ts +++ b/services/api/app/src/api/public/campuses/campuses.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Campus } from 'src/type-orm/entities/campuses/campus.entity'; +import { Campus } from 'src/api/public/campuses/entities/campus.entity'; import { In, Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/campuses/campuses.service.ts b/services/api/app/src/api/public/campuses/campuses.service.ts index 3e9e2a1..f635197 100644 --- a/services/api/app/src/api/public/campuses/campuses.service.ts +++ b/services/api/app/src/api/public/campuses/campuses.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { CampusListResult } from 'src/api/public/campuses/dtos/results/campus-list-result.dto'; -import { CampusesRepository } from 'src/type-orm/entities/campuses/campuses.repository'; +import { CampusesRepository } from 'src/api/public/campuses/campuses.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/type-orm/entities/campuses/campus.entity.ts b/services/api/app/src/api/public/campuses/entities/campus.entity.ts similarity index 74% rename from services/api/app/src/type-orm/entities/campuses/campus.entity.ts rename to services/api/app/src/api/public/campuses/entities/campus.entity.ts index 644016a..2b35870 100644 --- a/services/api/app/src/type-orm/entities/campuses/campus.entity.ts +++ b/services/api/app/src/api/public/campuses/entities/campus.entity.ts @@ -1,6 +1,6 @@ -import { Cafeteria } from 'src/type-orm/entities/cafeterias/cafeteria.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { User } from '../users/users.entity'; +import { User } from 'src/api/public/users/entities/users.entity'; @Entity('campus') export class Campus { diff --git a/services/api/app/src/api/public/colleges/colleges.module.ts b/services/api/app/src/api/public/colleges/colleges.module.ts index 5803cd6..f27bdc8 100644 --- a/services/api/app/src/api/public/colleges/colleges.module.ts +++ b/services/api/app/src/api/public/colleges/colleges.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { College } from 'src/type-orm/entities/colleges/college.entity'; -import { CollegesRepository } from 'src/type-orm/entities/colleges/colleges.repository'; +import { College } from 'src/api/public/colleges/entities/college.entity'; +import { CollegesRepository } from 'src/api/public/colleges/colleges.repository'; import { CollegesService } from './colleges.service'; @Module({ diff --git a/services/api/app/src/type-orm/entities/colleges/colleges.repository.ts b/services/api/app/src/api/public/colleges/colleges.repository.ts similarity index 92% rename from services/api/app/src/type-orm/entities/colleges/colleges.repository.ts rename to services/api/app/src/api/public/colleges/colleges.repository.ts index 045626b..d9102fe 100644 --- a/services/api/app/src/type-orm/entities/colleges/colleges.repository.ts +++ b/services/api/app/src/api/public/colleges/colleges.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ListCardConfig } from 'src/api/common/utils/constants'; import { Repository } from 'typeorm'; -import { College } from './college.entity'; +import { College } from './entities/college.entity'; @Injectable() export class CollegesRepository { diff --git a/services/api/app/src/api/public/colleges/colleges.service.ts b/services/api/app/src/api/public/colleges/colleges.service.ts index be0ca2f..822c802 100644 --- a/services/api/app/src/api/public/colleges/colleges.service.ts +++ b/services/api/app/src/api/public/colleges/colleges.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { CollegeListResult } from 'src/api/public/colleges/dtos/results/college-list-result.dto'; -import { CollegesRepository } from 'src/type-orm/entities/colleges/colleges.repository'; +import { CollegesRepository } from 'src/api/public/colleges/colleges.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/type-orm/entities/colleges/college.entity.ts b/services/api/app/src/api/public/colleges/entities/college.entity.ts similarity index 84% rename from services/api/app/src/type-orm/entities/colleges/college.entity.ts rename to services/api/app/src/api/public/colleges/entities/college.entity.ts index 7f881e8..fb55793 100644 --- a/services/api/app/src/type-orm/entities/colleges/college.entity.ts +++ b/services/api/app/src/api/public/colleges/entities/college.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { Department } from '../departments/department.entity'; +import { Department } from 'src/api/public/departments/entities/department.entity'; @Entity('college') export class College { diff --git a/services/api/app/src/api/public/departments/departments.module.ts b/services/api/app/src/api/public/departments/departments.module.ts index 7de1182..6322525 100644 --- a/services/api/app/src/api/public/departments/departments.module.ts +++ b/services/api/app/src/api/public/departments/departments.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Department } from 'src/type-orm/entities/departments/department.entity'; -import { DepartmentsRepository } from 'src/type-orm/entities/departments/departments.repository'; +import { Department } from 'src/api/public/departments/entities/department.entity'; +import { DepartmentsRepository } from 'src/api/public/departments/departments.repository'; import { DepartmentsService } from './departments.service'; @Module({ diff --git a/services/api/app/src/type-orm/entities/departments/departments.repository.ts b/services/api/app/src/api/public/departments/departments.repository.ts similarity index 90% rename from services/api/app/src/type-orm/entities/departments/departments.repository.ts rename to services/api/app/src/api/public/departments/departments.repository.ts index b8961fc..33cead3 100644 --- a/services/api/app/src/type-orm/entities/departments/departments.repository.ts +++ b/services/api/app/src/api/public/departments/departments.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ListCardConfig } from 'src/api/common/utils/constants'; -import { Department } from 'src/type-orm/entities/departments/department.entity'; +import { Department } from 'src/api/public/departments/entities/department.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/departments/departments.service.ts b/services/api/app/src/api/public/departments/departments.service.ts index a404bd4..814a32e 100644 --- a/services/api/app/src/api/public/departments/departments.service.ts +++ b/services/api/app/src/api/public/departments/departments.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DepartmentListResult } from 'src/api/public/departments/dtos/results/department-list-result.dto'; -import { DepartmentsRepository } from 'src/type-orm/entities/departments/departments.repository'; +import { DepartmentsRepository } from 'src/api/public/departments/departments.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/type-orm/entities/departments/department.entity.ts b/services/api/app/src/api/public/departments/entities/department.entity.ts similarity index 83% rename from services/api/app/src/type-orm/entities/departments/department.entity.ts rename to services/api/app/src/api/public/departments/entities/department.entity.ts index 63d975b..8560beb 100644 --- a/services/api/app/src/type-orm/entities/departments/department.entity.ts +++ b/services/api/app/src/api/public/departments/entities/department.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { College } from '../colleges/college.entity'; -import { User } from '../users/users.entity'; +import { College } from 'src/api/public/colleges/entities/college.entity'; +import { User } from 'src/api/public/users/entities/users.entity'; @Entity('department') export class Department { diff --git a/services/api/app/src/type-orm/entities/notices/notice-category.entity.ts b/services/api/app/src/api/public/notices/entities/notice-category.entity.ts similarity index 90% rename from services/api/app/src/type-orm/entities/notices/notice-category.entity.ts rename to services/api/app/src/api/public/notices/entities/notice-category.entity.ts index 87674c1..07b17ca 100644 --- a/services/api/app/src/type-orm/entities/notices/notice-category.entity.ts +++ b/services/api/app/src/api/public/notices/entities/notice-category.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { Department } from '../departments/department.entity'; +import { Department } from 'src/api/public/departments/entities/department.entity'; import { Notice } from './notice.entity'; @Entity('notice_category') diff --git a/services/api/app/src/type-orm/entities/notices/notice.entity.ts b/services/api/app/src/api/public/notices/entities/notice.entity.ts similarity index 100% rename from services/api/app/src/type-orm/entities/notices/notice.entity.ts rename to services/api/app/src/api/public/notices/entities/notice.entity.ts diff --git a/services/api/app/src/type-orm/entities/notices/notice-categories.repository.ts b/services/api/app/src/api/public/notices/notice-categories.repository.ts similarity index 96% rename from services/api/app/src/type-orm/entities/notices/notice-categories.repository.ts rename to services/api/app/src/api/public/notices/notice-categories.repository.ts index c61ca65..6d3cf45 100644 --- a/services/api/app/src/type-orm/entities/notices/notice-categories.repository.ts +++ b/services/api/app/src/api/public/notices/notice-categories.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { NoticeCategory } from 'src/type-orm/entities/notices/notice-category.entity'; +import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/notices/notices.controller.ts b/services/api/app/src/api/public/notices/notices.controller.ts index 45a2991..7b9f6ec 100644 --- a/services/api/app/src/api/public/notices/notices.controller.ts +++ b/services/api/app/src/api/public/notices/notices.controller.ts @@ -7,7 +7,7 @@ import { CommonMessageFactory } from 'src/api/public/common/common-message.facto import { NoticeMessageFactory } from 'src/api/public/notices/notice-message.factory'; import { CurrentUser } from 'src/api/public/users/decorators/current-user.decorator'; import { FetchCurrentUser } from 'src/api/public/users/decorators/fetch-current-user.decorator'; -import { User } from 'src/type-orm/entities/users/users.entity'; +import { User } from 'src/api/public/users/entities/users.entity'; import { ListDepartmentNoticeRequestDto } from './dtos/requests/list-department-notice-request.dto'; import { ListUniversityNoticeRequestDto } from './dtos/requests/list-university-notice-request.dto'; import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; diff --git a/services/api/app/src/api/public/notices/notices.module.ts b/services/api/app/src/api/public/notices/notices.module.ts index edded7b..7229661 100644 --- a/services/api/app/src/api/public/notices/notices.module.ts +++ b/services/api/app/src/api/public/notices/notices.module.ts @@ -1,14 +1,24 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; +import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/entities/notice.entity'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; import { NoticeMessageFactory } from 'src/api/public/notices/notice-message.factory'; -import { NoticesRepositoryModule } from 'src/type-orm/entities/notices/notices-repository.module'; +import { NoticesRepository } from 'src/api/public/notices/notices.repository'; import { NoticesController } from './notices.controller'; import { NoticesService } from './notices.service'; @Module({ - imports: [NoticesRepositoryModule], + imports: [TypeOrmModule.forFeature([Notice, NoticeCategory])], controllers: [NoticesController], - providers: [NoticesService, NoticeMessageFactory, CommonMessageFactory], + providers: [ + NoticesService, + NoticesRepository, + NoticeCategoriesRepository, + NoticeMessageFactory, + CommonMessageFactory, + ], exports: [NoticesService], }) export class NoticesModule {} diff --git a/services/api/app/src/type-orm/entities/notices/notices.repository.ts b/services/api/app/src/api/public/notices/notices.repository.ts similarity index 96% rename from services/api/app/src/type-orm/entities/notices/notices.repository.ts rename to services/api/app/src/api/public/notices/notices.repository.ts index a93733a..cdd47d9 100644 --- a/services/api/app/src/type-orm/entities/notices/notices.repository.ts +++ b/services/api/app/src/api/public/notices/notices.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Notice } from 'src/type-orm/entities/notices/notice.entity'; +import { Notice } from 'src/api/public/notices/entities/notice.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/notices/notices.service.spec.ts b/services/api/app/src/api/public/notices/notices.service.spec.ts index 6998cd9..435ac01 100644 --- a/services/api/app/src/api/public/notices/notices.service.spec.ts +++ b/services/api/app/src/api/public/notices/notices.service.spec.ts @@ -1,9 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { NoticeCategoriesRepository } from 'src/type-orm/entities/notices/notice-categories.repository'; -import { NoticeCategory } from 'src/type-orm/entities/notices/notice-category.entity'; -import { Notice } from 'src/type-orm/entities/notices/notice.entity'; -import { NoticesRepository } from 'src/type-orm/entities/notices/notices.repository'; -import { User } from 'src/type-orm/entities/users/users.entity'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; +import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/entities/notice.entity'; +import { NoticesRepository } from 'src/api/public/notices/notices.repository'; +import { User } from 'src/api/public/users/entities/users.entity'; import { NoticesService } from './notices.service'; const makeCategory = (overrides: Partial = {}): NoticeCategory => diff --git a/services/api/app/src/api/public/notices/notices.service.ts b/services/api/app/src/api/public/notices/notices.service.ts index 0a35b15..5ca86af 100644 --- a/services/api/app/src/api/public/notices/notices.service.ts +++ b/services/api/app/src/api/public/notices/notices.service.ts @@ -3,11 +3,11 @@ import { NoticeCategoryResult, NoticeListResult, } from 'src/api/public/notices/dtos/results/notice-list-result.dto'; -import { NoticeCategoriesRepository } from 'src/type-orm/entities/notices/notice-categories.repository'; -import { NoticeCategory } from 'src/type-orm/entities/notices/notice-category.entity'; -import { Notice } from 'src/type-orm/entities/notices/notice.entity'; -import { NoticesRepository } from 'src/type-orm/entities/notices/notices.repository'; -import { User } from 'src/type-orm/entities/users/users.entity'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; +import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/entities/notice.entity'; +import { NoticesRepository } from 'src/api/public/notices/notices.repository'; +import { User } from 'src/api/public/users/entities/users.entity'; @Injectable() export class NoticesService { diff --git a/services/api/app/src/type-orm/entities/academic-calendars/academic-calendars.repository.ts b/services/api/app/src/api/public/schedules/academic-calendars.repository.ts similarity index 95% rename from services/api/app/src/type-orm/entities/academic-calendars/academic-calendars.repository.ts rename to services/api/app/src/api/public/schedules/academic-calendars.repository.ts index 90d4a51..e9fb8e5 100644 --- a/services/api/app/src/type-orm/entities/academic-calendars/academic-calendars.repository.ts +++ b/services/api/app/src/api/public/schedules/academic-calendars.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { AcademicCalendar } from './academic-calendar.entity'; +import { AcademicCalendar } from './entities/academic-calendar.entity'; @Injectable() export class AcademicCalendarsRepository { diff --git a/services/api/app/src/type-orm/entities/academic-calendars/academic-calendar.entity.ts b/services/api/app/src/api/public/schedules/entities/academic-calendar.entity.ts similarity index 100% rename from services/api/app/src/type-orm/entities/academic-calendars/academic-calendar.entity.ts rename to services/api/app/src/api/public/schedules/entities/academic-calendar.entity.ts diff --git a/services/api/app/src/api/public/schedules/schedules.module.ts b/services/api/app/src/api/public/schedules/schedules.module.ts index 0490ed8..1732400 100644 --- a/services/api/app/src/api/public/schedules/schedules.module.ts +++ b/services/api/app/src/api/public/schedules/schedules.module.ts @@ -1,14 +1,21 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; +import { AcademicCalendarsRepository } from 'src/api/public/schedules/academic-calendars.repository'; +import { AcademicCalendar } from 'src/api/public/schedules/entities/academic-calendar.entity'; import { ScheduleMessageFactory } from 'src/api/public/schedules/schedule-message.factory'; -import { AcademicCalendarsRepositoryModule } from 'src/type-orm/entities/academic-calendars/academic-calendars-repository.module'; import { SchedulesController } from 'src/api/public/schedules/schedules.controller'; import { SchedulesService } from './schedules.service'; @Module({ - imports: [AcademicCalendarsRepositoryModule], + imports: [TypeOrmModule.forFeature([AcademicCalendar])], controllers: [SchedulesController], - providers: [SchedulesService, ScheduleMessageFactory, CommonMessageFactory], + providers: [ + SchedulesService, + AcademicCalendarsRepository, + ScheduleMessageFactory, + CommonMessageFactory, + ], exports: [SchedulesService], }) export class SchedulesModule {} diff --git a/services/api/app/src/api/public/schedules/schedules.service.ts b/services/api/app/src/api/public/schedules/schedules.service.ts index f1134b8..7dc12bb 100644 --- a/services/api/app/src/api/public/schedules/schedules.service.ts +++ b/services/api/app/src/api/public/schedules/schedules.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { AcademicScheduleResult } from 'src/api/public/schedules/dtos/results/academic-schedule-result.dto'; -import { AcademicCalendarsRepository } from 'src/type-orm/entities/academic-calendars/academic-calendars.repository'; +import { AcademicCalendarsRepository } from 'src/api/public/schedules/academic-calendars.repository'; @Injectable() export class SchedulesService { diff --git a/services/api/app/src/type-orm/entities/shuttle-timetables/shuttle-timetable.entity.ts b/services/api/app/src/api/public/shuttles/entities/shuttle-timetable.entity.ts similarity index 100% rename from services/api/app/src/type-orm/entities/shuttle-timetables/shuttle-timetable.entity.ts rename to services/api/app/src/api/public/shuttles/entities/shuttle-timetable.entity.ts diff --git a/services/api/app/src/type-orm/entities/shuttle-timetables/shuttle-timetable.repository.ts b/services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts similarity index 89% rename from services/api/app/src/type-orm/entities/shuttle-timetables/shuttle-timetable.repository.ts rename to services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts index ff336a3..c05a1aa 100644 --- a/services/api/app/src/type-orm/entities/shuttle-timetables/shuttle-timetable.repository.ts +++ b/services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ShuttleTimetable } from './shuttle-timetable.entity'; +import { ShuttleTimetable } from './entities/shuttle-timetable.entity'; @Injectable() export class ShuttleTimetableRepository { diff --git a/services/api/app/src/api/public/shuttles/shuttles.module.ts b/services/api/app/src/api/public/shuttles/shuttles.module.ts index 8c025f2..04f66af 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.module.ts +++ b/services/api/app/src/api/public/shuttles/shuttles.module.ts @@ -1,8 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { DatabaseModule } from 'src/type-orm/database.module'; -import { ShuttleTimetable } from 'src/type-orm/entities/shuttle-timetables/shuttle-timetable.entity'; -import { ShuttleTimetableRepository } from 'src/type-orm/entities/shuttle-timetables/shuttle-timetable.repository'; +import { ShuttleTimetable } from 'src/api/public/shuttles/entities/shuttle-timetable.entity'; +import { ShuttleTimetableRepository } from 'src/api/public/shuttles/shuttle-timetable.repository'; import { ShuttleMessageFactory } from './shuttle-message.factory'; import { ShuttleTimetableCalculator } from './shuttle-timetable.calculator'; import { ShuttlesController } from './shuttles.controller'; @@ -10,7 +9,7 @@ import { ShuttlesNativeController } from './shuttles-native.controller'; import { ShuttlesService } from './shuttles.service'; @Module({ - imports: [DatabaseModule, TypeOrmModule.forFeature([ShuttleTimetable])], + imports: [TypeOrmModule.forFeature([ShuttleTimetable])], controllers: [ShuttlesController, ShuttlesNativeController], providers: [ ShuttlesService, diff --git a/services/api/app/src/api/public/shuttles/shuttles.service.ts b/services/api/app/src/api/public/shuttles/shuttles.service.ts index 3d5896a..37ede15 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.service.ts +++ b/services/api/app/src/api/public/shuttles/shuttles.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { ShuttleRouteListResult } from 'src/api/public/shuttles/dtos/results/shuttle-route-list-result.dto'; import { ShuttleTimetableResult } from 'src/api/public/shuttles/dtos/results/shuttle-timetable-result.dto'; -import { ShuttleTimetableRepository } from 'src/type-orm/entities/shuttle-timetables/shuttle-timetable.repository'; +import { ShuttleTimetableRepository } from 'src/api/public/shuttles/shuttle-timetable.repository'; @Injectable() export class ShuttlesService { diff --git a/services/api/app/src/type-orm/entities/users/users.entity.ts b/services/api/app/src/api/public/users/entities/users.entity.ts similarity index 71% rename from services/api/app/src/type-orm/entities/users/users.entity.ts rename to services/api/app/src/api/public/users/entities/users.entity.ts index bfe2d0e..df5f794 100644 --- a/services/api/app/src/type-orm/entities/users/users.entity.ts +++ b/services/api/app/src/api/public/users/entities/users.entity.ts @@ -1,6 +1,6 @@ import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Campus } from '../campuses/campus.entity'; -import { Department } from '../departments/department.entity'; +import { Campus } from 'src/api/public/campuses/entities/campus.entity'; +import { Department } from 'src/api/public/departments/entities/department.entity'; @Entity('kakao-user') export class User { diff --git a/services/api/app/src/api/public/users/users.controller.ts b/services/api/app/src/api/public/users/users.controller.ts index 2e8cd6e..9ef79ba 100644 --- a/services/api/app/src/api/public/users/users.controller.ts +++ b/services/api/app/src/api/public/users/users.controller.ts @@ -13,7 +13,7 @@ import { CollegeMessageFactory } from 'src/api/public/colleges/college-message.f import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; import { DepartmentMessageFactory } from 'src/api/public/departments/department-message.factory'; import { UserMessageFactory } from 'src/api/public/users/user-message.factory'; -import { User } from '../../../type-orm/entities/users/users.entity'; +import { User } from 'src/api/public/users/entities/users.entity'; import { CurrentUser } from './decorators/current-user.decorator'; import { FetchCurrentUser } from './decorators/fetch-current-user.decorator'; import { ListCollegesRequestDto } from './dtos/requests/list-college-request.dto'; diff --git a/services/api/app/src/api/public/users/users.module.ts b/services/api/app/src/api/public/users/users.module.ts index 1ce3bde..788e24e 100644 --- a/services/api/app/src/api/public/users/users.module.ts +++ b/services/api/app/src/api/public/users/users.module.ts @@ -5,9 +5,8 @@ import { CampusesModule } from 'src/api/public/campuses/campuses.module'; import { CollegesModule } from 'src/api/public/colleges/colleges.module'; import { DepartmentsModule } from 'src/api/public/departments/departments.module'; import { CurrentUserInterceptor } from 'src/api/public/users/interceptors/current-user.interceptor'; -import { DatabaseModule } from '../../../type-orm/database.module'; -import { User } from '../../../type-orm/entities/users/users.entity'; -import { UsersRepository } from '../../../type-orm/entities/users/users.repository'; +import { User } from 'src/api/public/users/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/users.repository'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { UserMessageFactory } from 'src/api/public/users/user-message.factory'; @@ -17,13 +16,7 @@ import { CollegeMessageFactory } from 'src/api/public/colleges/college-message.f import { DepartmentMessageFactory } from 'src/api/public/departments/department-message.factory'; @Module({ - imports: [ - DatabaseModule, - TypeOrmModule.forFeature([User]), - CampusesModule, - CollegesModule, - DepartmentsModule, - ], + imports: [TypeOrmModule.forFeature([User]), CampusesModule, CollegesModule, DepartmentsModule], controllers: [UsersController], providers: [ UsersService, diff --git a/services/api/app/src/type-orm/entities/users/users.repository.ts b/services/api/app/src/api/public/users/users.repository.ts similarity index 94% rename from services/api/app/src/type-orm/entities/users/users.repository.ts rename to services/api/app/src/api/public/users/users.repository.ts index 24b1a09..5dee638 100644 --- a/services/api/app/src/type-orm/entities/users/users.repository.ts +++ b/services/api/app/src/api/public/users/users.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { User } from './users.entity'; +import { User } from './entities/users.entity'; @Injectable() export class UsersRepository { diff --git a/services/api/app/src/api/public/users/users.service.spec.ts b/services/api/app/src/api/public/users/users.service.spec.ts index 0fa881e..8e06c51 100644 --- a/services/api/app/src/api/public/users/users.service.spec.ts +++ b/services/api/app/src/api/public/users/users.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; -import { User } from 'src/type-orm/entities/users/users.entity'; -import { UsersRepository } from 'src/type-orm/entities/users/users.repository'; +import { User } from 'src/api/public/users/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/users.repository'; const makeUser = (overrides: Partial = {}): User => ({ diff --git a/services/api/app/src/api/public/users/users.service.ts b/services/api/app/src/api/public/users/users.service.ts index c749d15..7a04b73 100644 --- a/services/api/app/src/api/public/users/users.service.ts +++ b/services/api/app/src/api/public/users/users.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { UpsertDepartmentResult } from 'src/api/public/users/dtos/results/upsert-department-result.dto'; import { UserProfileResult } from 'src/api/public/users/dtos/results/user-profile-result.dto'; -import { User } from '../../../type-orm/entities/users/users.entity'; -import { UsersRepository } from '../../../type-orm/entities/users/users.repository'; +import { User } from 'src/api/public/users/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/users.repository'; @Injectable() export class UsersService { diff --git a/services/api/app/src/app.module.ts b/services/api/app/src/app.module.ts index f308ea4..c1cf574 100644 --- a/services/api/app/src/app.module.ts +++ b/services/api/app/src/app.module.ts @@ -8,13 +8,13 @@ import { makeHistogramProvider, PrometheusModule, } from '@willsoto/nestjs-prometheus'; -import { LoggerModule } from 'src/api/internal/logger/logger.module'; +import { LoggerModule } from 'src/infrastructure/logger/logger.module'; import { CafeteriasModule } from 'src/api/public/cafeterias/cafeterias.module'; import { CampusesModule } from './api/public/campuses/campuses.module'; import { CollegesModule } from './api/public/colleges/colleges.module'; import { DepartmentsModule } from './api/public/departments/departments.module'; import { UsersModule } from './api/public/users/users.module'; -import { DatabaseModule } from './type-orm/database.module'; +import { DatabaseModule } from './infrastructure/type-orm/database.module'; import { NoticesModule } from './api/public/notices/notices.module'; import { SchedulesModule } from './api/public/schedules/schedules.module'; import { ShuttlesModule } from './api/public/shuttles/shuttles.module'; diff --git a/services/api/app/src/api/internal/logger/logger.module.ts b/services/api/app/src/infrastructure/logger/logger.module.ts similarity index 100% rename from services/api/app/src/api/internal/logger/logger.module.ts rename to services/api/app/src/infrastructure/logger/logger.module.ts diff --git a/services/api/app/src/type-orm/database.module.ts b/services/api/app/src/infrastructure/type-orm/database.module.ts similarity index 100% rename from services/api/app/src/type-orm/database.module.ts rename to services/api/app/src/infrastructure/type-orm/database.module.ts diff --git a/services/api/app/src/type-orm/entities/academic-calendars/academic-calendars-repository.module.ts b/services/api/app/src/type-orm/entities/academic-calendars/academic-calendars-repository.module.ts deleted file mode 100644 index d427a3f..0000000 --- a/services/api/app/src/type-orm/entities/academic-calendars/academic-calendars-repository.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AcademicCalendar } from './academic-calendar.entity'; -import { AcademicCalendarsRepository } from './academic-calendars.repository'; - -@Module({ - imports: [TypeOrmModule.forFeature([AcademicCalendar])], - providers: [AcademicCalendarsRepository], - exports: [AcademicCalendarsRepository], -}) -export class AcademicCalendarsRepositoryModule {} diff --git a/services/api/app/src/type-orm/entities/cafeterias/cafeterias-repository.module.ts b/services/api/app/src/type-orm/entities/cafeterias/cafeterias-repository.module.ts deleted file mode 100644 index f1f2181..0000000 --- a/services/api/app/src/type-orm/entities/cafeterias/cafeterias-repository.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CafeteriaDiet } from 'src/type-orm/entities/cafeterias/cafeteria-diet.entity'; -import { CafeteriasRepository } from 'src/type-orm/entities/cafeterias/cafeterias.repository'; -import { Cafeteria } from './cafeteria.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([Cafeteria, CafeteriaDiet])], - providers: [CafeteriasRepository], - exports: [CafeteriasRepository], -}) -export class CafeteriasRepositoryModule {} diff --git a/services/api/app/src/type-orm/entities/notices/notices-repository.module.ts b/services/api/app/src/type-orm/entities/notices/notices-repository.module.ts deleted file mode 100644 index 189b0b8..0000000 --- a/services/api/app/src/type-orm/entities/notices/notices-repository.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { NoticeCategoriesRepository } from 'src/type-orm/entities/notices/notice-categories.repository'; -import { NoticeCategory } from 'src/type-orm/entities/notices/notice-category.entity'; -import { Notice } from 'src/type-orm/entities/notices/notice.entity'; -import { NoticesRepository } from 'src/type-orm/entities/notices/notices.repository'; - -@Module({ - imports: [TypeOrmModule.forFeature([Notice, NoticeCategory])], - providers: [NoticesRepository, NoticeCategoriesRepository], - exports: [NoticesRepository, NoticeCategoriesRepository], -}) -export class NoticesRepositoryModule {} From 546add3d024f93ba7cba172a8bae6a4215c42d53 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 11 Jun 2026 18:19:16 +0900 Subject: [PATCH 10/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20public?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - presentation/application/domain/infrastructure 기준으로 파일 재배치 --- .../cafeterias.service.spec.ts | 8 +++---- .../{ => application}/cafeterias.service.ts | 8 +++---- .../dtos/results/cafeteria-diet-result.dto.ts | 4 ++-- .../dtos/results/cafeteria-list-result.dto.ts | 2 +- .../{ => application}/utils/time.spec.ts | 0 .../{ => application}/utils/time.ts | 2 +- .../public/cafeterias/cafeterias.module.ts | 16 +++++++------- .../entities/cafeteria-diet.entity.ts | 2 +- .../{ => domain}/entities/cafeteria.entity.ts | 4 ++-- .../cafeterias.repository.ts | 6 ++--- .../cafeteria-message.factory.ts | 8 +++---- .../cafeterias-native.controller.ts | 4 ++-- .../cafeterias.controller.ts | 22 +++++++++---------- .../requests/get-cafeteria-diet-query.dto.ts | 0 .../dtos/requests/get-cafeterias-query.dto.ts | 0 .../list-cafeteria-diet-request.dto.ts | 0 .../requests/list-cafeteria-request.dto.ts | 0 .../responses/cafeteria-diet-response.dto.ts | 0 .../dtos/responses/cafeteria-response.dto.ts | 2 +- .../{ => application}/campuses.service.ts | 4 ++-- .../dtos/results/campus-list-result.dto.ts | 0 .../api/public/campuses/campuses.module.ts | 8 +++---- .../{ => domain}/entities/campus.entity.ts | 4 ++-- .../campuses.repository.ts | 2 +- .../campus-message.factory.ts | 2 +- .../campuses-native.controller.ts | 4 ++-- .../dtos/campus-response.dto.ts | 0 .../{ => application}/colleges.service.ts | 4 ++-- .../dtos/results/college-list-result.dto.ts | 0 .../api/public/colleges/colleges.module.ts | 6 ++--- .../{ => domain}/entities/college.entity.ts | 2 +- .../colleges.repository.ts | 2 +- .../college-message.factory.ts | 2 +- .../common-message.factory.ts | 0 .../{ => application}/departments.service.ts | 4 ++-- .../results/department-list-result.dto.ts | 0 .../public/departments/departments.module.ts | 6 ++--- .../entities/department.entity.ts | 4 ++-- .../departments.repository.ts | 2 +- .../department-message.factory.ts | 4 ++-- .../dtos/results/notice-list-result.dto.ts | 0 .../{ => application}/notices.service.spec.ts | 12 +++++----- .../{ => application}/notices.service.ts | 12 +++++----- .../entities/notice-category.entity.ts | 2 +- .../{ => domain}/entities/notice.entity.ts | 0 .../notice-categories.repository.ts | 2 +- .../notices.repository.ts | 2 +- .../src/api/public/notices/notices.module.ts | 16 +++++++------- .../list-department-notice-request.dto.ts | 0 .../list-university-notice-request.dto.ts | 0 .../notice-message.factory.spec.ts | 4 ++-- .../notice-message.factory.ts | 2 +- .../{ => presentation}/notices.controller.ts | 14 ++++++------ .../results/academic-schedule-result.dto.ts | 0 .../{ => application}/schedules.service.ts | 4 ++-- .../entities/academic-calendar.entity.ts | 0 .../academic-calendars.repository.ts | 2 +- .../list-academic-schedule-request.dto.ts | 0 .../schedule-message.factory.ts | 2 +- .../schedules.controller.ts | 8 +++---- .../api/public/schedules/schedules.module.ts | 12 +++++----- .../results/shuttle-route-list-result.dto.ts | 0 .../results/shuttle-timetable-result.dto.ts | 0 .../shuttle-timetable.calculator.spec.ts | 2 +- .../shuttle-timetable.calculator.ts | 0 .../{ => application}/shuttles.service.ts | 6 ++--- .../entities/shuttle-timetable.entity.ts | 0 .../shuttle-timetable.repository.ts | 2 +- .../get-shuttle-timetable-request.dto.ts | 0 .../responses/shuttle-route-response.dto.ts | 0 .../shuttle-timetable-response.dto.ts | 0 .../shuttle-message.factory.ts | 4 ++-- .../shuttles-native.controller.ts | 6 ++--- .../{ => presentation}/shuttles.controller.ts | 6 ++--- .../api/public/shuttles/shuttles.module.ts | 14 ++++++------ .../results/upsert-department-result.dto.ts | 0 .../dtos/results/user-profile-result.dto.ts | 0 .../{ => application}/users.service.spec.ts | 6 ++--- .../users/{ => application}/users.service.ts | 8 +++---- .../{ => domain}/entities/users.entity.ts | 4 ++-- .../{ => infrastructure}/users.repository.ts | 2 +- .../decorators/current-user.decorator.ts | 0 .../fetch-current-user.decorator.ts | 0 .../dtos/requests/list-college-request.dto.ts | 0 .../requests/list-department-request.dto.ts | 0 .../requests/upsert-department-request.dto.ts | 0 .../dtos/responses/profile-response.dto.ts | 0 .../guards/jwt-auth.guard.ts | 0 .../guards/kakao-auth.guard.ts | 0 .../current-user.interceptor.spec.ts | 4 ++-- .../interceptors/current-user.interceptor.ts | 4 ++-- .../user-message.factory.ts | 2 +- .../{ => presentation}/users.controller.ts | 20 ++++++++--------- .../app/src/api/public/users/users.module.ts | 20 ++++++++--------- .../type-orm/database.module.ts | 2 +- services/api/app/test/cafeterias.e2e-spec.ts | 12 +++++----- services/api/app/test/notices.e2e-spec.ts | 12 +++++----- 97 files changed, 189 insertions(+), 189 deletions(-) rename services/api/app/src/api/public/cafeterias/{ => application}/cafeterias.service.spec.ts (95%) rename services/api/app/src/api/public/cafeterias/{ => application}/cafeterias.service.ts (89%) rename services/api/app/src/api/public/cafeterias/{ => application}/dtos/results/cafeteria-diet-result.dto.ts (61%) rename services/api/app/src/api/public/cafeterias/{ => application}/dtos/results/cafeteria-list-result.dto.ts (65%) rename services/api/app/src/api/public/cafeterias/{ => application}/utils/time.spec.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => application}/utils/time.ts (94%) rename services/api/app/src/api/public/cafeterias/{ => domain}/entities/cafeteria-diet.entity.ts (87%) rename services/api/app/src/api/public/cafeterias/{ => domain}/entities/cafeteria.entity.ts (76%) rename services/api/app/src/api/public/cafeterias/{ => infrastructure}/cafeterias.repository.ts (84%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/cafeteria-message.factory.ts (89%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/cafeterias-native.controller.ts (92%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/cafeterias.controller.ts (75%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/requests/get-cafeteria-diet-query.dto.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/requests/get-cafeterias-query.dto.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/requests/list-cafeteria-diet-request.dto.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/requests/list-cafeteria-request.dto.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/responses/cafeteria-diet-response.dto.ts (100%) rename services/api/app/src/api/public/cafeterias/{ => presentation}/dtos/responses/cafeteria-response.dto.ts (80%) rename services/api/app/src/api/public/campuses/{ => application}/campuses.service.ts (79%) rename services/api/app/src/api/public/campuses/{ => application}/dtos/results/campus-list-result.dto.ts (100%) rename services/api/app/src/api/public/campuses/{ => domain}/entities/campus.entity.ts (72%) rename services/api/app/src/api/public/campuses/{ => infrastructure}/campuses.repository.ts (89%) rename services/api/app/src/api/public/campuses/{ => presentation}/campus-message.factory.ts (89%) rename services/api/app/src/api/public/campuses/{ => presentation}/campuses-native.controller.ts (82%) rename services/api/app/src/api/public/campuses/{ => presentation}/dtos/campus-response.dto.ts (100%) rename services/api/app/src/api/public/colleges/{ => application}/colleges.service.ts (80%) rename services/api/app/src/api/public/colleges/{ => application}/dtos/results/college-list-result.dto.ts (100%) rename services/api/app/src/api/public/colleges/{ => domain}/entities/college.entity.ts (83%) rename services/api/app/src/api/public/colleges/{ => infrastructure}/colleges.repository.ts (89%) rename services/api/app/src/api/public/colleges/{ => presentation}/college-message.factory.ts (94%) rename services/api/app/src/api/public/common/{ => presentation}/common-message.factory.ts (100%) rename services/api/app/src/api/public/departments/{ => application}/departments.service.ts (91%) rename services/api/app/src/api/public/departments/{ => application}/dtos/results/department-list-result.dto.ts (100%) rename services/api/app/src/api/public/departments/{ => domain}/entities/department.entity.ts (81%) rename services/api/app/src/api/public/departments/{ => infrastructure}/departments.repository.ts (89%) rename services/api/app/src/api/public/departments/{ => presentation}/department-message.factory.ts (94%) rename services/api/app/src/api/public/notices/{ => application}/dtos/results/notice-list-result.dto.ts (100%) rename services/api/app/src/api/public/notices/{ => application}/notices.service.spec.ts (94%) rename services/api/app/src/api/public/notices/{ => application}/notices.service.ts (87%) rename services/api/app/src/api/public/notices/{ => domain}/entities/notice-category.entity.ts (89%) rename services/api/app/src/api/public/notices/{ => domain}/entities/notice.entity.ts (100%) rename services/api/app/src/api/public/notices/{ => infrastructure}/notice-categories.repository.ts (96%) rename services/api/app/src/api/public/notices/{ => infrastructure}/notices.repository.ts (96%) rename services/api/app/src/api/public/notices/{ => presentation}/dtos/requests/list-department-notice-request.dto.ts (100%) rename services/api/app/src/api/public/notices/{ => presentation}/dtos/requests/list-university-notice-request.dto.ts (100%) rename services/api/app/src/api/public/notices/{ => presentation}/notice-message.factory.spec.ts (97%) rename services/api/app/src/api/public/notices/{ => presentation}/notice-message.factory.ts (98%) rename services/api/app/src/api/public/notices/{ => presentation}/notices.controller.ts (76%) rename services/api/app/src/api/public/schedules/{ => application}/dtos/results/academic-schedule-result.dto.ts (100%) rename services/api/app/src/api/public/schedules/{ => application}/schedules.service.ts (90%) rename services/api/app/src/api/public/schedules/{ => domain}/entities/academic-calendar.entity.ts (100%) rename services/api/app/src/api/public/schedules/{ => infrastructure}/academic-calendars.repository.ts (94%) rename services/api/app/src/api/public/schedules/{ => presentation}/dtos/requests/list-academic-schedule-request.dto.ts (100%) rename services/api/app/src/api/public/schedules/{ => presentation}/schedule-message.factory.ts (98%) rename services/api/app/src/api/public/schedules/{ => presentation}/schedules.controller.ts (83%) rename services/api/app/src/api/public/shuttles/{ => application}/dtos/results/shuttle-route-list-result.dto.ts (100%) rename services/api/app/src/api/public/shuttles/{ => application}/dtos/results/shuttle-timetable-result.dto.ts (100%) rename services/api/app/src/api/public/shuttles/{ => application}/shuttle-timetable.calculator.spec.ts (94%) rename services/api/app/src/api/public/shuttles/{ => application}/shuttle-timetable.calculator.ts (100%) rename services/api/app/src/api/public/shuttles/{ => application}/shuttles.service.ts (87%) rename services/api/app/src/api/public/shuttles/{ => domain}/entities/shuttle-timetable.entity.ts (100%) rename services/api/app/src/api/public/shuttles/{ => infrastructure}/shuttle-timetable.repository.ts (85%) rename services/api/app/src/api/public/shuttles/{ => presentation}/dtos/request/get-shuttle-timetable-request.dto.ts (100%) rename services/api/app/src/api/public/shuttles/{ => presentation}/dtos/responses/shuttle-route-response.dto.ts (100%) rename services/api/app/src/api/public/shuttles/{ => presentation}/dtos/responses/shuttle-timetable-response.dto.ts (100%) rename services/api/app/src/api/public/shuttles/{ => presentation}/shuttle-message.factory.ts (95%) rename services/api/app/src/api/public/shuttles/{ => presentation}/shuttles-native.controller.ts (86%) rename services/api/app/src/api/public/shuttles/{ => presentation}/shuttles.controller.ts (84%) rename services/api/app/src/api/public/users/{ => application}/dtos/results/upsert-department-result.dto.ts (100%) rename services/api/app/src/api/public/users/{ => application}/dtos/results/user-profile-result.dto.ts (100%) rename services/api/app/src/api/public/users/{ => application}/users.service.spec.ts (89%) rename services/api/app/src/api/public/users/{ => application}/users.service.ts (72%) rename services/api/app/src/api/public/users/{ => domain}/entities/users.entity.ts (69%) rename services/api/app/src/api/public/users/{ => infrastructure}/users.repository.ts (92%) rename services/api/app/src/api/public/users/{ => presentation}/decorators/current-user.decorator.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/decorators/fetch-current-user.decorator.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/dtos/requests/list-college-request.dto.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/dtos/requests/list-department-request.dto.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/dtos/requests/upsert-department-request.dto.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/dtos/responses/profile-response.dto.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/guards/jwt-auth.guard.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/guards/kakao-auth.guard.ts (100%) rename services/api/app/src/api/public/users/{ => presentation}/interceptors/current-user.interceptor.spec.ts (96%) rename services/api/app/src/api/public/users/{ => presentation}/interceptors/current-user.interceptor.ts (84%) rename services/api/app/src/api/public/users/{ => presentation}/user-message.factory.ts (89%) rename services/api/app/src/api/public/users/{ => presentation}/users.controller.ts (81%) diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.spec.ts similarity index 95% rename from services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts rename to services/api/app/src/api/public/cafeterias/application/cafeterias.service.spec.ts index 732c187..18875c2 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.spec.ts +++ b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.spec.ts @@ -1,10 +1,10 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; -import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; -import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; -import { CafeteriasService } from './cafeterias.service'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/domain/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/infrastructure/cafeterias.repository'; +import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; const makeCafeteria = (overrides: Partial = {}): Cafeteria => ({ diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts similarity index 89% rename from services/api/app/src/api/public/cafeterias/cafeterias.service.ts rename to services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts index 2cf4e8f..4078922 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.service.ts +++ b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts @@ -1,14 +1,14 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; -import { DietTime } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; +import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; import { CafeteriaDietItemResult, CafeteriaDietResult, CafeteriaMenuGroupResult, -} from 'src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto'; -import { CafeteriaListResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto'; -import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; +} from 'src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto'; +import { CafeteriaListResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/infrastructure/cafeterias.repository'; @Injectable() export class CafeteriasService { diff --git a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts similarity index 61% rename from services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts rename to services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts index 148f673..3f7403c 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto.ts +++ b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts @@ -1,5 +1,5 @@ -import { DietTime } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { CafeteriaItemResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto'; +import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { CafeteriaItemResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; export interface CafeteriaDietItemResult { dishCategory: string | null; diff --git a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto.ts b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto.ts similarity index 65% rename from services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto.ts rename to services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto.ts index 964650a..b5050d4 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto.ts +++ b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto.ts @@ -1,4 +1,4 @@ -import { CampusItemResult } from 'src/api/public/campuses/dtos/results/campus-list-result.dto'; +import { CampusItemResult } from 'src/api/public/campuses/application/dtos/results/campus-list-result.dto'; export interface CafeteriaItemResult { id: number; diff --git a/services/api/app/src/api/public/cafeterias/utils/time.spec.ts b/services/api/app/src/api/public/cafeterias/application/utils/time.spec.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/utils/time.spec.ts rename to services/api/app/src/api/public/cafeterias/application/utils/time.spec.ts diff --git a/services/api/app/src/api/public/cafeterias/utils/time.ts b/services/api/app/src/api/public/cafeterias/application/utils/time.ts similarity index 94% rename from services/api/app/src/api/public/cafeterias/utils/time.ts rename to services/api/app/src/api/public/cafeterias/application/utils/time.ts index 7ca4d91..4d7ef87 100644 --- a/services/api/app/src/api/public/cafeterias/utils/time.ts +++ b/services/api/app/src/api/public/cafeterias/application/utils/time.ts @@ -1,4 +1,4 @@ -import { DietDate } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; +import { DietDate } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; /** 한국은 연중 UTC+9 (DST 없음). Intl 없이 서울 벽시계 시·분을 쓸 때 사용 */ const KST_OFFSET_MS = 9 * 60 * 60 * 1000; diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts index a8c67e7..4ec98e6 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts @@ -1,14 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CafeteriaMessageFactory } from 'src/api/public/cafeterias/cafeteria-message.factory'; -import { CafeteriasRepository } from 'src/api/public/cafeterias/cafeterias.repository'; -import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; -import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; +import { CafeteriaMessageFactory } from 'src/api/public/cafeterias/presentation/cafeteria-message.factory'; +import { CafeteriasRepository } from 'src/api/public/cafeterias/infrastructure/cafeterias.repository'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/domain/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; import { CampusesModule } from 'src/api/public/campuses/campuses.module'; -import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.factory'; -import { CafeteriasNativeController } from './cafeterias-native.controller'; -import { CafeteriasController } from './cafeterias.controller'; -import { CafeteriasService } from './cafeterias.service'; +import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; +import { CafeteriasNativeController } from 'src/api/public/cafeterias/presentation/cafeterias-native.controller'; +import { CafeteriasController } from 'src/api/public/cafeterias/presentation/cafeterias.controller'; +import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; @Module({ imports: [CampusesModule, TypeOrmModule.forFeature([Cafeteria, CafeteriaDiet])], diff --git a/services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts similarity index 87% rename from services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts rename to services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts index 0e0c681..a0cd889 100644 --- a/services/api/app/src/api/public/cafeterias/entities/cafeteria-diet.entity.ts +++ b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts @@ -1,4 +1,4 @@ -import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; @Entity('cafeteria_diet') diff --git a/services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts similarity index 76% rename from services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts rename to services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts index 33d5859..525b309 100644 --- a/services/api/app/src/api/public/cafeterias/entities/cafeteria.entity.ts +++ b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts @@ -1,5 +1,5 @@ -import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; -import { Campus } from 'src/api/public/campuses/entities/campus.entity'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/domain/entities/cafeteria-diet.entity'; +import { Campus } from 'src/api/public/campuses/domain/entities/campus.entity'; import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; @Entity('cafeteria') diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.repository.ts b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts similarity index 84% rename from services/api/app/src/api/public/cafeterias/cafeterias.repository.ts rename to services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts index fe9d6b3..35153da 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.repository.ts +++ b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DietTime } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { CafeteriaDiet } from 'src/api/public/cafeterias/entities/cafeteria-diet.entity'; -import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; +import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { CafeteriaDiet } from 'src/api/public/cafeterias/domain/entities/cafeteria-diet.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts b/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts similarity index 89% rename from services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts rename to services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts index 2036223..1fa178c 100644 --- a/services/api/app/src/api/public/cafeterias/cafeteria-message.factory.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts @@ -7,13 +7,13 @@ import { BlockId } from 'src/api/common/utils/constants'; import { DietDate, DietTime, -} from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { CafeteriaDietResult } from 'src/api/public/cafeterias/dtos/results/cafeteria-diet-result.dto'; +} from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { CafeteriaDietResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto'; import { CafeteriaItemResult, CafeteriaListResult, -} from 'src/api/public/cafeterias/dtos/results/cafeteria-list-result.dto'; -import { getDayWeek } from 'src/api/public/cafeterias/utils/time'; +} from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; +import { getDayWeek } from 'src/api/public/cafeterias/application/utils/time'; @Injectable() export class CafeteriaMessageFactory { diff --git a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts b/services/api/app/src/api/public/cafeterias/presentation/cafeterias-native.controller.ts similarity index 92% rename from services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts rename to services/api/app/src/api/public/cafeterias/presentation/cafeterias-native.controller.ts index b149cf9..5051f03 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias-native.controller.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/cafeterias-native.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get, Param, ParseIntPipe, Query, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { NativeResponseDto } from 'src/api/common/dtos/native-response.dto'; -import { JwtAuthGuard } from 'src/api/public/users/guards/jwt-auth.guard'; +import { JwtAuthGuard } from 'src/api/public/users/presentation/guards/jwt-auth.guard'; import { GetCafeteriaDietQueryDto } from './dtos/requests/get-cafeteria-diet-query.dto'; import { GetCafeteriasQueryDto } from './dtos/requests/get-cafeterias-query.dto'; import { CafeteriaDietResponseDto } from './dtos/responses/cafeteria-diet-response.dto'; import { CafeteriaResponseDto } from './dtos/responses/cafeteria-response.dto'; -import { CafeteriasService } from './cafeterias.service'; +import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; @ApiTags('cafeterias') @Controller('cafeterias') diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts b/services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts similarity index 75% rename from services/api/app/src/api/public/cafeterias/cafeterias.controller.ts rename to services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts index 2215936..5cd2c50 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.controller.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts @@ -4,18 +4,18 @@ import { ApiTags } from '@nestjs/swagger'; import { ApiSkillBody } from 'src/api/common/decorators/api-skill-body.decorator'; import { ClientExtra } from 'src/api/common/decorators/skill-extra.decorator'; import { ResponseDTO } from 'src/api/common/dtos/response.dto'; -import { ListCafeteriaDietExtraRequestDto } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto'; -import { ListCafeteriaRequestDto } from 'src/api/public/cafeterias/dtos/requests/list-cafeteria-request.dto'; -import { CafeteriaMessageFactory } from 'src/api/public/cafeterias/cafeteria-message.factory'; -import { CampusesService } from 'src/api/public/campuses/campuses.service'; -import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.factory'; -import { CurrentUser } from 'src/api/public/users/decorators/current-user.decorator'; -import { FetchCurrentUser } from 'src/api/public/users/decorators/fetch-current-user.decorator'; +import { ListCafeteriaDietExtraRequestDto } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { ListCafeteriaRequestDto } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-request.dto'; +import { CafeteriaMessageFactory } from 'src/api/public/cafeterias/presentation/cafeteria-message.factory'; +import { CampusesService } from 'src/api/public/campuses/application/campuses.service'; +import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; +import { CurrentUser } from 'src/api/public/users/presentation/decorators/current-user.decorator'; +import { FetchCurrentUser } from 'src/api/public/users/presentation/decorators/fetch-current-user.decorator'; import { BlockId } from 'src/api/common/utils/constants'; -import { User } from 'src/api/public/users/entities/users.entity'; -import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; -import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/utils/time'; -import { CafeteriasService } from './cafeterias.service'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/application/utils/time'; +import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; @ApiTags('cafeterias') @Controller('cafeterias') diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeteria-diet-query.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeterias-query.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeterias-query.dto.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/dtos/requests/get-cafeterias-query.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeterias-query.dto.ts diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-diet-request.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts diff --git a/services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-request.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/dtos/requests/list-cafeteria-request.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-request.dto.ts diff --git a/services/api/app/src/api/public/cafeterias/dtos/responses/cafeteria-diet-response.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/responses/cafeteria-diet-response.dto.ts similarity index 100% rename from services/api/app/src/api/public/cafeterias/dtos/responses/cafeteria-diet-response.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/responses/cafeteria-diet-response.dto.ts diff --git a/services/api/app/src/api/public/cafeterias/dtos/responses/cafeteria-response.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/responses/cafeteria-response.dto.ts similarity index 80% rename from services/api/app/src/api/public/cafeterias/dtos/responses/cafeteria-response.dto.ts rename to services/api/app/src/api/public/cafeterias/presentation/dtos/responses/cafeteria-response.dto.ts index 290eb6d..8d3b3b1 100644 --- a/services/api/app/src/api/public/cafeterias/dtos/responses/cafeteria-response.dto.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/dtos/responses/cafeteria-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { CampusResponseDto } from 'src/api/public/campuses/dtos/campus-response.dto'; +import { CampusResponseDto } from 'src/api/public/campuses/presentation/dtos/campus-response.dto'; export class CafeteriaResponseDto { @ApiProperty({ description: '식당 ID' }) diff --git a/services/api/app/src/api/public/campuses/campuses.service.ts b/services/api/app/src/api/public/campuses/application/campuses.service.ts similarity index 79% rename from services/api/app/src/api/public/campuses/campuses.service.ts rename to services/api/app/src/api/public/campuses/application/campuses.service.ts index f635197..d3f855d 100644 --- a/services/api/app/src/api/public/campuses/campuses.service.ts +++ b/services/api/app/src/api/public/campuses/application/campuses.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { CampusListResult } from 'src/api/public/campuses/dtos/results/campus-list-result.dto'; -import { CampusesRepository } from 'src/api/public/campuses/campuses.repository'; +import { CampusListResult } from 'src/api/public/campuses/application/dtos/results/campus-list-result.dto'; +import { CampusesRepository } from 'src/api/public/campuses/infrastructure/campuses.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/api/public/campuses/dtos/results/campus-list-result.dto.ts b/services/api/app/src/api/public/campuses/application/dtos/results/campus-list-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/campuses/dtos/results/campus-list-result.dto.ts rename to services/api/app/src/api/public/campuses/application/dtos/results/campus-list-result.dto.ts diff --git a/services/api/app/src/api/public/campuses/campuses.module.ts b/services/api/app/src/api/public/campuses/campuses.module.ts index d1eb525..7a49c8c 100644 --- a/services/api/app/src/api/public/campuses/campuses.module.ts +++ b/services/api/app/src/api/public/campuses/campuses.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Campus } from 'src/api/public/campuses/entities/campus.entity'; -import { CampusesRepository } from 'src/api/public/campuses/campuses.repository'; -import { CampusesNativeController } from './campuses-native.controller'; -import { CampusesService } from './campuses.service'; +import { Campus } from 'src/api/public/campuses/domain/entities/campus.entity'; +import { CampusesRepository } from 'src/api/public/campuses/infrastructure/campuses.repository'; +import { CampusesNativeController } from 'src/api/public/campuses/presentation/campuses-native.controller'; +import { CampusesService } from 'src/api/public/campuses/application/campuses.service'; @Module({ imports: [TypeOrmModule.forFeature([Campus])], diff --git a/services/api/app/src/api/public/campuses/entities/campus.entity.ts b/services/api/app/src/api/public/campuses/domain/entities/campus.entity.ts similarity index 72% rename from services/api/app/src/api/public/campuses/entities/campus.entity.ts rename to services/api/app/src/api/public/campuses/domain/entities/campus.entity.ts index 2b35870..aacbb32 100644 --- a/services/api/app/src/api/public/campuses/entities/campus.entity.ts +++ b/services/api/app/src/api/public/campuses/domain/entities/campus.entity.ts @@ -1,6 +1,6 @@ -import { Cafeteria } from 'src/api/public/cafeterias/entities/cafeteria.entity'; +import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { User } from 'src/api/public/users/entities/users.entity'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; @Entity('campus') export class Campus { diff --git a/services/api/app/src/api/public/campuses/campuses.repository.ts b/services/api/app/src/api/public/campuses/infrastructure/campuses.repository.ts similarity index 89% rename from services/api/app/src/api/public/campuses/campuses.repository.ts rename to services/api/app/src/api/public/campuses/infrastructure/campuses.repository.ts index 6eaac0b..a369f23 100644 --- a/services/api/app/src/api/public/campuses/campuses.repository.ts +++ b/services/api/app/src/api/public/campuses/infrastructure/campuses.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Campus } from 'src/api/public/campuses/entities/campus.entity'; +import { Campus } from 'src/api/public/campuses/domain/entities/campus.entity'; import { In, Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/campuses/campus-message.factory.ts b/services/api/app/src/api/public/campuses/presentation/campus-message.factory.ts similarity index 89% rename from services/api/app/src/api/public/campuses/campus-message.factory.ts rename to services/api/app/src/api/public/campuses/presentation/campus-message.factory.ts index 05f69e0..6ba89b8 100644 --- a/services/api/app/src/api/public/campuses/campus-message.factory.ts +++ b/services/api/app/src/api/public/campuses/presentation/campus-message.factory.ts @@ -3,7 +3,7 @@ import { ListCard } from 'src/api/common/interfaces/response/fields/component'; import { ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard } from 'src/api/common/utils/component'; -import { CampusListResult } from 'src/api/public/campuses/dtos/results/campus-list-result.dto'; +import { CampusListResult } from 'src/api/public/campuses/application/dtos/results/campus-list-result.dto'; @Injectable() export class CampusMessageFactory { diff --git a/services/api/app/src/api/public/campuses/campuses-native.controller.ts b/services/api/app/src/api/public/campuses/presentation/campuses-native.controller.ts similarity index 82% rename from services/api/app/src/api/public/campuses/campuses-native.controller.ts rename to services/api/app/src/api/public/campuses/presentation/campuses-native.controller.ts index 187ae2e..922e8a2 100644 --- a/services/api/app/src/api/public/campuses/campuses-native.controller.ts +++ b/services/api/app/src/api/public/campuses/presentation/campuses-native.controller.ts @@ -1,9 +1,9 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { NativeResponseDto } from 'src/api/common/dtos/native-response.dto'; -import { JwtAuthGuard } from 'src/api/public/users/guards/jwt-auth.guard'; +import { JwtAuthGuard } from 'src/api/public/users/presentation/guards/jwt-auth.guard'; import { CampusResponseDto } from './dtos/campus-response.dto'; -import { CampusesService } from './campuses.service'; +import { CampusesService } from 'src/api/public/campuses/application/campuses.service'; @ApiTags('campuses') @Controller('campuses') diff --git a/services/api/app/src/api/public/campuses/dtos/campus-response.dto.ts b/services/api/app/src/api/public/campuses/presentation/dtos/campus-response.dto.ts similarity index 100% rename from services/api/app/src/api/public/campuses/dtos/campus-response.dto.ts rename to services/api/app/src/api/public/campuses/presentation/dtos/campus-response.dto.ts diff --git a/services/api/app/src/api/public/colleges/colleges.service.ts b/services/api/app/src/api/public/colleges/application/colleges.service.ts similarity index 80% rename from services/api/app/src/api/public/colleges/colleges.service.ts rename to services/api/app/src/api/public/colleges/application/colleges.service.ts index 822c802..f20488a 100644 --- a/services/api/app/src/api/public/colleges/colleges.service.ts +++ b/services/api/app/src/api/public/colleges/application/colleges.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { CollegeListResult } from 'src/api/public/colleges/dtos/results/college-list-result.dto'; -import { CollegesRepository } from 'src/api/public/colleges/colleges.repository'; +import { CollegeListResult } from 'src/api/public/colleges/application/dtos/results/college-list-result.dto'; +import { CollegesRepository } from 'src/api/public/colleges/infrastructure/colleges.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/api/public/colleges/dtos/results/college-list-result.dto.ts b/services/api/app/src/api/public/colleges/application/dtos/results/college-list-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/colleges/dtos/results/college-list-result.dto.ts rename to services/api/app/src/api/public/colleges/application/dtos/results/college-list-result.dto.ts diff --git a/services/api/app/src/api/public/colleges/colleges.module.ts b/services/api/app/src/api/public/colleges/colleges.module.ts index f27bdc8..f7e9db4 100644 --- a/services/api/app/src/api/public/colleges/colleges.module.ts +++ b/services/api/app/src/api/public/colleges/colleges.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { College } from 'src/api/public/colleges/entities/college.entity'; -import { CollegesRepository } from 'src/api/public/colleges/colleges.repository'; -import { CollegesService } from './colleges.service'; +import { College } from 'src/api/public/colleges/domain/entities/college.entity'; +import { CollegesRepository } from 'src/api/public/colleges/infrastructure/colleges.repository'; +import { CollegesService } from 'src/api/public/colleges/application/colleges.service'; @Module({ imports: [TypeOrmModule.forFeature([College])], diff --git a/services/api/app/src/api/public/colleges/entities/college.entity.ts b/services/api/app/src/api/public/colleges/domain/entities/college.entity.ts similarity index 83% rename from services/api/app/src/api/public/colleges/entities/college.entity.ts rename to services/api/app/src/api/public/colleges/domain/entities/college.entity.ts index fb55793..c72d570 100644 --- a/services/api/app/src/api/public/colleges/entities/college.entity.ts +++ b/services/api/app/src/api/public/colleges/domain/entities/college.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { Department } from 'src/api/public/departments/entities/department.entity'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; @Entity('college') export class College { diff --git a/services/api/app/src/api/public/colleges/colleges.repository.ts b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts similarity index 89% rename from services/api/app/src/api/public/colleges/colleges.repository.ts rename to services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts index d9102fe..d92fff2 100644 --- a/services/api/app/src/api/public/colleges/colleges.repository.ts +++ b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ListCardConfig } from 'src/api/common/utils/constants'; import { Repository } from 'typeorm'; -import { College } from './entities/college.entity'; +import { College } from 'src/api/public/colleges/domain/entities/college.entity'; @Injectable() export class CollegesRepository { diff --git a/services/api/app/src/api/public/colleges/college-message.factory.ts b/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts similarity index 94% rename from services/api/app/src/api/public/colleges/college-message.factory.ts rename to services/api/app/src/api/public/colleges/presentation/college-message.factory.ts index f710b19..a677d61 100644 --- a/services/api/app/src/api/public/colleges/college-message.factory.ts +++ b/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts @@ -4,7 +4,7 @@ import { ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard } from 'src/api/common/utils/component'; import { BlockId, ListCardConfig } from 'src/api/common/utils/constants'; -import { CollegeListResult } from 'src/api/public/colleges/dtos/results/college-list-result.dto'; +import { CollegeListResult } from 'src/api/public/colleges/application/dtos/results/college-list-result.dto'; @Injectable() export class CollegeMessageFactory { diff --git a/services/api/app/src/api/public/common/common-message.factory.ts b/services/api/app/src/api/public/common/presentation/common-message.factory.ts similarity index 100% rename from services/api/app/src/api/public/common/common-message.factory.ts rename to services/api/app/src/api/public/common/presentation/common-message.factory.ts diff --git a/services/api/app/src/api/public/departments/departments.service.ts b/services/api/app/src/api/public/departments/application/departments.service.ts similarity index 91% rename from services/api/app/src/api/public/departments/departments.service.ts rename to services/api/app/src/api/public/departments/application/departments.service.ts index 814a32e..a6b1298 100644 --- a/services/api/app/src/api/public/departments/departments.service.ts +++ b/services/api/app/src/api/public/departments/application/departments.service.ts @@ -1,7 +1,7 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DepartmentListResult } from 'src/api/public/departments/dtos/results/department-list-result.dto'; -import { DepartmentsRepository } from 'src/api/public/departments/departments.repository'; +import { DepartmentListResult } from 'src/api/public/departments/application/dtos/results/department-list-result.dto'; +import { DepartmentsRepository } from 'src/api/public/departments/infrastructure/departments.repository'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; @Injectable() diff --git a/services/api/app/src/api/public/departments/dtos/results/department-list-result.dto.ts b/services/api/app/src/api/public/departments/application/dtos/results/department-list-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/departments/dtos/results/department-list-result.dto.ts rename to services/api/app/src/api/public/departments/application/dtos/results/department-list-result.dto.ts diff --git a/services/api/app/src/api/public/departments/departments.module.ts b/services/api/app/src/api/public/departments/departments.module.ts index 6322525..9c3a4e8 100644 --- a/services/api/app/src/api/public/departments/departments.module.ts +++ b/services/api/app/src/api/public/departments/departments.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Department } from 'src/api/public/departments/entities/department.entity'; -import { DepartmentsRepository } from 'src/api/public/departments/departments.repository'; -import { DepartmentsService } from './departments.service'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; +import { DepartmentsRepository } from 'src/api/public/departments/infrastructure/departments.repository'; +import { DepartmentsService } from 'src/api/public/departments/application/departments.service'; @Module({ imports: [TypeOrmModule.forFeature([Department])], diff --git a/services/api/app/src/api/public/departments/entities/department.entity.ts b/services/api/app/src/api/public/departments/domain/entities/department.entity.ts similarity index 81% rename from services/api/app/src/api/public/departments/entities/department.entity.ts rename to services/api/app/src/api/public/departments/domain/entities/department.entity.ts index 8560beb..2c7551c 100644 --- a/services/api/app/src/api/public/departments/entities/department.entity.ts +++ b/services/api/app/src/api/public/departments/domain/entities/department.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { College } from 'src/api/public/colleges/entities/college.entity'; -import { User } from 'src/api/public/users/entities/users.entity'; +import { College } from 'src/api/public/colleges/domain/entities/college.entity'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; @Entity('department') export class Department { diff --git a/services/api/app/src/api/public/departments/departments.repository.ts b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts similarity index 89% rename from services/api/app/src/api/public/departments/departments.repository.ts rename to services/api/app/src/api/public/departments/infrastructure/departments.repository.ts index 33cead3..a8a8010 100644 --- a/services/api/app/src/api/public/departments/departments.repository.ts +++ b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ListCardConfig } from 'src/api/common/utils/constants'; -import { Department } from 'src/api/public/departments/entities/department.entity'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/departments/department-message.factory.ts b/services/api/app/src/api/public/departments/presentation/department-message.factory.ts similarity index 94% rename from services/api/app/src/api/public/departments/department-message.factory.ts rename to services/api/app/src/api/public/departments/presentation/department-message.factory.ts index ed294bd..a0ab847 100644 --- a/services/api/app/src/api/public/departments/department-message.factory.ts +++ b/services/api/app/src/api/public/departments/presentation/department-message.factory.ts @@ -4,8 +4,8 @@ import { ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard } from 'src/api/common/utils/component'; import { BlockId, ListCardConfig } from 'src/api/common/utils/constants'; -import { DepartmentListResult } from 'src/api/public/departments/dtos/results/department-list-result.dto'; -import { ListDepartmentsRequestDto } from 'src/api/public/users/dtos/requests/list-department-request.dto'; +import { DepartmentListResult } from 'src/api/public/departments/application/dtos/results/department-list-result.dto'; +import { ListDepartmentsRequestDto } from 'src/api/public/users/presentation/dtos/requests/list-department-request.dto'; @Injectable() export class DepartmentMessageFactory { diff --git a/services/api/app/src/api/public/notices/dtos/results/notice-list-result.dto.ts b/services/api/app/src/api/public/notices/application/dtos/results/notice-list-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/notices/dtos/results/notice-list-result.dto.ts rename to services/api/app/src/api/public/notices/application/dtos/results/notice-list-result.dto.ts diff --git a/services/api/app/src/api/public/notices/notices.service.spec.ts b/services/api/app/src/api/public/notices/application/notices.service.spec.ts similarity index 94% rename from services/api/app/src/api/public/notices/notices.service.spec.ts rename to services/api/app/src/api/public/notices/application/notices.service.spec.ts index 435ac01..69c54e2 100644 --- a/services/api/app/src/api/public/notices/notices.service.spec.ts +++ b/services/api/app/src/api/public/notices/application/notices.service.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; -import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; -import { Notice } from 'src/api/public/notices/entities/notice.entity'; -import { NoticesRepository } from 'src/api/public/notices/notices.repository'; -import { User } from 'src/api/public/users/entities/users.entity'; -import { NoticesService } from './notices.service'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/infrastructure/notice-categories.repository'; +import { NoticeCategory } from 'src/api/public/notices/domain/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/domain/entities/notice.entity'; +import { NoticesRepository } from 'src/api/public/notices/infrastructure/notices.repository'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; +import { NoticesService } from 'src/api/public/notices/application/notices.service'; const makeCategory = (overrides: Partial = {}): NoticeCategory => ({ diff --git a/services/api/app/src/api/public/notices/notices.service.ts b/services/api/app/src/api/public/notices/application/notices.service.ts similarity index 87% rename from services/api/app/src/api/public/notices/notices.service.ts rename to services/api/app/src/api/public/notices/application/notices.service.ts index 5ca86af..5fe5faa 100644 --- a/services/api/app/src/api/public/notices/notices.service.ts +++ b/services/api/app/src/api/public/notices/application/notices.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common'; import { NoticeCategoryResult, NoticeListResult, -} from 'src/api/public/notices/dtos/results/notice-list-result.dto'; -import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; -import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; -import { Notice } from 'src/api/public/notices/entities/notice.entity'; -import { NoticesRepository } from 'src/api/public/notices/notices.repository'; -import { User } from 'src/api/public/users/entities/users.entity'; +} from 'src/api/public/notices/application/dtos/results/notice-list-result.dto'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/infrastructure/notice-categories.repository'; +import { NoticeCategory } from 'src/api/public/notices/domain/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/domain/entities/notice.entity'; +import { NoticesRepository } from 'src/api/public/notices/infrastructure/notices.repository'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; @Injectable() export class NoticesService { diff --git a/services/api/app/src/api/public/notices/entities/notice-category.entity.ts b/services/api/app/src/api/public/notices/domain/entities/notice-category.entity.ts similarity index 89% rename from services/api/app/src/api/public/notices/entities/notice-category.entity.ts rename to services/api/app/src/api/public/notices/domain/entities/notice-category.entity.ts index 07b17ca..38cac33 100644 --- a/services/api/app/src/api/public/notices/entities/notice-category.entity.ts +++ b/services/api/app/src/api/public/notices/domain/entities/notice-category.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { Department } from 'src/api/public/departments/entities/department.entity'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; import { Notice } from './notice.entity'; @Entity('notice_category') diff --git a/services/api/app/src/api/public/notices/entities/notice.entity.ts b/services/api/app/src/api/public/notices/domain/entities/notice.entity.ts similarity index 100% rename from services/api/app/src/api/public/notices/entities/notice.entity.ts rename to services/api/app/src/api/public/notices/domain/entities/notice.entity.ts diff --git a/services/api/app/src/api/public/notices/notice-categories.repository.ts b/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts similarity index 96% rename from services/api/app/src/api/public/notices/notice-categories.repository.ts rename to services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts index 6d3cf45..cda35ea 100644 --- a/services/api/app/src/api/public/notices/notice-categories.repository.ts +++ b/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; +import { NoticeCategory } from 'src/api/public/notices/domain/entities/notice-category.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/notices/notices.repository.ts b/services/api/app/src/api/public/notices/infrastructure/notices.repository.ts similarity index 96% rename from services/api/app/src/api/public/notices/notices.repository.ts rename to services/api/app/src/api/public/notices/infrastructure/notices.repository.ts index cdd47d9..1fe29f6 100644 --- a/services/api/app/src/api/public/notices/notices.repository.ts +++ b/services/api/app/src/api/public/notices/infrastructure/notices.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Notice } from 'src/api/public/notices/entities/notice.entity'; +import { Notice } from 'src/api/public/notices/domain/entities/notice.entity'; import { Repository } from 'typeorm'; @Injectable() diff --git a/services/api/app/src/api/public/notices/notices.module.ts b/services/api/app/src/api/public/notices/notices.module.ts index 7229661..0fcdcea 100644 --- a/services/api/app/src/api/public/notices/notices.module.ts +++ b/services/api/app/src/api/public/notices/notices.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { NoticeCategory } from 'src/api/public/notices/entities/notice-category.entity'; -import { Notice } from 'src/api/public/notices/entities/notice.entity'; -import { NoticeCategoriesRepository } from 'src/api/public/notices/notice-categories.repository'; -import { NoticeMessageFactory } from 'src/api/public/notices/notice-message.factory'; -import { NoticesRepository } from 'src/api/public/notices/notices.repository'; -import { NoticesController } from './notices.controller'; -import { NoticesService } from './notices.service'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { NoticeCategory } from 'src/api/public/notices/domain/entities/notice-category.entity'; +import { Notice } from 'src/api/public/notices/domain/entities/notice.entity'; +import { NoticeCategoriesRepository } from 'src/api/public/notices/infrastructure/notice-categories.repository'; +import { NoticeMessageFactory } from 'src/api/public/notices/presentation/notice-message.factory'; +import { NoticesRepository } from 'src/api/public/notices/infrastructure/notices.repository'; +import { NoticesController } from 'src/api/public/notices/presentation/notices.controller'; +import { NoticesService } from 'src/api/public/notices/application/notices.service'; @Module({ imports: [TypeOrmModule.forFeature([Notice, NoticeCategory])], diff --git a/services/api/app/src/api/public/notices/dtos/requests/list-department-notice-request.dto.ts b/services/api/app/src/api/public/notices/presentation/dtos/requests/list-department-notice-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/notices/dtos/requests/list-department-notice-request.dto.ts rename to services/api/app/src/api/public/notices/presentation/dtos/requests/list-department-notice-request.dto.ts diff --git a/services/api/app/src/api/public/notices/dtos/requests/list-university-notice-request.dto.ts b/services/api/app/src/api/public/notices/presentation/dtos/requests/list-university-notice-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/notices/dtos/requests/list-university-notice-request.dto.ts rename to services/api/app/src/api/public/notices/presentation/dtos/requests/list-university-notice-request.dto.ts diff --git a/services/api/app/src/api/public/notices/notice-message.factory.spec.ts b/services/api/app/src/api/public/notices/presentation/notice-message.factory.spec.ts similarity index 97% rename from services/api/app/src/api/public/notices/notice-message.factory.spec.ts rename to services/api/app/src/api/public/notices/presentation/notice-message.factory.spec.ts index 2bc9c4c..39e4b7f 100644 --- a/services/api/app/src/api/public/notices/notice-message.factory.spec.ts +++ b/services/api/app/src/api/public/notices/presentation/notice-message.factory.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { NoticeMessageFactory } from 'src/api/public/notices/notice-message.factory'; +import { NoticeMessageFactory } from 'src/api/public/notices/presentation/notice-message.factory'; import { NoticeCategoryResult, NoticeItemResult, NoticeListResult, -} from 'src/api/public/notices/dtos/results/notice-list-result.dto'; +} from 'src/api/public/notices/application/dtos/results/notice-list-result.dto'; const makeCategory = (overrides: Partial = {}): NoticeCategoryResult => ({ category: '학사', diff --git a/services/api/app/src/api/public/notices/notice-message.factory.ts b/services/api/app/src/api/public/notices/presentation/notice-message.factory.ts similarity index 98% rename from services/api/app/src/api/public/notices/notice-message.factory.ts rename to services/api/app/src/api/public/notices/presentation/notice-message.factory.ts index 9d6d6c5..e665cd2 100644 --- a/services/api/app/src/api/public/notices/notice-message.factory.ts +++ b/services/api/app/src/api/public/notices/presentation/notice-message.factory.ts @@ -5,7 +5,7 @@ import { SkillTemplate } from 'src/api/common/interfaces/response/fields/templat import { NoticeCategoryResult, NoticeListResult, -} from 'src/api/public/notices/dtos/results/notice-list-result.dto'; +} from 'src/api/public/notices/application/dtos/results/notice-list-result.dto'; @Injectable() export class NoticeMessageFactory { diff --git a/services/api/app/src/api/public/notices/notices.controller.ts b/services/api/app/src/api/public/notices/presentation/notices.controller.ts similarity index 76% rename from services/api/app/src/api/public/notices/notices.controller.ts rename to services/api/app/src/api/public/notices/presentation/notices.controller.ts index 7b9f6ec..820ea72 100644 --- a/services/api/app/src/api/public/notices/notices.controller.ts +++ b/services/api/app/src/api/public/notices/presentation/notices.controller.ts @@ -3,15 +3,15 @@ import { ApiTags } from '@nestjs/swagger'; import { ApiSkillBody } from 'src/api/common/decorators/api-skill-body.decorator'; import { ResponseDTO } from 'src/api/common/dtos/response.dto'; import { OpenBuilderExceptionFilter } from 'src/api/common/filters/open-builder-exception.filter'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { NoticeMessageFactory } from 'src/api/public/notices/notice-message.factory'; -import { CurrentUser } from 'src/api/public/users/decorators/current-user.decorator'; -import { FetchCurrentUser } from 'src/api/public/users/decorators/fetch-current-user.decorator'; -import { User } from 'src/api/public/users/entities/users.entity'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { NoticeMessageFactory } from 'src/api/public/notices/presentation/notice-message.factory'; +import { CurrentUser } from 'src/api/public/users/presentation/decorators/current-user.decorator'; +import { FetchCurrentUser } from 'src/api/public/users/presentation/decorators/fetch-current-user.decorator'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; import { ListDepartmentNoticeRequestDto } from './dtos/requests/list-department-notice-request.dto'; import { ListUniversityNoticeRequestDto } from './dtos/requests/list-university-notice-request.dto'; -import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; -import { NoticesService } from './notices.service'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { NoticesService } from 'src/api/public/notices/application/notices.service'; @ApiTags('notices') @Controller('notices') diff --git a/services/api/app/src/api/public/schedules/dtos/results/academic-schedule-result.dto.ts b/services/api/app/src/api/public/schedules/application/dtos/results/academic-schedule-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/schedules/dtos/results/academic-schedule-result.dto.ts rename to services/api/app/src/api/public/schedules/application/dtos/results/academic-schedule-result.dto.ts diff --git a/services/api/app/src/api/public/schedules/schedules.service.ts b/services/api/app/src/api/public/schedules/application/schedules.service.ts similarity index 90% rename from services/api/app/src/api/public/schedules/schedules.service.ts rename to services/api/app/src/api/public/schedules/application/schedules.service.ts index 7dc12bb..2e43703 100644 --- a/services/api/app/src/api/public/schedules/schedules.service.ts +++ b/services/api/app/src/api/public/schedules/application/schedules.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { AcademicScheduleResult } from 'src/api/public/schedules/dtos/results/academic-schedule-result.dto'; -import { AcademicCalendarsRepository } from 'src/api/public/schedules/academic-calendars.repository'; +import { AcademicScheduleResult } from 'src/api/public/schedules/application/dtos/results/academic-schedule-result.dto'; +import { AcademicCalendarsRepository } from 'src/api/public/schedules/infrastructure/academic-calendars.repository'; @Injectable() export class SchedulesService { diff --git a/services/api/app/src/api/public/schedules/entities/academic-calendar.entity.ts b/services/api/app/src/api/public/schedules/domain/entities/academic-calendar.entity.ts similarity index 100% rename from services/api/app/src/api/public/schedules/entities/academic-calendar.entity.ts rename to services/api/app/src/api/public/schedules/domain/entities/academic-calendar.entity.ts diff --git a/services/api/app/src/api/public/schedules/academic-calendars.repository.ts b/services/api/app/src/api/public/schedules/infrastructure/academic-calendars.repository.ts similarity index 94% rename from services/api/app/src/api/public/schedules/academic-calendars.repository.ts rename to services/api/app/src/api/public/schedules/infrastructure/academic-calendars.repository.ts index e9fb8e5..a48ad27 100644 --- a/services/api/app/src/api/public/schedules/academic-calendars.repository.ts +++ b/services/api/app/src/api/public/schedules/infrastructure/academic-calendars.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { AcademicCalendar } from './entities/academic-calendar.entity'; +import { AcademicCalendar } from 'src/api/public/schedules/domain/entities/academic-calendar.entity'; @Injectable() export class AcademicCalendarsRepository { diff --git a/services/api/app/src/api/public/schedules/dtos/requests/list-academic-schedule-request.dto.ts b/services/api/app/src/api/public/schedules/presentation/dtos/requests/list-academic-schedule-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/schedules/dtos/requests/list-academic-schedule-request.dto.ts rename to services/api/app/src/api/public/schedules/presentation/dtos/requests/list-academic-schedule-request.dto.ts diff --git a/services/api/app/src/api/public/schedules/schedule-message.factory.ts b/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts similarity index 98% rename from services/api/app/src/api/public/schedules/schedule-message.factory.ts rename to services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts index ebde184..f9a9922 100644 --- a/services/api/app/src/api/public/schedules/schedule-message.factory.ts +++ b/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts @@ -4,7 +4,7 @@ import { Button, QuickReply } from 'src/api/common/interfaces/response/fields/et import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createTextCard } from 'src/api/common/utils/component'; import { BlockId } from 'src/api/common/utils/constants'; -import { AcademicScheduleResult } from 'src/api/public/schedules/dtos/results/academic-schedule-result.dto'; +import { AcademicScheduleResult } from 'src/api/public/schedules/application/dtos/results/academic-schedule-result.dto'; @Injectable() export class ScheduleMessageFactory { diff --git a/services/api/app/src/api/public/schedules/schedules.controller.ts b/services/api/app/src/api/public/schedules/presentation/schedules.controller.ts similarity index 83% rename from services/api/app/src/api/public/schedules/schedules.controller.ts rename to services/api/app/src/api/public/schedules/presentation/schedules.controller.ts index 251376e..a61a4bf 100644 --- a/services/api/app/src/api/public/schedules/schedules.controller.ts +++ b/services/api/app/src/api/public/schedules/presentation/schedules.controller.ts @@ -4,11 +4,11 @@ import { ApiSkillBody } from 'src/api/common/decorators/api-skill-body.decorator import { ClientExtra } from 'src/api/common/decorators/skill-extra.decorator'; import { ResponseDTO } from 'src/api/common/dtos/response.dto'; import { OpenBuilderExceptionFilter } from 'src/api/common/filters/open-builder-exception.filter'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { ScheduleMessageFactory } from 'src/api/public/schedules/schedule-message.factory'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { ScheduleMessageFactory } from 'src/api/public/schedules/presentation/schedule-message.factory'; import { ListAcademicScheduleExtraDto } from './dtos/requests/list-academic-schedule-request.dto'; -import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; -import { SchedulesService } from './schedules.service'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { SchedulesService } from 'src/api/public/schedules/application/schedules.service'; @ApiTags('schedules') @Controller('schedules') diff --git a/services/api/app/src/api/public/schedules/schedules.module.ts b/services/api/app/src/api/public/schedules/schedules.module.ts index 1732400..380ee6a 100644 --- a/services/api/app/src/api/public/schedules/schedules.module.ts +++ b/services/api/app/src/api/public/schedules/schedules.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { AcademicCalendarsRepository } from 'src/api/public/schedules/academic-calendars.repository'; -import { AcademicCalendar } from 'src/api/public/schedules/entities/academic-calendar.entity'; -import { ScheduleMessageFactory } from 'src/api/public/schedules/schedule-message.factory'; -import { SchedulesController } from 'src/api/public/schedules/schedules.controller'; -import { SchedulesService } from './schedules.service'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { AcademicCalendarsRepository } from 'src/api/public/schedules/infrastructure/academic-calendars.repository'; +import { AcademicCalendar } from 'src/api/public/schedules/domain/entities/academic-calendar.entity'; +import { ScheduleMessageFactory } from 'src/api/public/schedules/presentation/schedule-message.factory'; +import { SchedulesController } from 'src/api/public/schedules/presentation/schedules.controller'; +import { SchedulesService } from 'src/api/public/schedules/application/schedules.service'; @Module({ imports: [TypeOrmModule.forFeature([AcademicCalendar])], diff --git a/services/api/app/src/api/public/shuttles/dtos/results/shuttle-route-list-result.dto.ts b/services/api/app/src/api/public/shuttles/application/dtos/results/shuttle-route-list-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/dtos/results/shuttle-route-list-result.dto.ts rename to services/api/app/src/api/public/shuttles/application/dtos/results/shuttle-route-list-result.dto.ts diff --git a/services/api/app/src/api/public/shuttles/dtos/results/shuttle-timetable-result.dto.ts b/services/api/app/src/api/public/shuttles/application/dtos/results/shuttle-timetable-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/dtos/results/shuttle-timetable-result.dto.ts rename to services/api/app/src/api/public/shuttles/application/dtos/results/shuttle-timetable-result.dto.ts diff --git a/services/api/app/src/api/public/shuttles/shuttle-timetable.calculator.spec.ts b/services/api/app/src/api/public/shuttles/application/shuttle-timetable.calculator.spec.ts similarity index 94% rename from services/api/app/src/api/public/shuttles/shuttle-timetable.calculator.spec.ts rename to services/api/app/src/api/public/shuttles/application/shuttle-timetable.calculator.spec.ts index 29c071b..d4a6a7e 100644 --- a/services/api/app/src/api/public/shuttles/shuttle-timetable.calculator.spec.ts +++ b/services/api/app/src/api/public/shuttles/application/shuttle-timetable.calculator.spec.ts @@ -1,4 +1,4 @@ -import { ShuttleTimetableCalculator } from './shuttle-timetable.calculator'; +import { ShuttleTimetableCalculator } from 'src/api/public/shuttles/application/shuttle-timetable.calculator'; describe('ShuttleTimetableCalculator', () => { const calculator = new ShuttleTimetableCalculator(); diff --git a/services/api/app/src/api/public/shuttles/shuttle-timetable.calculator.ts b/services/api/app/src/api/public/shuttles/application/shuttle-timetable.calculator.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/shuttle-timetable.calculator.ts rename to services/api/app/src/api/public/shuttles/application/shuttle-timetable.calculator.ts diff --git a/services/api/app/src/api/public/shuttles/shuttles.service.ts b/services/api/app/src/api/public/shuttles/application/shuttles.service.ts similarity index 87% rename from services/api/app/src/api/public/shuttles/shuttles.service.ts rename to services/api/app/src/api/public/shuttles/application/shuttles.service.ts index 37ede15..4a37374 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.service.ts +++ b/services/api/app/src/api/public/shuttles/application/shuttles.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { ShuttleRouteListResult } from 'src/api/public/shuttles/dtos/results/shuttle-route-list-result.dto'; -import { ShuttleTimetableResult } from 'src/api/public/shuttles/dtos/results/shuttle-timetable-result.dto'; -import { ShuttleTimetableRepository } from 'src/api/public/shuttles/shuttle-timetable.repository'; +import { ShuttleRouteListResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-route-list-result.dto'; +import { ShuttleTimetableResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-timetable-result.dto'; +import { ShuttleTimetableRepository } from 'src/api/public/shuttles/infrastructure/shuttle-timetable.repository'; @Injectable() export class ShuttlesService { diff --git a/services/api/app/src/api/public/shuttles/entities/shuttle-timetable.entity.ts b/services/api/app/src/api/public/shuttles/domain/entities/shuttle-timetable.entity.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/entities/shuttle-timetable.entity.ts rename to services/api/app/src/api/public/shuttles/domain/entities/shuttle-timetable.entity.ts diff --git a/services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts b/services/api/app/src/api/public/shuttles/infrastructure/shuttle-timetable.repository.ts similarity index 85% rename from services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts rename to services/api/app/src/api/public/shuttles/infrastructure/shuttle-timetable.repository.ts index c05a1aa..7c0b82c 100644 --- a/services/api/app/src/api/public/shuttles/shuttle-timetable.repository.ts +++ b/services/api/app/src/api/public/shuttles/infrastructure/shuttle-timetable.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ShuttleTimetable } from './entities/shuttle-timetable.entity'; +import { ShuttleTimetable } from 'src/api/public/shuttles/domain/entities/shuttle-timetable.entity'; @Injectable() export class ShuttleTimetableRepository { diff --git a/services/api/app/src/api/public/shuttles/dtos/request/get-shuttle-timetable-request.dto.ts b/services/api/app/src/api/public/shuttles/presentation/dtos/request/get-shuttle-timetable-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/dtos/request/get-shuttle-timetable-request.dto.ts rename to services/api/app/src/api/public/shuttles/presentation/dtos/request/get-shuttle-timetable-request.dto.ts diff --git a/services/api/app/src/api/public/shuttles/dtos/responses/shuttle-route-response.dto.ts b/services/api/app/src/api/public/shuttles/presentation/dtos/responses/shuttle-route-response.dto.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/dtos/responses/shuttle-route-response.dto.ts rename to services/api/app/src/api/public/shuttles/presentation/dtos/responses/shuttle-route-response.dto.ts diff --git a/services/api/app/src/api/public/shuttles/dtos/responses/shuttle-timetable-response.dto.ts b/services/api/app/src/api/public/shuttles/presentation/dtos/responses/shuttle-timetable-response.dto.ts similarity index 100% rename from services/api/app/src/api/public/shuttles/dtos/responses/shuttle-timetable-response.dto.ts rename to services/api/app/src/api/public/shuttles/presentation/dtos/responses/shuttle-timetable-response.dto.ts diff --git a/services/api/app/src/api/public/shuttles/shuttle-message.factory.ts b/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts similarity index 95% rename from services/api/app/src/api/public/shuttles/shuttle-message.factory.ts rename to services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts index e855516..0357c1a 100644 --- a/services/api/app/src/api/public/shuttles/shuttle-message.factory.ts +++ b/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts @@ -3,8 +3,8 @@ import { Button, ListItem } from 'src/api/common/interfaces/response/fields/etc' import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard, createTextCard } from 'src/api/common/utils/component'; import { BlockId } from 'src/api/common/utils/constants'; -import { ShuttleRouteListResult } from 'src/api/public/shuttles/dtos/results/shuttle-route-list-result.dto'; -import { ShuttleTimetableResult } from 'src/api/public/shuttles/dtos/results/shuttle-timetable-result.dto'; +import { ShuttleRouteListResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-route-list-result.dto'; +import { ShuttleTimetableResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-timetable-result.dto'; @Injectable() export class ShuttleMessageFactory { diff --git a/services/api/app/src/api/public/shuttles/shuttles-native.controller.ts b/services/api/app/src/api/public/shuttles/presentation/shuttles-native.controller.ts similarity index 86% rename from services/api/app/src/api/public/shuttles/shuttles-native.controller.ts rename to services/api/app/src/api/public/shuttles/presentation/shuttles-native.controller.ts index 7adf7b4..83c05d0 100644 --- a/services/api/app/src/api/public/shuttles/shuttles-native.controller.ts +++ b/services/api/app/src/api/public/shuttles/presentation/shuttles-native.controller.ts @@ -2,11 +2,11 @@ import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { NativeResponseDto } from 'src/api/common/dtos/native-response.dto'; import { formatLocalDateDotSeparated } from 'src/api/common/utils/date-format'; -import { JwtAuthGuard } from 'src/api/public/users/guards/jwt-auth.guard'; +import { JwtAuthGuard } from 'src/api/public/users/presentation/guards/jwt-auth.guard'; import { ShuttleRouteResponseDto } from './dtos/responses/shuttle-route-response.dto'; import { ShuttleTimetableResponseDto } from './dtos/responses/shuttle-timetable-response.dto'; -import { ShuttleTimetableCalculator } from './shuttle-timetable.calculator'; -import { ShuttlesService } from './shuttles.service'; +import { ShuttleTimetableCalculator } from 'src/api/public/shuttles/application/shuttle-timetable.calculator'; +import { ShuttlesService } from 'src/api/public/shuttles/application/shuttles.service'; @ApiTags('shuttles') @Controller('shuttles') diff --git a/services/api/app/src/api/public/shuttles/shuttles.controller.ts b/services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts similarity index 84% rename from services/api/app/src/api/public/shuttles/shuttles.controller.ts rename to services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts index b936a75..4bc08fb 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.controller.ts +++ b/services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts @@ -5,9 +5,9 @@ import { ClientExtra } from 'src/api/common/decorators/skill-extra.decorator'; import { ResponseDTO } from 'src/api/common/dtos/response.dto'; import { OpenBuilderExceptionFilter } from 'src/api/common/filters/open-builder-exception.filter'; import { GetShuttleTimetableRequestDto } from './dtos/request/get-shuttle-timetable-request.dto'; -import { ShuttleMessageFactory } from './shuttle-message.factory'; -import { KakaoAuthGuard } from 'src/api/public/users/guards/kakao-auth.guard'; -import { ShuttlesService } from './shuttles.service'; +import { ShuttleMessageFactory } from 'src/api/public/shuttles/presentation/shuttle-message.factory'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { ShuttlesService } from 'src/api/public/shuttles/application/shuttles.service'; @ApiTags('shuttles') @Controller('shuttles') diff --git a/services/api/app/src/api/public/shuttles/shuttles.module.ts b/services/api/app/src/api/public/shuttles/shuttles.module.ts index 04f66af..1b93ec3 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.module.ts +++ b/services/api/app/src/api/public/shuttles/shuttles.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ShuttleTimetable } from 'src/api/public/shuttles/entities/shuttle-timetable.entity'; -import { ShuttleTimetableRepository } from 'src/api/public/shuttles/shuttle-timetable.repository'; -import { ShuttleMessageFactory } from './shuttle-message.factory'; -import { ShuttleTimetableCalculator } from './shuttle-timetable.calculator'; -import { ShuttlesController } from './shuttles.controller'; -import { ShuttlesNativeController } from './shuttles-native.controller'; -import { ShuttlesService } from './shuttles.service'; +import { ShuttleTimetable } from 'src/api/public/shuttles/domain/entities/shuttle-timetable.entity'; +import { ShuttleTimetableRepository } from 'src/api/public/shuttles/infrastructure/shuttle-timetable.repository'; +import { ShuttleMessageFactory } from 'src/api/public/shuttles/presentation/shuttle-message.factory'; +import { ShuttleTimetableCalculator } from 'src/api/public/shuttles/application/shuttle-timetable.calculator'; +import { ShuttlesController } from 'src/api/public/shuttles/presentation/shuttles.controller'; +import { ShuttlesNativeController } from 'src/api/public/shuttles/presentation/shuttles-native.controller'; +import { ShuttlesService } from 'src/api/public/shuttles/application/shuttles.service'; @Module({ imports: [TypeOrmModule.forFeature([ShuttleTimetable])], diff --git a/services/api/app/src/api/public/users/dtos/results/upsert-department-result.dto.ts b/services/api/app/src/api/public/users/application/dtos/results/upsert-department-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/results/upsert-department-result.dto.ts rename to services/api/app/src/api/public/users/application/dtos/results/upsert-department-result.dto.ts diff --git a/services/api/app/src/api/public/users/dtos/results/user-profile-result.dto.ts b/services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/results/user-profile-result.dto.ts rename to services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts diff --git a/services/api/app/src/api/public/users/users.service.spec.ts b/services/api/app/src/api/public/users/application/users.service.spec.ts similarity index 89% rename from services/api/app/src/api/public/users/users.service.spec.ts rename to services/api/app/src/api/public/users/application/users.service.spec.ts index 8e06c51..431972a 100644 --- a/services/api/app/src/api/public/users/users.service.spec.ts +++ b/services/api/app/src/api/public/users/application/users.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from './users.service'; -import { User } from 'src/api/public/users/entities/users.entity'; -import { UsersRepository } from 'src/api/public/users/users.repository'; +import { UsersService } from 'src/api/public/users/application/users.service'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/infrastructure/users.repository'; const makeUser = (overrides: Partial = {}): User => ({ diff --git a/services/api/app/src/api/public/users/users.service.ts b/services/api/app/src/api/public/users/application/users.service.ts similarity index 72% rename from services/api/app/src/api/public/users/users.service.ts rename to services/api/app/src/api/public/users/application/users.service.ts index 7a04b73..d690025 100644 --- a/services/api/app/src/api/public/users/users.service.ts +++ b/services/api/app/src/api/public/users/application/users.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { UpsertDepartmentResult } from 'src/api/public/users/dtos/results/upsert-department-result.dto'; -import { UserProfileResult } from 'src/api/public/users/dtos/results/user-profile-result.dto'; -import { User } from 'src/api/public/users/entities/users.entity'; -import { UsersRepository } from 'src/api/public/users/users.repository'; +import { UpsertDepartmentResult } from 'src/api/public/users/application/dtos/results/upsert-department-result.dto'; +import { UserProfileResult } from 'src/api/public/users/application/dtos/results/user-profile-result.dto'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/infrastructure/users.repository'; @Injectable() export class UsersService { diff --git a/services/api/app/src/api/public/users/entities/users.entity.ts b/services/api/app/src/api/public/users/domain/entities/users.entity.ts similarity index 69% rename from services/api/app/src/api/public/users/entities/users.entity.ts rename to services/api/app/src/api/public/users/domain/entities/users.entity.ts index df5f794..ab57d8a 100644 --- a/services/api/app/src/api/public/users/entities/users.entity.ts +++ b/services/api/app/src/api/public/users/domain/entities/users.entity.ts @@ -1,6 +1,6 @@ import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { Campus } from 'src/api/public/campuses/entities/campus.entity'; -import { Department } from 'src/api/public/departments/entities/department.entity'; +import { Campus } from 'src/api/public/campuses/domain/entities/campus.entity'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; @Entity('kakao-user') export class User { diff --git a/services/api/app/src/api/public/users/users.repository.ts b/services/api/app/src/api/public/users/infrastructure/users.repository.ts similarity index 92% rename from services/api/app/src/api/public/users/users.repository.ts rename to services/api/app/src/api/public/users/infrastructure/users.repository.ts index 5dee638..b8c7cdc 100644 --- a/services/api/app/src/api/public/users/users.repository.ts +++ b/services/api/app/src/api/public/users/infrastructure/users.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { User } from './entities/users.entity'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; @Injectable() export class UsersRepository { diff --git a/services/api/app/src/api/public/users/decorators/current-user.decorator.ts b/services/api/app/src/api/public/users/presentation/decorators/current-user.decorator.ts similarity index 100% rename from services/api/app/src/api/public/users/decorators/current-user.decorator.ts rename to services/api/app/src/api/public/users/presentation/decorators/current-user.decorator.ts diff --git a/services/api/app/src/api/public/users/decorators/fetch-current-user.decorator.ts b/services/api/app/src/api/public/users/presentation/decorators/fetch-current-user.decorator.ts similarity index 100% rename from services/api/app/src/api/public/users/decorators/fetch-current-user.decorator.ts rename to services/api/app/src/api/public/users/presentation/decorators/fetch-current-user.decorator.ts diff --git a/services/api/app/src/api/public/users/dtos/requests/list-college-request.dto.ts b/services/api/app/src/api/public/users/presentation/dtos/requests/list-college-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/requests/list-college-request.dto.ts rename to services/api/app/src/api/public/users/presentation/dtos/requests/list-college-request.dto.ts diff --git a/services/api/app/src/api/public/users/dtos/requests/list-department-request.dto.ts b/services/api/app/src/api/public/users/presentation/dtos/requests/list-department-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/requests/list-department-request.dto.ts rename to services/api/app/src/api/public/users/presentation/dtos/requests/list-department-request.dto.ts diff --git a/services/api/app/src/api/public/users/dtos/requests/upsert-department-request.dto.ts b/services/api/app/src/api/public/users/presentation/dtos/requests/upsert-department-request.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/requests/upsert-department-request.dto.ts rename to services/api/app/src/api/public/users/presentation/dtos/requests/upsert-department-request.dto.ts diff --git a/services/api/app/src/api/public/users/dtos/responses/profile-response.dto.ts b/services/api/app/src/api/public/users/presentation/dtos/responses/profile-response.dto.ts similarity index 100% rename from services/api/app/src/api/public/users/dtos/responses/profile-response.dto.ts rename to services/api/app/src/api/public/users/presentation/dtos/responses/profile-response.dto.ts diff --git a/services/api/app/src/api/public/users/guards/jwt-auth.guard.ts b/services/api/app/src/api/public/users/presentation/guards/jwt-auth.guard.ts similarity index 100% rename from services/api/app/src/api/public/users/guards/jwt-auth.guard.ts rename to services/api/app/src/api/public/users/presentation/guards/jwt-auth.guard.ts diff --git a/services/api/app/src/api/public/users/guards/kakao-auth.guard.ts b/services/api/app/src/api/public/users/presentation/guards/kakao-auth.guard.ts similarity index 100% rename from services/api/app/src/api/public/users/guards/kakao-auth.guard.ts rename to services/api/app/src/api/public/users/presentation/guards/kakao-auth.guard.ts diff --git a/services/api/app/src/api/public/users/interceptors/current-user.interceptor.spec.ts b/services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.spec.ts similarity index 96% rename from services/api/app/src/api/public/users/interceptors/current-user.interceptor.spec.ts rename to services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.spec.ts index 1bc2e05..a3ca8f6 100644 --- a/services/api/app/src/api/public/users/interceptors/current-user.interceptor.spec.ts +++ b/services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.spec.ts @@ -1,8 +1,8 @@ import { ExecutionContext, CallHandler } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { of } from 'rxjs'; -import { FETCH_CURRENT_USER_KEY } from 'src/api/public/users/decorators/fetch-current-user.decorator'; -import { UsersService } from 'src/api/public/users/users.service'; +import { FETCH_CURRENT_USER_KEY } from 'src/api/public/users/presentation/decorators/fetch-current-user.decorator'; +import { UsersService } from 'src/api/public/users/application/users.service'; import { CurrentUserInterceptor } from './current-user.interceptor'; const makeContext = (overrides: { diff --git a/services/api/app/src/api/public/users/interceptors/current-user.interceptor.ts b/services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.ts similarity index 84% rename from services/api/app/src/api/public/users/interceptors/current-user.interceptor.ts rename to services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.ts index 24c0f15..f44612b 100644 --- a/services/api/app/src/api/public/users/interceptors/current-user.interceptor.ts +++ b/services/api/app/src/api/public/users/presentation/interceptors/current-user.interceptor.ts @@ -1,7 +1,7 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { FETCH_CURRENT_USER_KEY } from 'src/api/public/users/decorators/fetch-current-user.decorator'; -import { UsersService } from 'src/api/public/users/users.service'; +import { FETCH_CURRENT_USER_KEY } from 'src/api/public/users/presentation/decorators/fetch-current-user.decorator'; +import { UsersService } from 'src/api/public/users/application/users.service'; @Injectable() export class CurrentUserInterceptor implements NestInterceptor { diff --git a/services/api/app/src/api/public/users/user-message.factory.ts b/services/api/app/src/api/public/users/presentation/user-message.factory.ts similarity index 89% rename from services/api/app/src/api/public/users/user-message.factory.ts rename to services/api/app/src/api/public/users/presentation/user-message.factory.ts index c488397..c42b01b 100644 --- a/services/api/app/src/api/public/users/user-message.factory.ts +++ b/services/api/app/src/api/public/users/presentation/user-message.factory.ts @@ -4,7 +4,7 @@ import { Button } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createTextCard } from 'src/api/common/utils/component'; import { BlockId } from 'src/api/common/utils/constants'; -import { UserProfileResult } from 'src/api/public/users/dtos/results/user-profile-result.dto'; +import { UserProfileResult } from 'src/api/public/users/application/dtos/results/user-profile-result.dto'; @Injectable() export class UserMessageFactory { diff --git a/services/api/app/src/api/public/users/users.controller.ts b/services/api/app/src/api/public/users/presentation/users.controller.ts similarity index 81% rename from services/api/app/src/api/public/users/users.controller.ts rename to services/api/app/src/api/public/users/presentation/users.controller.ts index 9ef79ba..7e8f9bd 100644 --- a/services/api/app/src/api/public/users/users.controller.ts +++ b/services/api/app/src/api/public/users/presentation/users.controller.ts @@ -5,22 +5,22 @@ import { ClientExtra } from 'src/api/common/decorators/skill-extra.decorator'; import { ResponseDTO } from 'src/api/common/dtos/response.dto'; import { OpenBuilderExceptionFilter } from 'src/api/common/filters/open-builder-exception.filter'; import { BlockId } from 'src/api/common/utils/constants'; -import { CampusesService } from 'src/api/public/campuses/campuses.service'; -import { CollegesService } from 'src/api/public/colleges/colleges.service'; -import { DepartmentsService } from 'src/api/public/departments/departments.service'; -import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.factory'; -import { CollegeMessageFactory } from 'src/api/public/colleges/college-message.factory'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { DepartmentMessageFactory } from 'src/api/public/departments/department-message.factory'; -import { UserMessageFactory } from 'src/api/public/users/user-message.factory'; -import { User } from 'src/api/public/users/entities/users.entity'; +import { CampusesService } from 'src/api/public/campuses/application/campuses.service'; +import { CollegesService } from 'src/api/public/colleges/application/colleges.service'; +import { DepartmentsService } from 'src/api/public/departments/application/departments.service'; +import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; +import { CollegeMessageFactory } from 'src/api/public/colleges/presentation/college-message.factory'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { DepartmentMessageFactory } from 'src/api/public/departments/presentation/department-message.factory'; +import { UserMessageFactory } from 'src/api/public/users/presentation/user-message.factory'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; import { CurrentUser } from './decorators/current-user.decorator'; import { FetchCurrentUser } from './decorators/fetch-current-user.decorator'; import { ListCollegesRequestDto } from './dtos/requests/list-college-request.dto'; import { ListDepartmentsRequestDto } from './dtos/requests/list-department-request.dto'; import { UpsertDepartmentRequestDto } from './dtos/requests/upsert-department-request.dto'; import { KakaoAuthGuard } from './guards/kakao-auth.guard'; -import { UsersService } from './users.service'; +import { UsersService } from 'src/api/public/users/application/users.service'; @ApiTags('users') @Controller('users') diff --git a/services/api/app/src/api/public/users/users.module.ts b/services/api/app/src/api/public/users/users.module.ts index 788e24e..c9560ed 100644 --- a/services/api/app/src/api/public/users/users.module.ts +++ b/services/api/app/src/api/public/users/users.module.ts @@ -4,16 +4,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CampusesModule } from 'src/api/public/campuses/campuses.module'; import { CollegesModule } from 'src/api/public/colleges/colleges.module'; import { DepartmentsModule } from 'src/api/public/departments/departments.module'; -import { CurrentUserInterceptor } from 'src/api/public/users/interceptors/current-user.interceptor'; -import { User } from 'src/api/public/users/entities/users.entity'; -import { UsersRepository } from 'src/api/public/users/users.repository'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; -import { UserMessageFactory } from 'src/api/public/users/user-message.factory'; -import { CommonMessageFactory } from 'src/api/public/common/common-message.factory'; -import { CampusMessageFactory } from 'src/api/public/campuses/campus-message.factory'; -import { CollegeMessageFactory } from 'src/api/public/colleges/college-message.factory'; -import { DepartmentMessageFactory } from 'src/api/public/departments/department-message.factory'; +import { CurrentUserInterceptor } from 'src/api/public/users/presentation/interceptors/current-user.interceptor'; +import { User } from 'src/api/public/users/domain/entities/users.entity'; +import { UsersRepository } from 'src/api/public/users/infrastructure/users.repository'; +import { UsersController } from 'src/api/public/users/presentation/users.controller'; +import { UsersService } from 'src/api/public/users/application/users.service'; +import { UserMessageFactory } from 'src/api/public/users/presentation/user-message.factory'; +import { CommonMessageFactory } from 'src/api/public/common/presentation/common-message.factory'; +import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; +import { CollegeMessageFactory } from 'src/api/public/colleges/presentation/college-message.factory'; +import { DepartmentMessageFactory } from 'src/api/public/departments/presentation/department-message.factory'; @Module({ imports: [TypeOrmModule.forFeature([User]), CampusesModule, CollegesModule, DepartmentsModule], diff --git a/services/api/app/src/infrastructure/type-orm/database.module.ts b/services/api/app/src/infrastructure/type-orm/database.module.ts index 02f0431..5ef681e 100644 --- a/services/api/app/src/infrastructure/type-orm/database.module.ts +++ b/services/api/app/src/infrastructure/type-orm/database.module.ts @@ -15,7 +15,7 @@ import * as path from 'path'; password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), synchronize: false, - entities: [path.join(__dirname, '../**/*.entity{.ts,.js}')], + entities: [path.join(__dirname, '../../**/*.entity{.ts,.js}')], logging: configService.get('NODE_ENV') === 'production' ? ['error', 'warn'] : 'all', logger: 'advanced-console', maxQueryExecutionTime: 1000, // 롱 쿼리 로그 출력 시간 diff --git a/services/api/app/test/cafeterias.e2e-spec.ts b/services/api/app/test/cafeterias.e2e-spec.ts index 44190ca..ffba139 100644 --- a/services/api/app/test/cafeterias.e2e-spec.ts +++ b/services/api/app/test/cafeterias.e2e-spec.ts @@ -2,11 +2,11 @@ import { ForbiddenException, INestApplication, Module, ValidationPipe } from '@n import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; -import { CafeteriasController } from 'src/api/public/cafeterias/cafeterias.controller'; -import { CafeteriasService } from 'src/api/public/cafeterias/cafeterias.service'; -import { AuthGuard } from 'src/api/public/users/guards/auth.guard'; -import { CurrentUserInterceptor } from 'src/api/public/users/interceptors/current-user.interceptor'; -import { UsersService } from 'src/api/public/users/users.service'; +import { CafeteriasController } from 'src/api/public/cafeterias/presentation/cafeterias.controller'; +import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { CurrentUserInterceptor } from 'src/api/public/users/presentation/interceptors/current-user.interceptor'; +import { UsersService } from 'src/api/public/users/application/users.service'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; const CAMPUS_LIST_TEMPLATE: SkillTemplate = { @@ -37,7 +37,7 @@ const mockUsersService = { providers: [ { provide: CafeteriasService, useValue: mockCafeteriasService }, { provide: UsersService, useValue: mockUsersService }, - { provide: APP_GUARD, useClass: AuthGuard }, + { provide: APP_GUARD, useClass: KakaoAuthGuard }, { provide: APP_INTERCEPTOR, useClass: CurrentUserInterceptor }, ], }) diff --git a/services/api/app/test/notices.e2e-spec.ts b/services/api/app/test/notices.e2e-spec.ts index f6c1e8e..e73b740 100644 --- a/services/api/app/test/notices.e2e-spec.ts +++ b/services/api/app/test/notices.e2e-spec.ts @@ -2,11 +2,11 @@ import { INestApplication, Module, ValidationPipe } from '@nestjs/common'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; -import { NoticesController } from 'src/api/public/notices/notices.controller'; -import { NoticesService } from 'src/api/public/notices/notices.service'; -import { AuthGuard } from 'src/api/public/users/guards/auth.guard'; -import { CurrentUserInterceptor } from 'src/api/public/users/interceptors/current-user.interceptor'; -import { UsersService } from 'src/api/public/users/users.service'; +import { NoticesController } from 'src/api/public/notices/presentation/notices.controller'; +import { NoticesService } from 'src/api/public/notices/application/notices.service'; +import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; +import { CurrentUserInterceptor } from 'src/api/public/users/presentation/interceptors/current-user.interceptor'; +import { UsersService } from 'src/api/public/users/application/users.service'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; const AUTH_REQUIRED_TEMPLATE: SkillTemplate = { @@ -35,7 +35,7 @@ const mockUsersService = { providers: [ { provide: NoticesService, useValue: mockNoticesService }, { provide: UsersService, useValue: mockUsersService }, - { provide: APP_GUARD, useClass: AuthGuard }, + { provide: APP_GUARD, useClass: KakaoAuthGuard }, { provide: APP_INTERCEPTOR, useClass: CurrentUserInterceptor }, ], }) From 0ceb4e70f769c3d5d9079f4a0668b0c1119c351a Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 11 Jun 2026 21:22:30 +0900 Subject: [PATCH 11/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20TypeORM?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/entities/cafeteria-diet.entity.ts | 3 ++ .../domain/entities/cafeteria.entity.ts | 3 ++ .../infrastructure/cafeterias.repository.ts | 38 ++++++++++++------- .../infrastructure/colleges.repository.ts | 21 ++++++---- .../application/departments.service.ts | 8 +++- .../domain/entities/department.entity.ts | 3 ++ .../infrastructure/departments.repository.ts | 19 ++++++---- .../notice-categories.repository.ts | 13 ++++--- .../users/application/users.service.spec.ts | 18 ++++++--- .../public/users/application/users.service.ts | 15 +++++--- .../users/domain/entities/users.entity.ts | 14 ++++++- .../users/infrastructure/users.repository.ts | 28 +++++++------- 12 files changed, 121 insertions(+), 62 deletions(-) diff --git a/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts index a0cd889..2e14f7d 100644 --- a/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts +++ b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria-diet.entity.ts @@ -6,6 +6,9 @@ export class CafeteriaDiet { @PrimaryGeneratedColumn({ name: 'diet_id' }) id: number; + @Column({ name: 'cafeteria_id' }) + cafeteriaId: number; + @Column() date: Date; diff --git a/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts index 525b309..d4ed015 100644 --- a/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts +++ b/services/api/app/src/api/public/cafeterias/domain/entities/cafeteria.entity.ts @@ -7,6 +7,9 @@ export class Cafeteria { @PrimaryGeneratedColumn() id: number; + @Column({ name: 'campus_id' }) + campusId: number; + @Column({ name: 'cafeteria_name_ko' }) name: string; diff --git a/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts index 35153da..4b8a364 100644 --- a/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts +++ b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts @@ -14,7 +14,7 @@ export class CafeteriasRepository { private readonly cafeteriaDietRepository: Repository, ) {} - findCafeteriasByCampusId(campusId = 1): Promise { + findCafeteriasByCampusId(campusId: number): Promise { return this.cafeteriaRepository.find({ where: { campus: { @@ -24,16 +24,21 @@ export class CafeteriasRepository { order: { name: 'ASC', }, - relations: ['campus'], + relations: { + campus: true, + }, }); } findCafeteriaById(cafeteriaId: number): Promise { - return this.cafeteriaRepository - .createQueryBuilder('cafeteria') - .leftJoinAndSelect('cafeteria.campus', 'campus') - .where('cafeteria.id = :cafeteriaId', { cafeteriaId }) - .getOne(); + return this.cafeteriaRepository.findOne({ + where: { + id: cafeteriaId, + }, + relations: { + campus: true, + }, + }); } /** @@ -47,12 +52,17 @@ export class CafeteriasRepository { date: Date, time: DietTime, ): Promise { - return this.cafeteriaDietRepository - .createQueryBuilder('diet') - .select(['diet.dishCategory', 'diet.dishType', 'diet.dishName']) - .where('diet.date = :date', { date }) - .andWhere('diet.cafeteria_id = :cafeteriaId', { cafeteriaId }) - .andWhere('diet.time = :time', { time }) - .getMany(); + return this.cafeteriaDietRepository.find({ + select: { + dishCategory: true, + dishType: true, + dishName: true, + }, + where: { + date, + cafeteriaId, + time, + }, + }); } } diff --git a/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts index d92fff2..a0a1ec6 100644 --- a/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts +++ b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ListCardConfig } from 'src/api/common/utils/constants'; -import { Repository } from 'typeorm'; +import { Not, Repository } from 'typeorm'; import { College } from 'src/api/public/colleges/domain/entities/college.entity'; @Injectable() @@ -12,12 +12,17 @@ export class CollegesRepository { ) {} findAll(page: number): Promise<[College[], number]> { - return this.collegesRepository - .createQueryBuilder('college') - .where("college.name != '공통'") - .orderBy('college.name', 'ASC') - .take(ListCardConfig.LIMIT) - .skip(ListCardConfig.LIMIT * (page - 1)) - .getManyAndCount(); + const limit = ListCardConfig.LIMIT; + + return this.collegesRepository.findAndCount({ + where: { + name: Not('공통'), + }, + order: { + name: 'ASC', + }, + take: limit, + skip: limit * (page - 1), + }); } } diff --git a/services/api/app/src/api/public/departments/application/departments.service.ts b/services/api/app/src/api/public/departments/application/departments.service.ts index a6b1298..f026252 100644 --- a/services/api/app/src/api/public/departments/application/departments.service.ts +++ b/services/api/app/src/api/public/departments/application/departments.service.ts @@ -19,7 +19,13 @@ export class DepartmentsService { }, }) public async findAll(collegeId: number, page: number): Promise { - const [departments, total] = await this.departmentsRepository.findByCollegeId(collegeId, page); + const safePage = Math.max(page, 1); + + const [departments, total] = await this.departmentsRepository.findByCollegeId( + collegeId, + safePage, + ); + return { departments: departments.map(department => ({ id: department.id, diff --git a/services/api/app/src/api/public/departments/domain/entities/department.entity.ts b/services/api/app/src/api/public/departments/domain/entities/department.entity.ts index 2c7551c..b5c31e9 100644 --- a/services/api/app/src/api/public/departments/domain/entities/department.entity.ts +++ b/services/api/app/src/api/public/departments/domain/entities/department.entity.ts @@ -7,6 +7,9 @@ export class Department { @PrimaryGeneratedColumn() id: number; + @Column({ name: 'college_id' }) + collegeId: number; + @Column({ name: 'department_ko' }) name: string; diff --git a/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts index a8a8010..2926246 100644 --- a/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts +++ b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts @@ -12,14 +12,17 @@ export class DepartmentsRepository { ) {} findByCollegeId(collegeId: number, page: number): Promise<[Department[], number]> { - return this.departmentsRepository - .createQueryBuilder('department') - .where('department.college_id = :collegeId', { + const limit = ListCardConfig.LIMIT; + + return this.departmentsRepository.findAndCount({ + where: { collegeId, - }) - .orderBy('department.name', 'ASC') - .take(ListCardConfig.LIMIT) - .skip(ListCardConfig.LIMIT * (page - 1)) - .getManyAndCount(); + }, + order: { + name: 'ASC', + }, + take: limit, + skip: limit * (page - 1), + }); } } diff --git a/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts b/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts index cda35ea..b4eb5e1 100644 --- a/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts +++ b/services/api/app/src/api/public/notices/infrastructure/notice-categories.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { NoticeCategory } from 'src/api/public/notices/domain/entities/notice-category.entity'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; @Injectable() export class NoticeCategoriesRepository { @@ -54,11 +54,12 @@ export class NoticeCategoriesRepository { departmentId: number, categoryNames: string[], ): Promise { - return this.noticeCategoryRepository - .createQueryBuilder('category') - .where('category.department_id = :departmentId', { departmentId }) - .andWhere('category.category IN (:...categoryNames)', { categoryNames }) - .getMany(); + return this.noticeCategoryRepository.find({ + where: { + departmentId, + category: In(categoryNames), + }, + }); } /** diff --git a/services/api/app/src/api/public/users/application/users.service.spec.ts b/services/api/app/src/api/public/users/application/users.service.spec.ts index 431972a..6207249 100644 --- a/services/api/app/src/api/public/users/application/users.service.spec.ts +++ b/services/api/app/src/api/public/users/application/users.service.spec.ts @@ -6,6 +6,8 @@ import { UsersRepository } from 'src/api/public/users/infrastructure/users.repos const makeUser = (overrides: Partial = {}): User => ({ id: 'kakao-user-id', + campusId: 1, + departmentId: 10, campus: { id: 1, name: '가좌캠퍼스' } as any, department: { id: 10, @@ -61,16 +63,22 @@ describe('UsersService', () => { describe('upsert', () => { it('userId와 campusId, departmentId로 사용자 학과 정보를 저장한다', async () => { - const user = makeUser(); + const user = makeUser({ campusId: 2, departmentId: 20 }); usersRepository.save.mockResolvedValue(user); - const result = await service.upsert('kakao-user-id', 1, 10); + const result = await service.upsert('kakao-user-id', 2, 20); - expect(usersRepository.save).toHaveBeenCalledWith('kakao-user-id', 1, 10); + expect(usersRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'kakao-user-id', + campusId: 2, + departmentId: 20, + }), + ); expect(result).toEqual({ userId: user.id, - campusId: user.campus.id, - departmentId: user.department.id, + campusId: user.campusId, + departmentId: user.departmentId, }); }); }); diff --git a/services/api/app/src/api/public/users/application/users.service.ts b/services/api/app/src/api/public/users/application/users.service.ts index d690025..b4aa8a6 100644 --- a/services/api/app/src/api/public/users/application/users.service.ts +++ b/services/api/app/src/api/public/users/application/users.service.ts @@ -8,7 +8,7 @@ import { UsersRepository } from 'src/api/public/users/infrastructure/users.repos export class UsersService { constructor(private readonly usersRepository: UsersRepository) {} - public findOne(userId: string): Promise { + public findOne(userId: string): Promise { return this.usersRepository.findOne(userId); } @@ -32,11 +32,16 @@ export class UsersService { campusId: number, departmentId: number, ): Promise { - const user = await this.usersRepository.save(userId, campusId, departmentId); + const user = new User(); + + user.updateProfile(userId, campusId, departmentId); + + const userResult = await this.usersRepository.save(user); + return { - userId: user.id, - campusId: user.campus.id, - departmentId: user.department.id, + userId: userResult.id, + campusId: userResult.campusId, + departmentId: userResult.departmentId, }; } } diff --git a/services/api/app/src/api/public/users/domain/entities/users.entity.ts b/services/api/app/src/api/public/users/domain/entities/users.entity.ts index ab57d8a..4289ac4 100644 --- a/services/api/app/src/api/public/users/domain/entities/users.entity.ts +++ b/services/api/app/src/api/public/users/domain/entities/users.entity.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { Campus } from 'src/api/public/campuses/domain/entities/campus.entity'; import { Department } from 'src/api/public/departments/domain/entities/department.entity'; @@ -7,6 +7,12 @@ export class User { @PrimaryColumn() id: string; + @Column({ name: 'campus_id' }) + campusId: number; + + @Column({ name: 'department_id' }) + departmentId: number; + @ManyToOne(() => Campus, campus => campus.users) @JoinColumn({ name: 'campus_id' }) campus: Campus; @@ -14,4 +20,10 @@ export class User { @ManyToOne(() => Department, department => department.users) @JoinColumn({ name: 'department_id' }) department: Department; + + updateProfile(userId: string, campusId: number, departmentId: number) { + this.id = userId; + this.campusId = campusId; + this.departmentId = departmentId; + } } diff --git a/services/api/app/src/api/public/users/infrastructure/users.repository.ts b/services/api/app/src/api/public/users/infrastructure/users.repository.ts index b8c7cdc..83af68c 100644 --- a/services/api/app/src/api/public/users/infrastructure/users.repository.ts +++ b/services/api/app/src/api/public/users/infrastructure/users.repository.ts @@ -10,21 +10,21 @@ export class UsersRepository { private readonly usersRepository: Repository, ) {} - save(userId: string, campusId: number, departmentId: number): Promise { - return this.usersRepository.save({ - id: userId, - campus: { id: campusId }, - department: { id: departmentId }, - }); + save(user: Partial): Promise { + return this.usersRepository.save(user); } - findOne(userId: string): Promise { - return this.usersRepository - .createQueryBuilder('user') - .leftJoinAndSelect('user.campus', 'campus') - .leftJoinAndSelect('user.department', 'department') - .leftJoinAndSelect('department.college', 'college') - .where('user.id = :userId', { userId }) - .getOne(); + findOne(userId: string): Promise { + return this.usersRepository.findOne({ + where: { + id: userId, + }, + relations: { + campus: true, + department: { + college: true, + }, + }, + }); } } From 8623b63dbd90e720f70eb376c043ce41a6a4f6d9 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Fri, 12 Jun 2026 15:06:19 +0900 Subject: [PATCH 12/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=ED=91=9C=ED=98=84=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kakao.constants.ts} | 8 +- .../application/cafeterias.service.ts | 2 +- .../dtos/results/cafeteria-diet-result.dto.ts | 2 +- .../cafeterias/application/utils/time.ts | 7 +- .../public/cafeterias/cafeterias.module.ts | 4 +- .../infrastructure/cafeterias.repository.ts | 2 +- .../presentation/cafeteria-message.factory.ts | 21 ++--- ...ller.ts => cafeterias-kakao.controller.ts} | 14 +++- .../requests/get-cafeteria-diet-query.dto.ts | 2 +- .../list-cafeteria-diet-request.dto.ts | 4 +- .../application/colleges.service.spec.ts | 78 +++++++++++++++++++ .../colleges/application/colleges.service.ts | 8 +- .../infrastructure/colleges.repository.ts | 5 +- .../presentation/college-message.factory.ts | 11 ++- .../presentation/common-message.factory.ts | 4 +- .../application/departments.service.spec.ts | 76 ++++++++++++++++++ .../application/departments.service.ts | 14 +++- .../infrastructure/departments.repository.ts | 5 +- .../department-message.factory.ts | 11 ++- .../application/notices.service.spec.ts | 42 ++++++++++ .../notices/application/notices.service.ts | 15 ++-- .../src/api/public/notices/notices.module.ts | 4 +- ...troller.ts => notices-kakao.controller.ts} | 11 ++- .../presentation/schedule-message.factory.ts | 4 +- ...oller.ts => schedules-kakao.controller.ts} | 2 +- .../api/public/schedules/schedules.module.ts | 4 +- .../presentation/shuttle-message.factory.ts | 6 +- ...roller.ts => shuttles-kakao.controller.ts} | 2 +- .../api/public/shuttles/shuttles.module.ts | 4 +- .../dtos/results/user-profile-result.dto.ts | 20 ++++- .../users/application/users.service.spec.ts | 37 +++++++++ .../public/users/application/users.service.ts | 27 ++++--- .../presentation/user-message.factory.spec.ts | 45 +++++++++++ .../presentation/user-message.factory.ts | 11 ++- ...ontroller.ts => users-kakao.controller.ts} | 24 ++++-- .../app/src/api/public/users/users.module.ts | 4 +- services/api/app/test/cafeterias.e2e-spec.ts | 4 +- services/api/app/test/notices.e2e-spec.ts | 4 +- 38 files changed, 448 insertions(+), 100 deletions(-) rename services/api/app/src/api/common/{utils/constants.ts => presentation/kakao.constants.ts} (74%) rename services/api/app/src/api/public/cafeterias/presentation/{cafeterias.controller.ts => cafeterias-kakao.controller.ts} (91%) create mode 100644 services/api/app/src/api/public/colleges/application/colleges.service.spec.ts create mode 100644 services/api/app/src/api/public/departments/application/departments.service.spec.ts rename services/api/app/src/api/public/notices/presentation/{notices.controller.ts => notices-kakao.controller.ts} (90%) rename services/api/app/src/api/public/schedules/presentation/{schedules.controller.ts => schedules-kakao.controller.ts} (97%) rename services/api/app/src/api/public/shuttles/presentation/{shuttles.controller.ts => shuttles-kakao.controller.ts} (97%) create mode 100644 services/api/app/src/api/public/users/presentation/user-message.factory.spec.ts rename services/api/app/src/api/public/users/presentation/{users.controller.ts => users-kakao.controller.ts} (90%) diff --git a/services/api/app/src/api/common/utils/constants.ts b/services/api/app/src/api/common/presentation/kakao.constants.ts similarity index 74% rename from services/api/app/src/api/common/utils/constants.ts rename to services/api/app/src/api/common/presentation/kakao.constants.ts index d62f759..5d9399c 100644 --- a/services/api/app/src/api/common/utils/constants.ts +++ b/services/api/app/src/api/common/presentation/kakao.constants.ts @@ -1,4 +1,4 @@ -export const enum BlockId { +export const enum KakaoBlockId { // COMMON COLLEGE_LIST = '66cf226fa14fe55e3fc92498', DEPARTMENT_LIST = '66d05728e5715f75b2552b2c', @@ -21,6 +21,6 @@ export const enum BlockId { SHUTTLE_TIMETABLE = '69b2c0babb823869c1c7fae6', } -export const enum ListCardConfig { - LIMIT = 5, -} +export const KAKAO_LIST_CARD_ITEM_LIMIT = 5; +export const KAKAO_NOTICE_CAROUSEL_ITEM_LIMIT = 4; +export const KAKAO_REQUEST_CAMPUS_SELECTION_ID = -1; diff --git a/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts index 4078922..01cc65f 100644 --- a/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts +++ b/services/api/app/src/api/public/cafeterias/application/cafeterias.service.ts @@ -1,13 +1,13 @@ import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { CacheKey } from 'src/api/common/decorators/cache-key.decorator'; -import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; import { CafeteriaDietItemResult, CafeteriaDietResult, CafeteriaMenuGroupResult, } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto'; import { CafeteriaListResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; +import { DietTime } from 'src/api/public/cafeterias/application/utils/time'; import { CafeteriasRepository } from 'src/api/public/cafeterias/infrastructure/cafeterias.repository'; @Injectable() diff --git a/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts index 3f7403c..920c15e 100644 --- a/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts +++ b/services/api/app/src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto.ts @@ -1,4 +1,4 @@ -import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { DietTime } from 'src/api/public/cafeterias/application/utils/time'; import { CafeteriaItemResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; export interface CafeteriaDietItemResult { diff --git a/services/api/app/src/api/public/cafeterias/application/utils/time.ts b/services/api/app/src/api/public/cafeterias/application/utils/time.ts index 4d7ef87..2fbc534 100644 --- a/services/api/app/src/api/public/cafeterias/application/utils/time.ts +++ b/services/api/app/src/api/public/cafeterias/application/utils/time.ts @@ -1,4 +1,5 @@ -import { DietDate } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +export type RelativeDietDate = '오늘' | '내일'; +export type DietTime = '아침' | '점심' | '저녁'; /** 한국은 연중 UTC+9 (DST 없음). Intl 없이 서울 벽시계 시·분을 쓸 때 사용 */ const KST_OFFSET_MS = 9 * 60 * 60 * 1000; @@ -28,7 +29,7 @@ const getTomorrow = (): Date => { * - dietDate가 제공되면 해당 날짜 반환 * - 제공되지 않으면 서울 시간 기준 19시 전후로 오늘/내일 결정 */ -export const getTodayOrTomorrow = (dietDate?: DietDate): Date => { +export const getTodayOrTomorrow = (dietDate?: RelativeDietDate): Date => { if (dietDate) { return dietDate === '오늘' ? new Date() : getTomorrow(); } @@ -42,7 +43,7 @@ export const getTodayOrTomorrow = (dietDate?: DietDate): Date => { * - 서울 시간으로 변환 후 아침 시간 범위 내면 아침, 점심 시간 범위 내면 점심, 이후면 저녁 * - 19:00 ~ 09:29: 아침, 09:30 ~ 13:29: 점심, 13:30 ~ 23:59: 저녁 */ -export const getDietTime = (date: Date) => { +export const getDietTime = (date: Date): DietTime => { const { hours, minutes } = getSeoulHoursMinutes(date); const totalMinutes = hours * 60 + minutes; diff --git a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts index 4ec98e6..27afd35 100644 --- a/services/api/app/src/api/public/cafeterias/cafeterias.module.ts +++ b/services/api/app/src/api/public/cafeterias/cafeterias.module.ts @@ -7,12 +7,12 @@ import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.e import { CampusesModule } from 'src/api/public/campuses/campuses.module'; import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; import { CafeteriasNativeController } from 'src/api/public/cafeterias/presentation/cafeterias-native.controller'; -import { CafeteriasController } from 'src/api/public/cafeterias/presentation/cafeterias.controller'; +import { CafeteriasKakaoController } from 'src/api/public/cafeterias/presentation/cafeterias-kakao.controller'; import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeterias.service'; @Module({ imports: [CampusesModule, TypeOrmModule.forFeature([Cafeteria, CafeteriaDiet])], - controllers: [CafeteriasController, CafeteriasNativeController], + controllers: [CafeteriasKakaoController, CafeteriasNativeController], providers: [ CafeteriasService, CafeteriasRepository, diff --git a/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts index 4b8a364..12a8c54 100644 --- a/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts +++ b/services/api/app/src/api/public/cafeterias/infrastructure/cafeterias.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DietTime } from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; +import { DietTime } from 'src/api/public/cafeterias/application/utils/time'; import { CafeteriaDiet } from 'src/api/public/cafeterias/domain/entities/cafeteria-diet.entity'; import { Cafeteria } from 'src/api/public/cafeterias/domain/entities/cafeteria.entity'; import { Repository } from 'typeorm'; diff --git a/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts b/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts index 1fa178c..f1e3bbe 100644 --- a/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/cafeteria-message.factory.ts @@ -3,17 +3,20 @@ import { BasicCard, ListCard } from 'src/api/common/interfaces/response/fields/c import { Button, ListItem, QuickReply } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createBasicCard, createListCard } from 'src/api/common/utils/component'; -import { BlockId } from 'src/api/common/utils/constants'; import { - DietDate, + KakaoBlockId, + KAKAO_REQUEST_CAMPUS_SELECTION_ID, +} from 'src/api/common/presentation/kakao.constants'; +import { DietTime, -} from 'src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto'; + getDayWeek, + RelativeDietDate, +} from 'src/api/public/cafeterias/application/utils/time'; import { CafeteriaDietResult } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-diet-result.dto'; import { CafeteriaItemResult, CafeteriaListResult, } from 'src/api/public/cafeterias/application/dtos/results/cafeteria-list-result.dto'; -import { getDayWeek } from 'src/api/public/cafeterias/application/utils/time'; @Injectable() export class CafeteriaMessageFactory { @@ -27,7 +30,7 @@ export class CafeteriaMessageFactory { description: cafeteria.campus.name, imageUrl: cafeteria.thumbnailUrl, action: 'block', - blockId: BlockId.CAFETERIA_DIET_LIST, + blockId: KakaoBlockId.CAFETERIA_DIET_LIST, extra: { cafeteriaId: cafeteria.id, }, @@ -37,9 +40,9 @@ export class CafeteriaMessageFactory { { label: '더보기', action: 'block', - blockId: BlockId.CAFETERIA_LIST, + blockId: KakaoBlockId.CAFETERIA_LIST, extra: { - campusId: -1, + campusId: KAKAO_REQUEST_CAMPUS_SELECTION_ID, }, }, ]; @@ -96,14 +99,14 @@ export class CafeteriaMessageFactory { } private createDishDateQuickReplies(cafeteria: CafeteriaItemResult): QuickReply[] { - const dishDateTypes: DietDate[] = ['오늘', '내일']; + const dishDateTypes: RelativeDietDate[] = ['오늘', '내일']; const times: DietTime[] = ['아침', '점심', '저녁']; return dishDateTypes.flatMap(dishDateType => times.map(time => ({ label: `${dishDateType} ${time}`, action: 'block', - blockId: BlockId.CAFETERIA_DIET_LIST, + blockId: KakaoBlockId.CAFETERIA_DIET_LIST, extra: { cafeteriaId: cafeteria.id, date: dishDateType, diff --git a/services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts b/services/api/app/src/api/public/cafeterias/presentation/cafeterias-kakao.controller.ts similarity index 91% rename from services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts rename to services/api/app/src/api/public/cafeterias/presentation/cafeterias-kakao.controller.ts index 5cd2c50..ad1163c 100644 --- a/services/api/app/src/api/public/cafeterias/presentation/cafeterias.controller.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/cafeterias-kakao.controller.ts @@ -11,7 +11,10 @@ import { CampusesService } from 'src/api/public/campuses/application/campuses.se import { CampusMessageFactory } from 'src/api/public/campuses/presentation/campus-message.factory'; import { CurrentUser } from 'src/api/public/users/presentation/decorators/current-user.decorator'; import { FetchCurrentUser } from 'src/api/public/users/presentation/decorators/fetch-current-user.decorator'; -import { BlockId } from 'src/api/common/utils/constants'; +import { + KakaoBlockId, + KAKAO_REQUEST_CAMPUS_SELECTION_ID, +} from 'src/api/common/presentation/kakao.constants'; import { User } from 'src/api/public/users/domain/entities/users.entity'; import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; import { getDietTime, getTodayOrTomorrow } from 'src/api/public/cafeterias/application/utils/time'; @@ -21,7 +24,7 @@ import { CafeteriasService } from 'src/api/public/cafeterias/application/cafeter @Controller('cafeterias') @UseGuards(KakaoAuthGuard) @UseFilters(OpenBuilderExceptionFilter) -export class CafeteriasController { +export class CafeteriasKakaoController { constructor( private readonly cafeteriasService: CafeteriasService, private readonly cafeteriaMessageFactory: CafeteriaMessageFactory, @@ -40,11 +43,14 @@ export class CafeteriasController { const userCampusId = user.campus?.id; // '더보기' 버튼 또는 캠퍼스 미설정 → 캠퍼스 선택 카드 반환 - if (requestedCampusId === -1 || (!requestedCampusId && !userCampusId)) { + if ( + requestedCampusId === KAKAO_REQUEST_CAMPUS_SELECTION_ID || + (!requestedCampusId && !userCampusId) + ) { const result = await this.campusesService.findAll(); const template = this.campusMessageFactory.createCampusListCard( result, - BlockId.CAFETERIA_LIST, + KakaoBlockId.CAFETERIA_LIST, ); return new ResponseDTO(template); } diff --git a/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts index ea0926f..3821b1c 100644 --- a/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/get-cafeteria-diet-query.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsDefined, IsIn, IsISO8601 } from 'class-validator'; -import { DietTime } from './list-cafeteria-diet-request.dto'; +import { DietTime } from 'src/api/public/cafeterias/application/utils/time'; export class GetCafeteriaDietQueryDto { @IsDefined({ message: 'date는 필수입니다.' }) diff --git a/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts index 44233bd..b80e8c3 100644 --- a/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts +++ b/services/api/app/src/api/public/cafeterias/presentation/dtos/requests/list-cafeteria-diet-request.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { ClientExtraDto } from 'src/api/common/dtos/requests'; +import { DietTime, RelativeDietDate } from 'src/api/public/cafeterias/application/utils/time'; -export type DietDate = '오늘' | '내일'; -export type DietTime = '아침' | '점심' | '저녁'; +export type DietDate = RelativeDietDate; export class ListCafeteriaDietExtraRequestDto extends ClientExtraDto { @ApiProperty({ description: '식당 ID', example: 1 }) diff --git a/services/api/app/src/api/public/colleges/application/colleges.service.spec.ts b/services/api/app/src/api/public/colleges/application/colleges.service.spec.ts new file mode 100644 index 0000000..6d7cfbb --- /dev/null +++ b/services/api/app/src/api/public/colleges/application/colleges.service.spec.ts @@ -0,0 +1,78 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Test, TestingModule } from '@nestjs/testing'; +import { College } from 'src/api/public/colleges/domain/entities/college.entity'; +import { CollegesRepository } from 'src/api/public/colleges/infrastructure/colleges.repository'; +import { CollegesService } from 'src/api/public/colleges/application/colleges.service'; + +const makeCollege = (overrides: Partial = {}): College => + ({ + id: 1, + name: '공과대학', + thumbnailUrl: 'https://example.com/college.jpg', + ...overrides, + } as College); + +describe('CollegesService', () => { + let service: CollegesService; + let collegesRepository: jest.Mocked; + let cacheManager: { get: jest.Mock; set: jest.Mock }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CollegesService, + { + provide: CollegesRepository, + useValue: { + findAll: jest.fn(), + }, + }, + { + provide: CACHE_MANAGER, + useValue: { + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }).compile(); + + service = module.get(CollegesService); + collegesRepository = module.get(CollegesRepository); + cacheManager = module.get(CACHE_MANAGER); + }); + + it('page와 pageSize로 단과대학 목록을 조회한다', async () => { + collegesRepository.findAll.mockResolvedValue([[makeCollege()], 1]); + + const result = await service.findAll(2, 10); + + expect(collegesRepository.findAll).toHaveBeenCalledWith(2, 10); + expect(result).toEqual({ + colleges: [ + { + id: 1, + name: '공과대학', + thumbnailUrl: 'https://example.com/college.jpg', + }, + ], + total: 1, + }); + }); + + it('page가 1보다 작으면 1로 보정한다', async () => { + collegesRepository.findAll.mockResolvedValue([[], 0]); + + await service.findAll(0, 10); + + expect(collegesRepository.findAll).toHaveBeenCalledWith(1, 10); + }); + + it('cache miss이면 pageSize를 포함한 캐시 키로 저장한다', async () => { + collegesRepository.findAll.mockResolvedValue([[], 0]); + + const result = await service.findAll(0, 10); + + expect(cacheManager.set).toHaveBeenCalledWith('colleges:page:1:size:10', result); + }); +}); diff --git a/services/api/app/src/api/public/colleges/application/colleges.service.ts b/services/api/app/src/api/public/colleges/application/colleges.service.ts index f20488a..2dc4f06 100644 --- a/services/api/app/src/api/public/colleges/application/colleges.service.ts +++ b/services/api/app/src/api/public/colleges/application/colleges.service.ts @@ -14,10 +14,12 @@ export class CollegesService { ) {} @CacheKey({ - key: ([page]) => `colleges:page:${page as number}`, + key: ([page, pageSize]) => + `colleges:page:${Math.max(page as number, 1)}:size:${pageSize as number}`, }) - public async findAll(page: number): Promise { - const [colleges, total] = await this.collegesRepository.findAll(page); + public async findAll(page: number, pageSize: number): Promise { + const safePage = Math.max(page, 1); + const [colleges, total] = await this.collegesRepository.findAll(safePage, pageSize); return { colleges: colleges.map(college => ({ id: college.id, diff --git a/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts index a0a1ec6..970026e 100644 --- a/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts +++ b/services/api/app/src/api/public/colleges/infrastructure/colleges.repository.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ListCardConfig } from 'src/api/common/utils/constants'; import { Not, Repository } from 'typeorm'; import { College } from 'src/api/public/colleges/domain/entities/college.entity'; @@ -11,9 +10,7 @@ export class CollegesRepository { private readonly collegesRepository: Repository, ) {} - findAll(page: number): Promise<[College[], number]> { - const limit = ListCardConfig.LIMIT; - + findAll(page: number, limit: number): Promise<[College[], number]> { return this.collegesRepository.findAndCount({ where: { name: Not('공통'), diff --git a/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts b/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts index a677d61..42c47ea 100644 --- a/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts +++ b/services/api/app/src/api/public/colleges/presentation/college-message.factory.ts @@ -3,7 +3,10 @@ import { ListCard } from 'src/api/common/interfaces/response/fields/component'; import { ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard } from 'src/api/common/utils/component'; -import { BlockId, ListCardConfig } from 'src/api/common/utils/constants'; +import { + KakaoBlockId, + KAKAO_LIST_CARD_ITEM_LIMIT, +} from 'src/api/common/presentation/kakao.constants'; import { CollegeListResult } from 'src/api/public/colleges/application/dtos/results/college-list-result.dto'; @Injectable() @@ -33,14 +36,14 @@ export class CollegeMessageFactory { const collegeListCard: ListCard = createListCard(header, items); - const totalPages = Math.ceil(result.total / ListCardConfig.LIMIT); + const totalPages = Math.ceil(result.total / KAKAO_LIST_CARD_ITEM_LIMIT); const paginationButtons = []; if (page > 1) { paginationButtons.push({ label: '이전', action: 'block', - blockId: BlockId.COLLEGE_LIST, + blockId: KakaoBlockId.COLLEGE_LIST, extra: { campusId: campusId, page: page - 1, @@ -52,7 +55,7 @@ export class CollegeMessageFactory { paginationButtons.push({ label: '다음', action: 'block', - blockId: BlockId.COLLEGE_LIST, + blockId: KakaoBlockId.COLLEGE_LIST, extra: { campusId: campusId, page: page + 1, diff --git a/services/api/app/src/api/public/common/presentation/common-message.factory.ts b/services/api/app/src/api/public/common/presentation/common-message.factory.ts index e54aeff..e06dad6 100644 --- a/services/api/app/src/api/public/common/presentation/common-message.factory.ts +++ b/services/api/app/src/api/public/common/presentation/common-message.factory.ts @@ -3,7 +3,7 @@ import { TextCard } from 'src/api/common/interfaces/response/fields/component'; import { Button } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createSimpleText, createTextCard } from 'src/api/common/utils/component'; -import { BlockId } from 'src/api/common/utils/constants'; +import { KakaoBlockId } from 'src/api/common/presentation/kakao.constants'; @Injectable() export class CommonMessageFactory { @@ -22,7 +22,7 @@ export class CommonMessageFactory { { label: '학과 등록', action: 'block', - blockId: BlockId.CHANGE_PROFILE, + blockId: KakaoBlockId.CHANGE_PROFILE, }, ]; diff --git a/services/api/app/src/api/public/departments/application/departments.service.spec.ts b/services/api/app/src/api/public/departments/application/departments.service.spec.ts new file mode 100644 index 0000000..f29a35e --- /dev/null +++ b/services/api/app/src/api/public/departments/application/departments.service.spec.ts @@ -0,0 +1,76 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Department } from 'src/api/public/departments/domain/entities/department.entity'; +import { DepartmentsRepository } from 'src/api/public/departments/infrastructure/departments.repository'; +import { DepartmentsService } from 'src/api/public/departments/application/departments.service'; + +const makeDepartment = (overrides: Partial = {}): Department => + ({ + id: 1, + name: '컴퓨터공학부', + ...overrides, + } as Department); + +describe('DepartmentsService', () => { + let service: DepartmentsService; + let departmentsRepository: jest.Mocked; + let cacheManager: { get: jest.Mock; set: jest.Mock }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DepartmentsService, + { + provide: DepartmentsRepository, + useValue: { + findByCollegeId: jest.fn(), + }, + }, + { + provide: CACHE_MANAGER, + useValue: { + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }).compile(); + + service = module.get(DepartmentsService); + departmentsRepository = module.get(DepartmentsRepository); + cacheManager = module.get(CACHE_MANAGER); + }); + + it('collegeId, page, pageSize로 학과 목록을 조회한다', async () => { + departmentsRepository.findByCollegeId.mockResolvedValue([[makeDepartment()], 1]); + + const result = await service.findAll(3, 2, 10); + + expect(departmentsRepository.findByCollegeId).toHaveBeenCalledWith(3, 2, 10); + expect(result).toEqual({ + departments: [ + { + id: 1, + name: '컴퓨터공학부', + }, + ], + total: 1, + }); + }); + + it('page가 1보다 작으면 1로 보정한다', async () => { + departmentsRepository.findByCollegeId.mockResolvedValue([[], 0]); + + await service.findAll(3, 0, 10); + + expect(departmentsRepository.findByCollegeId).toHaveBeenCalledWith(3, 1, 10); + }); + + it('cache miss이면 pageSize를 포함한 캐시 키로 저장한다', async () => { + departmentsRepository.findByCollegeId.mockResolvedValue([[], 0]); + + const result = await service.findAll(3, 0, 10); + + expect(cacheManager.set).toHaveBeenCalledWith('departments:college:3:page:1:size:10', result); + }); +}); diff --git a/services/api/app/src/api/public/departments/application/departments.service.ts b/services/api/app/src/api/public/departments/application/departments.service.ts index f026252..0ea4ef9 100644 --- a/services/api/app/src/api/public/departments/application/departments.service.ts +++ b/services/api/app/src/api/public/departments/application/departments.service.ts @@ -14,16 +14,24 @@ export class DepartmentsService { ) {} @CacheKey({ - key: ([collegeId, page]) => { - return `departments:college:${collegeId}:page:${page}`; + key: ([collegeId, page, pageSize]) => { + return `departments:college:${collegeId}:page:${Math.max( + page as number, + 1, + )}:size:${pageSize}`; }, }) - public async findAll(collegeId: number, page: number): Promise { + public async findAll( + collegeId: number, + page: number, + pageSize: number, + ): Promise { const safePage = Math.max(page, 1); const [departments, total] = await this.departmentsRepository.findByCollegeId( collegeId, safePage, + pageSize, ); return { diff --git a/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts index 2926246..e08281f 100644 --- a/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts +++ b/services/api/app/src/api/public/departments/infrastructure/departments.repository.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ListCardConfig } from 'src/api/common/utils/constants'; import { Department } from 'src/api/public/departments/domain/entities/department.entity'; import { Repository } from 'typeorm'; @@ -11,9 +10,7 @@ export class DepartmentsRepository { private readonly departmentsRepository: Repository, ) {} - findByCollegeId(collegeId: number, page: number): Promise<[Department[], number]> { - const limit = ListCardConfig.LIMIT; - + findByCollegeId(collegeId: number, page: number, limit: number): Promise<[Department[], number]> { return this.departmentsRepository.findAndCount({ where: { collegeId, diff --git a/services/api/app/src/api/public/departments/presentation/department-message.factory.ts b/services/api/app/src/api/public/departments/presentation/department-message.factory.ts index a0ab847..e047277 100644 --- a/services/api/app/src/api/public/departments/presentation/department-message.factory.ts +++ b/services/api/app/src/api/public/departments/presentation/department-message.factory.ts @@ -3,7 +3,10 @@ import { ListCard } from 'src/api/common/interfaces/response/fields/component'; import { ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard } from 'src/api/common/utils/component'; -import { BlockId, ListCardConfig } from 'src/api/common/utils/constants'; +import { + KakaoBlockId, + KAKAO_LIST_CARD_ITEM_LIMIT, +} from 'src/api/common/presentation/kakao.constants'; import { DepartmentListResult } from 'src/api/public/departments/application/dtos/results/department-list-result.dto'; import { ListDepartmentsRequestDto } from 'src/api/public/users/presentation/dtos/requests/list-department-request.dto'; @@ -32,14 +35,14 @@ export class DepartmentMessageFactory { const departmentListCard: ListCard = createListCard(header, items); - const totalPages = Math.ceil(result.total / ListCardConfig.LIMIT); + const totalPages = Math.ceil(result.total / KAKAO_LIST_CARD_ITEM_LIMIT); const paginationButtons = []; if (extra.page > 1) { paginationButtons.push({ label: '이전', action: 'block', - blockId: BlockId.DEPARTMENT_LIST, + blockId: KakaoBlockId.DEPARTMENT_LIST, extra: { campusId: extra.campusId, collegeId: extra.collegeId, @@ -52,7 +55,7 @@ export class DepartmentMessageFactory { paginationButtons.push({ label: '다음', action: 'block', - blockId: BlockId.DEPARTMENT_LIST, + blockId: KakaoBlockId.DEPARTMENT_LIST, extra: { campusId: extra.campusId, collegeId: extra.collegeId, diff --git a/services/api/app/src/api/public/notices/application/notices.service.spec.ts b/services/api/app/src/api/public/notices/application/notices.service.spec.ts index 69c54e2..43e2a86 100644 --- a/services/api/app/src/api/public/notices/application/notices.service.spec.ts +++ b/services/api/app/src/api/public/notices/application/notices.service.spec.ts @@ -165,6 +165,26 @@ describe('NoticesService', () => { expect.any(Array), ); }); + + it('기본 limitPerCategory 10으로 공지사항을 조회한다', async () => { + const category = makeCategory({ id: 1, category: '학사' }); + noticeCategoriesRepository.findByDepartmentIdAndCategories.mockResolvedValue([category]); + noticesRepository.findRecentByCategoryIds.mockResolvedValue(new Map()); + + await service.getUniversityNotices(); + + expect(noticesRepository.findRecentByCategoryIds).toHaveBeenCalledWith([1], 10); + }); + + it('limitPerCategory 옵션으로 카테고리별 조회 개수를 변경한다', async () => { + const category = makeCategory({ id: 1, category: '학사' }); + noticeCategoriesRepository.findByDepartmentIdAndCategories.mockResolvedValue([category]); + noticesRepository.findRecentByCategoryIds.mockResolvedValue(new Map()); + + await service.getUniversityNotices({ limitPerCategory: 10 }); + + expect(noticesRepository.findRecentByCategoryIds).toHaveBeenCalledWith([1], 10); + }); }); describe('getDepartmentNotices', () => { @@ -242,5 +262,27 @@ describe('NoticesService', () => { expect(noticeCategoriesRepository.findByDepartmentIds).toHaveBeenCalledWith([10]); }); + + it('기본 limitPerCategory 10으로 학과 공지사항을 조회한다', async () => { + const user = makeUser(); + const category = makeCategory({ id: 1 }); + noticeCategoriesRepository.findByDepartmentIds.mockResolvedValue([category]); + noticesRepository.findRecentByCategoryIds.mockResolvedValue(new Map()); + + await service.getDepartmentNotices(user); + + expect(noticesRepository.findRecentByCategoryIds).toHaveBeenCalledWith([1], 10); + }); + + it('limitPerCategory 옵션으로 학과 공지사항 조회 개수를 변경한다', async () => { + const user = makeUser(); + const category = makeCategory({ id: 1 }); + noticeCategoriesRepository.findByDepartmentIds.mockResolvedValue([category]); + noticesRepository.findRecentByCategoryIds.mockResolvedValue(new Map()); + + await service.getDepartmentNotices(user, { limitPerCategory: 10 }); + + expect(noticesRepository.findRecentByCategoryIds).toHaveBeenCalledWith([1], 10); + }); }); }); diff --git a/services/api/app/src/api/public/notices/application/notices.service.ts b/services/api/app/src/api/public/notices/application/notices.service.ts index 5fe5faa..75f4d0d 100644 --- a/services/api/app/src/api/public/notices/application/notices.service.ts +++ b/services/api/app/src/api/public/notices/application/notices.service.ts @@ -15,7 +15,7 @@ export class NoticesService { private readonly TARGET_CATEGORIES = ['기관', '채용', '장학', '외부기관 행사', '학사']; - private readonly CAROUSEL_ITEMS_LIMIT = 4; + private readonly DEFAULT_LIMIT_PER_CATEGORY = 10; constructor( private readonly noticesRepository: NoticesRepository, @@ -26,7 +26,8 @@ export class NoticesService { * 학교 공지사항 조회 * @returns 카테고리별 공지사항 Map (데이터가 없으면 빈 Map) */ - async getUniversityNotices(): Promise { + async getUniversityNotices(options?: { limitPerCategory?: number }): Promise { + const limitPerCategory = options?.limitPerCategory ?? this.DEFAULT_LIMIT_PER_CATEGORY; const categories = await this.noticeCategoriesRepository.findByDepartmentIdAndCategories( this.UNIVERSITY_DEPARTMENT_ID, this.TARGET_CATEGORIES, @@ -39,7 +40,7 @@ export class NoticesService { const categoryIds = categories.map(category => category.id); const noticesByCategory = await this.noticesRepository.findRecentByCategoryIds( categoryIds, - this.CAROUSEL_ITEMS_LIMIT, + limitPerCategory, ); const categoriesResult: NoticeCategoryResult[] = []; @@ -62,10 +63,14 @@ export class NoticesService { * @param user 현재 사용자 (department 미설정 시 빈 Map 반환) * @returns 카테고리별 공지사항 Map */ - async getDepartmentNotices(user: User): Promise { + async getDepartmentNotices( + user: User, + options?: { limitPerCategory?: number }, + ): Promise { if (!user.department) { return { categories: [] }; } + const limitPerCategory = options?.limitPerCategory ?? this.DEFAULT_LIMIT_PER_CATEGORY; const departmentIds: number[] = [user.department.id]; if (user.department.parentDepartmentId) { @@ -81,7 +86,7 @@ export class NoticesService { const categoryIds = categories.map(category => category.id); const noticesByCategory = await this.noticesRepository.findRecentByCategoryIds( categoryIds, - this.CAROUSEL_ITEMS_LIMIT, + limitPerCategory, ); const categoriesResult: NoticeCategoryResult[] = []; diff --git a/services/api/app/src/api/public/notices/notices.module.ts b/services/api/app/src/api/public/notices/notices.module.ts index 0fcdcea..c1bd951 100644 --- a/services/api/app/src/api/public/notices/notices.module.ts +++ b/services/api/app/src/api/public/notices/notices.module.ts @@ -6,12 +6,12 @@ import { Notice } from 'src/api/public/notices/domain/entities/notice.entity'; import { NoticeCategoriesRepository } from 'src/api/public/notices/infrastructure/notice-categories.repository'; import { NoticeMessageFactory } from 'src/api/public/notices/presentation/notice-message.factory'; import { NoticesRepository } from 'src/api/public/notices/infrastructure/notices.repository'; -import { NoticesController } from 'src/api/public/notices/presentation/notices.controller'; +import { NoticesKakaoController } from 'src/api/public/notices/presentation/notices-kakao.controller'; import { NoticesService } from 'src/api/public/notices/application/notices.service'; @Module({ imports: [TypeOrmModule.forFeature([Notice, NoticeCategory])], - controllers: [NoticesController], + controllers: [NoticesKakaoController], providers: [ NoticesService, NoticesRepository, diff --git a/services/api/app/src/api/public/notices/presentation/notices.controller.ts b/services/api/app/src/api/public/notices/presentation/notices-kakao.controller.ts similarity index 90% rename from services/api/app/src/api/public/notices/presentation/notices.controller.ts rename to services/api/app/src/api/public/notices/presentation/notices-kakao.controller.ts index 820ea72..fb863b7 100644 --- a/services/api/app/src/api/public/notices/presentation/notices.controller.ts +++ b/services/api/app/src/api/public/notices/presentation/notices-kakao.controller.ts @@ -12,12 +12,13 @@ import { ListDepartmentNoticeRequestDto } from './dtos/requests/list-department- import { ListUniversityNoticeRequestDto } from './dtos/requests/list-university-notice-request.dto'; import { KakaoAuthGuard } from 'src/api/public/users/presentation/guards/kakao-auth.guard'; import { NoticesService } from 'src/api/public/notices/application/notices.service'; +import { KAKAO_NOTICE_CAROUSEL_ITEM_LIMIT } from 'src/api/common/presentation/kakao.constants'; @ApiTags('notices') @Controller('notices') @UseGuards(KakaoAuthGuard) @UseFilters(OpenBuilderExceptionFilter) -export class NoticesController { +export class NoticesKakaoController { constructor( private readonly noticesService: NoticesService, private readonly noticeMessageFactory: NoticeMessageFactory, @@ -27,7 +28,9 @@ export class NoticesController { @Post('university') @ApiSkillBody(ListUniversityNoticeRequestDto) async listUniversityNotices() { - const result = await this.noticesService.getUniversityNotices(); + const result = await this.noticesService.getUniversityNotices({ + limitPerCategory: KAKAO_NOTICE_CAROUSEL_ITEM_LIMIT, + }); if (result.categories.length === 0) { const template = this.commonMessageFactory.createSimpleText('현재 등록된 공지사항이 없어!'); @@ -47,7 +50,9 @@ export class NoticesController { return new ResponseDTO(template); } - const result = await this.noticesService.getDepartmentNotices(user); + const result = await this.noticesService.getDepartmentNotices(user, { + limitPerCategory: KAKAO_NOTICE_CAROUSEL_ITEM_LIMIT, + }); if (result.categories.length === 0) { const template = this.commonMessageFactory.createSimpleText('현재 등록된 공지사항이 없어!'); diff --git a/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts b/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts index f9a9922..05b3430 100644 --- a/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts +++ b/services/api/app/src/api/public/schedules/presentation/schedule-message.factory.ts @@ -3,7 +3,7 @@ import { TextCard } from 'src/api/common/interfaces/response/fields/component'; import { Button, QuickReply } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createTextCard } from 'src/api/common/utils/component'; -import { BlockId } from 'src/api/common/utils/constants'; +import { KakaoBlockId } from 'src/api/common/presentation/kakao.constants'; import { AcademicScheduleResult } from 'src/api/public/schedules/application/dtos/results/academic-schedule-result.dto'; @Injectable() @@ -75,7 +75,7 @@ export class ScheduleMessageFactory { replies.push({ label: `${month}월`, action: 'block', - blockId: BlockId.ACADEMIC_SCHEDULE, + blockId: KakaoBlockId.ACADEMIC_SCHEDULE, extra: { month }, }); } diff --git a/services/api/app/src/api/public/schedules/presentation/schedules.controller.ts b/services/api/app/src/api/public/schedules/presentation/schedules-kakao.controller.ts similarity index 97% rename from services/api/app/src/api/public/schedules/presentation/schedules.controller.ts rename to services/api/app/src/api/public/schedules/presentation/schedules-kakao.controller.ts index a61a4bf..a798af2 100644 --- a/services/api/app/src/api/public/schedules/presentation/schedules.controller.ts +++ b/services/api/app/src/api/public/schedules/presentation/schedules-kakao.controller.ts @@ -14,7 +14,7 @@ import { SchedulesService } from 'src/api/public/schedules/application/schedules @Controller('schedules') @UseGuards(KakaoAuthGuard) @UseFilters(OpenBuilderExceptionFilter) -export class SchedulesController { +export class SchedulesKakaoController { constructor( private readonly schedulesService: SchedulesService, private readonly scheduleMessageFactory: ScheduleMessageFactory, diff --git a/services/api/app/src/api/public/schedules/schedules.module.ts b/services/api/app/src/api/public/schedules/schedules.module.ts index 380ee6a..befc08b 100644 --- a/services/api/app/src/api/public/schedules/schedules.module.ts +++ b/services/api/app/src/api/public/schedules/schedules.module.ts @@ -4,12 +4,12 @@ import { CommonMessageFactory } from 'src/api/public/common/presentation/common- import { AcademicCalendarsRepository } from 'src/api/public/schedules/infrastructure/academic-calendars.repository'; import { AcademicCalendar } from 'src/api/public/schedules/domain/entities/academic-calendar.entity'; import { ScheduleMessageFactory } from 'src/api/public/schedules/presentation/schedule-message.factory'; -import { SchedulesController } from 'src/api/public/schedules/presentation/schedules.controller'; +import { SchedulesKakaoController } from 'src/api/public/schedules/presentation/schedules-kakao.controller'; import { SchedulesService } from 'src/api/public/schedules/application/schedules.service'; @Module({ imports: [TypeOrmModule.forFeature([AcademicCalendar])], - controllers: [SchedulesController], + controllers: [SchedulesKakaoController], providers: [ SchedulesService, AcademicCalendarsRepository, diff --git a/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts b/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts index 0357c1a..2c3d88a 100644 --- a/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts +++ b/services/api/app/src/api/public/shuttles/presentation/shuttle-message.factory.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Button, ListItem } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createListCard, createTextCard } from 'src/api/common/utils/component'; -import { BlockId } from 'src/api/common/utils/constants'; +import { KakaoBlockId } from 'src/api/common/presentation/kakao.constants'; import { ShuttleRouteListResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-route-list-result.dto'; import { ShuttleTimetableResult } from 'src/api/public/shuttles/application/dtos/results/shuttle-timetable-result.dto'; @@ -22,7 +22,7 @@ export class ShuttleMessageFactory { const items: ListItem[] = result.routes.map(route => ({ title: route.routeName, action: 'block', - blockId: BlockId.SHUTTLE_TIMETABLE, + blockId: KakaoBlockId.SHUTTLE_TIMETABLE, extra: { routeName: route.routeName }, })); @@ -49,7 +49,7 @@ export class ShuttleMessageFactory { { label: '뒤로 가기', action: 'block', - blockId: BlockId.SHUTTLE_ROUTES, + blockId: KakaoBlockId.SHUTTLE_ROUTES, }, { label: '공식 홈페이지', diff --git a/services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts b/services/api/app/src/api/public/shuttles/presentation/shuttles-kakao.controller.ts similarity index 97% rename from services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts rename to services/api/app/src/api/public/shuttles/presentation/shuttles-kakao.controller.ts index 4bc08fb..8641b8d 100644 --- a/services/api/app/src/api/public/shuttles/presentation/shuttles.controller.ts +++ b/services/api/app/src/api/public/shuttles/presentation/shuttles-kakao.controller.ts @@ -13,7 +13,7 @@ import { ShuttlesService } from 'src/api/public/shuttles/application/shuttles.se @Controller('shuttles') @UseGuards(KakaoAuthGuard) @UseFilters(OpenBuilderExceptionFilter) -export class ShuttlesController { +export class ShuttlesKakaoController { constructor( private readonly shuttlesService: ShuttlesService, private readonly shuttleMessageFactory: ShuttleMessageFactory, diff --git a/services/api/app/src/api/public/shuttles/shuttles.module.ts b/services/api/app/src/api/public/shuttles/shuttles.module.ts index 1b93ec3..913008a 100644 --- a/services/api/app/src/api/public/shuttles/shuttles.module.ts +++ b/services/api/app/src/api/public/shuttles/shuttles.module.ts @@ -4,13 +4,13 @@ import { ShuttleTimetable } from 'src/api/public/shuttles/domain/entities/shuttl import { ShuttleTimetableRepository } from 'src/api/public/shuttles/infrastructure/shuttle-timetable.repository'; import { ShuttleMessageFactory } from 'src/api/public/shuttles/presentation/shuttle-message.factory'; import { ShuttleTimetableCalculator } from 'src/api/public/shuttles/application/shuttle-timetable.calculator'; -import { ShuttlesController } from 'src/api/public/shuttles/presentation/shuttles.controller'; +import { ShuttlesKakaoController } from 'src/api/public/shuttles/presentation/shuttles-kakao.controller'; import { ShuttlesNativeController } from 'src/api/public/shuttles/presentation/shuttles-native.controller'; import { ShuttlesService } from 'src/api/public/shuttles/application/shuttles.service'; @Module({ imports: [TypeOrmModule.forFeature([ShuttleTimetable])], - controllers: [ShuttlesController, ShuttlesNativeController], + controllers: [ShuttlesKakaoController, ShuttlesNativeController], providers: [ ShuttlesService, ShuttleTimetableRepository, diff --git a/services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts b/services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts index 3d712ad..4663d66 100644 --- a/services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts +++ b/services/api/app/src/api/public/users/application/dtos/results/user-profile-result.dto.ts @@ -1,5 +1,21 @@ +export interface UserProfileCampusResult { + id: number; + name: string; +} + +export interface UserProfileCollegeResult { + id: number; + name: string; +} + +export interface UserProfileDepartmentResult { + id: number; + name: string; +} + export interface UserProfileResult { userId: string; - campusName: string; - affiliationName: string; + campus: UserProfileCampusResult | null; + college: UserProfileCollegeResult | null; + department: UserProfileDepartmentResult | null; } diff --git a/services/api/app/src/api/public/users/application/users.service.spec.ts b/services/api/app/src/api/public/users/application/users.service.spec.ts index 6207249..2d65eaa 100644 --- a/services/api/app/src/api/public/users/application/users.service.spec.ts +++ b/services/api/app/src/api/public/users/application/users.service.spec.ts @@ -61,6 +61,43 @@ describe('UsersService', () => { }); }); + describe('createProfileResult', () => { + it('사용자 프로필을 구조화된 값으로 반환한다', () => { + const user = makeUser(); + + const result = service.createProfileResult(user); + + expect(result).toEqual({ + userId: 'kakao-user-id', + campus: { + id: 1, + name: '가좌캠퍼스', + }, + college: { + id: 3, + name: '공과대학', + }, + department: { + id: 10, + name: '컴퓨터공학부', + }, + }); + }); + + it('캠퍼스와 학과가 없으면 null로 반환한다', () => { + const user = makeUser({ campus: null, department: null }); + + const result = service.createProfileResult(user); + + expect(result).toEqual({ + userId: 'kakao-user-id', + campus: null, + college: null, + department: null, + }); + }); + }); + describe('upsert', () => { it('userId와 campusId, departmentId로 사용자 학과 정보를 저장한다', async () => { const user = makeUser({ campusId: 2, departmentId: 20 }); diff --git a/services/api/app/src/api/public/users/application/users.service.ts b/services/api/app/src/api/public/users/application/users.service.ts index b4aa8a6..da66e94 100644 --- a/services/api/app/src/api/public/users/application/users.service.ts +++ b/services/api/app/src/api/public/users/application/users.service.ts @@ -13,17 +13,26 @@ export class UsersService { } public createProfileResult(user: User): UserProfileResult { - const campusName = user.campus?.name || '미등록'; - const collegeName = user.department?.college?.name; - const departmentName = user.department?.name; - return { userId: user.id, - campusName, - affiliationName: - !collegeName && !departmentName - ? '미등록' - : [collegeName, departmentName].filter(Boolean).join(' '), + campus: user.campus + ? { + id: user.campus.id, + name: user.campus.name, + } + : null, + college: user.department?.college + ? { + id: user.department.college.id, + name: user.department.college.name, + } + : null, + department: user.department + ? { + id: user.department.id, + name: user.department.name, + } + : null, }; } diff --git a/services/api/app/src/api/public/users/presentation/user-message.factory.spec.ts b/services/api/app/src/api/public/users/presentation/user-message.factory.spec.ts new file mode 100644 index 0000000..6bebe6f --- /dev/null +++ b/services/api/app/src/api/public/users/presentation/user-message.factory.spec.ts @@ -0,0 +1,45 @@ +import { UserMessageFactory } from 'src/api/public/users/presentation/user-message.factory'; + +describe('UserMessageFactory', () => { + let factory: UserMessageFactory; + + beforeEach(() => { + factory = new UserMessageFactory(); + }); + + it('구조화된 프로필을 기존 카카오 프로필 문구로 렌더링한다', () => { + const result = factory.createProfileMessage({ + userId: 'kakao-user-id', + campus: { id: 1, name: '가좌캠퍼스' }, + college: { id: 3, name: '공과대학' }, + department: { id: 10, name: '컴퓨터공학부' }, + }); + + expect(result.outputs[0]).toEqual( + expect.objectContaining({ + textCard: expect.objectContaining({ + title: '내 정보', + description: + '[ID]\nkakao-user-id\n\n[캠퍼스]\n가좌캠퍼스\n\n[전공]\n공과대학 컴퓨터공학부', + }), + }), + ); + }); + + it('미설정 프로필을 기존 미등록 문구로 렌더링한다', () => { + const result = factory.createProfileMessage({ + userId: 'kakao-user-id', + campus: null, + college: null, + department: null, + }); + + expect(result.outputs[0]).toEqual( + expect.objectContaining({ + textCard: expect.objectContaining({ + description: '[ID]\nkakao-user-id\n\n[캠퍼스]\n미등록\n\n[전공]\n미등록', + }), + }), + ); + }); +}); diff --git a/services/api/app/src/api/public/users/presentation/user-message.factory.ts b/services/api/app/src/api/public/users/presentation/user-message.factory.ts index c42b01b..dd6fee7 100644 --- a/services/api/app/src/api/public/users/presentation/user-message.factory.ts +++ b/services/api/app/src/api/public/users/presentation/user-message.factory.ts @@ -3,23 +3,28 @@ import { TextCard } from 'src/api/common/interfaces/response/fields/component'; import { Button } from 'src/api/common/interfaces/response/fields/etc'; import { SkillTemplate } from 'src/api/common/interfaces/response/fields/template'; import { createTextCard } from 'src/api/common/utils/component'; -import { BlockId } from 'src/api/common/utils/constants'; +import { KakaoBlockId } from 'src/api/common/presentation/kakao.constants'; import { UserProfileResult } from 'src/api/public/users/application/dtos/results/user-profile-result.dto'; @Injectable() export class UserMessageFactory { createProfileMessage(result: UserProfileResult): SkillTemplate { + const campusName = result.campus?.name ?? '미등록'; + const affiliationName = + !result.college && !result.department + ? '미등록' + : [result.college?.name, result.department?.name].filter(Boolean).join(' '); const buttons: Array + + + + +
+ + +
+ + +
+
+

경상국립대학교

+ + + + + +
+ +
+ + +
+
+ + + + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + +
+
+

대학생활/취업

+

혁신을 이끄는 국가거점국립대학의 리더

+
+
+ + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + +

셔틀버스

+
+ + + + +
+ + +
+ + + +
+ +
+ +
+
+
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + +
셔틀버스 안내

2026학년도 학기 중 셔틀버스 운행 알림!

  1. 01

    운행기간 : 2026. 3. 3. ~

    ※ 방학 중 미운영

  2. 02

    가좌캠퍼스 승·하차 장소 안내

    ‣ (승차) ① 교양학관 앞
    ‣ (하차) ① 대학본부 앞    ② 학생회관 앞

  3. 03

    버스 탑승 시 유의사항

    • ‣ 버스 입석 금지 (도로교통법 적용)
    • ‣ 도로 상황에 따라 지연 가능성 있음
    • ‣ 음식물 반입 제한 (일회용컵, 악취 물품 등)
    •    ※ 개인용 텀블러 소지 허용

★ 버스 운행시간에는 전화 통화가 어려울 수 있습니다.

                                                           < 가좌캠퍼스 승.하차 장소 안내>                              ※ (승차) 초록색, (하차) 빨간색

셔틀버스 운행 상세 안내

 2026년도 캠퍼스간 셔틀버스

운행구간: 가좌 ↔ 칠암

가좌캠퍼스 → 칠암캠퍼스
(출발지: 가좌캠퍼스)
오전오후

08 : 20

13 : 10 (금요일 미운행)

09 : 00  (금요일 미운행)

13 : 40

09 : 30

13 : 50

09 : 50

14 : 00

10 : 00 (금요일 미운행)

14 : 40

10 : 30

14 : 50

10 : 40

15 : 00

11 : 00

15 : 30

11 : 30

16 : 00


16 : 10


16 : 30


16 : 40


17 : 30



칠암캠퍼스 → 가좌캠퍼스
(출발지: 칠암캠퍼스)
오전오후

08 : 05

13 : 00

08 : 10

13 : 10

08 : 15

13 : 20

08 : 20

13 : 40 (금요일 미운행)

08 : 25

14 : 05

08 : 30 (금요일 미운행)

14 : 10

08 : 40

14 : 15

09 : 30(금요일 미운행)

15 : 10

10 : 00

15 : 20

10 : 20

15 : 30

10 : 30 (금요일 미운행)

16 : 00

10 : 55

16 : 15

11 : 00

17 : 00

11 : 15


+ +
+ 최근 업데이트 일시 : 2026/03/13 09:47:30 +
+ + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + +
+ +
+
    + + +
  • 소속 : 총무과
  • +
  • 전화번호 : 055-772-0344
  • + +
+

게재된 내용 및 운영에 대한 개선사항이 있으면 담당자에게 연락하시기 바랍니다.

+
+ +
+ + +
+ +
+ +
+ + + + + + + + + +
+
+
+

경상국립대학교

+ +
+
    +
  • 가좌캠퍼스 [대학본부] 52828 경상남도 진주시 진주대로 501
  • +
  • Tel. 055) 772 - 0114,1114
  • +
+
    +
  • 칠암캠퍼스 52725 경상남도 진주시 동진로 33 Tel. 055-772-0114
  • +
  • [의·간호] 52727 경상남도 진주시 진주대로 816번길 15 Tel. 055-772-8017
  • +
+
    +
  • 통영캠퍼스 53064 경상남도 통영시 통영해안로 2
  • +
  • Tel. 055-772-9099
  • +
+
    +
  • 창원산학캠퍼스 51391 경상남도 창원시 의창구 차룡로 48번길 54
  • +
  • Tel. 055-250-7300
  • +
+
    +
  • 내동캠퍼스 52849 경상남도 진주시 내동로 139번길 8
  • +
  • Tel. 055-772-0114
  • +
+
    +
  • 사천캠퍼스 52537 경상남도 사천시 용현면 선진공원길 102
  • +
  • Tel. 055-855-4190
  • +
+
+ + + +

+ 과학기술정보통신부 WA(WEB접근성) 품질인증 마크, 웹와치(WebWatch) 2024.5.26~2025.5.25 +

+ +
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.client.spec.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.client.spec.ts new file mode 100644 index 0000000..2c62be4 --- /dev/null +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.client.spec.ts @@ -0,0 +1,14 @@ +import { FetchHttpClient } from '../../http/fetch-http.client'; +import { ShuttleClient } from './shuttle.client'; + +describe('ShuttleClient', () => { + it('공통 HTTP 클라이언트에서 셔틀 HTML을 가져온다', async () => { + const httpClient = { + getText: jest.fn().mockResolvedValue(''), + } as unknown as FetchHttpClient; + const client = new ShuttleClient(httpClient); + + await expect(client.fetch()).resolves.toBe(''); + expect(httpClient.getText).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.client.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.client.ts index e69de29..445369d 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.client.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.client.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { FetchHttpClient } from '../../http/fetch-http.client'; + +const SHUTTLE_TIMETABLE_URL = + 'https://www.gnu.ac.kr/main/cm/cntnts/cntntsView.do?mi=1358&cntntsId=1194'; + +@Injectable() +export class ShuttleClient { + constructor(private readonly httpClient: FetchHttpClient) {} + + async fetch(): Promise { + return this.httpClient.getText(SHUTTLE_TIMETABLE_URL); + } +} diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts index 3dca9c2..b0dc26a 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; import { BatchJob } from '../batch-job.interface'; +import { ShuttleClient } from './shuttle.client'; +import { ShuttleParser } from './shuttle.parser'; +import { ShuttleRepository } from './shuttle.repository'; @Injectable() export class ShuttleJob implements BatchJob { @@ -11,5 +14,9 @@ export class ShuttleJob implements BatchJob { private readonly repository: ShuttleRepository, ) {} - async run(): Promise {} + async run(): Promise { + const raw = await this.client.fetch(); + const shuttles = this.parser.parse(raw); + //await this.repository.save(shuttles); + } } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts index e69de29..f534049 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { ShuttleTimetable } from './shuttle.interface'; + +@Injectable() +export class ShuttleParser { + parse(_raw: string): ShuttleTimetable[] { + return []; + } +} diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts index e69de29..537405a 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { ShuttleTimetable } from './shuttle.interface'; + +@Injectable() +export class ShuttleRepository { + async save(_shuttles: ShuttleTimetable[]): Promise {} +} diff --git a/services/api/batch/src/batch/utils/delay.ts b/services/api/batch/src/batch/utils/delay.ts new file mode 100644 index 0000000..ab70a49 --- /dev/null +++ b/services/api/batch/src/batch/utils/delay.ts @@ -0,0 +1,7 @@ +export async function delay(delayMs: number): Promise { + if (delayMs <= 0) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, delayMs)); +} diff --git a/services/api/batch/tsconfig.json b/services/api/batch/tsconfig.json index aba29b0..1bc8823 100644 --- a/services/api/batch/tsconfig.json +++ b/services/api/batch/tsconfig.json @@ -12,6 +12,7 @@ "allowSyntheticDefaultImports": true, "target": "ES2023", "sourceMap": true, + "types": ["node", "jest"], "outDir": "./dist", "baseUrl": "./", "incremental": true, From be5736b2daca312e813cd63574913a90e55893bf Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sun, 14 Jun 2026 21:14:11 +0900 Subject: [PATCH 17/29] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=85=94=ED=8B=80=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=8C=EC=84=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 데이터베이스 연결 설정을 위한 DatabaseModule 추가 * 셔틀 시간표 저장을 위한 ShuttleTimetable 엔티티 생성 * 기존 셔틀 인터페이스 제거 후 파싱된 시간표 타입으로 대체 * HTML 셔틀 시간표를 구조화된 데이터로 변환하는 ShuttleParser 구현 * ShuttleParser의 시간표 파싱 동작을 검증하는 단위 테스트 추가 * ShuttleRepository가 새로운 ShuttleTimetable 엔티티를 사용하도록 수정 --- services/api/batch/package.json | 6 +- services/api/batch/pnpm-lock.yaml | 317 ++++++++++++++++++ .../src/batch/database/database.module.ts | 25 ++ .../domain/shuttle-timetable.entity.ts | 22 ++ .../batch/jobs/shuttle/shuttle.interface.ts | 10 - .../src/batch/jobs/shuttle/shuttle.job.ts | 1 + .../batch/jobs/shuttle/shuttle.parser.spec.ts | 29 ++ .../src/batch/jobs/shuttle/shuttle.parser.ts | 113 ++++++- .../batch/jobs/shuttle/shuttle.repository.ts | 2 +- .../shuttle/type/parsed-shuttle-timetable.ts | 18 + services/api/batch/tsconfig.json | 1 + 11 files changed, 529 insertions(+), 15 deletions(-) create mode 100644 services/api/batch/src/batch/database/database.module.ts create mode 100644 services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts delete mode 100644 services/api/batch/src/batch/jobs/shuttle/shuttle.interface.ts create mode 100644 services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts create mode 100644 services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts diff --git a/services/api/batch/package.json b/services/api/batch/package.json index 62363b1..6209540 100644 --- a/services/api/batch/package.json +++ b/services/api/batch/package.json @@ -15,11 +15,15 @@ }, "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^6.1.3", + "@nestjs/typeorm": "^11.0.1", + "pg": "^8.21.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^1.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/services/api/batch/pnpm-lock.yaml b/services/api/batch/pnpm-lock.yaml index 4374e76..f9bbd12 100644 --- a/services/api/batch/pnpm-lock.yaml +++ b/services/api/batch/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@nestjs/common': specifier: ^11.0.1 version: 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 version: 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -20,12 +23,21 @@ importers: '@nestjs/schedule': specifier: ^6.1.3 version: 6.1.3(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/typeorm': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.0.0(pg@8.21.0)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.9.3))) + pg: + specifier: ^8.21.0 + version: 8.21.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 rxjs: specifier: ^7.8.1 version: 7.8.2 + typeorm: + specifier: ^1.0.0 + version: 1.0.0(pg@8.21.0)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.9.3)) devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -668,6 +680,12 @@ packages: class-validator: optional: true + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/core@11.1.26': resolution: {integrity: sha512-K45zUwYpowEsVqm8qNIzsMcl4LJev0MK9zVhDnmym7YRTJ2/caslqVeKYhPRd5+Fh81IkvWUVu6vEo46uZ5mgQ==} engines: {node: '>= 20'} @@ -720,6 +738,15 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/typeorm@11.0.1': + resolution: {integrity: sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 || ^1.0.0-dev + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -744,6 +771,9 @@ packages: '@sinonjs/fake-timers@15.4.0': resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1351,6 +1381,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1444,6 +1478,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1490,6 +1527,18 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1507,6 +1556,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1775,6 +1827,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2419,6 +2475,40 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.14.0: + resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.21.0: + resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2442,6 +2532,22 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2620,9 +2726,17 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -2647,6 +2761,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2873,6 +2991,58 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeorm@1.0.0: + resolution: {integrity: sha512-2mSKNqucP8vo+xQLP59xlHUcqLvG6qajxA7q7tnhJgeZjTrA6lK/Ar7LRyiAxdXhyXmGbIPsArPmcUB9Xg+M7w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^8.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^12.0.0 + ioredis: ^5.0.4 + mongodb: ^7.0.0 + mssql: ^12.0.0 + mysql2: ^3.15.3 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^5.0.0 + sql.js: ^1.4.0 + ts-node: ^10.9.2 + typeorm-aurora-data-api-driver: ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + typescript-eslint@8.61.0: resolution: {integrity: sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2987,6 +3157,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2994,6 +3168,10 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3005,10 +3183,18 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3776,6 +3962,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/config@4.0.4(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + '@nestjs/core@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3828,6 +4022,14 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26) + '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.0.0(pg@8.21.0)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.9.3)))': + dependencies: + '@nestjs/common': 11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.26(@nestjs/common@11.1.26(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.26)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 1.0.0(pg@8.21.0)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.9.3)) + '@noble/hashes@1.8.0': {} '@paralleldrive/cuid2@2.3.1': @@ -3849,6 +4051,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sqltools/formatter@1.2.5': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -4512,6 +4716,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + clone@1.0.4: {} co@4.6.0: {} @@ -4589,6 +4799,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dayjs@1.11.21: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -4616,6 +4828,14 @@ snapshots: diff@4.0.4: {} + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4630,6 +4850,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -4945,6 +5167,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5721,6 +5945,41 @@ snapshots: path-type@4.0.0: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.13.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.21.0): + dependencies: + pg: 8.21.0 + + pg-protocol@1.14.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.21.0: + dependencies: + pg-connection-string: 2.13.0 + pg-pool: 3.14.0(pg@8.21.0) + pg-protocol: 1.14.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5735,6 +5994,16 @@ snapshots: pluralize@8.0.0: {} + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -5925,8 +6194,12 @@ snapshots: source-map@0.7.6: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} + sql-highlight@6.1.0: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -5952,6 +6225,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6144,6 +6423,25 @@ snapshots: typedarray@0.0.6: {} + typeorm@1.0.0(pg@8.21.0)(ts-node@10.9.2(@types/node@22.19.21)(typescript@5.9.3)): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 4.2.0 + dayjs: 1.11.21 + debug: 4.4.3 + dedent: 1.7.2 + reflect-metadata: 0.2.2 + sql-highlight: 6.1.0 + tinyglobby: 0.2.17 + tslib: 2.8.1 + yargs: 18.0.0 + optionalDependencies: + pg: 8.21.0 + ts-node: 10.9.2(@types/node@22.19.21)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + typescript-eslint@8.61.0(eslint@9.39.4)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) @@ -6303,6 +6601,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} write-file-atomic@5.0.1: @@ -6310,12 +6614,16 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -6326,6 +6634,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/services/api/batch/src/batch/database/database.module.ts b/services/api/batch/src/batch/database/database.module.ts new file mode 100644 index 0000000..c46759f --- /dev/null +++ b/services/api/batch/src/batch/database/database.module.ts @@ -0,0 +1,25 @@ +// import { Module } from '@nestjs/common'; +// import { ConfigService } from '@nestjs/config'; +// import { TypeOrmModule } from '@nestjs/typeorm'; +// import * as path from 'path'; + +// @Module({ +// imports: [ +// TypeOrmModule.forRootAsync({ +// inject: [ConfigService], +// useFactory: (configService: ConfigService) => ({ +// type: 'postgres', +// host: configService.get('DB_HOST'), +// port: Number(configService.get('DB_PORT')), +// username: configService.get('DB_USERNAME'), +// password: configService.get('DB_PASSWORD'), +// database: configService.get('DB_DATABASE'), +// synchronize: false, +// entities: [path.join(__dirname, '../../**/*.entity{.ts,.js}')], +// }), +// }), +// ], +// controllers: [], +// providers: [], +// }) +// export class DatabaseModule {} diff --git a/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts b/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts new file mode 100644 index 0000000..d9a7c15 --- /dev/null +++ b/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts @@ -0,0 +1,22 @@ +import { + Column, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import type { ShuttleTimetableMap } from '../type/parsed-shuttle-timetable'; + +@Entity('shuttle_timetable') +export class ShuttleTimetable { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'route_name', length: 100, unique: true }) + routeName: string; + + @Column({ type: 'jsonb' }) + timetable: ShuttleTimetableMap; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' }) + updatedAt: Date; +} diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.interface.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.interface.ts deleted file mode 100644 index e9209c1..0000000 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - "오전": ["08:20", "09:00 (금요일 미운행)"], - "오후": ["13:10 (금요일 미운행)", "13:40"], - */ -export type ShuttleTimetableMap = Record; - -export interface ShuttleTimetable { - routeName: string; - timetable?: Record; -} diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts index b0dc26a..58fa47c 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts @@ -17,6 +17,7 @@ export class ShuttleJob implements BatchJob { async run(): Promise { const raw = await this.client.fetch(); const shuttles = this.parser.parse(raw); + console.log(shuttles); //await this.repository.save(shuttles); } } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts new file mode 100644 index 0000000..4bd1d9a --- /dev/null +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts @@ -0,0 +1,29 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { ShuttleParser } from './shuttle.parser'; + +describe('ShuttleParser', () => { + const parser = new ShuttleParser(); + + it('교내 셔틀 시간표 HTML을 방향별 시간표로 변환한다', () => { + const html = readFileSync(join(__dirname, '__fixtures__/shuttle-timetable.html'), 'utf8'); + + const result = parser.parse(html); + + expect(result).toHaveLength(2); + expect(result[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); + expect(result[0].timetable.오전).toEqual( + expect.arrayContaining(['08:20', '09:00 (금요일 미운행)', '09:30']), + ); + expect(result[0].timetable.오후).toEqual( + expect.arrayContaining(['13:10 (금요일 미운행)', '13:40', '13:50', '17:30']), + ); + expect(result[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); + expect(result[1].timetable.오전).toEqual( + expect.arrayContaining(['08:05', '08:10', '08:15']), + ); + expect(result[1].timetable.오후).toEqual( + expect.arrayContaining(['13:00', '13:10', '13:20']), + ); + }); +}); diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts index f534049..4155e7e 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts @@ -1,9 +1,116 @@ import { Injectable } from '@nestjs/common'; -import { ShuttleTimetable } from './shuttle.interface'; +import { + ParsedShuttleTimetable, + ShuttleTimetableMap, +} from './type/parsed-shuttle-timetable'; @Injectable() export class ShuttleParser { - parse(_raw: string): ShuttleTimetable[] { - return []; + parse(raw: string): ParsedShuttleTimetable[] { + return this.extractTables(raw) + .map((table) => this.parseTable(table)) + .filter((timetable): timetable is ParsedShuttleTimetable => timetable !== null); + } + + private parseTable(table: string): ParsedShuttleTimetable | null { + const rows = this.extractRows(table); + const headerIndex = rows.findIndex((row) => { + const headers = this.extractCells(row, 'th'); + + return headers.includes('오전') && headers.includes('오후'); + }); + + if (headerIndex === -1) { + return null; + } + + const routeName = this.extractRouteName(rows.slice(0, headerIndex)); + + if (!routeName) { + return null; + } + + const timeGroups = this.extractCells(rows[headerIndex], 'th'); + const timetable = rows.slice(headerIndex + 1).reduce( + (acc, row) => { + const times = this.extractCells(row, 'td', { removeEmpty: false }); + + timeGroups.forEach((timeGroup, index) => { + const time = times[index]; + + if (!time) { + return; + } + + acc[timeGroup] = [...(acc[timeGroup] ?? []), time]; + }); + + return acc; + }, + {}, + ); + + return { routeName, timetable }; + } + + private extractTables(raw: string): string[] { + return raw.match(//gi) ?? []; + } + + private extractRows(table: string): string[] { + return table.match(//gi) ?? []; + } + + private extractCells( + row: string, + tagName: 'td' | 'th', + options: { removeEmpty?: boolean } = { removeEmpty: true }, + ): string[] { + const cellRegex = new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'gi'); + const cells = [...row.matchAll(cellRegex)]; + const texts = cells.map((cell) => this.normalizeText(cell[1])); + + if (options.removeEmpty === false) { + return texts; + } + + return texts.filter((cell) => cell.length > 0); + } + + private extractRouteName(rows: string[]): string | null { + for (const row of rows) { + const routeName = this.extractCells(row, 'th')[0] + ?.split('\n')[0] + ?.replace(/\s*\(출발지:[^)]+\).*$/, '') + .trim(); + + if (routeName?.includes('→')) { + return routeName; + } + } + + return null; + } + + private normalizeText(html: string): string { + return this.decodeHtmlEntities(html) + .replace(//gi, '\n') + .replace(/<[^>]*>/g, ' ') + .replace(/[ \t\r\f\v]+/g, ' ') + .replace(/ *\n */g, '\n') + .replace(/(\d{1,2})\s*:\s*(\d)\s+(\d)/g, '$1:$2$3') + .replace(/(\d{1,2})\s*:\s*(\d{2})/g, '$1:$2') + .replace(/\s+(\([^)]*\))/g, ' $1') + .trim(); + } + + private decodeHtmlEntities(text: string): string { + return text + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'/gi, "'"); } } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts index 537405a..f55d302 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ShuttleTimetable } from './shuttle.interface'; +import { ShuttleTimetable } from './domain/shuttle-timetable.entity'; @Injectable() export class ShuttleRepository { diff --git a/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts b/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts new file mode 100644 index 0000000..5432bfa --- /dev/null +++ b/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts @@ -0,0 +1,18 @@ +/** + * HTML 셔틀 시간표를 파싱한 저장 전 중간 데이터입니다. + * + * 예: + * { + * routeName: '가좌캠퍼스 → 칠암캠퍼스', + * timetable: { + * 오전: ['08:20', '09:00 (금요일 미운행)'], + * 오후: ['13:10 (금요일 미운행)', '13:40'], + * }, + * } + */ +export type ShuttleTimetableMap = Record; + +export interface ParsedShuttleTimetable { + routeName: string; + timetable: ShuttleTimetableMap; +} diff --git a/services/api/batch/tsconfig.json b/services/api/batch/tsconfig.json index 1bc8823..448afa9 100644 --- a/services/api/batch/tsconfig.json +++ b/services/api/batch/tsconfig.json @@ -16,6 +16,7 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, + "strictPropertyInitialization": false, "skipLibCheck": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, From e7e886fec5377bb6d08fff5987aa095503939d8e Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Sun, 14 Jun 2026 21:42:45 +0900 Subject: [PATCH 18/29] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=85=94=ED=8B=80=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=ED=8C=8C=EC=84=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/batch/jobs/shuttle/shuttle.job.ts | 4 +- .../batch/jobs/shuttle/shuttle.parser.spec.ts | 41 ++++++++++++---- .../src/batch/jobs/shuttle/shuttle.parser.ts | 47 +++++++++++++++---- .../shuttle/type/parsed-shuttle-timetable.ts | 5 ++ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts index 58fa47c..654b00b 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts @@ -16,8 +16,8 @@ export class ShuttleJob implements BatchJob { async run(): Promise { const raw = await this.client.fetch(); - const shuttles = this.parser.parse(raw); - console.log(shuttles); + const parsedPage = this.parser.parse(raw); + console.log(parsedPage); //await this.repository.save(shuttles); } } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts index 4bd1d9a..a473115 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts @@ -5,25 +5,46 @@ import { ShuttleParser } from './shuttle.parser'; describe('ShuttleParser', () => { const parser = new ShuttleParser(); - it('교내 셔틀 시간표 HTML을 방향별 시간표로 변환한다', () => { - const html = readFileSync(join(__dirname, '__fixtures__/shuttle-timetable.html'), 'utf8'); + it('교내 셔틀 버스 노선 2개의 시간표를 파싱한다', () => { + const html = readFileSync( + join(__dirname, '__fixtures__/shuttle-timetable.html'), + 'utf8', + ); const result = parser.parse(html); - expect(result).toHaveLength(2); - expect(result[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); - expect(result[0].timetable.오전).toEqual( + expect(result.updatedAt).toEqual(new Date(2026, 2, 13, 9, 47, 30)); + expect(result.timetables).toHaveLength(2); + expect(result.timetables[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); + expect(result.timetables[0].timetable.오전).toEqual( expect.arrayContaining(['08:20', '09:00 (금요일 미운행)', '09:30']), ); - expect(result[0].timetable.오후).toEqual( - expect.arrayContaining(['13:10 (금요일 미운행)', '13:40', '13:50', '17:30']), + expect(result.timetables[0].timetable.오후).toEqual( + expect.arrayContaining([ + '13:10 (금요일 미운행)', + '13:40', + '13:50', + '17:30', + ]), ); - expect(result[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); - expect(result[1].timetable.오전).toEqual( + expect(result.timetables[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); + expect(result.timetables[1].timetable.오전).toEqual( expect.arrayContaining(['08:05', '08:10', '08:15']), ); - expect(result[1].timetable.오후).toEqual( + expect(result.timetables[1].timetable.오후).toEqual( expect.arrayContaining(['13:00', '13:10', '13:20']), ); }); + + it('최근 업데이트 일시가 없으면 에러를 던진다', () => { + expect(() => parser.parse('')).toThrow( + '셔틀 시간표 테이블을 찾을 수 없습니다.', + ); + }); + + it('최근 업데이트 일시는 있지만 시간표 테이블이 없으면 에러를 던진다', () => { + expect(() => + parser.parse('최근 업데이트 일시 : 2026/03/13 09:47:30'), + ).toThrow('셔틀 시간표 테이블을 찾을 수 없습니다.'); + }); }); diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts index 4155e7e..bd2e301 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.ts @@ -1,15 +1,27 @@ import { Injectable } from '@nestjs/common'; import { ParsedShuttleTimetable, + ParsedShuttleTimetablePage, ShuttleTimetableMap, } from './type/parsed-shuttle-timetable'; @Injectable() export class ShuttleParser { - parse(raw: string): ParsedShuttleTimetable[] { - return this.extractTables(raw) + parse(raw: string): ParsedShuttleTimetablePage { + const timetables = this.extractTables(raw) .map((table) => this.parseTable(table)) - .filter((timetable): timetable is ParsedShuttleTimetable => timetable !== null); + .filter( + (timetable): timetable is ParsedShuttleTimetable => timetable !== null, + ); + + if (timetables.length !== 2) { + throw new Error('셔틀 시간표 테이블을 찾을 수 없습니다.'); + } + + return { + updatedAt: this.extractUpdatedAt(raw), + timetables, + }; } private parseTable(table: string): ParsedShuttleTimetable | null { @@ -31,8 +43,9 @@ export class ShuttleParser { } const timeGroups = this.extractCells(rows[headerIndex], 'th'); - const timetable = rows.slice(headerIndex + 1).reduce( - (acc, row) => { + const timetable = rows + .slice(headerIndex + 1) + .reduce((acc, row) => { const times = this.extractCells(row, 'td', { removeEmpty: false }); timeGroups.forEach((timeGroup, index) => { @@ -46,9 +59,7 @@ export class ShuttleParser { }); return acc; - }, - {}, - ); + }, {}); return { routeName, timetable }; } @@ -66,7 +77,10 @@ export class ShuttleParser { tagName: 'td' | 'th', options: { removeEmpty?: boolean } = { removeEmpty: true }, ): string[] { - const cellRegex = new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'gi'); + const cellRegex = new RegExp( + `<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, + 'gi', + ); const cells = [...row.matchAll(cellRegex)]; const texts = cells.map((cell) => this.normalizeText(cell[1])); @@ -92,6 +106,21 @@ export class ShuttleParser { return null; } + private extractUpdatedAt(raw: string): Date { + const updatedAtMatch = raw.match( + /최근\s*업데이트\s*일시\s*:\s*(\d{4})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/, + ); + + if (!updatedAtMatch) { + throw new Error('셔틀 시간표 최근 업데이트 일시를 찾을 수 없습니다.'); + } + + const [, year, month, day, hour, minute, second] = + updatedAtMatch.map(Number); + + return new Date(year, month - 1, day, hour, minute, second); + } + private normalizeText(html: string): string { return this.decodeHtmlEntities(html) .replace(//gi, '\n') diff --git a/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts b/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts index 5432bfa..621a8ff 100644 --- a/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts +++ b/services/api/batch/src/batch/jobs/shuttle/type/parsed-shuttle-timetable.ts @@ -16,3 +16,8 @@ export interface ParsedShuttleTimetable { routeName: string; timetable: ShuttleTimetableMap; } + +export interface ParsedShuttleTimetablePage { + updatedAt: Date; + timetables: ParsedShuttleTimetable[]; +} From d409159b9e1e09237306fe59a9ed7fc953d875c2 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Mon, 15 Jun 2026 11:08:29 +0900 Subject: [PATCH 19/29] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20HTTP=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EA=B0=9C=EC=84=A0,=20?= =?UTF-8?q?=EC=85=94=ED=8B=80=20=EC=8B=9C=EA=B0=84=ED=91=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lint-staged.config.js | 1 + services/api/batch/src/batch/batch.service.ts | 24 +++- .../batch/src/batch/http/fetch-http.client.ts | 22 +++- .../domain/shuttle-timetable.entity.ts | 24 ++-- .../src/batch/jobs/shuttle/shuttle.job.ts | 3 +- .../batch/jobs/shuttle/shuttle.parser.spec.ts | 113 ++++++++++++------ services/api/batch/src/main.ts | 6 +- 7 files changed, 139 insertions(+), 54 deletions(-) diff --git a/lint-staged.config.js b/lint-staged.config.js index 4eae2eb..1d4b7cf 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -14,4 +14,5 @@ const projectCommands = (projectDir, files) => { module.exports = { 'services/api/app/**/*.{js,ts}': files => projectCommands('services/api/app', files), 'mobile/**/*.{js,jsx,ts,tsx}': files => projectCommands('mobile', files), + 'services/api/batch/**/*.{js,ts}': files => projectCommands('services/api/batch', files), }; diff --git a/services/api/batch/src/batch/batch.service.ts b/services/api/batch/src/batch/batch.service.ts index 96dcf51..ae4ce3f 100644 --- a/services/api/batch/src/batch/batch.service.ts +++ b/services/api/batch/src/batch/batch.service.ts @@ -4,6 +4,9 @@ import { BATCH_JOBS, BatchJob } from './jobs/batch-job.interface'; @Injectable() export class BatchService implements OnApplicationBootstrap { + private readonly name = 'BatchService'; + private isRunning = false; + constructor( @Inject(BATCH_JOBS) private readonly jobs: BatchJob[], @@ -11,8 +14,27 @@ export class BatchService implements OnApplicationBootstrap { @Cron('0 0 * * * *') // 매 정각마다 실행 async run(): Promise { + if (this.isRunning) { + console.warn( + `[${this.name}] 이전 작업이 아직 완료되지 않았습니다. 이번 실행을 건너뜁니다.`, + ); + return; + } + + this.isRunning = true; + for (const job of this.jobs) { - await job.run(); + try { + await job.run(); + } catch (error) { + // TODO: 에러 로깅 및 알림 시스템 연동 + console.error( + `[${this.name}] 잡 실행 중 에러 발생: ${job.name}`, + error, + ); + } finally { + this.isRunning = false; + } } } diff --git a/services/api/batch/src/batch/http/fetch-http.client.ts b/services/api/batch/src/batch/http/fetch-http.client.ts index d6896cf..9302d29 100644 --- a/services/api/batch/src/batch/http/fetch-http.client.ts +++ b/services/api/batch/src/batch/http/fetch-http.client.ts @@ -72,7 +72,7 @@ export class FetchHttpClient implements HttpClient { options: HttpRequestOptions, timeoutMs: number, ): Promise { - const { retry: _retry, signal, ...fetchOptions } = options; + const { signal, ...fetchOptions } = options; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); @@ -91,10 +91,24 @@ export class FetchHttpClient implements HttpClient { } private getRetryOptions(retry?: RetryOptions): Required { + const retries = retry?.retries ?? DEFAULT_RETRIES; + const retryDelayMs = retry?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS; + const timeoutMs = retry?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + + if (!Number.isInteger(retries) || retries < 0) { + throw new HttpRequestError('Invalid retry.retries: must be >= 0'); + } + if (retryDelayMs < 0) { + throw new HttpRequestError('Invalid retry.retryDelayMs: must be >= 0'); + } + if (timeoutMs <= 0) { + throw new HttpRequestError('Invalid retry.timeoutMs: must be > 0'); + } + return { - retries: retry?.retries ?? DEFAULT_RETRIES, - retryDelayMs: retry?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS, - timeoutMs: retry?.timeoutMs ?? DEFAULT_TIMEOUT_MS, + retries, + retryDelayMs, + timeoutMs, retryOnStatuses: retry?.retryOnStatuses ?? DEFAULT_RETRY_ON_STATUSES, }; } diff --git a/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts b/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts index d9a7c15..0dedb24 100644 --- a/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts +++ b/services/api/batch/src/batch/jobs/shuttle/domain/shuttle-timetable.entity.ts @@ -1,9 +1,4 @@ -import { - Column, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import type { ShuttleTimetableMap } from '../type/parsed-shuttle-timetable'; @Entity('shuttle_timetable') @@ -14,9 +9,22 @@ export class ShuttleTimetable { @Column({ name: 'route_name', length: 100, unique: true }) routeName: string; - @Column({ type: 'jsonb' }) + @Column({ type: 'jsonb', nullable: false }) timetable: ShuttleTimetableMap; - @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' }) + @Column({ + name: 'updated_at', + type: 'timestamp', + nullable: false, + default: () => 'CURRENT_TIMESTAMP', + }) updatedAt: Date; + + @Column({ + name: 'last_success_at', + type: 'timestamp', + nullable: false, + default: () => 'CURRENT_TIMESTAMP', + }) + lastSuccessAt: Date; } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts index 654b00b..86bebf0 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.job.ts @@ -16,8 +16,7 @@ export class ShuttleJob implements BatchJob { async run(): Promise { const raw = await this.client.fetch(); - const parsedPage = this.parser.parse(raw); - console.log(parsedPage); + this.parser.parse(raw); //await this.repository.save(shuttles); } } diff --git a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts index a473115..560110e 100644 --- a/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts +++ b/services/api/batch/src/batch/jobs/shuttle/shuttle.parser.spec.ts @@ -4,47 +4,84 @@ import { ShuttleParser } from './shuttle.parser'; describe('ShuttleParser', () => { const parser = new ShuttleParser(); + const fixture = readFileSync( + join(__dirname, '__fixtures__/shuttle-timetable.html'), + 'utf8', + ); + const htmlWithTablesOnly = ` + + + + + +
가좌캠퍼스 → 칠암캠퍼스
(출발지: 가좌캠퍼스)
오전오후
08 : 2013 : 10
+ + + + +
칠암캠퍼스 → 가좌캠퍼스
(출발지: 칠암캠퍼스)
오전오후
08 : 0513 : 00
+ + `; - it('교내 셔틀 버스 노선 2개의 시간표를 파싱한다', () => { - const html = readFileSync( - join(__dirname, '__fixtures__/shuttle-timetable.html'), - 'utf8', - ); - - const result = parser.parse(html); - - expect(result.updatedAt).toEqual(new Date(2026, 2, 13, 9, 47, 30)); - expect(result.timetables).toHaveLength(2); - expect(result.timetables[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); - expect(result.timetables[0].timetable.오전).toEqual( - expect.arrayContaining(['08:20', '09:00 (금요일 미운행)', '09:30']), - ); - expect(result.timetables[0].timetable.오후).toEqual( - expect.arrayContaining([ - '13:10 (금요일 미운행)', - '13:40', - '13:50', - '17:30', - ]), - ); - expect(result.timetables[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); - expect(result.timetables[1].timetable.오전).toEqual( - expect.arrayContaining(['08:05', '08:10', '08:15']), - ); - expect(result.timetables[1].timetable.오후).toEqual( - expect.arrayContaining(['13:00', '13:10', '13:20']), - ); - }); + describe('정상 HTML을 파싱할 때', () => { + const result = parser.parse(fixture); + + it('최근 업데이트 일시를 Date로 변환한다', () => { + expect(result.updatedAt).toEqual(new Date(2026, 2, 13, 9, 47, 30)); + }); + + it('캠퍼스 간 셔틀 노선 2개를 찾는다', () => { + expect(result.timetables).toHaveLength(2); + expect(result.timetables.map((timetable) => timetable.routeName)).toEqual( + ['가좌캠퍼스 → 칠암캠퍼스', '칠암캠퍼스 → 가좌캠퍼스'], + ); + }); + + it('가좌캠퍼스에서 칠암캠퍼스로 가는 오전 시간표를 파싱한다', () => { + expect(result.timetables[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); + expect(result.timetables[0].timetable.오전).toEqual( + expect.arrayContaining(['08:20', '09:00 (금요일 미운행)', '09:30']), + ); + }); - it('최근 업데이트 일시가 없으면 에러를 던진다', () => { - expect(() => parser.parse('')).toThrow( - '셔틀 시간표 테이블을 찾을 수 없습니다.', - ); + it('가좌캠퍼스에서 칠암캠퍼스로 가는 오후 시간표를 파싱한다', () => { + expect(result.timetables[0].routeName).toBe('가좌캠퍼스 → 칠암캠퍼스'); + expect(result.timetables[0].timetable.오후).toEqual( + expect.arrayContaining([ + '13:10 (금요일 미운행)', + '13:40', + '13:50', + '17:30', + ]), + ); + }); + + it('칠암캠퍼스에서 가좌캠퍼스로 가는 오전 시간표를 파싱한다', () => { + expect(result.timetables[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); + expect(result.timetables[1].timetable.오전).toEqual( + expect.arrayContaining(['08:05', '08:10', '08:15']), + ); + }); + + it('칠암캠퍼스에서 가좌캠퍼스로 가는 오후 시간표를 파싱한다', () => { + expect(result.timetables[1].routeName).toBe('칠암캠퍼스 → 가좌캠퍼스'); + expect(result.timetables[1].timetable.오후).toEqual( + expect.arrayContaining(['13:00', '13:10', '13:20']), + ); + }); }); - it('최근 업데이트 일시는 있지만 시간표 테이블이 없으면 에러를 던진다', () => { - expect(() => - parser.parse('최근 업데이트 일시 : 2026/03/13 09:47:30'), - ).toThrow('셔틀 시간표 테이블을 찾을 수 없습니다.'); + describe('필수 HTML 요소가 없을 때', () => { + it('최근 업데이트 일시가 없으면 에러를 던진다', () => { + expect(() => parser.parse(htmlWithTablesOnly)).toThrow( + '셔틀 시간표 최근 업데이트 일시를 찾을 수 없습니다.', + ); + }); + + it('최근 업데이트 일시는 있지만 시간표 테이블이 없으면 에러를 던진다', () => { + expect(() => + parser.parse('최근 업데이트 일시 : 2026/03/13 09:47:30'), + ).toThrow('셔틀 시간표 테이블을 찾을 수 없습니다.'); + }); }); }); diff --git a/services/api/batch/src/main.ts b/services/api/batch/src/main.ts index d25386f..c4a332d 100644 --- a/services/api/batch/src/main.ts +++ b/services/api/batch/src/main.ts @@ -5,4 +5,8 @@ async function bootstrap() { const app = await NestFactory.create(BatchModule); await app.init(); } -bootstrap(); + +bootstrap().catch((error) => { + console.error('[main] 배치 애플리케이션 실행 중 에러 발생:', error); + process.exit(1); +}); From ff158068c158448887372238a69ee8dc2b57fffe Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:43 +0900 Subject: [PATCH 20/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20API=20CI/CD=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20AWS=20EC2=20?= =?UTF-8?q?=EC=9D=B4=EC=A0=84=20=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ...026-09-24-api-cicd-aws-migration-design.md | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md diff --git a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md new file mode 100644 index 0000000..db4c530 --- /dev/null +++ b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md @@ -0,0 +1,218 @@ +# API CI/CD 리팩토링 및 AWS EC2 이전 설계 + +- 작성일: 2026-09-24 +- 대상: `.github/workflows/api-ci.yml`, `.github/workflows/api-cd.yml`, `services/api/docker-compose.yml` + +## 1. 목표 + +API 서버(app + nginx) 배포 대상을 GCP VM에서 AWS EC2(t3.micro, amd64, Amazon Linux)로 옮기고, 그 과정에서 GitHub Actions CI/CD를 정리한다. + +성공 기준: + +- `main` push 시 lint/test/build를 통과한 커밋만 EC2에 배포된다. +- 배포 후 app 컨테이너가 `healthy`가 되지 않으면 워크플로가 실패한다. +- `nginx/prod.conf` 변경이 배포 시 실제로 반영된다. +- EC2에서 GCP 전용 설정(`gcplogs`) 없이 모든 컨테이너가 기동한다. + +## 2. 범위 + +포함: + +- API(app + nginx) CI/CD 워크플로 수정 +- `services/api/docker-compose.yml`의 로깅·nginx 볼륨 수정 +- 배포 전 테스트 게이트, 배포 후 헬스체크 대기, nginx 설정 반영, 액션 버전·concurrency 정리 + +제외: + +- batch 서비스, monitoring 배포 +- DNS 전환 및 GCP 서버 정리 +- 자동 롤백, SHA 태그 기반 배포 +- OIDC/SSM 등 SSH 외 접속 방식, CloudWatch 로깅 + +## 3. 결정 사항 + +| 항목 | 결정 | 이유 | +|---|---|---| +| EC2 접근 | SSH 키 (`appleboy/scp-action`, `appleboy/ssh-action`) | 기존 흐름 유지, 마이그레이션 위험 최소화 | +| 로깅 | `json-file` + 로테이션 (`10m` × 3) | 추가 AWS 설정 없음, 에러 추적은 Sentry가 담당 | +| 파일 동기화 | `docker-compose.yml`, `nginx/prod.conf`만 | `letsencrypt/`는 root 소유이고 인증서는 certbot이 관리 | +| 워크플로 구조 | CI를 `workflow_call`로 재사용, CD가 호출 | CI 정의 단일화, 파일 역할 유지 | +| 이미지 레지스트리 | DockerHub 유지 | 변경 필요 없음 | +| 빌드 아키텍처 | amd64 (기존 `ubuntu-latest`) | t3.micro는 x86_64 | + +## 4. 전체 흐름 + +``` +PR (main/dev) → api-ci.yml : verify (lint → test → build) +push main / 수동 실행 → api-cd.yml : ci (api-ci.yml 호출) → build-and-push → deploy +``` + +## 5. CI: `api-ci.yml` + +트리거: + +- `pull_request`: `main`, `dev` 대상, `paths`: `services/api/app/**`, `.github/workflows/api-ci.yml` +- `workflow_call` (신규) +- `workflow_dispatch` + +설정: + +- `permissions: contents: read` +- `concurrency: { group: api-ci-${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }` + - `workflow_call`로 호출되면 `github.workflow`는 호출자 이름(`API CD`)이 되므로 CD의 concurrency 그룹(`api-cd-...`)과 겹치지 않는다. + +Job `verify` (기존 `lint-and-test` 이름 변경), `working-directory: services/api/app`: + +1. checkout +2. pnpm 9.9.0 설치 +3. Node 24 설정, pnpm 캐시 (`cache-dependency-path: services/api/app/pnpm-lock.yaml`) +4. `pnpm install --frozen-lockfile` +5. `pnpm lint:check` +6. `pnpm test` +7. `pnpm build` (신규) + +액션(`actions/checkout`, `actions/setup-node`, `pnpm/action-setup`)은 구현 시점의 최신 메이저 태그로 올린다. + +## 6. CD: `api-cd.yml` + +트리거: + +- `push`: `main`, `paths`: + - `services/api/app/src/**` + - `services/api/app/package.json` + - `services/api/app/pnpm-lock.yaml` + - `services/api/app/Dockerfile` + - `services/api/app/.dockerignore` (신규) + - `services/api/app/tsconfig*.json` (신규) + - `services/api/app/nest-cli.json` (신규) + - `services/api/docker-compose.yml` + - `services/api/nginx/**` + - `.github/workflows/api-cd.yml` +- `workflow_dispatch` + +설정: + +- `permissions: contents: read` +- `concurrency: { group: api-cd-${{ github.ref }}, cancel-in-progress: false }` — 진행 중인 배포를 취소하지 않고 대기시킨다. +- `env.DEPLOY_DIR: /opt/connectgnu/api` + +### 6.1 Job `ci` + +`uses: ./.github/workflows/api-ci.yml`. 실패 시 이후 job은 실행되지 않는다. + +### 6.2 Job `build-and-push` (`needs: ci`) + +1. checkout +2. `docker/setup-buildx-action` +3. `docker/login-action` (DockerHub) +4. `docker/build-push-action` + - `context: services/api/app` + - `target: prod` (신규, 명시) + - `push: true`, `pull: true` + - 태그: `${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}:latest`, `:${{ github.sha }}` + - `cache-from: type=gha`, `cache-to: type=gha,mode=max` + +docker 액션은 구현 시점의 최신 메이저 태그로 올린다. + +### 6.3 Job `deploy` (`needs: build-and-push`) + +1. checkout +2. `appleboy/scp-action` (최신 v1) + - `source: services/api/docker-compose.yml,services/api/nginx/prod.conf` + - `target: ${{ env.DEPLOY_DIR }}` + - `strip_components: 2` +3. `appleboy/ssh-action` (최신 v1), 스크립트: + +```bash +set -euo pipefail +cd /opt/connectgnu/api # 워크플로에서는 ${{ env.DEPLOY_DIR }} + +docker compose pull app +docker compose up -d --no-deps app + +# 헬스체크 대기 (최대 약 90초) +cid=$(docker compose ps -q app) +status="" +for i in $(seq 1 30); do + status=$(docker inspect -f '{{.State.Health.Status}}' "$cid") + [ "$status" = healthy ] && break + [ "$status" = unhealthy ] && break + sleep 3 +done +if [ "$status" != healthy ]; then + echo "app health status: $status" + docker compose logs --tail 100 app + exit 1 +fi + +# nginx 설정 반영 +docker compose up -d --no-deps nginx +docker compose exec -T nginx nginx -t +docker compose exec -T nginx nginx -s reload + +docker image prune -f +``` + +동작 규칙: + +- 헬스체크 실패 시 로그를 출력하고 실패 처리한다. nginx reload와 prune은 실행하지 않는다. 자동 롤백은 하지 않는다. +- `nginx -t`가 실패하면 reload하지 않고 워크플로가 실패한다. 기존 nginx 프로세스는 이전 설정으로 계속 동작한다. +- `sudo`를 쓰지 않는다 (`ec2-user`가 `docker` 그룹 소속이라는 전제). +- `docker image prune -af` 대신 `-f`를 사용해 dangling 이미지만 정리한다. + +## 7. `services/api/docker-compose.yml` 변경 + +1. 로깅을 공통 앵커로 정의하고 app, nginx, certbot에 적용한다. + + ```yaml + x-logging: &default-logging + driver: json-file + options: + max-size: "10m" + max-file: "3" + ``` + + app, nginx의 `gcplogs` 설정을 제거한다. + +2. nginx 설정을 단일 파일 마운트에서 디렉터리 마운트로 변경한다. + + ```yaml + # before + - ./nginx/prod.conf:/etc/nginx/conf.d/default.conf + # after + - ./nginx:/etc/nginx/conf.d:ro + ``` + + 이유: scp-action은 tar 해제로 파일을 새로 생성하므로 inode가 바뀐다. 단일 파일 bind mount는 기존 inode를 계속 참조하므로 `nginx -s reload`를 해도 새 설정이 반영되지 않는다. 디렉터리 마운트는 이 문제가 없다. + +3. 이미지, healthcheck, certbot, 네트워크 설정은 유지한다. `docker-compose.dev.yml`은 변경하지 않는다. + +## 8. GitHub Secrets + +| Secret | 값 | +|---|---| +| `API_SSH_HOST` | EC2 Elastic IP (또는 고정 DNS) | +| `API_SSH_USERNAME` | `ec2-user` | +| `API_SSH_PRIVATE_KEY` | EC2 키 페어 개인키 | +| `DOCKER_USERNAME`, `DOCKER_PASSWORD`, `DOCKER_IMAGE_NAME` | 유지 | + +## 9. 서버 사전 조건 (수동, 1회) + +- [ ] Docker 및 compose 플러그인 설치 (`docker compose version` 동작) +- [ ] `ec2-user`를 `docker` 그룹에 추가 +- [ ] `/opt/connectgnu/api` 소유자 `ec2-user`, `.env` 존재 +- [ ] `/opt/connectgnu/api/nginx/`에 `prod.conf` 하나만 존재 (다른 `.conf`가 있으면 server 블록 중복) +- [ ] `/opt/connectgnu/api/letsencrypt/conf`에 인증서 발급 완료 +- [ ] 보안 그룹 인바운드: 22 (키 인증만 허용), 80, 443 +- [ ] Elastic IP 연결 + +## 10. 검증 + +- 로컬: `actionlint`로 두 워크플로 검사, `docker compose -f services/api/docker-compose.yml config`로 compose 검증 +- PR(`dev` 대상): CI `verify`가 lint → test → build 순서로 통과하는지 확인 +- `main` 머지 후: CD가 `ci` → `build-and-push` → `deploy` 순서로 실행되고 헬스 대기를 통과하는지 확인, `curl https://<도메인>/api/health` 응답 확인 + +## 11. 주의 사항 + +- `workflow_dispatch`는 선택한 브랜치의 코드를 배포한다. 수동 배포 시 `main`을 선택한다. +- GitHub Secrets를 EC2 값으로 바꾸는 시점부터 GCP 서버로는 배포되지 않는다. DNS 전환 시점과 맞춘다. From 4d5f3a9cbcbefd7d9804ba4be480105cd805e7d2 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:43 +0900 Subject: [PATCH 21/29] =?UTF-8?q?=F0=9F=94=A7=20chore:=20API=20=EC=9A=B4?= =?UTF-8?q?=EC=98=81=20compose=20=EB=B0=8F=20nginx=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20EC2=20=EC=84=9C=EB=B2=84=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=EB=B3=B8=EC=9C=BC=EB=A1=9C=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app healthcheck, nginx depends_on 유지 - gcplogs 제거, 전 서비스 json-file 로깅(10m x 3)으로 통일 - prod.conf 도메인 api.connectgnu.kro.kr로 통일, /api/metrics 접근 제한 유지 Co-Authored-By: Claude Opus 5.5 --- services/api/docker-compose.yml | 22 +++++++++------- services/api/nginx/prod.conf | 46 ++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/services/api/docker-compose.yml b/services/api/docker-compose.yml index 2783e2b..bce9e45 100644 --- a/services/api/docker-compose.yml +++ b/services/api/docker-compose.yml @@ -4,28 +4,29 @@ services: pull_policy: always restart: always expose: - - "3000" + - '3000' env_file: - .env networks: - connect-gnu-network healthcheck: - test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/api/health || exit 1"] + test: ['CMD-SHELL', 'curl -fsS http://127.0.0.1:3000/api/health || exit 1'] interval: 10s timeout: 3s retries: 3 start_period: 20s logging: - driver: gcplogs + driver: json-file options: - gcp-log-cmd: "true" + max-size: '10m' + max-file: '3' nginx: image: nginx:1.29.5-alpine restart: always ports: - - "80:80" - - "443:443" + - '80:80' + - '443:443' volumes: - ./nginx/prod.conf:/etc/nginx/conf.d/default.conf - ./letsencrypt/conf:/etc/letsencrypt @@ -36,9 +37,10 @@ services: depends_on: - app logging: - driver: gcplogs + driver: json-file options: - gcp-log-cmd: "true" + max-size: '10m' + max-file: '3' certbot: image: certbot/certbot:v5.3.1 @@ -50,8 +52,8 @@ services: logging: driver: json-file options: - max-size: "10m" - max-file: "3" + max-size: '10m' + max-file: '3' networks: connect-gnu-network: diff --git a/services/api/nginx/prod.conf b/services/api/nginx/prod.conf index db8d040..ab4e4de 100644 --- a/services/api/nginx/prod.conf +++ b/services/api/nginx/prod.conf @@ -2,44 +2,41 @@ upstream app { server app:3000; } -# HTTP 설정 +# HTTP server { listen 80; - server_name connectgnu.kro.kr; - underscores_in_headers on; + server_name api.connectgnu.kro.kr; + underscores_in_headers on; - location / { - return 301 https://$host$request_uri; - } - - # Let's Encrypt 인증서 발급을 위한 설정 + # Let's Encrypt 인증서 발급 location /.well-known/acme-challenge/ { - allow all; root /var/www/certbot; } + + # HTTP → HTTPS + location / { + return 301 https://$host$request_uri; + } } -# HTTPS 설정 +# HTTPS server { listen 443 ssl; http2 on; - server_name connectgnu.kro.kr; - - # SSL 인증서 설정 - ssl_certificate /etc/letsencrypt/live/connectgnu.kro.kr-0001/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/connectgnu.kro.kr-0001/privkey.pem; + server_name api.connectgnu.kro.kr; + underscores_in_headers on; - # SSL 보안 설정 - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + # SSL 인증서 + ssl_certificate /etc/letsencrypt/live/api.connectgnu.kro.kr-0001/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.connectgnu.kro.kr-0001/privkey.pem; # Swagger 경로 location /api/api-docs { proxy_pass http://app; proxy_http_version 1.1; - proxy_pass_request_headers on; + proxy_pass_request_headers on; proxy_set_header Connection ""; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; @@ -53,9 +50,9 @@ server { location /api/metrics { proxy_pass http://app; proxy_http_version 1.1; - proxy_pass_request_headers on; + proxy_pass_request_headers on; proxy_set_header Connection ""; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; @@ -68,15 +65,16 @@ server { location /api { proxy_pass http://app; proxy_http_version 1.1; - proxy_pass_request_headers on; + proxy_pass_request_headers on; proxy_set_header Connection ""; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; } + # 그 외 경로 차단 location / { deny all; } From cf529346dac72a3901a9018fc46bdd062ae5a9ae Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:43 +0900 Subject: [PATCH 22/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20API=20CI/CD=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C=EC=97=90=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B0=98=EC=98=81=20=EC=84=A4=EC=A0=95=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-api-cicd-aws-migration-design.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md index db4c530..39837e0 100644 --- a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md +++ b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md @@ -34,7 +34,7 @@ API 서버(app + nginx) 배포 대상을 GCP VM에서 AWS EC2(t3.micro, amd64, A | 항목 | 결정 | 이유 | |---|---|---| | EC2 접근 | SSH 키 (`appleboy/scp-action`, `appleboy/ssh-action`) | 기존 흐름 유지, 마이그레이션 위험 최소화 | -| 로깅 | `json-file` + 로테이션 (`10m` × 3) | 추가 AWS 설정 없음, 에러 추적은 Sentry가 담당 | +| 로깅 | `json-file` + 로테이션 (`10m` × 3), 서비스별 동일 블록 | 추가 AWS 설정 없음, 에러 추적은 Sentry가 담당 | | 파일 동기화 | `docker-compose.yml`, `nginx/prod.conf`만 | `letsencrypt/`는 root 소유이고 인증서는 certbot이 관리 | | 워크플로 구조 | CI를 `workflow_call`로 재사용, CD가 호출 | CI 정의 단일화, 파일 역할 유지 | | 이미지 레지스트리 | DockerHub 유지 | 변경 필요 없음 | @@ -162,18 +162,16 @@ docker image prune -f ## 7. `services/api/docker-compose.yml` 변경 -1. 로깅을 공통 앵커로 정의하고 app, nginx, certbot에 적용한다. +1. 로깅: app, nginx, certbot 모두 아래 블록을 서비스별로 명시한다 (적용 완료). ```yaml - x-logging: &default-logging + logging: driver: json-file options: max-size: "10m" max-file: "3" ``` - app, nginx의 `gcplogs` 설정을 제거한다. - 2. nginx 설정을 단일 파일 마운트에서 디렉터리 마운트로 변경한다. ```yaml @@ -185,7 +183,9 @@ docker image prune -f 이유: scp-action은 tar 해제로 파일을 새로 생성하므로 inode가 바뀐다. 단일 파일 bind mount는 기존 inode를 계속 참조하므로 `nginx -s reload`를 해도 새 설정이 반영되지 않는다. 디렉터리 마운트는 이 문제가 없다. -3. 이미지, healthcheck, certbot, 네트워크 설정은 유지한다. `docker-compose.dev.yml`은 변경하지 않는다. +3. 이미지, app healthcheck, nginx `depends_on: app`, certbot, 네트워크 설정은 유지한다. 배포 스크립트의 헬스 대기는 app healthcheck에 의존하므로 제거하면 안 된다. `docker-compose.dev.yml`은 변경하지 않는다. + +4. `nginx/prod.conf`는 서버 반영본 기준이다: 도메인 `api.connectgnu.kro.kr`, `/api/metrics`는 모니터링 서버 IP(`35.212.243.112`, GCP 유지)만 허용. ## 8. GitHub Secrets From b998539f3a63cac126a88ffd5bec3cb6d046e80b Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:43 +0900 Subject: [PATCH 23/29] =?UTF-8?q?=F0=9F=93=9D=20docs:=20API=20CI/CD=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EA=B3=84=ED=9A=8D=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=B0=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EA=B3=84=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-api-cicd-aws-migration.md | 636 ++++++++++++++++++ ...026-09-24-api-cicd-aws-migration-design.md | 34 +- 2 files changed, 661 insertions(+), 9 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-24-api-cicd-aws-migration.md diff --git a/docs/superpowers/plans/2026-09-24-api-cicd-aws-migration.md b/docs/superpowers/plans/2026-09-24-api-cicd-aws-migration.md new file mode 100644 index 0000000..1ed5353 --- /dev/null +++ b/docs/superpowers/plans/2026-09-24-api-cicd-aws-migration.md @@ -0,0 +1,636 @@ +# API CI/CD 리팩토링 및 AWS EC2 이전 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `main` push 시 lint/test/build를 통과한 커밋만 AWS EC2(`/opt/connectgnu/api`)에 배포되고, 배포 후 app이 healthy가 아니면 워크플로가 실패하도록 GitHub Actions를 재구성한다. + +**Architecture:** `api-ci.yml`을 `workflow_call`로 재사용 가능하게 만들고, `api-cd.yml`이 `ci → build-and-push → deploy` 순서로 호출한다. 배포는 SSH(appleboy scp/ssh)로 compose 파일과 `nginx/prod.conf`만 동기화한 뒤 서버에서 `docker compose`로 app을 교체하고 헬스체크 대기 → nginx 설정 검사/reload 순으로 진행한다. nginx는 디렉터리 마운트로 바꿔 scp로 교체된 설정이 reload로 반영되게 한다. + +**Tech Stack:** GitHub Actions, Docker Compose v2 plugin, DockerHub, nginx 1.29.5-alpine, pnpm 9.9.0, Node 24, NestJS + +**Spec:** `docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md` + +**Branch:** `chore/api-cicd-aws` (dev에서 분기) + +## Global Constraints + +- 배포 경로: `/opt/connectgnu/api` (워크플로 `env.DEPLOY_DIR`) +- 서버 사용자: `ec2-user`, `sudo` 사용 금지 (`docker` 그룹 소속 전제) +- compose 명령은 `docker compose`(v2 플러그인). `docker-compose` 금지 +- 서버로 동기화하는 파일은 `services/api/docker-compose.yml`, `services/api/nginx/prod.conf` 두 개뿐. `letsencrypt/`, `nginx/dev.conf`, `.env`는 절대 건드리지 않는다 +- 로깅은 모든 서비스에 `json-file`, `max-size: "10m"`, `max-file: "3"` 블록을 서비스별로 명시 (이미 적용됨, 유지) +- app `healthcheck`와 nginx `depends_on: app`은 유지 (배포 헬스 대기가 healthcheck에 의존) +- 이미지: `dongho18/connect-gnu-node` (compose 하드코딩). CD의 `${DOCKER_USERNAME}/${DOCKER_IMAGE_NAME}`와 같아야 한다 +- 빌드 아키텍처: amd64 (`ubuntu-latest`). t3.micro +- 액션 버전: `actions/checkout@v7`, `actions/setup-node@v7`, `pnpm/action-setup@v6`, `docker/setup-buildx-action@v4`, `docker/login-action@v4`, `docker/build-push-action@v7`, `appleboy/scp-action@v1.0.0`, `appleboy/ssh-action@v1.2.5` +- `appleboy/ssh-action` v1.2.x에는 `script_stop`이 없다. 스크립트 첫 줄 `set -euo pipefail`로 실패 시 중단 +- 커밋 메시지 규칙: 저장소 관례(gitmoji + 한글, 예: `👷 ci: ...`, `🔧 chore: ...`), 끝에 `Co-Authored-By: Claude Opus 5.5 ` +- 범위 밖: batch/monitoring, 자동 롤백, SHA 태그 배포(Quire #11), OIDC/SSM, CloudWatch + +## Review Focus + +1. **app 컨테이너에 healthcheck가 없음** → `docker inspect`의 `.State.Health`가 없어 템플릿 에러로 로그 없이 종료되면 안 된다. `none`으로 판별해 즉시 로그 출력 후 실패해야 한다. (Task 4 Step 2 시뮬레이션의 `none` 케이스) +2. **app이 unhealthy로 전환** → 90초를 다 기다리지 말고 즉시 로그 출력 후 exit 1, nginx reload와 prune은 실행되지 않아야 한다. (Task 4 Step 2 `unhealthy` 케이스) +3. **nginx 컨테이너가 방금 재생성됨** (첫 배포 시 볼륨 변경으로 recreate) → master 프로세스가 뜨기 전 `nginx -s reload`가 pid 파일 없음으로 실패할 수 있다. 재시도 후에도 실패하면 명시적으로 실패해야 한다. (Task 4 Step 2 스크립트의 reload 재시도 루프, Task 2 Step 2 검증) +4. **서버 `nginx/` 디렉터리에 `prod.conf` 외 `.conf`가 있음** (예: 저장소처럼 `dev.conf`가 섞임) → 디렉터리 마운트 시 server 블록이 중복된다. 동기화 대상은 `prod.conf`만이어야 한다. (Task 2 Step 2에서 prod.conf만 담은 디렉터리로 `nginx -t` 검증, Task 4 scp `source` 확인) +5. **`workflow_call`로 호출된 CI의 concurrency 그룹이 CD 그룹과 충돌** → 교착/취소가 없어야 한다. CI 그룹은 `api-ci-` 접두어 + 호출자 워크플로 이름, CD는 `api-cd-` 접두어. (Task 1 Step 3, Task 3 Step 3의 actionlint 및 그룹 문자열 확인) + +--- + +## File Map + +| 파일 | 역할 | Task | +|---|---|---| +| `.github/workflows/api-ci.yml` | PR 검증 + CD에서 재사용되는 CI (lint/test/build) | 1 | +| `services/api/docker-compose.yml` | 운영 compose. nginx 볼륨을 디렉터리 마운트로 변경 | 2 | +| `.github/workflows/api-cd.yml` | CI 게이트 → 이미지 빌드/푸시 → EC2 배포 | 3, 4 | + +--- + +### Task 1: CI를 재사용 워크플로로 전환 + +Quire: [#17](https://quire.io/w/jt27208/17) (태스크 #32, #33, #34) + +**Files:** +- Modify: `.github/workflows/api-ci.yml` (전체 교체) + +**Interfaces:** +- Consumes: 없음 +- Produces: `workflow_call` 트리거가 있는 재사용 워크플로 `./.github/workflows/api-ci.yml`. 입력/시크릿 없음. job 이름 `verify`. Task 3이 `uses: ./.github/workflows/api-ci.yml`로 호출한다. + +- [ ] **Step 1: 현재 워크플로가 lint를 통과하는지 기준선 확인** + +Run: +```bash +cd /Users/dongho/Desktop/Github/Server-Node +docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color .github/workflows/api-ci.yml +``` +Expected: 출력 없이 exit 0 (기존 파일 기준선). 에러가 나오면 기록해 두고 Step 2에서 함께 해소한다. + +- [ ] **Step 2: `api-ci.yml` 전체 교체** + +```yaml +name: API CI + +on: + pull_request: + branches: + - main + - dev + paths: + - 'services/api/app/**' + - '.github/workflows/api-ci.yml' + + workflow_call: + + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: api-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Lint, Test and Build + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/api/app + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + package_json_file: services/api/app/package.json + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: services/api/app/pnpm-lock.yaml + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Run Lint + run: pnpm lint:check + + - name: Run Tests + run: pnpm test + + - name: Build + run: pnpm build +``` + +변경 이유: +- `pnpm/action-setup`은 `version: 9.9.0` 대신 `package_json_file`로 app의 `packageManager`(`pnpm@9.9.0+sha512...`)를 읽는다. 버전 정의를 한 곳으로 모은다. +- `workflow_call`로 호출되면 `github.workflow`는 호출자 이름(`API CD`)이 되어 그룹이 `api-ci-API CD-refs/heads/main`이 된다. CD의 `api-cd-...` 그룹과 겹치지 않는다. + +- [ ] **Step 3: actionlint 통과 확인** + +Run: +```bash +docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color .github/workflows/api-ci.yml +``` +Expected: 출력 없이 exit 0 + +- [ ] **Step 4: CI 명령이 로컬에서 통과하는지 확인** + +Run: +```bash +cd services/api/app && pnpm install --frozen-lockfile && pnpm lint:check && pnpm test && pnpm build; cd - +``` +Expected: 네 명령 모두 성공. 실패하면 워크플로 문제가 아니라 코드 문제이므로 원인을 보고하고 멈춘다(이 계획 범위에서 앱 코드를 고치지 않는다). + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/api-ci.yml +git commit -m "👷 ci: API CI를 재사용 워크플로로 전환하고 빌드 검증 추가 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2: nginx 설정을 디렉터리 마운트로 변경 + +Quire: [#19](https://quire.io/w/jt27208/19) 태스크 #43 (nginx 볼륨 디렉터리 마운트) + +**Files:** +- Modify: `services/api/docker-compose.yml` (nginx `volumes` 첫 줄) + +**Interfaces:** +- Consumes: 없음 +- Produces: nginx 컨테이너가 `/opt/connectgnu/api/nginx/` 디렉터리 전체를 `/etc/nginx/conf.d`로 읽는다. Task 4의 scp가 `nginx/prod.conf`를 교체하면 `nginx -s reload`로 반영된다. + +- [ ] **Step 1: 볼륨 변경** + +`services/api/docker-compose.yml`의 nginx `volumes`에서: + +```yaml + - ./nginx/prod.conf:/etc/nginx/conf.d/default.conf +``` +를 +```yaml + - ./nginx:/etc/nginx/conf.d:ro +``` +로 바꾼다. 다른 줄은 건드리지 않는다. + +- [ ] **Step 2: prod.conf만 담은 디렉터리로 nginx 문법 검증** + +서버와 같은 조건(디렉터리에 `prod.conf`만, 인증서 경로 존재, upstream `app` 해석 가능)을 로컬에 만든다. 이 검증 파일들은 스크래치 디렉터리에만 만들고 커밋하지 않는다. + +Run: +```bash +T="${TMPDIR:-/tmp}/api-cicd-nginx-check" +rm -rf "$T" +mkdir -p "$T/nginx" "$T/le/live/api.connectgnu.kro.kr-0001" +cp services/api/nginx/prod.conf "$T/nginx/" +openssl req -x509 -nodes -newkey rsa:2048 -days 1 -subj "/CN=api.connectgnu.kro.kr" \ + -keyout "$T/le/live/api.connectgnu.kro.kr-0001/privkey.pem" \ + -out "$T/le/live/api.connectgnu.kro.kr-0001/fullchain.pem" 2>/dev/null +docker run --rm --add-host app:127.0.0.1 \ + -v "$T/nginx":/etc/nginx/conf.d:ro \ + -v "$T/le":/etc/letsencrypt:ro \ + nginx:1.29.5-alpine nginx -t +``` +Expected: `nginx: configuration file /etc/nginx/nginx.conf test is successful` + +- [ ] **Step 3: 저장소 nginx 디렉터리를 통째로 마운트하면 실패함을 확인 (Review Focus 4)** + +Run (Step 2의 `$T`를 다시 지정한다. 단계마다 셸이 새로 뜰 수 있다): +```bash +T="${TMPDIR:-/tmp}/api-cicd-nginx-check" +cp services/api/nginx/dev.conf "$T/nginx/" +docker run --rm --add-host app:127.0.0.1 \ + -v "$T/nginx":/etc/nginx/conf.d:ro \ + -v "$T/le":/etc/letsencrypt:ro \ + nginx:1.29.5-alpine nginx -t; echo "exit=$?" +rm -rf "$T" +``` +Expected: 에러 또는 `conflicting server name` 경고가 나온다. 이 결과는 Task 4에서 scp `source`를 `nginx/prod.conf` 단일 파일로 제한해야 하는 근거다. (에러 없이 통과하더라도 계획은 바뀌지 않는다. 결과만 기록한다.) + +- [ ] **Step 4: compose 문법 검증** + +Run: +```bash +docker compose -f services/api/docker-compose.yml config -q && echo OK +``` +Expected: `OK` + +- [ ] **Step 5: Commit** + +```bash +git add services/api/docker-compose.yml +git commit -m "🔧 chore: nginx 설정을 디렉터리 마운트로 변경해 배포 시 reload 반영 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: CD에 CI 게이트 추가 및 빌드/푸시 정리 + +Quire: [#18](https://quire.io/w/jt27208/18) (태스크 #35, #36, #37, #38) + +**Files:** +- Modify: `.github/workflows/api-cd.yml` (트리거·설정·`ci`·`build-and-push` 영역. `deploy` job은 Task 4에서 교체하므로 이 Task에서는 기존 내용을 그대로 둔다) + +**Interfaces:** +- Consumes: Task 1의 `./.github/workflows/api-ci.yml` (`workflow_call`) +- Produces: job id `ci`, `build-and-push`. 워크플로 `env.DEPLOY_DIR: /opt/connectgnu/api`. Task 4의 `deploy` job은 `needs: build-and-push`와 `env.DEPLOY_DIR`을 사용한다. + +- [ ] **Step 1: 기준선 actionlint** + +Run: +```bash +docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color .github/workflows/api-cd.yml +``` +Expected: 결과 기록 (기존 파일 기준선) + +- [ ] **Step 2: `api-cd.yml`의 `deploy` job 이전 부분을 교체** + +파일 시작부터 `jobs:` 아래 `build-and-push` job 끝까지를 아래로 바꾼다. 기존 `deploy:` job 블록은 그대로 뒤에 남긴다(Task 4에서 교체). + +```yaml +name: API CD + +on: + push: + branches: + - main + paths: + - 'services/api/app/src/**' + - 'services/api/app/package.json' + - 'services/api/app/pnpm-lock.yaml' + - 'services/api/app/Dockerfile' + - 'services/api/app/.dockerignore' + - 'services/api/app/tsconfig*.json' + - 'services/api/app/nest-cli.json' + - 'services/api/docker-compose.yml' + - 'services/api/nginx/**' + - '.github/workflows/api-cd.yml' + + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: api-cd-${{ github.ref }} + cancel-in-progress: false + +env: + DEPLOY_DIR: /opt/connectgnu/api + +jobs: + ci: + name: CI + uses: ./.github/workflows/api-ci.yml + + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: ci + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v7 + with: + context: services/api/app + target: prod + push: true + pull: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest + ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max +``` + +- [ ] **Step 3: actionlint 통과 및 concurrency 그룹 확인** + +Run: +```bash +docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color .github/workflows/api-cd.yml .github/workflows/api-ci.yml +grep -n "group:" .github/workflows/api-ci.yml .github/workflows/api-cd.yml +``` +Expected: actionlint 출력 없음. `group:` 두 줄이 각각 `api-ci-${{ github.workflow }}-${{ github.ref }}`, `api-cd-${{ github.ref }}` (Review Focus 5). 기존 `deploy` job의 `appleboy` 구버전 관련 경고가 남아 있다면 Task 4에서 해소되므로 기록만 한다. + +- [ ] **Step 4: prod 타깃 이미지가 로컬에서 빌드되는지 확인** + +Run: +```bash +docker build --target prod -t connect-gnu-node:plan-check services/api/app && docker image rm connect-gnu-node:plan-check +``` +Expected: 빌드 성공 + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/api-cd.yml +git commit -m "👷 ci: API CD에 CI 게이트 추가 및 이미지 빌드 설정 정리 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: EC2 배포 job 구현 + +Quire: [#19](https://quire.io/w/jt27208/19) (태스크 #39, #40, #41, #42, #44) + +**Files:** +- Modify: `.github/workflows/api-cd.yml` (`deploy` job 전체 교체) + +**Interfaces:** +- Consumes: Task 3의 `needs: build-and-push`, `env.DEPLOY_DIR`. Task 2의 nginx 디렉터리 마운트. compose의 app `healthcheck`. +- Produces: 없음 (최종 job) + +- [ ] **Step 1: 헬스 대기 로직 시뮬레이션 준비 (Review Focus 1, 2)** + +배포 스크립트의 헬스 판정 부분을 로컬 Docker로 먼저 검증한다. 스크래치 디렉터리에만 만들고 커밋하지 않는다. + +```bash +S="${TMPDIR:-/tmp}/api-cicd-health-sim" +rm -rf "$S" && mkdir -p "$S" +cat > "$S/health-wait.sh" <<'EOF' +set -euo pipefail +cid=$(docker compose ps -q app) +if [ -z "$cid" ]; then + echo "app container not found" + docker compose ps + exit 1 +fi + +status=none +for _ in $(seq 1 30); do + status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid") + case "$status" in + healthy|unhealthy|none) break ;; + esac + sleep 3 +done + +if [ "$status" != healthy ]; then + echo "app health status: $status" + docker compose logs --tail 100 app + exit 1 +fi +echo "app is healthy" +EOF + +mkdir -p "$S/healthy" "$S/unhealthy" "$S/none" +cat > "$S/healthy/compose.yml" <<'EOF' +services: + app: + image: busybox:1.36 + command: sh -c 'touch /tmp/ok; sleep 3600' + healthcheck: + test: ['CMD-SHELL', 'test -f /tmp/ok'] + interval: 2s + retries: 1 +EOF +cat > "$S/unhealthy/compose.yml" <<'EOF' +services: + app: + image: busybox:1.36 + command: sleep 3600 + healthcheck: + test: ['CMD-SHELL', 'false'] + interval: 2s + retries: 1 +EOF +cat > "$S/none/compose.yml" <<'EOF' +services: + app: + image: busybox:1.36 + command: sleep 3600 +EOF +``` + +- [ ] **Step 2: 세 가지 케이스 실행** + +Run: +```bash +S="${TMPDIR:-/tmp}/api-cicd-health-sim" +for c in healthy unhealthy none; do + ( + cd "$S/$c" + export COMPOSE_PROJECT_NAME="hw-$c" + docker compose up -d >/dev/null 2>&1 + start=$(date +%s) + bash "$S/health-wait.sh" + echo "[$c] exit=$? elapsed=$(( $(date +%s) - start ))s" + docker compose down -t 0 >/dev/null 2>&1 + ) +done +rm -rf "$S" +``` + +`set -euo pipefail`은 `bash` 자식 프로세스 안에서만 적용되므로 실패 케이스에서도 바깥 루프는 계속 돈다. `COMPOSE_PROJECT_NAME`을 export해야 스크립트 안의 `docker compose ps`가 같은 프로젝트를 찾는다. + +Expected: +- `[healthy] exit=0` + `app is healthy` +- `[unhealthy] exit=1` + `app health status: unhealthy`, 10초 안쪽 +- `[none] exit=1` + `app health status: none`, 즉시 + +- [ ] **Step 3: `deploy` job 교체** + +`api-cd.yml`의 기존 `deploy:` job 블록 전체를 아래로 바꾼다. 헬스 판정 부분은 Step 1에서 검증한 로직과 동일해야 한다. + +```yaml + deploy: + name: Deploy to API Server + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Copy files to server + uses: appleboy/scp-action@v1.0.0 + with: + host: ${{ secrets.API_SSH_HOST }} + username: ${{ secrets.API_SSH_USERNAME }} + key: ${{ secrets.API_SSH_PRIVATE_KEY }} + source: services/api/docker-compose.yml,services/api/nginx/prod.conf + target: ${{ env.DEPLOY_DIR }} + strip_components: 2 + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.2.5 + with: + host: ${{ secrets.API_SSH_HOST }} + username: ${{ secrets.API_SSH_USERNAME }} + key: ${{ secrets.API_SSH_PRIVATE_KEY }} + script: | + set -euo pipefail + cd ${{ env.DEPLOY_DIR }} + + docker compose pull app + docker compose up -d --no-deps app + + cid=$(docker compose ps -q app) + if [ -z "$cid" ]; then + echo "app container not found" + docker compose ps + exit 1 + fi + + status=none + for _ in $(seq 1 30); do + status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid") + case "$status" in + healthy|unhealthy|none) break ;; + esac + sleep 3 + done + + if [ "$status" != healthy ]; then + echo "app health status: $status" + docker compose logs --tail 100 app + exit 1 + fi + + docker compose up -d --no-deps nginx + docker compose exec -T nginx nginx -t + + reloaded=false + for _ in 1 2 3 4 5; do + if docker compose exec -T nginx nginx -s reload; then + reloaded=true + break + fi + sleep 2 + done + if [ "$reloaded" != true ]; then + echo "nginx reload failed" + docker compose logs --tail 50 nginx + exit 1 + fi + + docker image prune -f +``` + +포인트: +- scp `source`는 `nginx/prod.conf` 단일 파일. `strip_components: 2`로 `services/api/`가 제거되어 서버에 `docker-compose.yml`, `nginx/prod.conf`로 놓인다. +- `{{ ... }}`(Go 템플릿)는 `${{ }}`가 아니므로 GitHub Actions 표현식으로 해석되지 않는다. +- reload 재시도 루프는 첫 배포처럼 nginx가 방금 재생성된 경우를 위한 것이다(Review Focus 3). +- 헬스 실패·`nginx -t` 실패 시 reload와 prune은 실행되지 않는다. + +- [ ] **Step 4: 전체 워크플로 검증** + +Run: +```bash +docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color .github/workflows/api-ci.yml .github/workflows/api-cd.yml +grep -nE "sudo|docker-compose |prune -af|connect-gnu/services" .github/workflows/api-cd.yml || echo "no legacy patterns" +``` +Expected: actionlint 출력 없음, `no legacy patterns` + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/api-cd.yml +git commit -m "👷 ci: EC2 배포 job에 헬스체크 대기와 nginx 설정 반영 추가 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 5: Secrets·서버 사전 조건 확인 및 파이프라인 검증 (사람 작업 포함) + +Quire: [#20](https://quire.io/w/jt27208/20) (#45, #46, #48), [#21](https://quire.io/w/jt27208/21) (#49, #50, #51) + +이 Task는 GitHub 설정과 EC2 접속이 필요하다. 에이전트는 체크리스트를 제시하고 사용자 확인을 받은 뒤 진행한다. Secrets 값 입력과 서버 명령은 사용자가 직접 수행한다. + +**Files:** 없음 (코드 변경 없음) + +**Interfaces:** +- Consumes: Task 1~4의 워크플로 +- Produces: 없음 + +- [ ] **Step 1: 서버 사전 조건 확인 (사용자가 EC2에서 실행)** + +```bash +docker compose version # v2 플러그인 동작 +id -nG | grep -w docker # ec2-user가 docker 그룹 +ls -la /opt/connectgnu/api # .env, docker-compose.yml, nginx/, letsencrypt/ 존재, 소유자 ec2-user +ls /opt/connectgnu/api/nginx # prod.conf 하나만 +sudo ls /opt/connectgnu/api/letsencrypt/conf/live/ # api.connectgnu.kro.kr-0001 존재 +``` +Expected: 모두 조건 충족. `docker` 그룹이 아니면 `sudo usermod -aG docker ec2-user` 후 재로그인. + +- [ ] **Step 2: GitHub Secrets 확인 (사용자)** + +`Settings → Secrets and variables → Actions`에서: +- `API_SSH_HOST` = EC2 Elastic IP +- `API_SSH_USERNAME` = `ec2-user` +- `API_SSH_PRIVATE_KEY` = EC2 키 페어 개인키 (PEM 전체) +- `DOCKER_USERNAME`=`dongho18`, `DOCKER_IMAGE_NAME`=`connect-gnu-node` 인지 확인 (compose 이미지와 일치해야 함) + +보안 그룹 인바운드 22/80/443 확인. + +- [ ] **Step 3: 브랜치 푸시 및 dev 대상 PR 생성** + +사용자 확인 후: +```bash +git push -u origin chore/api-cicd-aws +gh pr create --base dev --title "👷 ci: API CI/CD 리팩토링 및 AWS EC2 배포 전환" --body "$(cat <<'EOF' +## 요약 +- API CI를 재사용 워크플로로 전환하고 build 검증 추가 +- CD에 CI 게이트 추가, EC2(/opt/connectgnu/api) 배포로 전환 +- 배포 후 app 헬스체크 대기, nginx -t 후 reload +- 운영 compose/nginx를 서버 반영본으로 동기화, nginx 디렉터리 마운트 + +## 스펙/계획 +- docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md +- docs/superpowers/plans/2026-09-24-api-cicd-aws-migration.md + +## 배포 전 확인 +- [ ] GitHub Secrets(API_SSH_*) EC2 값으로 교체 +- [ ] EC2 ec2-user docker 그룹, compose 플러그인 + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` +Expected: PR 생성. `API CI / Lint, Test and Build`가 실행되어 통과. + +- [ ] **Step 4: main 반영 후 CD 확인 (사용자가 머지 시점 결정)** + +dev → main 머지 후: +```bash +gh run list --workflow "API CD" --limit 1 +gh run watch +curl -fsS https://api.connectgnu.kro.kr/api/health +``` +Expected: `CI → Build and Push → Deploy` 모두 성공, health 응답 200. + +첫 배포에서는 nginx 볼륨 변경 때문에 nginx 컨테이너가 재생성되어 1~2초 끊김이 있을 수 있다. + +- [ ] **Step 5: Quire 상태 갱신** + +에픽 [#10](https://quire.io/w/jt27208/10) 하위 #17~#21 스토리와 해당 태스크를 완료 처리한다. diff --git a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md index 39837e0..efe9e30 100644 --- a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md +++ b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md @@ -64,7 +64,7 @@ push main / 수동 실행 → api-cd.yml : ci (api-ci.yml 호출) → build Job `verify` (기존 `lint-and-test` 이름 변경), `working-directory: services/api/app`: 1. checkout -2. pnpm 9.9.0 설치 +2. pnpm 설치 (`package_json_file: services/api/app/package.json`의 `packageManager` = pnpm 9.9.0) 3. Node 24 설정, pnpm 캐시 (`cache-dependency-path: services/api/app/pnpm-lock.yaml`) 4. `pnpm install --frozen-lockfile` 5. `pnpm lint:check` @@ -130,13 +130,20 @@ cd /opt/connectgnu/api # 워크플로에서는 ${{ env.DEPLOY_DIR }} docker compose pull app docker compose up -d --no-deps app -# 헬스체크 대기 (최대 약 90초) cid=$(docker compose ps -q app) -status="" -for i in $(seq 1 30); do - status=$(docker inspect -f '{{.State.Health.Status}}' "$cid") - [ "$status" = healthy ] && break - [ "$status" = unhealthy ] && break +if [ -z "$cid" ]; then + echo "app container not found" + docker compose ps + exit 1 +fi + +# 헬스체크 대기 (최대 약 90초). healthcheck가 없으면 none으로 즉시 실패 +status=none +for _ in $(seq 1 30); do + status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid") + case "$status" in + healthy|unhealthy|none) break ;; + esac sleep 3 done if [ "$status" != healthy ]; then @@ -145,10 +152,19 @@ if [ "$status" != healthy ]; then exit 1 fi -# nginx 설정 반영 +# nginx 설정 반영 (재생성 직후 pid 미생성 대비 reload 재시도) docker compose up -d --no-deps nginx docker compose exec -T nginx nginx -t -docker compose exec -T nginx nginx -s reload +reloaded=false +for _ in 1 2 3 4 5; do + if docker compose exec -T nginx nginx -s reload; then reloaded=true; break; fi + sleep 2 +done +if [ "$reloaded" != true ]; then + echo "nginx reload failed" + docker compose logs --tail 50 nginx + exit 1 +fi docker image prune -f ``` From 059951aacc9e1d985fea7e4eba4fa8c4354aabd4 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 24/29] =?UTF-8?q?=F0=9F=91=B7=20ci:=20API=20CI=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EB=A1=9C=20=EC=A0=84=ED=99=98=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .github/workflows/api-ci.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/api-ci.yml b/.github/workflows/api-ci.yml index 5fef625..3aa6829 100644 --- a/.github/workflows/api-ci.yml +++ b/.github/workflows/api-ci.yml @@ -5,11 +5,12 @@ on: branches: - main - dev - paths: - 'services/api/app/**' - '.github/workflows/api-ci.yml' + workflow_call: + workflow_dispatch: permissions: @@ -20,8 +21,8 @@ concurrency: cancel-in-progress: true jobs: - lint-and-test: - name: Lint and Test + verify: + name: Lint, Test and Build runs-on: ubuntu-latest defaults: run: @@ -29,15 +30,15 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: 9.9.0 + package_json_file: services/api/app/package.json - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: 24 cache: pnpm @@ -51,3 +52,6 @@ jobs: - name: Run Tests run: pnpm test + + - name: Build + run: pnpm build From 185490b90f9aa425b23bb13e44d3a2dcbf91f2d8 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 25/29] =?UTF-8?q?=F0=9F=94=A7=20chore:=20nginx=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=B4=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C=20reload=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- services/api/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/docker-compose.yml b/services/api/docker-compose.yml index bce9e45..75fef3d 100644 --- a/services/api/docker-compose.yml +++ b/services/api/docker-compose.yml @@ -28,7 +28,7 @@ services: - '80:80' - '443:443' volumes: - - ./nginx/prod.conf:/etc/nginx/conf.d/default.conf + - ./nginx:/etc/nginx/conf.d:ro - ./letsencrypt/conf:/etc/letsencrypt - ./letsencrypt/www:/var/www/certbot command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"''' From 29327024a0b30a0e237ff88dd784fe00d774e21a Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 26/29] =?UTF-8?q?=F0=9F=91=B7=20ci:=20API=20CD=EC=97=90=20?= =?UTF-8?q?CI=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .github/workflows/api-cd.yml | 44 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/api-cd.yml b/.github/workflows/api-cd.yml index 6af859d..bb10709 100644 --- a/.github/workflows/api-cd.yml +++ b/.github/workflows/api-cd.yml @@ -5,43 +5,59 @@ on: branches: - main paths: - - "services/api/app/src/**" - - "services/api/app/package.json" - - "services/api/app/pnpm-lock.yaml" - - "services/api/app/Dockerfile" - - "services/api/docker-compose.yml" - - "services/api/nginx/**" - - ".github/workflows/api-cd.yml" + - 'services/api/app/src/**' + - 'services/api/app/package.json' + - 'services/api/app/pnpm-lock.yaml' + - 'services/api/app/Dockerfile' + - 'services/api/app/.dockerignore' + - 'services/api/app/tsconfig*.json' + - 'services/api/app/nest-cli.json' + - 'services/api/docker-compose.yml' + - 'services/api/nginx/**' + - '.github/workflows/api-cd.yml' workflow_dispatch: +permissions: + contents: read + +concurrency: + group: api-cd-${{ github.ref }} + cancel-in-progress: false + env: - ROOT_DIR: ./connect-gnu/services/api + DEPLOY_DIR: /opt/connectgnu/api jobs: + ci: + name: CI + uses: ./.github/workflows/api-ci.yml + build-and-push: name: Build and Push Docker Image runs-on: ubuntu-latest + needs: ci steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Push Docker Image - uses: docker/build-push-action@v6 + - name: Build and Push Docker Image + uses: docker/build-push-action@v7 with: + context: services/api/app + target: prod push: true pull: true - context: services/api/app tags: | ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }} From 1575c5f1a10334e23b4b58dd0b9a433eb654e637 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 27/29] =?UTF-8?q?=F0=9F=91=B7=20ci:=20EC2=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20job=EC=97=90=20=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EB=8C=80=EA=B8=B0=EC=99=80=20nginx=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .github/workflows/api-cd.yml | 61 ++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/.github/workflows/api-cd.yml b/.github/workflows/api-cd.yml index bb10709..4081ecc 100644 --- a/.github/workflows/api-cd.yml +++ b/.github/workflows/api-cd.yml @@ -68,28 +68,71 @@ jobs: name: Deploy to API Server runs-on: ubuntu-latest needs: build-and-push + steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Copy files to server - uses: appleboy/scp-action@v0.1.7 + uses: appleboy/scp-action@v1.0.0 with: host: ${{ secrets.API_SSH_HOST }} username: ${{ secrets.API_SSH_USERNAME }} key: ${{ secrets.API_SSH_PRIVATE_KEY }} - source: services/api/docker-compose.yml,services/api/nginx,services/api/letsencrypt/init-letsencrypt.sh - target: ${{ env.ROOT_DIR }} + source: services/api/docker-compose.yml,services/api/nginx/prod.conf + target: ${{ env.DEPLOY_DIR }} strip_components: 2 - name: Deploy via SSH - uses: appleboy/ssh-action@v1.0.3 + uses: appleboy/ssh-action@v1.2.5 with: host: ${{ secrets.API_SSH_HOST }} username: ${{ secrets.API_SSH_USERNAME }} key: ${{ secrets.API_SSH_PRIVATE_KEY }} script: | - cd ${{ env.ROOT_DIR }} - sudo docker-compose pull app - sudo docker-compose up -d app --no-deps - sudo docker image prune -af + set -euo pipefail + cd ${{ env.DEPLOY_DIR }} + + docker compose pull app + docker compose up -d --no-deps app + + cid=$(docker compose ps -q app) + if [ -z "$cid" ]; then + echo "app container not found" + docker compose ps + exit 1 + fi + + status=none + for _ in $(seq 1 30); do + status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid") + case "$status" in + healthy|unhealthy|none) break ;; + esac + sleep 3 + done + + if [ "$status" != healthy ]; then + echo "app health status: $status" + docker compose logs --tail 100 app + exit 1 + fi + + docker compose up -d --no-deps nginx + docker compose exec -T nginx nginx -t + + reloaded=false + for _ in 1 2 3 4 5; do + if docker compose exec -T nginx nginx -s reload; then + reloaded=true + break + fi + sleep 2 + done + if [ "$reloaded" != true ]; then + echo "nginx reload failed" + docker compose logs --tail 50 nginx + exit 1 + fi + + docker image prune -f From 54337de8f6a969c6c57df72bd06941a8077bf9b0 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 28/29] =?UTF-8?q?=F0=9F=90=9B=20fix:=20nginx=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B2=80=EC=82=AC=EB=A5=BC=20=EC=BB=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EC=A0=84?= =?UTF-8?q?=EC=97=90=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 잘못된 설정이 배포될 때 nginx가 재생성되며 crash-loop로 80/443이 내려가는 문제를 막는다. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/api-cd.yml | 5 ++++- .../specs/2026-09-24-api-cicd-aws-migration-design.md | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api-cd.yml b/.github/workflows/api-cd.yml index 4081ecc..bcbcf26 100644 --- a/.github/workflows/api-cd.yml +++ b/.github/workflows/api-cd.yml @@ -118,8 +118,11 @@ jobs: exit 1 fi + if ! docker compose run --rm --no-deps -T nginx nginx -t; then + echo "nginx config test failed" + exit 1 + fi docker compose up -d --no-deps nginx - docker compose exec -T nginx nginx -t reloaded=false for _ in 1 2 3 4 5; do diff --git a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md index efe9e30..e9c04a1 100644 --- a/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md +++ b/docs/superpowers/specs/2026-09-24-api-cicd-aws-migration-design.md @@ -152,9 +152,14 @@ if [ "$status" != healthy ]; then exit 1 fi -# nginx 설정 반영 (재생성 직후 pid 미생성 대비 reload 재시도) +# nginx 설정 반영: 실행 중인 컨테이너를 건드리기 전에 일회성 컨테이너로 검사 +# (먼저 up 하면 compose 정의 변경 시 재생성되어, 잘못된 설정이면 80/443이 내려간다) +if ! docker compose run --rm --no-deps -T nginx nginx -t; then + echo "nginx config test failed" + exit 1 +fi docker compose up -d --no-deps nginx -docker compose exec -T nginx nginx -t +# 재생성 직후 pid 미생성 대비 reload 재시도 reloaded=false for _ in 1 2 3 4 5; do if docker compose exec -T nginx nginx -s reload; then reloaded=true; break; fi @@ -172,7 +177,7 @@ docker image prune -f 동작 규칙: - 헬스체크 실패 시 로그를 출력하고 실패 처리한다. nginx reload와 prune은 실행하지 않는다. 자동 롤백은 하지 않는다. -- `nginx -t`가 실패하면 reload하지 않고 워크플로가 실패한다. 기존 nginx 프로세스는 이전 설정으로 계속 동작한다. +- `nginx -t`는 `docker compose run --rm`으로 실행 중인 nginx와 분리해 먼저 검사한다. 실패하면 nginx를 재생성·reload하지 않고 워크플로가 실패하며, 기존 nginx는 이전 설정으로 계속 동작한다. - `sudo`를 쓰지 않는다 (`ec2-user`가 `docker` 그룹 소속이라는 전제). - `docker image prune -af` 대신 `-f`를 사용해 dangling 이미지만 정리한다. From 96aafab32a88bb20b8a40bea0fef7fda5ec44338 Mon Sep 17 00:00:00 2001 From: Jang-DongHo Date: Thu, 24 Sep 2026 22:02:44 +0900 Subject: [PATCH 29/29] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=A3=A8=ED=8A=B8?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=20pnpm=20=EB=B2=84=EC=A0=84=EA=B3=BC=20?= =?UTF-8?q?=EB=AC=B4=EA=B4=80=ED=95=98=EA=B2=8C=20jest=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EC=A0=84=EB=8B=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fcb646c..771b8b3 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "prepare": "husky", "lint-staged": "lint-staged", "test": "pnpm run test:api && pnpm run test:mobile", - "test:api": "pnpm --dir services/api/app test -- --runInBand", - "test:mobile": "pnpm --dir mobile test -- --runInBand" + "test:api": "pnpm --dir services/api/app exec jest --runInBand", + "test:mobile": "pnpm --dir mobile exec jest --runInBand" }, "devDependencies": { "husky": "^9.1.7",