기본 파일 구조 설정 및 초기화 '중'

This commit is contained in:
이정수 2026-01-21 17:53:52 +09:00
parent f691e51f5c
commit 49eb1c6744
24 changed files with 6125 additions and 0 deletions

37
.cursor/rules/project.mdc Normal file
View File

@ -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`.

12
.cursorrules Normal file
View File

@ -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는 사용자가 명시적으로 요청할 때만 수행.

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
node_modules
coverage
dist
.git
.cursor
.vscode
.DS_Store
*.log
.env

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
# Server
PORT=3000
# NODE_ENV=development

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
node-linker=hoisted
# In CI/containers, prefer deterministic installs
strict-peer-dependencies=false

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
dist
coverage
node_modules
pnpm-lock.yaml

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

32
Dockerfile Normal file
View File

@ -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"]

13
docker-compose.yml Normal file
View File

@ -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

26
eslint.config.mjs Normal file
View File

@ -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/**'],
},
];

15
jest.config.ts Normal file
View File

@ -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;

5
nest-cli.json Normal file
View File

@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

55
package.json Normal file
View File

@ -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"
}
}

5769
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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 });
});
});

13
src/app.controller.ts Normal file
View File

@ -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();
}
}

11
src/app.module.ts Normal file
View File

@ -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 {}

12
src/app.service.ts Normal file
View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
health() {
return {
ok: true,
service: 'chzzk-server',
ts: new Date().toISOString(),
};
}
}

19
src/main.ts Normal file
View File

@ -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();

19
test/app.e2e-spec.ts Normal file
View File

@ -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();
});
});

12
test/jest-e2e.json Normal file
View File

@ -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
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

26
tsconfig.json Normal file
View File

@ -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
}
}