feat: Re:Link MVP 초기 구현 - 도메인/서비스/프론트엔드 전체
- 모노레포 구조 (Turborepo + pnpm): @relink/domain, @relink/shared, @relink/infrastructure, @relink/database, @relink/web - 도메인 레이어: 매장(store), 매칭(matching), 업체(vendor), 보조금(subsidy), 계약/에스크로(contract) TDD 완료 (158 단위 테스트) - 서비스 레이어: 전 도메인 서비스 함수 + 통합 테스트 (58 테스트) - 프론트엔드: Next.js 15 App Router, 13개 페이지 (사용자 6 + 관리자 7) - 인프라: PostgreSQL 16 + PostGIS, Prisma ORM, Docker Compose, AuditLog + OutboxEvent 패턴 - .env 파일 포함 (로컬 개발 기본값만 포함, 실제 시크릿 없음) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@relink/shared",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const APP_NAME = 'Re:Link' as const;
|
||||
|
||||
export const PAGINATION = {
|
||||
DEFAULT_PAGE: 1,
|
||||
DEFAULT_LIMIT: 20,
|
||||
MAX_LIMIT: 100,
|
||||
} as const;
|
||||
|
||||
export const HTTP_STATUS = {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
CONFLICT: 409,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
} as const;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const envSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
DATABASE_URL: z.string().url(),
|
||||
DATABASE_TEST_URL: z.string().url().optional(),
|
||||
REDIS_URL: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
export function validateEnv(env: Record<string, string | undefined> = process.env): Env {
|
||||
const result = envSchema.safeParse(env);
|
||||
if (!result.success) {
|
||||
const formatted = result.error.flatten().fieldErrors;
|
||||
throw new Error(`Invalid environment variables: ${JSON.stringify(formatted)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type { Result, Success, Failure, AppError } from './types/index.js';
|
||||
export { success, failure, appError } from './types/index.js';
|
||||
export { envSchema, validateEnv } from './env.js';
|
||||
export type { Env } from './env.js';
|
||||
export { APP_NAME, PAGINATION, HTTP_STATUS } from './constants.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { AppError } from '../types/app-error.js';
|
||||
import type { Result } from '../types/result.js';
|
||||
|
||||
/**
|
||||
* Assert that a Result is a Success and return the value.
|
||||
*/
|
||||
export function expectSuccess<T>(result: Result<T>): T {
|
||||
if (!result.ok) {
|
||||
throw new Error(`Expected success but got failure: ${JSON.stringify(result.error)}`);
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a Result is a Failure and return the error.
|
||||
*/
|
||||
export function expectFailure<T, E = AppError>(result: Result<T, E>): E {
|
||||
if (result.ok) {
|
||||
throw new Error(`Expected failure but got success: ${JSON.stringify(result.value)}`);
|
||||
}
|
||||
return result.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test database URL with a unique database name for isolation.
|
||||
*/
|
||||
export function createTestDatabaseUrl(baseUrl?: string): string {
|
||||
const base = baseUrl ?? 'postgresql://relink:relink_test@localhost:5433/relink_test';
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a given number of milliseconds.
|
||||
*/
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface AppError {
|
||||
readonly code: string;
|
||||
readonly message: string;
|
||||
readonly details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function appError(
|
||||
code: string,
|
||||
message: string,
|
||||
details?: Record<string, unknown>,
|
||||
): AppError {
|
||||
return { code, message, details };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type { Result, Success, Failure } from './result.js';
|
||||
export { success, failure } from './result.js';
|
||||
export type { AppError } from './app-error.js';
|
||||
export { appError } from './app-error.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AppError } from './app-error.js';
|
||||
|
||||
export type Result<T, E = AppError> = Success<T> | Failure<E>;
|
||||
|
||||
export interface Success<T> {
|
||||
readonly ok: true;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
export interface Failure<E> {
|
||||
readonly ok: false;
|
||||
readonly error: E;
|
||||
}
|
||||
|
||||
export function success<T>(value: T): Success<T> {
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
export function failure<E>(error: E): Failure<E> {
|
||||
return { ok: false, error };
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user