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:
Johngreen
2026-03-07 17:39:56 +09:00
commit 16bd2cb92a
170 changed files with 23628 additions and 0 deletions
+30
View File
@@ -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"
}
}
+18
View File
@@ -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;
+19
View File
@@ -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;
}
+5
View File
@@ -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';
+37
View File
@@ -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));
}
+13
View File
@@ -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 };
}
+4
View File
@@ -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';
+21
View File
@@ -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 };
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
});
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});