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
+1
View File
@@ -0,0 +1 @@
/Users/johngreen/Dev/Re_Link/.env
+1 -1
View File
@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
transpilePackages: ['@relink/ui', '@relink/shared'],
transpilePackages: ['@startover/ui', '@startover/shared'],
};
export default nextConfig;
+6 -6
View File
@@ -1,5 +1,5 @@
{
"name": "@relink/web",
"name": "@startover/web",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -15,11 +15,11 @@
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
"@prisma/client": "^6.1.0",
"@relink/database": "workspace:*",
"@relink/domain": "workspace:*",
"@relink/infrastructure": "workspace:*",
"@relink/shared": "workspace:*",
"@relink/ui": "workspace:*",
"@startover/database": "workspace:*",
"@startover/domain": "workspace:*",
"@startover/infrastructure": "workspace:*",
"@startover/shared": "workspace:*",
"@startover/ui": "workspace:*",
"argon2": "^0.44.0",
"next": "^15.1.0",
"next-auth": "5.0.0-beta.30",
@@ -5,7 +5,7 @@ import {
teardownTestDatabase,
cleanAllTables,
seedTestMasterData,
} from '@relink/database';
} from '@startover/database';
import {
createStoreDraftService,
submitStoreService,
@@ -5,7 +5,7 @@ import {
teardownTestDatabase,
cleanAllTables,
seedTestMasterData,
} from '@relink/database';
} from '@startover/database';
import {
createStoreDraftService,
submitStoreService,
@@ -71,7 +71,12 @@ describe('Match Request Service Integration Tests', () => {
if (!createResult.ok) throw new Error('createStoreDraft failed');
await submitStoreService(prisma, createResult.value.publicId, ownerUserId.toString());
await reviewStoreService(prisma, createResult.value.publicId, 'APPROVED', operatorUserId.toString());
await reviewStoreService(
prisma,
createResult.value.publicId,
'APPROVED',
operatorUserId.toString(),
);
const policyVersion = await prisma.policyVersion.create({
data: {
@@ -5,7 +5,7 @@ import {
teardownTestDatabase,
cleanAllTables,
seedTestMasterData,
} from '@relink/database';
} from '@startover/database';
import {
createStoreDraftService,
submitStoreService,
@@ -5,7 +5,7 @@ import {
teardownTestDatabase,
cleanAllTables,
seedTestMasterData,
} from '@relink/database';
} from '@startover/database';
import {
createStoreDraftService,
submitStoreService,
@@ -5,7 +5,7 @@ import {
teardownTestDatabase,
cleanAllTables,
seedTestMasterData,
} from '@relink/database';
} from '@startover/database';
import {
applyVendorCertificationService,
reviewVendorCertificationService,
+1 -1
View File
@@ -1,5 +1,5 @@
import { revalidatePath } from 'next/cache';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { releaseEscrowService } from '@/services/contract-service';
import ContractActionButtons from './ContractActionButtons';
+17 -4
View File
@@ -4,7 +4,7 @@ export default function AdminDashboardPage() {
return (
<main className="p-8">
<h1 className="text-2xl font-bold text-gray-900"> </h1>
<p className="mt-1 text-sm text-gray-500">Re:Link </p>
<p className="mt-1 text-sm text-gray-500">Startover </p>
{/* KPI 요약 */}
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
@@ -75,10 +75,23 @@ export default function AdminDashboardPage() {
{[
{ action: '매장 승인', target: '강남역 카페 양도', actor: '김운영', time: '10분 전' },
{ action: '업체 인증 승인', target: '(주)클린철거', actor: '이운영', time: '1시간 전' },
{ action: '에스크로 입금 확인', target: '선릉역 한식당 계약', actor: 'SYSTEM', time: '2시간 전' },
{ action: '매칭 요청 수락', target: '홍대 디저트카페', actor: '박운영', time: '3시간 전' },
{
action: '에스크로 입금 확인',
target: '선릉역 한식당 계약',
actor: 'SYSTEM',
time: '2시간 전',
},
{
action: '매칭 요청 수락',
target: '홍대 디저트카페',
actor: '박운영',
time: '3시간 전',
},
].map((log, i) => (
<div key={i} className="flex items-center justify-between border-b border-gray-50 py-2 last:border-0">
<div
key={i}
className="flex items-center justify-between border-b border-gray-50 py-2 last:border-0"
>
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-900">{log.action}</span>
<span className="text-sm text-gray-500">{log.target}</span>
@@ -1,6 +1,6 @@
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { InviteForm } from './invite-form';
const prisma = createPrismaClient();
+1 -1
View File
@@ -1,5 +1,5 @@
import { revalidatePath } from 'next/cache';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { reviewStoreService } from '@/services/store-service';
import StoreActionButtons from './StoreActionButtons';
+1 -1
View File
@@ -1,5 +1,5 @@
import { revalidatePath } from 'next/cache';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { reviewSubsidyCaseService } from '@/services/subsidy-case-service';
import SubsidyActionButtons from './SubsidyActionButtons';
+1 -1
View File
@@ -1,5 +1,5 @@
import { revalidatePath } from 'next/cache';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { reviewVendorCertificationService } from '@/services/vendor-certification-service';
import VendorActionButtons from './VendorActionButtons';
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import type { ConsentType, ProfileType, UserRole } from '@prisma/client';
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import argon2 from 'argon2';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import type { UserRole } from '@prisma/client';
+1 -1
View File
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
@@ -1,13 +1,10 @@
import { NextResponse, type NextRequest } from 'next/server';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { submitStoreService } from '@/services/store-service';
const prisma = createPrismaClient();
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const body = await request.json().catch(() => ({}));
const actorUserId = (body as Record<string, string>).actorUserId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { NextResponse, type NextRequest } from 'next/server';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { createStoreDraftService } from '@/services/store-service';
import { z } from 'zod';
+1 -1
View File
@@ -2,7 +2,7 @@
import { z } from 'zod';
import argon2 from 'argon2';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
import { randomBytes } from 'crypto';
+1 -1
View File
@@ -1,6 +1,6 @@
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
+16 -5
View File
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
@@ -74,10 +74,19 @@ export default async function ContractsPage() {
<p className="text-center text-sm text-gray-500"> </p>
) : (
contracts.map((contract) => {
const statusInfo = STATUS_MAP[contract.status] ?? { label: contract.status, color: 'bg-gray-100 text-gray-700' };
const escrowInfo = ESCROW_MAP[contract.escrowStatus] ?? { label: contract.escrowStatus, color: 'text-gray-500' };
const statusInfo = STATUS_MAP[contract.status] ?? {
label: contract.status,
color: 'bg-gray-100 text-gray-700',
};
const escrowInfo = ESCROW_MAP[contract.escrowStatus] ?? {
label: contract.escrowStatus,
color: 'text-gray-500',
};
return (
<div key={contract.publicId} className="rounded-lg border border-gray-200 bg-white p-5">
<div
key={contract.publicId}
className="rounded-lg border border-gray-200 bg-white p-5"
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
@@ -90,7 +99,9 @@ export default async function ContractsPage() {
: {new Date(contract.createdAt).toLocaleDateString('ko-KR')}
</p>
</div>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}>
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}
>
{statusInfo.label}
</span>
</div>
+3 -3
View File
@@ -6,8 +6,8 @@ import { AuthButtons } from './auth-buttons';
import './globals.css';
export const metadata: Metadata = {
title: 'Re:Link',
description: 'Re:Link - 폐업 · 양도 · 창업을 잇는 중개 플랫폼',
title: 'Startover',
description: 'Startover - 폐업 · 양도 · 창업을 잇는 중개 플랫폼',
};
const OPERATOR_ROLES = [
@@ -26,7 +26,7 @@ async function Navigation() {
<nav className="border-b border-gray-200 bg-white">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4">
<Link href="/" className="text-xl font-bold text-blue-600">
Re:Link
Startover
</Link>
<div className="flex items-center gap-6">
<Link href="/stores" className="text-sm text-gray-700 hover:text-blue-600">
+9 -6
View File
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
@@ -42,7 +42,10 @@ export default async function MatchingPage() {
<p className="text-center text-sm text-gray-500"> </p>
) : (
requests.map((req) => {
const statusInfo = STATUS_LABELS[req.status] ?? { label: req.status, color: 'bg-gray-100 text-gray-700' };
const statusInfo = STATUS_LABELS[req.status] ?? {
label: req.status,
color: 'bg-gray-100 text-gray-700',
};
return (
<div key={req.publicId} className="rounded-lg border border-gray-200 bg-white p-5">
<div className="flex items-start justify-between">
@@ -53,14 +56,14 @@ export default async function MatchingPage() {
{MATCH_TYPE_LABELS[req.matchType] ?? req.matchType}
</span>
</div>
{req.message && (
<p className="mt-1 text-sm text-gray-600">{req.message}</p>
)}
{req.message && <p className="mt-1 text-sm text-gray-600">{req.message}</p>}
<p className="mt-2 text-xs text-gray-400">
: {new Date(req.createdAt).toLocaleDateString('ko-KR')}
</p>
</div>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}>
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}
>
{statusInfo.label}
</span>
</div>
+13 -4
View File
@@ -4,7 +4,7 @@ export default function HomePage() {
return (
<main className="mx-auto max-w-7xl px-4 py-16">
<div className="text-center">
<h1 className="text-5xl font-bold text-gray-900">Re:Link</h1>
<h1 className="text-5xl font-bold text-gray-900">Startover</h1>
<p className="mt-4 text-xl text-gray-600"> · · </p>
<p className="mt-2 text-gray-500">
1 ··
@@ -32,7 +32,10 @@ export default function HomePage() {
<p className="mt-2 text-sm text-gray-600">
, , .
</p>
<Link href="/stores/new" className="mt-3 inline-block text-sm text-blue-600 hover:underline">
<Link
href="/stores/new"
className="mt-3 inline-block text-sm text-blue-600 hover:underline"
>
</Link>
</div>
@@ -66,7 +69,10 @@ export default function HomePage() {
<p className="mt-2 text-sm text-gray-600">
, .
</p>
<Link href="/subsidies" className="mt-3 inline-block text-sm text-blue-600 hover:underline">
<Link
href="/subsidies"
className="mt-3 inline-block text-sm text-blue-600 hover:underline"
>
</Link>
</div>
@@ -76,7 +82,10 @@ export default function HomePage() {
<p className="mt-2 text-sm text-gray-600">
, , .
</p>
<Link href="/contracts" className="mt-3 inline-block text-sm text-blue-600 hover:underline">
<Link
href="/contracts"
className="mt-3 inline-block text-sm text-blue-600 hover:underline"
>
</Link>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
+1 -1
View File
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
+1 -1
View File
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
+9 -4
View File
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
export const dynamic = 'force-dynamic';
@@ -40,7 +40,7 @@ export default async function SubsidiesPage() {
<div className="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
<h3 className="text-sm font-semibold text-blue-800"> </h3>
<p className="mt-1 text-sm text-blue-700">
Re:Link은 , · · ·
Startover은 , · · ·
.
</p>
</div>
@@ -51,7 +51,10 @@ export default async function SubsidiesPage() {
<p className="text-center text-sm text-gray-500"> </p>
) : (
cases.map((c) => {
const statusInfo = STATUS_MAP[c.status] ?? { label: c.status, color: 'bg-gray-100 text-gray-700' };
const statusInfo = STATUS_MAP[c.status] ?? {
label: c.status,
color: 'bg-gray-100 text-gray-700',
};
const total = c.checklistItems.length;
const checked = c.checklistItems.filter((item) => item.status === 'CHECKED').length;
return (
@@ -63,7 +66,9 @@ export default async function SubsidiesPage() {
: {new Date(c.createdAt).toLocaleDateString('ko-KR')}
</p>
</div>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}>
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${statusInfo.color}`}
>
{statusInfo.label}
</span>
</div>
+2 -2
View File
@@ -1,9 +1,9 @@
'use server';
import { revalidatePath } from 'next/cache';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import { applyVendorCertificationService } from '@/services/vendor-certification-service';
import type { VendorType } from '@relink/domain';
import type { VendorType } from '@startover/domain';
const prisma = createPrismaClient();
+1 -1
View File
@@ -1,4 +1,4 @@
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import VendorApplicationForm from './vendor-application-form';
export const dynamic = 'force-dynamic';
+4 -4
View File
@@ -2,7 +2,7 @@ import NextAuth from 'next-auth';
import type { NextAuthConfig } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Kakao from 'next-auth/providers/kakao';
import { createPrismaClient } from '@relink/database';
import { createPrismaClient } from '@startover/database';
import argon2 from 'argon2';
import type { UserRole } from '@prisma/client';
@@ -193,7 +193,7 @@ const fullConfig: NextAuthConfig = {
return {
id: String(profile.id),
name: kakaoProfile?.nickname ?? null,
email: (kakaoAccount?.email as string) || `kakao_${profile.id}@placeholder.relink`,
email: (kakaoAccount?.email as string) || `kakao_${profile.id}@placeholder.startover`,
image: kakaoProfile?.profile_image_url ?? null,
};
},
@@ -239,11 +239,11 @@ const fullConfig: NextAuthConfig = {
},
async signIn({ user, account }) {
if (account?.provider === 'kakao') {
// placeholder 이메일(@placeholder.relink)은 비즈 앱 전환 전 임시 처리
// placeholder 이메일(@placeholder.startover)은 비즈 앱 전환 전 임시 처리
const email = user.email;
if (!email) return false;
const isPlaceholder = email.endsWith('@placeholder.relink');
const isPlaceholder = email.endsWith('@placeholder.startover');
const existing = !isPlaceholder
? await prisma.user.findFirst({
where: { emailNormalized: email.toLowerCase().trim() },
+3 -3
View File
@@ -5,9 +5,9 @@ import {
checkIdempotency,
openDispute,
type ContractType,
} from '@relink/domain';
import { createAuditLog, enqueueOutboxEvent } from '@relink/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
} from '@startover/domain';
import { createAuditLog, enqueueOutboxEvent } from '@startover/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
// ---------------------------------------------------------------------------
// I012: 계약 생성 서비스
+20 -6
View File
@@ -6,9 +6,9 @@ import {
type MatchType,
type MatchSourceType,
type StoreSearchCriteria,
} from '@relink/domain';
import { createAuditLog, enqueueOutboxEvent } from '@relink/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
} from '@startover/domain';
import { createAuditLog, enqueueOutboxEvent } from '@startover/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface CreateMatchRequestServiceInput {
readonly storePublicId: string;
@@ -34,7 +34,9 @@ export async function createMatchRequestService(
});
if (!store) {
return failure(appError('NOT_FOUND', '매장을 찾을 수 없습니다.', { storePublicId: input.storePublicId }));
return failure(
appError('NOT_FOUND', '매장을 찾을 수 없습니다.', { storePublicId: input.storePublicId }),
);
}
const openRequest = await prisma.matchRequest.findFirst({
@@ -118,7 +120,9 @@ export async function acceptMatchRequestService(
});
if (!matchRequest) {
return failure(appError('NOT_FOUND', '매칭 요청을 찾을 수 없습니다.', { matchRequestPublicId }));
return failure(
appError('NOT_FOUND', '매칭 요청을 찾을 수 없습니다.', { matchRequestPublicId }),
);
}
const domainResult = acceptMatchRequest({
@@ -172,7 +176,17 @@ export async function acceptMatchRequestService(
export async function searchStoresService(
prisma: PrismaClient,
criteria: StoreSearchCriteria,
): Promise<Result<{ stores: Array<{ publicId: string; listingTitle: string; dealStatus: string }>; total: number; page: number; limit: number }, AppError>> {
): Promise<
Result<
{
stores: Array<{ publicId: string; listingTitle: string; dealStatus: string }>;
total: number;
page: number;
limit: number;
},
AppError
>
> {
const defaults = buildSearchDefaults(criteria);
const where: Record<string, unknown> = {
+3 -3
View File
@@ -12,9 +12,9 @@ import {
type StoreData,
type ViewerContext,
type FilteredStoreData,
} from '@relink/domain';
import { createAuditLog, enqueueOutboxEvent } from '@relink/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
} from '@startover/domain';
import { createAuditLog, enqueueOutboxEvent } from '@startover/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
function buildRegionChecker(regions: { code: string; isBetaEnabled: boolean }[]): RegionChecker {
const betaCodes = new Set(regions.filter((r) => r.isBetaEnabled).map((r) => r.code));
@@ -4,9 +4,9 @@ import {
reviewSubsidyCase,
type ChecklistItemTemplate,
type SubsidyReviewDecision,
} from '@relink/domain';
import { createAuditLog, enqueueOutboxEvent } from '@relink/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
} from '@startover/domain';
import { createAuditLog, enqueueOutboxEvent } from '@startover/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface CreateSubsidyCaseServiceInput {
readonly storePublicId: string;
@@ -5,9 +5,9 @@ import {
approveVendorCertification,
type VendorType,
type VendorCertificationDecision,
} from '@relink/domain';
import { createAuditLog, enqueueOutboxEvent } from '@relink/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@relink/shared';
} from '@startover/domain';
import { createAuditLog, enqueueOutboxEvent } from '@startover/infrastructure';
import { success, failure, appError, type Result, type AppError } from '@startover/shared';
export interface ApplyVendorCertificationServiceInput {
readonly ownerUserId: string;
+1 -1
View File
@@ -3,7 +3,7 @@ import { resolve } from 'path';
export default defineConfig({
test: {
name: '@relink/web',
name: '@startover/web',
globals: true,
environment: 'node',
testTimeout: 30000,