기본 파일 구조 설정 및 초기화 '중'
This commit is contained in:
parent
f691e51f5c
commit
49eb1c6744
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
description: Project guidance for chzzk-server (NestJS REST + Chzzk API proxy)
|
||||||
|
globs:
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.md"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 목표(Goal)
|
||||||
|
- 이 프로젝트는 Chzzk API로부터 데이터를 받아 가공(processing)하여 내부 서비스(internal services)가 쉽게 사용하도록 제공하는 NestJS REST API 서버.
|
||||||
|
|
||||||
|
## 핵심 설계 원칙(Principles)
|
||||||
|
- 외부 API 호출(Chzzk API call)은 서버에서만 수행(server-side only).
|
||||||
|
- 내부용 API(internal API)는 안정성(reliability) 우선: timeout, retry, rate limit, cache를 고려.
|
||||||
|
- 의존성 경계(boundary)를 분리:
|
||||||
|
- controller: HTTP 계약(contract)
|
||||||
|
- service/usecase: 도메인 로직(domain logic)
|
||||||
|
- adapter/client: Chzzk API 통신(HTTP client)
|
||||||
|
- infra: config, logging, postgres(pg) 연결
|
||||||
|
|
||||||
|
## REST API 스타일(Style)
|
||||||
|
- `/v1/...` 형태의 버저닝(versioning) 기본.
|
||||||
|
- DTO + validation(pipe) 사용.
|
||||||
|
- 공통 응답 포맷(response envelope)은 필요 시 도입하되, 과도한 래핑(wrapping)은 피함.
|
||||||
|
|
||||||
|
## 보안/인증(Security/Auth)
|
||||||
|
- 인증은 미정이므로, 최소한 다음을 쉽게 추가 가능하게 구조화:
|
||||||
|
- API key 헤더 기반(`x-api-key`)
|
||||||
|
- 또는 내부망(internal network)에서만 접근 가능한 방화벽/프록시(proxy) 구성
|
||||||
|
|
||||||
|
## PostgreSQL (No ORM)
|
||||||
|
- `pg` Pool 기반.
|
||||||
|
- 마이그레이션(migration)은 SQL 파일 방식 또는 간단한 migration runner(추가 가능).
|
||||||
|
|
||||||
|
## 운영(Ops)
|
||||||
|
- 환경변수(env): `.env`/`.env.example`로 관리.
|
||||||
|
- 로깅(logging): request id, latency 포함.
|
||||||
|
- 헬스체크(healthcheck): `/health`.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Cursor Project Rules (Cursor)
|
||||||
|
|
||||||
|
- 답변은 항상 한글(Korean)로 하되, 핵심 용어는 영어(English) 병기.
|
||||||
|
- 사용자가 원하는 목표 형태(architecture/shape)를 만들기 위한 작업이므로, 진행은 항상 "한 단계씩 질문(question) → 답변 반영" 방식.
|
||||||
|
- NestJS는 REST API 중심.
|
||||||
|
- Chzzk API는 외부 의존(external dependency)이므로, 호출은 서버 사이드(server-side)에서만 수행하고 브라우저(client)에서 직접 호출하지 않도록 설계.
|
||||||
|
- 에러 처리(error handling), 타임아웃(timeout), 재시도(retry) 정책을 명시적으로 둔다.
|
||||||
|
- 설정값(config)은 환경변수(env) 기반으로 모으고 코드에 하드코딩(hardcode)하지 않는다.
|
||||||
|
- 인증(authentication)은 미정이므로, "내부 서비스(internal services)" 호출 보호는 최소한 API key/API token 방식부터 쉽게 붙일 수 있게 구조화.
|
||||||
|
- DB는 PostgreSQL이지만 ORM/ODM은 사용하지 않음. (예: pg Pool 기반)
|
||||||
|
- 변경 후에는 빌드(build) 및 테스트(test) 가능한 최소 단위까지 맞춘다.
|
||||||
|
- git 커밋(commit)과 push는 사용자가 명시적으로 요청할 때만 수행.
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
node_modules
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.cursor
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Server
|
||||||
|
PORT=3000
|
||||||
|
# NODE_ENV=development
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
node-linker=hoisted
|
||||||
|
# In CI/containers, prefer deterministic installs
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Keep pnpm version consistent via corepack + package.json packageManager
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json pnpm-lock.yaml .npmrc ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM deps AS dev
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
# For file watching in Docker on macOS
|
||||||
|
ENV CHOKIDAR_USEPOLLING=true
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["pnpm", "start:dev"]
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM base AS prod
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/package.json ./package.json
|
||||||
|
COPY --from=build /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
command: pnpm start:dev
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['dist/**', 'coverage/**', 'node_modules/**'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { Config } from 'jest';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||||
|
rootDir: '.',
|
||||||
|
testRegex: '.*\\.spec\\.ts$',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(t|j)s$': 'ts-jest',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: ['src/**/*.(t|j)s'],
|
||||||
|
coverageDirectory: './coverage',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "chzzk-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Chzzk API proxy/adapter service (NestJS REST)",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.0.0",
|
||||||
|
"pnpm": "^9.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.3",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main.js",
|
||||||
|
"build": "nest build",
|
||||||
|
"lint": "eslint \"{src,test}/**/*.ts\"",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.0",
|
||||||
|
"@nestjs/core": "^11.0.0",
|
||||||
|
"@nestjs/platform-fastify": "^11.0.0",
|
||||||
|
"fastify": "^5.2.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.0",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^22.10.6",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||||
|
"@typescript-eslint/parser": "^8.18.2",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('health should return ok', () => {
|
||||||
|
expect(appController.health()).toMatchObject({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get('/health')
|
||||||
|
health() {
|
||||||
|
return this.appService.health();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
health() {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
service: 'chzzk-server',
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import {
|
||||||
|
FastifyAdapter,
|
||||||
|
type NestFastifyApplication,
|
||||||
|
} from '@nestjs/platform-fastify';
|
||||||
|
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
|
AppModule,
|
||||||
|
new FastifyAdapter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
||||||
|
await app.listen({ port, host: '0.0.0.0' });
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { AppModule } from '../src/app.module';
|
||||||
|
|
||||||
|
describe('App (e2e)', () => {
|
||||||
|
it('/health (GET)', async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const app = moduleRef.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
|
||||||
|
await request(app.getHttpServer()).get('/health').expect(200);
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".*\\.e2e-spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": ["../src/**/*.(t|j)s"],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testTimeout": 30000
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue