Skip to content

Commit 94fc0c0

Browse files
SOIVclaude
andcommitted
feat(setup): Phase 1.95.1~1.95.2 Setup 모드 전환 및 백엔드 API 구현
**1.95.1 모드 전환 시스템** - setup/mode.ts: installed.lock / fieldstack.config.json 유틸, applyConfigToEnv(), scheduleRestart() - index.ts: isInstalled() 기반 Setup/앱 모드 분기, config 파일 → process.env 자동 반영 - app.ts: createSetupApp() 팩토리 추가, 앱 모드에서 GET /setup/status → installed:true 반환 - config/env.ts: postgres DATABASE_URL refine 제거 (DB 검증을 initDb() 시점으로 이동) **1.95.2 Setup 백엔드 API** - routes/setup.ts: GET /setup/status, GET /setup/db/detect, POST /setup/db/provision (SSE), POST /setup/db/test, POST /setup/complete (SSE — DB→마이그레이션→관리자→config→lock→재시작) - setup/docker.ts: Docker 감지, postgres:16-alpine pull·컨테이너 프로비저닝, 연결 폴링 - setup/runtime.ts: Docker/systemd/native 런타임 병렬 감지 및 provisioner 추상화 - Docker: fieldstack-postgres 컨테이너 자동 생성 (--restart unless-stopped) - systemd: systemctl start + peer/sudo 폴백으로 fieldstack 유저·DB 생성 - native: 실행 중인 pg에서 fieldstack 유저·DB 생성 시도 **SQLite 제공자 구현 (개발/테스트 전용)** - db/providers/sqlite.ts: better-sqlite3 기반 실제 구현 - 데이터 디렉터리 자동 생성, gen_random_uuid()/now() 유저 함수 등록 - $N → ? 파라미터 변환, RETURNING 절 지원, BEGIN/COMMIT 수동 트랜잭션 - db/migrations/index.ts: {{SERIAL_PK}} 토큰 추가, _migrations 테이블에 applyDialect() 적용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c529a0d commit 94fc0c0

13 files changed

Lines changed: 1371 additions & 40 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
## Project Status
88

9-
Phase 1.5 진행 중 (2026-04-14 기준). Phase 1.9 (API 서버·DB·인증 백엔드·공유 링크) 완료. Phase 1.5.3 로그인 UX 완료 (실패/잠금/세션 만료, 비밀번호 복구 UI, mock 계정 시스템). `packages/controls` P0/P0.5 컴포넌트 구현 완료 (`ready: true`). Storybook 세팅 완료 (port 6007). `@fieldstack/core/browser` 브라우저 전용 엔트리 분리 완료 — 웹 앱은 반드시 이 경로로 import.
9+
Phase 1.95 진행 중 (2026-04-16 기준). Phase 1.9 (API 서버·DB·인증 백엔드·공유 링크) 완료. Phase 1.5 전 항목 완료. Phase 1.95.1 모드 전환 시스템 완료 (`installed.lock` / `fieldstack.config.json` 기반). Phase 1.95.2 Setup 백엔드 API 완료 (Docker/systemd/native 런타임 자동 감지·프로비저닝, SSE 스트리밍). SQLite 제공자 실제 구현 완료(`better-sqlite3`). `@fieldstack/core/browser` 브라우저 전용 엔트리 분리 완료 — 웹 앱은 반드시 이 경로로 import.
1010

1111
---
1212

apps/api/src/app.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { BackendRouteRegistration } from './loader';
2323
import { createAuthRouter } from './routes/auth';
2424
import { healthRouter } from './routes/health';
2525
import { createPublicRouter } from './routes/public';
26+
import { createSetupRouter } from './routes/setup';
2627
import { createShareRouter } from './routes/share';
2728

2829
// ── App 팩토리 ────────────────────────────────────────────────
@@ -59,6 +60,11 @@ export function createApp(services?: AppServices) {
5960
// ── Core routes ───────────────────────────────────────────────
6061
app.use('/health', healthRouter);
6162

63+
// 앱 모드에서 /setup/status는 installed:true를 반환 (프론트엔드 모드 감지용)
64+
app.get('/setup/status', (_req, res) => {
65+
res.json({ success: true, data: { installed: true } });
66+
});
67+
6268
if (services) {
6369
app.use('/auth', createAuthRouter(services));
6470
app.use('/core/share', createShareRouter(services));
@@ -83,13 +89,43 @@ export function createAppWithPublicRouter(
8389
app.use(express.urlencoded({ extended: true }));
8490

8591
app.use('/health', healthRouter);
92+
93+
// 앱 모드에서 /setup/status는 installed:true를 반환 (프론트엔드 모드 감지용)
94+
app.get('/setup/status', (_req, res) => {
95+
res.json({ success: true, data: { installed: true } });
96+
});
97+
8698
app.use('/auth', createAuthRouter(services));
8799
app.use('/core/share', createShareRouter(services));
88100
app.use('/s', createPublicRouter(services.sharedLink, getRenderer));
89101

90102
return app;
91103
}
92104

105+
// ── Setup 전용 앱 팩토리 (installed.lock 없을 때) ─────────────
106+
107+
export function createSetupApp(): express.Application {
108+
const app = express();
109+
110+
app.use(cors());
111+
app.use(express.json());
112+
app.use(express.urlencoded({ extended: true }));
113+
114+
app.use('/health', healthRouter);
115+
app.use('/setup', createSetupRouter());
116+
117+
// 프로덕션: 빌드된 프론트엔드 정적 파일 서빙
118+
const publicDir = path.join(__dirname, '..', 'public');
119+
if (fs.existsSync(publicDir)) {
120+
app.use(express.static(publicDir));
121+
app.get('*', (_req, res) => {
122+
res.sendFile(path.join(publicDir, 'index.html'));
123+
});
124+
}
125+
126+
return app;
127+
}
128+
93129
// ── 모듈 라우터 마운트 ────────────────────────────────────────
94130

95131
export async function mountModuleRouters(

apps/api/src/config/env.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ const EnvSchema = z.object({
1414
TOTP_ISSUER: z.string().default('Fieldstack'),
1515
// Shared Link
1616
PUBLIC_URL: z.string().url().optional(),
17-
}).refine(
18-
(env) => env.DB_PROVIDER !== 'postgres' || Boolean(env.DATABASE_URL),
19-
{ message: 'DATABASE_URL is required when DB_PROVIDER=postgres', path: ['DATABASE_URL'] },
20-
);
17+
});
18+
// DATABASE_URL 존재 여부는 initDb() 호출 시 검증한다.
19+
// Setup 모드에서는 DB 설정이 아직 없으므로 여기서 강제 검증하지 않는다.
2120

2221
export type Env = z.infer<typeof EnvSchema>;
2322

apps/api/src/index.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { validateEnv } from './config/env';
66
import {
77
createApp,
88
createAppWithPublicRouter,
9+
createSetupApp,
910
finalizeApp,
1011
initDb,
1112
initServices,
@@ -18,6 +19,10 @@ import {
1819
scanBackendModules,
1920
validateModuleDependencies,
2021
} from './loader';
22+
import { applyConfigToEnv, isInstalled } from './setup/mode';
23+
24+
// ── fieldstack.config.json → process.env 반영 (env vars 우선) ─
25+
applyConfigToEnv();
2126

2227
// ── 환경변수 검증 (누락·오류 시 즉시 종료) ────────────────────
2328
const env = validateEnv(process.env);
@@ -35,23 +40,40 @@ if (env.INSTALL_MODE === 'bypass') {
3540
// modules/ 디렉터리는 프로젝트 루트 기준 (apps/api/src → ../../../modules)
3641
const MODULES_DIR = path.join(__dirname, '..', '..', '..', 'modules');
3742

38-
// ── DB 초기화 → 마이그레이션 → 서비스 초기화 → 모듈 로드 → 서버 시작 ──
39-
async function start() {
43+
// ── Setup 모드 ─────────────────────────────────────────────────
44+
// installed.lock 없고 bypass 아닐 때 → Setup 마법사만 서빙
45+
async function startSetup() {
46+
console.log('[fieldstack][api] *** SETUP MODE — installation wizard active ***');
47+
const app = createSetupApp();
48+
finalizeApp(app);
49+
app.listen(env.PORT, () => {
50+
console.log(`[fieldstack][api] setup server listening on http://localhost:${env.PORT}`);
51+
});
52+
}
53+
54+
// ── 앱 모드 ────────────────────────────────────────────────────
55+
// DB 초기화 → 마이그레이션 → 서비스 초기화 → 모듈 로드 → 서버 시작
56+
async function startApp() {
4057
let services;
4158

4259
if (env.DB_PROVIDER === 'postgres' && env.DATABASE_URL) {
4360
const db = await initDb();
4461
await runMigrations(db);
4562
services = await initServices(db);
4663
console.log('[fieldstack][api] DB initialized and migrations applied');
64+
} else if (env.DB_PROVIDER === 'sqlite') {
65+
const db = await initDb();
66+
await runMigrations(db);
67+
services = await initServices(db);
68+
console.log('[fieldstack][api] SQLite DB initialized and migrations applied');
4769
}
4870

4971
let app;
5072
if (services) {
5173
const { getSharedLinkRenderer } = await import('@fieldstack/core');
5274
app = createAppWithPublicRouter(services, getSharedLinkRenderer);
5375
} else {
54-
// DB 없이 시작 (INSTALL_MODE=bypass) — 헬스체크만 동작
76+
// DB 없이 시작 (INSTALL_MODE=bypass, DB 미설정) — 헬스체크만 동작
5577
app = createApp();
5678
}
5779

@@ -82,7 +104,12 @@ async function start() {
82104
});
83105
}
84106

85-
start().catch((err) => {
107+
// ── 진입점 ──────────────────────────────────────────────────────
108+
const shouldSetup = !isInstalled() && env.INSTALL_MODE !== 'bypass';
109+
110+
const boot = shouldSetup ? startSetup : startApp;
111+
112+
boot().catch((err) => {
86113
console.error('[fieldstack][api] startup failed:', err);
87114
process.exit(1);
88115
});

0 commit comments

Comments
 (0)