기본 파일 구조 설정 및 초기화 '중'
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