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:
@@ -9,5 +9,11 @@ REDIS_URL="redis://localhost:6379"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_ADMIN_URL="http://localhost:3001"
|
||||
|
||||
# Auth.js
|
||||
AUTH_SECRET="iSceQlYVZ5fhUQdMyFBC0Z8kMo3nMfJRtyPSxbFo6Eg="
|
||||
NEXTAUTH_URL="http://localhost:4100"
|
||||
AUTH_KAKAO_CLIENT_ID="3644464b875f62dc139fe75fef96477c"
|
||||
AUTH_KAKAO_CLIENT_SECRET="cjijqCD14S5JXyfjeiatFIxIMU1w9Ldr"
|
||||
|
||||
# Node
|
||||
NODE_ENV="development"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Database
|
||||
DATABASE_URL="postgresql://relink:relink_dev@localhost:5432/relink_dev"
|
||||
DATABASE_TEST_URL="postgresql://relink:relink_test@localhost:5433/relink_test"
|
||||
DATABASE_URL="postgresql://startover:startover_dev@localhost:5432/startover_dev"
|
||||
DATABASE_TEST_URL="postgresql://startover:startover_test@localhost:5433/startover_test"
|
||||
|
||||
# Redis
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# ===========================================
|
||||
# Re:Link Production Environment Variables
|
||||
# Startover Production Environment Variables
|
||||
# Copy to .env.production and fill in values
|
||||
# ===========================================
|
||||
|
||||
# Database
|
||||
DB_USER=relink
|
||||
DB_USER=startover
|
||||
DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||
DB_NAME=relink_prod
|
||||
DB_NAME=startover_prod
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy Re:Link
|
||||
name: Deploy Startover
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
# Re:Link (리링크) 실행형 개발계획서
|
||||
# Startover (스타트오버) 실행형 개발계획서
|
||||
|
||||
> 폐업-철거-인테리어 선순환 통합 매칭 플랫폼
|
||||
> 작성일: 2026-03-07 | 기준 문서: 2026년 예비창업패키지 사업계획서
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
이 문서는 단순 기능 목록이 아니라, Re:Link MVP를 실제로 만들기 위해 먼저 고정해야 할 **정책, 운영, 계측, 아키텍처, 일정**을 정리한 실행 기준 문서다.
|
||||
이 문서는 단순 기능 목록이 아니라, Startover MVP를 실제로 만들기 위해 먼저 고정해야 할 **정책, 운영, 계측, 아키텍처, 일정**을 정리한 실행 기준 문서다.
|
||||
|
||||
### 1-1. 문서 역할
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
### 1-2. 토스식 의사결정 원칙
|
||||
|
||||
Re:Link의 MVP 의사결정은 아래 원칙을 기본값으로 삼는다.
|
||||
Startover의 MVP 의사결정은 아래 원칙을 기본값으로 삼는다.
|
||||
|
||||
| 원칙 | 적용 방식 |
|
||||
|------|-----------|
|
||||
@@ -529,7 +529,7 @@ re-link/
|
||||
|
||||
## 14. 최종 정리
|
||||
|
||||
Re:Link의 MVP는 단순한 매칭 앱이 아니라, **정책과 운영으로 신뢰를 만드는 거래 플랫폼**이다. 따라서 개발 우선순위는 `예쁜 UI`나 `기능 수`가 아니라 아래 순서를 따라야 한다.
|
||||
Startover의 MVP는 단순한 매칭 앱이 아니라, **정책과 운영으로 신뢰를 만드는 거래 플랫폼**이다. 따라서 개발 우선순위는 `예쁜 UI`나 `기능 수`가 아니라 아래 순서를 따라야 한다.
|
||||
|
||||
1. 정책 고정
|
||||
2. 운영 도구 구축
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# ============================================
|
||||
# Re:Link Turborepo Multi-stage Dockerfile
|
||||
# Startover Turborepo Multi-stage Dockerfile
|
||||
# Usage: docker build --build-arg APP_NAME=web .
|
||||
# ============================================
|
||||
|
||||
@@ -32,7 +32,7 @@ COPY . .
|
||||
|
||||
# Generate Prisma client & build
|
||||
RUN cd packages/database && npx prisma generate
|
||||
RUN pnpm turbo run build --filter=@relink/${APP_NAME}
|
||||
RUN pnpm turbo run build --filter=@startover/${APP_NAME}
|
||||
|
||||
# Ensure public directories exist for COPY
|
||||
RUN mkdir -p /app/apps/${APP_NAME}/public
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@relink/admin",
|
||||
"name": "@startover/admin",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean": "rm -rf .next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@relink/shared": "workspace:*",
|
||||
"@relink/ui": "workspace:*",
|
||||
"@startover/shared": "workspace:*",
|
||||
"@startover/ui": "workspace:*",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Re:Link Admin',
|
||||
description: 'Re:Link 운영 콘솔',
|
||||
title: 'Startover Admin',
|
||||
description: 'Startover 운영 콘솔',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function AdminHomePage() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||
<h1 className="text-4xl font-bold">Re:Link Admin</h1>
|
||||
<h1 className="text-4xl font-bold">Startover Admin</h1>
|
||||
<p className="mt-4 text-lg text-gray-600">운영 콘솔</p>
|
||||
</main>
|
||||
);
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/Users/johngreen/Dev/Re_Link/.env
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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,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,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
@@ -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,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,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';
|
||||
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,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,5 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
import { createPrismaClient } from '@relink/database';
|
||||
import { createPrismaClient } from '@startover/database';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Vendored
+2
-2
@@ -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();
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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';
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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: 계약 생성 서비스
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
name: '@relink/web',
|
||||
name: '@startover/web',
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
testTimeout: 30000,
|
||||
|
||||
+3
-3
@@ -2,14 +2,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ===========================================
|
||||
# Re:Link Auto Deploy Script
|
||||
# Startover Auto Deploy Script
|
||||
# Triggered by Gitea webhook on push to main
|
||||
# ===========================================
|
||||
|
||||
APP_DIR="$HOME/relink"
|
||||
APP_DIR="$HOME/startover"
|
||||
LOG_FILE="$APP_DIR/deploy.log"
|
||||
LOCK_FILE="$APP_DIR/deploy.lock"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/Re_Link.git"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/startover.git"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/bin/bash
|
||||
# ===========================================
|
||||
# Re:Link Poll-based Auto Deploy
|
||||
# Startover Poll-based Auto Deploy
|
||||
# Checks Gitea for new commits every minute
|
||||
# ===========================================
|
||||
|
||||
APP_DIR="$HOME/relink"
|
||||
APP_DIR="$HOME/startover"
|
||||
REPO_DIR="$APP_DIR/repo"
|
||||
LOG_FILE="$APP_DIR/deploy.log"
|
||||
LOCK_FILE="$APP_DIR/deploy.lock"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/Re_Link.git"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/startover.git"
|
||||
BRANCH="main"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; }
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ===========================================
|
||||
# Re:Link IDC Server Setup Script
|
||||
# Startover IDC Server Setup Script
|
||||
# Run once on the IDC server to initialize
|
||||
# ===========================================
|
||||
|
||||
echo "=== Re:Link Server Setup ==="
|
||||
echo "=== Startover Server Setup ==="
|
||||
|
||||
# 1. Install Docker Compose V2 plugin
|
||||
echo "[1/5] Installing Docker Compose V2 plugin..."
|
||||
@@ -31,7 +31,7 @@ echo "Act Runner v${ACT_RUNNER_VERSION} installed."
|
||||
|
||||
# 3. Create app directory
|
||||
echo "[3/5] Creating application directory..."
|
||||
APP_DIR="$HOME/relink"
|
||||
APP_DIR="$HOME/startover"
|
||||
mkdir -p "$APP_DIR"
|
||||
cd "$APP_DIR"
|
||||
|
||||
@@ -71,7 +71,7 @@ echo " cd $APP_DIR"
|
||||
echo " act_runner register \\"
|
||||
echo " --instance http://39.117.244.52:3000 \\"
|
||||
echo " --token <REGISTRATION_TOKEN> \\"
|
||||
echo " --name relink-runner \\"
|
||||
echo " --name startover-runner \\"
|
||||
echo " --labels ubuntu-latest:host"
|
||||
echo ""
|
||||
echo "2. Start the runner service:"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Re:Link Gitea Webhook Receiver
|
||||
Startover Gitea Webhook Receiver
|
||||
Listens for push events and triggers deployment.
|
||||
"""
|
||||
|
||||
@@ -13,11 +13,11 @@ import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from datetime import datetime
|
||||
|
||||
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "relink-deploy-secret")
|
||||
DEPLOY_SCRIPT = os.path.expanduser("~/relink/deploy.sh")
|
||||
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "startover-deploy-secret")
|
||||
DEPLOY_SCRIPT = os.path.expanduser("~/startover/deploy.sh")
|
||||
DEPLOY_BRANCH = "main"
|
||||
PORT = int(os.environ.get("WEBHOOK_PORT", "9000"))
|
||||
LOG_FILE = os.path.expanduser("~/relink/webhook.log")
|
||||
LOG_FILE = os.path.expanduser("~/startover/webhook.log")
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
|
||||
@@ -5,13 +5,13 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-relink}
|
||||
POSTGRES_USER: ${DB_USER:-startover}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||
POSTGRES_DB: ${DB_NAME:-relink_prod}
|
||||
POSTGRES_DB: ${DB_NAME:-startover_prod}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-relink} -d ${DB_NAME:-relink_prod}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-startover} -d ${DB_NAME:-startover_prod}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-relink}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-relink_prod}?schema=public
|
||||
DATABASE_URL: postgresql://${DB_USER:-startover}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-startover_prod}?schema=public
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET is required}
|
||||
@@ -88,7 +88,7 @@ services:
|
||||
ports:
|
||||
- "127.0.0.1:3001:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-relink}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-relink_prod}?schema=public
|
||||
DATABASE_URL: postgresql://${DB_USER:-startover}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-startover_prod}?schema=public
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
NODE_ENV: production
|
||||
labels:
|
||||
|
||||
+8
-8
@@ -4,13 +4,13 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: relink
|
||||
POSTGRES_PASSWORD: relink_dev
|
||||
POSTGRES_DB: relink_dev
|
||||
POSTGRES_USER: startover
|
||||
POSTGRES_PASSWORD: startover_dev
|
||||
POSTGRES_DB: startover_dev
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U relink -d relink_dev"]
|
||||
test: ["CMD-SHELL", "pg_isready -U startover -d startover_dev"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -20,11 +20,11 @@ services:
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_USER: relink
|
||||
POSTGRES_PASSWORD: relink_test
|
||||
POSTGRES_DB: relink_test
|
||||
POSTGRES_USER: startover
|
||||
POSTGRES_PASSWORD: startover_test
|
||||
POSTGRES_DB: startover_test
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U relink -d relink_test"]
|
||||
test: ["CMD-SHELL", "pg_isready -U startover -d startover_test"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Re:Link 이벤트 스키마 정의서
|
||||
# Startover 이벤트 스키마 정의서
|
||||
|
||||
> 문서 코드: D004
|
||||
> 버전: 1.0.0
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
### 1-1. 목적
|
||||
|
||||
이 문서는 Re:Link 플랫폼에서 `EventLog` 테이블에 저장되는 도메인 이벤트의 규격을 정의한다.
|
||||
이 문서는 Startover 플랫폼에서 `EventLog` 테이블에 저장되는 도메인 이벤트의 규격을 정의한다.
|
||||
|
||||
이벤트 로그는 세 가지 목적으로 활용된다.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Re:Link `schema.prisma` 설계 초안
|
||||
# Startover `schema.prisma` 설계 초안
|
||||
|
||||
## 목적
|
||||
|
||||
이 문서는 Re:Link MVP의 데이터 모델을 실제 `schema.prisma`로 내리기 전에, 엔티티/필드/관계/인덱스/제약 조건을 고정하기 위한 초안이다.
|
||||
이 문서는 Startover MVP의 데이터 모델을 실제 `schema.prisma`로 내리기 전에, 엔티티/필드/관계/인덱스/제약 조건을 고정하기 위한 초안이다.
|
||||
|
||||
대상 범위는 `assisted marketplace MVP`이며, 아래 원칙을 따른다.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Re:Link 베타 마스터 데이터 초안
|
||||
# Startover 베타 마스터 데이터 초안
|
||||
|
||||
## 목적
|
||||
|
||||
이 문서는 Re:Link MVP 베타에서 사용할 지역/업종 마스터 데이터의 구조와 초기 seed 원칙을 정의한다.
|
||||
이 문서는 Startover MVP 베타에서 사용할 지역/업종 마스터 데이터의 구조와 초기 seed 원칙을 정의한다.
|
||||
|
||||
핵심 목표는 아래 3가지다.
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Re:Link 계약-에스크로-분쟁 정책서
|
||||
# Startover 계약-에스크로-분쟁 정책서
|
||||
|
||||
> 문서 버전: 1.0.0
|
||||
> 기준일: 2026-03-07
|
||||
> 적용 범위: Re:Link MVP Phase 1
|
||||
> 적용 범위: Startover MVP Phase 1
|
||||
> DRI: 대표 (제품 정책), FINANCE_OPERATOR (정산 실행), TRUST_OPERATOR (계약·검수·분쟁)
|
||||
|
||||
---
|
||||
|
||||
## 목적
|
||||
|
||||
이 문서는 Re:Link 플랫폼에서 계약 생성, 전자서명 증적 수집, 에스크로 결제, 검수 절차, 정산 해제, 분쟁 처리, 환불, 수동 보정에 관한 운영 기준을 정의한다. 모든 개발 구현과 운영 의사결정은 이 문서를 최우선 기준으로 삼는다.
|
||||
이 문서는 Startover 플랫폼에서 계약 생성, 전자서명 증적 수집, 에스크로 결제, 검수 절차, 정산 해제, 분쟁 처리, 환불, 수동 보정에 관한 운영 기준을 정의한다. 모든 개발 구현과 운영 의사결정은 이 문서를 최우선 기준으로 삼는다.
|
||||
|
||||
---
|
||||
|
||||
@@ -91,7 +91,7 @@ DRAFT -> GENERATED -> SIGNING -> SIGNED -> IN_PROGRESS -> COMPLETED
|
||||
| 문서 버전 | `contractVersionId` | 서명 당시 적용된 `ContractVersion`의 ID |
|
||||
| 문서 해시 | `documentHash` | 서명 당시 문서의 SHA-256 해시 (저장 해시와 일치 여부 검증용) |
|
||||
| 증적 유형 | `evidenceType` | 서명 방식 코드 (아래 참조) |
|
||||
| 제공자 코드 | `providerCode` | 서명 기술 제공자 (예: `RELINK_CHECKBOX`, `KAKAOPAY_SIGN`) |
|
||||
| 제공자 코드 | `providerCode` | 서명 기술 제공자 (예: `STARTOVER_CHECKBOX`, `KAKAOPAY_SIGN`) |
|
||||
| 서명 해시 | `signatureHash` | 서명 행위 자체에 대한 해시 (재현 가능한 증적) |
|
||||
|
||||
### 2-2. MVP 서명 방식
|
||||
@@ -582,4 +582,4 @@ MVP Phase 1에서 아래 기능은 feature flag 뒤에 두고 기본값은 비
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 Re:Link MVP 운영 기준 문서이며, 구현 코드보다 이 문서가 우선합니다. 정책 변경이 필요한 경우 이 문서를 먼저 개정하고 개정 이력을 기록한 뒤 코드 구현에 반영합니다.*
|
||||
*이 문서는 Startover MVP 운영 기준 문서이며, 구현 코드보다 이 문서가 우선합니다. 정책 변경이 필요한 경우 이 문서를 먼저 개정하고 개정 이력을 기록한 뒤 코드 구현에 반영합니다.*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 폐업 정보 공개 정책서
|
||||
|
||||
> Re:Link MVP — 정보 공개 기준 및 역할별 열람 권한 정책
|
||||
> Startover MVP — 정보 공개 기준 및 역할별 열람 권한 정책
|
||||
> 문서 버전: v1.0.0
|
||||
> 최초 작성: 2026-03-07
|
||||
> 적용 범위: Phase 1 MVP 전체, Phase 2 데이터 구조 준비 포함
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## 1. 정보 공개 등급 체계
|
||||
|
||||
Re:Link는 폐업자의 민감한 정보를 보호하면서 거래 목적에 맞는 정보만 단계적으로 공개하는 4단계 등급 체계를 운영한다. 폐업 등록 즉시 모든 정보가 공개되지 않으며, 매칭 진행 상황과 역할에 따라 정보 접근이 제한적으로 확대된다.
|
||||
Startover는 폐업자의 민감한 정보를 보호하면서 거래 목적에 맞는 정보만 단계적으로 공개하는 4단계 등급 체계를 운영한다. 폐업 등록 즉시 모든 정보가 공개되지 않으며, 매칭 진행 상황과 역할에 따라 정보 접근이 제한적으로 확대된다.
|
||||
|
||||
### 1-1. 등급 정의
|
||||
|
||||
@@ -252,7 +252,7 @@ VENDOR_MANAGER의 경우 연락처는 운영자 중개 방식을 우선 사용
|
||||
|
||||
### 5-1. 동의 수집 항목
|
||||
|
||||
Re:Link는 `UserConsent` 모델을 통해 동의 유형별로 수집 시점, 버전, 철회 가능 여부를 관리한다.
|
||||
Startover는 `UserConsent` 모델을 통해 동의 유형별로 수집 시점, 버전, 철회 가능 여부를 관리한다.
|
||||
|
||||
| 동의 항목 | `consentType` 코드 | 수집 시점 | 철회 가능 여부 |
|
||||
|----------|-------------------|-----------|----------------|
|
||||
@@ -397,7 +397,7 @@ INTERNAL 정보에 접근하는 운영자 API는 아래 추가 보호를 적용
|
||||
|
||||
### 8-1. PII 식별 및 분리 저장
|
||||
|
||||
Re:Link에서 개인정보(PII)로 분류하는 항목과 저장 방식은 아래와 같다.
|
||||
Startover에서 개인정보(PII)로 분류하는 항목과 저장 방식은 아래와 같다.
|
||||
|
||||
| 분류 | 항목 | 저장 방식 | 접근 통제 |
|
||||
|------|------|----------|----------|
|
||||
@@ -407,11 +407,11 @@ Re:Link에서 개인정보(PII)로 분류하는 항목과 저장 방식은 아
|
||||
| 위치 정보 | 상세 주소 | 암호화 저장 | MATCHED_ONLY 이상 |
|
||||
| 사업자 정보 | 사업자등록번호 | 평문 저장 (공개 정보) | MATCHED_ONLY 이상 |
|
||||
|
||||
사업자등록번호는 공개 정보이나, Re:Link 내에서는 계약 목적으로만 사용하며 검색 노출은 MATCHED_ONLY로 제한한다.
|
||||
사업자등록번호는 공개 정보이나, Startover 내에서는 계약 목적으로만 사용하며 검색 노출은 MATCHED_ONLY로 제한한다.
|
||||
|
||||
### 8-2. 개인정보 수집 및 이용 목적
|
||||
|
||||
Re:Link가 수집하는 개인정보는 아래 목적으로만 사용한다.
|
||||
Startover가 수집하는 개인정보는 아래 목적으로만 사용한다.
|
||||
|
||||
| 수집 목적 | 해당 항목 | 보관 기간 |
|
||||
|----------|----------|----------|
|
||||
@@ -446,7 +446,7 @@ B2B 알림 상품 출시 시(Phase 2) 업체에 대한 정보 제공 범위가
|
||||
|
||||
### 8-5. 개인정보 처리 위탁
|
||||
|
||||
Re:Link가 개인정보 처리를 위탁하는 수탁자와 위탁 업무 범위는 개인정보 처리방침에 공개한다.
|
||||
Startover가 개인정보 처리를 위탁하는 수탁자와 위탁 업무 범위는 개인정보 처리방침에 공개한다.
|
||||
|
||||
| 수탁자 | 위탁 업무 |
|
||||
|--------|----------|
|
||||
@@ -497,4 +497,4 @@ Re:Link가 개인정보 처리를 위탁하는 수탁자와 위탁 업무 범위
|
||||
|------|------|----------|--------|
|
||||
| v1.0.0 | 2026-03-07 | 최초 작성 | 대표 + CTO |
|
||||
|
||||
> 이 문서는 Re:Link 운영 및 개발의 기준 정책 문서입니다. 변경 시 DRI(대표)의 승인을 거쳐야 하며, 변경 이력을 반드시 기록해야 합니다.
|
||||
> 이 문서는 Startover 운영 및 개발의 기준 정책 문서입니다. 변경 시 DRI(대표)의 승인을 거쳐야 하며, 변경 이력을 반드시 기록해야 합니다.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Re:Link 지원금 대행 정책서
|
||||
# Startover 지원금 대행 정책서
|
||||
|
||||
> 문서 코드: D001
|
||||
> 버전: 1.0.0
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
### 1-1. 포지션 원칙
|
||||
|
||||
Re:Link의 지원금 서비스는 **정부 사업의 경쟁자가 아닌 신청 보조 파트너**다.
|
||||
Startover의 지원금 서비스는 **정부 사업의 경쟁자가 아닌 신청 보조 파트너**다.
|
||||
|
||||
플랫폼은 폐업자가 정부 지원금을 받기 위해 스스로 준비해야 할 서류와 절차를 안내하고, 운영자가 서류 구비 상태를 검토하여 제출 준비 완료 여부를 판정하는 역할까지만 담당한다. 정부 기관을 대신하여 자동으로 서류를 제출하거나, 심사 결과에 영향을 주는 행위는 MVP에서 하지 않는다.
|
||||
|
||||
@@ -498,15 +498,15 @@ NOT_ELIGIBLE 판별 시: ELIGIBILITY_CHECKED 상태에서 케이스를 CLOSED
|
||||
|
||||
**[이용자 안내 및 면책 고지]**
|
||||
|
||||
Re:Link 지원금 서비스는 소상공인의 정부 지원금 신청 절차를 안내하는 보조 서비스입니다.
|
||||
Startover 지원금 서비스는 소상공인의 정부 지원금 신청 절차를 안내하는 보조 서비스입니다.
|
||||
|
||||
1. Re:Link는 정부 기관(소상공인시장진흥공단 등)을 대리하지 않으며, 지원금 심사 및 승인 결과에 영향을 주지 않습니다.
|
||||
1. Startover는 정부 기관(소상공인시장진흥공단 등)을 대리하지 않으며, 지원금 심사 및 승인 결과에 영향을 주지 않습니다.
|
||||
2. 플랫폼이 제공하는 자격 판별 결과는 입력 정보 기반의 안내용이며, 법적 효력이 없습니다.
|
||||
3. 최종 지원금 심사 및 승인 여부는 소상공인시장진흥공단의 결정에 따릅니다.
|
||||
4. Re:Link는 지원금 수령을 보장하거나 확약하지 않습니다.
|
||||
4. Startover는 지원금 수령을 보장하거나 확약하지 않습니다.
|
||||
5. 제출 서류의 진실성과 정확성에 대한 책임은 신청자 본인에게 있습니다.
|
||||
6. 허위 서류 제출 또는 부정 수급 시 관련 법령에 따라 환수 및 제재를 받을 수 있습니다.
|
||||
7. Re:Link는 세무·법률 전문가의 의견을 대체하는 자문을 제공하지 않습니다.
|
||||
7. Startover는 세무·법률 전문가의 의견을 대체하는 자문을 제공하지 않습니다.
|
||||
|
||||
### 8-2. 정부 승인 불보장 원칙
|
||||
|
||||
|
||||
+3
-3
@@ -8,9 +8,9 @@
|
||||
"test": "turbo run test",
|
||||
"type-check": "turbo run type-check",
|
||||
"clean": "turbo run clean && rm -rf node_modules",
|
||||
"db:generate": "pnpm --filter @relink/database prisma generate",
|
||||
"db:push": "pnpm --filter @relink/database prisma db push",
|
||||
"db:migrate": "pnpm --filter @relink/database prisma migrate dev",
|
||||
"db:generate": "pnpm --filter @startover/database prisma generate",
|
||||
"db:push": "pnpm --filter @startover/database prisma db push",
|
||||
"db:migrate": "pnpm --filter @startover/database prisma migrate dev",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* @relink/analytics - Event schemas and KPI calculations
|
||||
* @startover/analytics - Event schemas and KPI calculations
|
||||
*/
|
||||
|
||||
export type { AnalyticsEvent } from './event.js';
|
||||
|
||||
@@ -6,5 +6,5 @@ export default defineConfig({
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ['@relink/shared'],
|
||||
external: ['@startover/shared'],
|
||||
});
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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 @@
|
||||
DATABASE_URL="postgresql://relink:relink_dev@localhost:5432/relink_dev"
|
||||
DATABASE_URL="postgresql://startover:startover_dev@localhost:5432/startover_dev"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@relink/database",
|
||||
"name": "@startover/database",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Re:Link MVP - Prisma Schema
|
||||
// Startover MVP - Prisma Schema
|
||||
// =============================================================================
|
||||
// 설계 원칙:
|
||||
// - 내부 PK: BigInt @id @default(autoincrement())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @relink/database - Prisma client and database utilities
|
||||
* @startover/database - Prisma client and database utilities
|
||||
*/
|
||||
|
||||
export { createPrismaClient } from './client.js';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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', '승인된 매장만 공개할 수 있습니다.', {
|
||||
|
||||
@@ -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' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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개 이상 있어야 승인할 수 있습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "@relink/shared",
|
||||
"name": "@startover/shared",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "@relink/ui",
|
||||
"name": "@startover/ui",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @relink/ui - Shared UI components
|
||||
* @startover/ui - Shared UI components
|
||||
*/
|
||||
|
||||
export { Button } from './button.js';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 프로젝트: Re:Link MVP 구현 plan
|
||||
# 프로젝트: Startover MVP 구현 plan
|
||||
|
||||
## 개요
|
||||
|
||||
Re:Link MVP의 목표는 폐업자 매장 정보를 기반으로 창업자, 철거업체, 인테리어업체를 연결하는 `assisted marketplace`를 베타 수준으로 출시하는 것이다.
|
||||
Startover MVP의 목표는 폐업자 매장 정보를 기반으로 창업자, 철거업체, 인테리어업체를 연결하는 `assisted marketplace`를 베타 수준으로 출시하는 것이다.
|
||||
|
||||
이 plan은 `DEVELOPMENT_PLAN.md`를 실제 구현 단위로 쪼갠 작업 문서다. 정책 문서, 구조 작업, 테스트 시나리오, 에러 시나리오, 우선순위, 진행 상태를 한 곳에서 관리한다.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user