Rename project from Re:Link to Startover

Rebrand repository from "Re:Link" to "Startover" across the codebase. Updates include package names and scopes (@relink/* -> @startover/*), import paths, Next.js transpile settings, vitest name, UI text and docs, Dockerfile and CI/workflow names, deploy scripts and repo paths, and example/production env values. Also add auth-related env vars, an apps/web .env symlink, and small formatting/typing cleanups in several TSX/TS files and tests to accommodate the rename.
This commit is contained in:
Johngreen
2026-03-08 20:22:08 +09:00
parent 557559c654
commit 7f59b94dcf
101 changed files with 361 additions and 281 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/analytics",
"name": "@startover/analytics",
"version": "0.0.1",
"private": false,
"type": "module",
@@ -19,7 +19,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@relink/shared": "workspace:*"
"@startover/shared": "workspace:*"
},
"devDependencies": {
"tsup": "^8.3.5",
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @relink/analytics - Event schemas and KPI calculations
* @startover/analytics - Event schemas and KPI calculations
*/
export type { AnalyticsEvent } from './event.js';
+1 -1
View File
@@ -6,5 +6,5 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
external: ['@relink/shared'],
external: ['@startover/shared'],
});
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/application",
"name": "@startover/application",
"version": "0.0.1",
"private": false,
"type": "module",
@@ -19,8 +19,8 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@relink/domain": "workspace:*",
"@relink/shared": "workspace:*"
"@startover/domain": "workspace:*",
"@startover/shared": "workspace:*"
},
"devDependencies": {
"tsup": "^8.3.5",
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @relink/application - Use cases and application services
* @startover/application - Use cases and application services
*
* Orchestrates domain entities and defines application-level business rules.
*/
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Result } from '@relink/shared';
import type { Result } from '@startover/shared';
/**
* Base use case interface. All application use cases implement this.
+1 -1
View File
@@ -6,5 +6,5 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
external: ['@relink/domain', '@relink/shared'],
external: ['@startover/domain', '@startover/shared'],
});
+1 -1
View File
@@ -1 +1 @@
DATABASE_URL="postgresql://relink:relink_dev@localhost:5432/relink_dev"
DATABASE_URL="postgresql://startover:startover_dev@localhost:5432/startover_dev"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/database",
"name": "@startover/database",
"version": "0.0.1",
"private": false,
"type": "module",
+1 -1
View File
@@ -1,5 +1,5 @@
// =============================================================================
// Re:Link MVP - Prisma Schema
// Startover MVP - Prisma Schema
// =============================================================================
// 설계 원칙:
// - 내부 PK: BigInt @id @default(autoincrement())
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @relink/database - Prisma client and database utilities
* @startover/database - Prisma client and database utilities
*/
export { createPrismaClient } from './client.js';
+8 -5
View File
@@ -8,7 +8,7 @@ const SCHEMA_PATH = resolve(__dirname, '../prisma/schema.prisma');
const TEST_DATABASE_URL =
process.env['DATABASE_TEST_URL'] ??
'postgresql://relink:relink_test@localhost:5433/relink_test';
'postgresql://startover:startover_test@localhost:5433/startover_test';
let testClient: PrismaClient | null = null;
@@ -26,10 +26,13 @@ export function getTestPrismaClient(): PrismaClient {
}
export async function setupTestDatabase(): Promise<PrismaClient> {
execSync(`DATABASE_URL="${TEST_DATABASE_URL}" npx prisma db push --schema="${SCHEMA_PATH}" --skip-generate --accept-data-loss`, {
stdio: 'pipe',
cwd: resolve(__dirname, '..'),
});
execSync(
`DATABASE_URL="${TEST_DATABASE_URL}" npx prisma db push --schema="${SCHEMA_PATH}" --skip-generate --accept-data-loss`,
{
stdio: 'pipe',
cwd: resolve(__dirname, '..'),
},
);
const client = getTestPrismaClient();
await client.$connect();
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/domain",
"name": "@startover/domain",
"version": "0.0.1",
"private": false,
"type": "module",
@@ -19,7 +19,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@relink/shared": "workspace:*"
"@startover/shared": "workspace:*"
},
"devDependencies": {
"tsup": "^8.3.5",
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface CheckIdempotencyInput {
readonly idempotencyKey: string;
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type ContractType = 'ACQUISITION' | 'DEMOLITION' | 'INTERIOR';
@@ -16,9 +16,7 @@ export interface ContractDraft {
readonly policyVersionId: string;
}
export function createContract(
input: CreateContractInput,
): Result<ContractDraft, AppError> {
export function createContract(input: CreateContractInput): Result<ContractDraft, AppError> {
// U021: 수락된 매칭만 계약 생성 가능
if (input.matchRequestStatus !== 'ACCEPTED') {
return failure(
+9 -7
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface OpenDisputeInput {
readonly currentEscrowStatus: string;
@@ -17,9 +17,7 @@ const DISPUTABLE_CONTRACT_STATUSES = new Set(['ACTIVE', 'SIGNED']);
const DISPUTABLE_ESCROW_STATUSES = new Set(['HOLDING', 'RELEASE_REVIEW']);
// U025: 분쟁이 열리면 에스크로는 DISPUTED로 전환
export function openDispute(
input: OpenDisputeInput,
): Result<OpenDisputeResult, AppError> {
export function openDispute(input: OpenDisputeInput): Result<OpenDisputeResult, AppError> {
if (!DISPUTABLE_CONTRACT_STATUSES.has(input.contractStatus)) {
return failure(
appError('INVALID_CONTRACT_STATUS', '활성 상태의 계약만 분쟁을 열 수 있습니다.', {
@@ -30,9 +28,13 @@ export function openDispute(
if (!DISPUTABLE_ESCROW_STATUSES.has(input.currentEscrowStatus)) {
return failure(
appError('INVALID_ESCROW_STATUS', 'HOLDING 또는 RELEASE_REVIEW 상태에서만 분쟁을 열 수 있습니다.', {
currentEscrowStatus: input.currentEscrowStatus,
}),
appError(
'INVALID_ESCROW_STATUS',
'HOLDING 또는 RELEASE_REVIEW 상태에서만 분쟁을 열 수 있습니다.',
{
currentEscrowStatus: input.currentEscrowStatus,
},
),
);
}
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface ReleaseEscrowInput {
readonly currentEscrowStatus: string;
@@ -10,9 +10,7 @@ export interface ReleaseEscrowResult {
readonly escrowStatus: 'RELEASE_REVIEW';
}
export function releaseEscrow(
input: ReleaseEscrowInput,
): Result<ReleaseEscrowResult, AppError> {
export function releaseEscrow(input: ReleaseEscrowInput): Result<ReleaseEscrowResult, AppError> {
// U023: HOLDING 상태에서만 정산 해제 가능
if (input.currentEscrowStatus !== 'HOLDING') {
return failure(
@@ -31,9 +29,7 @@ export function releaseEscrow(
// U026: 분쟁이 열려있으면 정산 해제 차단
if (input.hasOpenDispute) {
return failure(
appError('DISPUTE_OPEN', '열린 분쟁이 있어 정산 해제가 차단됩니다.'),
);
return failure(appError('DISPUTE_OPEN', '열린 분쟁이 있어 정산 해제가 차단됩니다.'));
}
return success({
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @relink/domain - Pure TypeScript domain layer
* @startover/domain - Pure TypeScript domain layer
*
* Contains entities, value objects, and business rules.
* No external dependencies allowed.
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type MatchRequestStatus =
| 'OPEN'
@@ -26,9 +26,13 @@ export function acceptMatchRequest(
if (!acceptableStatuses.includes(input.currentStatus)) {
return failure(
appError('INVALID_STATUS_TRANSITION', 'OPEN 또는 REVIEWING 상태의 요청만 수락할 수 있습니다.', {
currentStatus: input.currentStatus,
}),
appError(
'INVALID_STATUS_TRANSITION',
'OPEN 또는 REVIEWING 상태의 요청만 수락할 수 있습니다.',
{
currentStatus: input.currentStatus,
},
),
);
}
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type MatchType = 'ACQUISITION' | 'DEMOLITION' | 'INTERIOR';
export type MatchSourceType = 'USER_REQUEST' | 'OPERATOR_RECOMMENDATION' | 'SYSTEM_MATCH';
@@ -1,6 +1,10 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
import { StoreLease, type StoreLeaseInput, type StoreLeaseProps } from './store-lease.js';
import { StoreFacility, type StoreFacilityInput, type StoreFacilityProps } from './store-facility.js';
import {
StoreFacility,
type StoreFacilityInput,
type StoreFacilityProps,
} from './store-facility.js';
// ---------------------------------------------------------------------------
// Dependency interfaces (DIP)
@@ -92,9 +96,7 @@ export function createStoreDraft(
if (fieldErrors.length === 1) {
const err = fieldErrors[0]!;
return failure(
appError('VALIDATION_ERROR', err.message, { field: err.field }),
);
return failure(appError('VALIDATION_ERROR', err.message, { field: err.field }));
}
if (fieldErrors.length > 1) {
+2 -4
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface PublishStoreInput {
readonly currentReviewStatus: string;
@@ -11,9 +11,7 @@ export interface PublishStoreResult {
readonly policyVersionId: string;
}
export function publishStore(
input: PublishStoreInput,
): Result<PublishStoreResult, AppError> {
export function publishStore(input: PublishStoreInput): Result<PublishStoreResult, AppError> {
if (input.currentReviewStatus !== 'APPROVED') {
return failure(
appError('NOT_APPROVED', '승인된 매장만 공개할 수 있습니다.', {
+3 -7
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type ReviewDecision = 'APPROVED' | 'REJECTED';
export type ReviewableStatus = 'DRAFT' | 'SUBMITTED' | 'REVIEWING' | 'APPROVED' | 'REJECTED';
@@ -17,9 +17,7 @@ export interface ReviewStoreResult {
readonly memo?: string;
}
export function reviewStore(
input: ReviewStoreInput,
): Result<ReviewStoreResult, AppError> {
export function reviewStore(input: ReviewStoreInput): Result<ReviewStoreResult, AppError> {
if (input.currentReviewStatus !== 'SUBMITTED') {
return failure(
appError('INVALID_STATUS_TRANSITION', 'SUBMITTED 상태의 매장만 검토할 수 있습니다.', {
@@ -35,9 +33,7 @@ export function reviewStore(
);
}
if (!input.memo?.trim()) {
return failure(
appError('VALIDATION_ERROR', '반려 시 메모는 필수입니다.', { field: 'memo' }),
);
return failure(appError('VALIDATION_ERROR', '반려 시 메모는 필수입니다.', { field: 'memo' }));
}
}
+4 -2
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface StoreFacilityInput {
readonly exclusiveAreaSqm: number;
@@ -28,7 +28,9 @@ export const StoreFacility = {
create(input: StoreFacilityInput): Result<StoreFacilityProps, AppError> {
if (input.exclusiveAreaSqm <= 0) {
return failure(
appError('VALIDATION_ERROR', '전용면적은 0보다 커야 합니다.', { field: 'exclusiveAreaSqm' }),
appError('VALIDATION_ERROR', '전용면적은 0보다 커야 합니다.', {
field: 'exclusiveAreaSqm',
}),
);
}
+4 -2
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface StoreLeaseInput {
readonly depositAmount: number;
@@ -42,7 +42,9 @@ export const StoreLease = {
if (input.maintenanceFeeAmount !== undefined && input.maintenanceFeeAmount < 0) {
return failure(
appError('VALIDATION_ERROR', '관리비는 0 이상이어야 합니다.', { field: 'maintenanceFeeAmount' }),
appError('VALIDATION_ERROR', '관리비는 0 이상이어야 합니다.', {
field: 'maintenanceFeeAmount',
}),
);
}
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type ChecklistItemStatus = 'PENDING' | 'CHECKED' | 'NOT_APPLICABLE';
@@ -25,9 +25,13 @@ export function advanceSubsidyToReady(
// DOCUMENTS_PENDING 상태에서만 READY_TO_SUBMIT으로 전이 가능
if (input.currentStatus !== 'DOCUMENTS_PENDING') {
return failure(
appError('INVALID_STATUS_TRANSITION', 'DOCUMENTS_PENDING 상태에서만 제출 준비로 전환할 수 있습니다.', {
currentStatus: input.currentStatus,
}),
appError(
'INVALID_STATUS_TRANSITION',
'DOCUMENTS_PENDING 상태에서만 제출 준비로 전환할 수 있습니다.',
{
currentStatus: input.currentStatus,
},
),
);
}
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type SubsidyCaseStatus =
| 'DRAFT'
@@ -45,18 +45,20 @@ export function createSubsidyCase(
if (!isPublished && !isReviewable) {
return failure(
appError('STORE_NOT_ELIGIBLE', '공개되었거나 검토 가능한 매장만 지원금 케이스를 시작할 수 있습니다.', {
reviewStatus: input.storeReviewStatus,
publicationStatus: input.storePublicationStatus,
}),
appError(
'STORE_NOT_ELIGIBLE',
'공개되었거나 검토 가능한 매장만 지원금 케이스를 시작할 수 있습니다.',
{
reviewStatus: input.storeReviewStatus,
publicationStatus: input.storePublicationStatus,
},
),
);
}
// U017: 체크리스트 항목이 없으면 케이스를 생성할 수 없다
if (input.checklistItems.length === 0) {
return failure(
appError('EMPTY_CHECKLIST', '체크리스트 항목이 최소 1개 이상 있어야 합니다.'),
);
return failure(appError('EMPTY_CHECKLIST', '체크리스트 항목이 최소 1개 이상 있어야 합니다.'));
}
return success({
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type SubsidyReviewDecision = 'APPROVED' | 'REJECTED';
@@ -22,9 +22,13 @@ export function reviewSubsidyCase(
): Result<ReviewSubsidyCaseResult, AppError> {
if (!REVIEWABLE_STATUSES.has(input.currentStatus)) {
return failure(
appError('INVALID_STATUS_TRANSITION', 'SUBMITTED 또는 REVIEWING 상태의 케이스만 검토할 수 있습니다.', {
currentStatus: input.currentStatus,
}),
appError(
'INVALID_STATUS_TRANSITION',
'SUBMITTED 또는 REVIEWING 상태의 케이스만 검토할 수 있습니다.',
{
currentStatus: input.currentStatus,
},
),
);
}
+3 -7
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export type VendorCertificationStatus =
| 'APPLIED'
@@ -29,9 +29,7 @@ export function applyVendorCertification(
input: ApplyVendorCertificationInput,
): Result<VendorCertificationApplication, AppError> {
if (!input.businessName.trim()) {
return failure(
appError('VALIDATION_ERROR', '업체명은 필수입니다.', { field: 'businessName' }),
);
return failure(appError('VALIDATION_ERROR', '업체명은 필수입니다.', { field: 'businessName' }));
}
if (!input.contactName.trim()) {
@@ -41,9 +39,7 @@ export function applyVendorCertification(
}
if (!input.hasBusinessRegistration) {
return failure(
appError('MISSING_BUSINESS_REGISTRATION', '사업자등록증이 필요합니다.'),
);
return failure(appError('MISSING_BUSINESS_REGISTRATION', '사업자등록증이 필요합니다.'));
}
if (input.coverageRegionCount === 0) {
+12 -5
View File
@@ -1,4 +1,4 @@
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
import type { VendorCertificationStatus } from './apply-vendor-certification.js';
@@ -24,9 +24,13 @@ export function approveVendorCertification(
): Result<ApproveVendorCertificationResult, AppError> {
if (!REVIEWABLE_STATUSES.has(input.currentStatus)) {
return failure(
appError('INVALID_STATUS_TRANSITION', 'APPLIED 또는 REVIEWING 상태의 인증만 검토할 수 있습니다.', {
currentStatus: input.currentStatus,
}),
appError(
'INVALID_STATUS_TRANSITION',
'APPLIED 또는 REVIEWING 상태의 인증만 검토할 수 있습니다.',
{
currentStatus: input.currentStatus,
},
),
);
}
@@ -41,7 +45,10 @@ export function approveVendorCertification(
// U018: 서비스 권역이 없으면 승인 불가
if (input.coverageRegionCount === 0) {
return failure(
appError('MISSING_COVERAGE_REGION', '서비스 권역이 최소 1개 이상 있어야 승인할 수 있습니다.'),
appError(
'MISSING_COVERAGE_REGION',
'서비스 권역이 최소 1개 이상 있어야 승인할 수 있습니다.',
),
);
}
}
+5 -5
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/infrastructure",
"name": "@startover/infrastructure",
"version": "0.0.1",
"private": false,
"type": "module",
@@ -20,10 +20,10 @@
},
"dependencies": {
"@prisma/client": "^6.1.0",
"@relink/application": "workspace:*",
"@relink/database": "workspace:*",
"@relink/domain": "workspace:*",
"@relink/shared": "workspace:*"
"@startover/application": "workspace:*",
"@startover/database": "workspace:*",
"@startover/domain": "workspace:*",
"@startover/shared": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.2",
+1 -1
View File
@@ -6,5 +6,5 @@ export default defineConfig({
dts: true,
clean: true,
sourcemap: true,
external: ['@relink/application', '@relink/domain', '@relink/shared'],
external: ['@startover/application', '@startover/domain', '@startover/shared'],
});
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/shared",
"name": "@startover/shared",
"version": "0.0.1",
"private": false,
"type": "module",
+1 -1
View File
@@ -1,4 +1,4 @@
export const APP_NAME = 'Re:Link' as const;
export const APP_NAME = 'Startover' as const;
export const PAGINATION = {
DEFAULT_PAGE: 1,
+1 -1
View File
@@ -25,7 +25,7 @@ export function expectFailure<T, E = AppError>(result: Result<T, E>): E {
* 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';
const base = baseUrl ?? 'postgresql://startover:startover_test@localhost:5433/startover_test';
return base;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/ui",
"name": "@startover/ui",
"version": "0.0.1",
"private": false,
"type": "module",
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @relink/ui - Shared UI components
* @startover/ui - Shared UI components
*/
export { Button } from './button.js';