Platform Docs
Phase 1: Foundation - 동적 모듈 레지스트리
/platform/phase1-foundationPhase 1: Foundation - 동적 모듈 레지스트리
Note: This document reflects an earlier planning snapshot. Legacy names such as
membleorhombmay appear in historical context. Current Membloc work should usememblocnaming.
하드코딩된 모듈 레지스트리를 백엔드 API 기반 동적 레지스트리로 전환한다. 기존 모듈은 그대로 동작하면서, 마켓플레이스의 데이터 기반을 구축한다.
1. 목표
- 모듈 메타데이터를 PostgreSQL에서 관리
- membloc-app-engine에 Marketplace API 추가
- Flutter 앱이 백엔드에서 모듈 목록을 fetch하여 레지스트리 구성
- 기존 9개 모듈(Core 4 + Plugin 5)을 백엔드에도 등록
- ModuleHubScreen에 마켓플레이스 탐색 UI 추가
2. 데이터베이스 마이그레이션
migrations/002_marketplace.up.sql
-- 퍼블리셔 (모듈 개발 사업체)
CREATE TABLE publishers (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
website TEXT,
logo_url TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','approved','suspended')),
firebase_uid TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 모듈 레지스트리
CREATE TABLE modules (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
module_key TEXT NOT NULL UNIQUE,
publisher_id TEXT NOT NULL REFERENCES publishers(id),
display_name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
long_description TEXT,
category TEXT NOT NULL DEFAULT 'general',
type TEXT NOT NULL DEFAULT 'native'
CHECK (type IN ('native','webview','server')),
icon_url TEXT,
entry_url TEXT,
manifest JSONB NOT NULL DEFAULT '{}',
permissions TEXT[] NOT NULL DEFAULT '{}',
settings_schema JSONB NOT NULL DEFAULT '{}',
pricing_model TEXT NOT NULL DEFAULT 'free'
CHECK (pricing_model IN ('free','paid','freemium','subscription')),
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','review','published','suspended')),
min_homb_version TEXT DEFAULT '1.0.0',
install_count INTEGER NOT NULL DEFAULT 0,
rating_avg NUMERIC(2,1) NOT NULL DEFAULT 0,
rating_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_modules_category ON modules(category);
CREATE INDEX idx_modules_publisher ON modules(publisher_id);
CREATE INDEX idx_modules_status ON modules(status);
CREATE INDEX idx_modules_key ON modules(module_key);
-- 모듈 버전
CREATE TABLE module_versions (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
module_id TEXT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
version TEXT NOT NULL,
changelog TEXT DEFAULT '',
manifest JSONB NOT NULL DEFAULT '{}',
entry_url TEXT,
status TEXT NOT NULL DEFAULT 'approved'
CHECK (status IN ('review','approved','rejected')),
reviewed_by TEXT,
reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(module_id, version)
);
-- 모듈 설치 (가족 단위, PostgreSQL 측)
CREATE TABLE module_installations (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
family_id TEXT NOT NULL,
module_id TEXT NOT NULL REFERENCES modules(id),
module_key TEXT NOT NULL,
version TEXT NOT NULL DEFAULT '1.0.0',
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active','disabled','uninstalled')),
settings JSONB NOT NULL DEFAULT '{}',
installed_by TEXT NOT NULL,
installed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(family_id, module_key)
);
CREATE INDEX idx_installations_family ON module_installations(family_id);
CREATE INDEX idx_installations_module ON module_installations(module_id);
-- 모듈 카테고리 마스터
CREATE TABLE module_categories (
key TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
icon_name TEXT,
sort_order INTEGER NOT NULL DEFAULT 0
);
-- 초기 카테고리 데이터
INSERT INTO module_categories (key, display_name, icon_name, sort_order) VALUES
('lifestyle', '생활', 'home', 1),
('communication','소통', 'chat', 2),
('finance', '가계/금융', 'account_balance', 3),
('health', '건강', 'favorite', 4),
('education', '교육', 'school', 5),
('entertainment','여가', 'sports_esports', 6),
('productivity', '생산성', 'task_alt', 7),
('religion', '종교/신앙', 'auto_stories', 8),
('general', '기타', 'extension', 99);
migrations/002_marketplace.down.sql
DROP TABLE IF EXISTS module_installations;
DROP TABLE IF EXISTS module_versions;
DROP TABLE IF EXISTS modules;
DROP TABLE IF EXISTS publishers;
DROP TABLE IF EXISTS module_categories;
기존 모듈 시드 데이터 (migrations/003_seed_modules.up.sql)
-- homb 공식 퍼블리셔
INSERT INTO publishers (id, name, email, website, status, firebase_uid)
VALUES ('homb-official', 'homb', 'homblabs@gmail.com', NULL, 'approved', 'system');
-- Core 모듈 (type=native, isCore 개념은 manifest에)
INSERT INTO modules (id, module_key, publisher_id, display_name, description, category, type, status, manifest) VALUES
('mod-family-core', 'family_core', 'homb-official', 'Family Core',
'그룹, 권한, 공유 같은 코어 기능', 'general', 'native', 'published',
'{"isCore": true, "navEnabledByDefault": false, "feedEnabledByDefault": false}'::jsonb),
('mod-channels', 'channels', 'homb-official', '채널',
'가족 대화와 소통', 'communication', 'native', 'published',
'{"isCore": true, "supportedEntityTypes": ["message"]}'::jsonb),
('mod-memories', 'memories', 'homb-official', '추억',
'사진과 기록 보관', 'lifestyle', 'native', 'published',
'{"isCore": true, "supportedEntityTypes": ["memory"], "supportsSharing": true}'::jsonb),
('mod-archives', 'archives', 'homb-official', '아카이브',
'가족 파일과 자료 보관', 'lifestyle', 'native', 'published',
'{"isCore": true, "supportedEntityTypes": ["archive"], "supportsSharing": true}'::jsonb),
-- Plugin 모듈
('mod-travel', 'travel', 'homb-official', '여행',
'가족 여행 일정과 준비물, 기록', 'lifestyle', 'native', 'published',
'{"supportedEntityTypes": ["trip"], "supportsSharing": true, "defaultInstallSettings": {"defaultStatus": "planning", "checklistEnabled": true}}'::jsonb),
('mod-chat', 'chat', 'homb-official', '대화방',
'채팅만 하는 독립 공간', 'communication', 'native', 'published',
'{"supportedEntityTypes": ["chat_room"], "defaultInstallSettings": {"channelType": "module", "purposeKey": "chat"}}'::jsonb),
('mod-religion', 'religion', 'homb-official', '종교',
'예배, 기도 제목, 가족 신앙 이벤트 관리', 'religion', 'native', 'published',
'{"supportedEntityTypes": ["service"], "defaultInstallSettings": {"serviceLabel": "예배", "prayerNotesEnabled": true}}'::jsonb),
('mod-shopping', 'shopping', 'homb-official', '장바구니',
'가족 장보기 목록 관리', 'lifestyle', 'native', 'published',
'{"supportedEntityTypes": ["shopping_list"]}'::jsonb),
('mod-chores', 'chores', 'homb-official', '집안일',
'가족 집안일 분담과 완료 추적', 'lifestyle', 'native', 'published',
'{"supportedEntityTypes": ["chore"]}'::jsonb);
-- 버전 정보
INSERT INTO module_versions (module_id, version, status) VALUES
('mod-family-core', '1.0.0', 'approved'),
('mod-channels', '1.0.0', 'approved'),
('mod-memories', '1.0.0', 'approved'),
('mod-archives', '1.0.0', 'approved'),
('mod-travel', '1.0.0', 'approved'),
('mod-chat', '1.0.0', 'approved'),
('mod-religion', '1.0.0', 'approved'),
('mod-shopping', '1.0.0', 'approved'),
('mod-chores', '1.0.0', 'approved');
3. 백엔드 구현 (membloc-app-engine)
3.1 새로운 파일 구조
internal/
├── auth/middleware.go # 기존
├── config/config.go # 기존
├── handler/
│ ├── health.go # 기존
│ ├── family.go # 기존
│ ├── marketplace.go # [신규] 마켓플레이스 조회 API
│ └── module_install.go # [신규] 모듈 설치/해제 API
├── model/
│ ├── family.go # 기존
│ ├── user.go # 기존
│ ├── module.go # [신규] Module, ModuleVersion
│ ├── publisher.go # [신규] Publisher
│ └── module_installation.go # [신규] ModuleInstallation
├── repository/
│ ├── family.go # 기존
│ ├── module.go # [신규] 모듈 CRUD
│ └── module_installation.go # [신규] 설치 CRUD
├── service/
│ ├── family.go # 기존
│ ├── marketplace.go # [신규] 마켓플레이스 비즈니스 로직
│ └── module_install.go # [신규] 설치 비즈니스 로직
3.2 Model 정의
// internal/model/module.go
package model
import "time"
type Module struct {
ID string `json:"id" db:"id"`
ModuleKey string `json:"moduleKey" db:"module_key"`
PublisherID string `json:"publisherId" db:"publisher_id"`
DisplayName string `json:"displayName" db:"display_name"`
Description string `json:"description" db:"description"`
LongDescription *string `json:"longDescription" db:"long_description"`
Category string `json:"category" db:"category"`
Type string `json:"type" db:"type"`
IconURL *string `json:"iconUrl" db:"icon_url"`
EntryURL *string `json:"entryUrl" db:"entry_url"`
Manifest map[string]any `json:"manifest" db:"manifest"`
Permissions []string `json:"permissions" db:"permissions"`
SettingsSchema map[string]any `json:"settingsSchema" db:"settings_schema"`
PricingModel string `json:"pricingModel" db:"pricing_model"`
Status string `json:"status" db:"status"`
MinHombVersion string `json:"minHombVersion" db:"min_homb_version"`
InstallCount int `json:"installCount" db:"install_count"`
RatingAvg float64 `json:"ratingAvg" db:"rating_avg"`
RatingCount int `json:"ratingCount" db:"rating_count"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
}
type ModuleVersion struct {
ID string `json:"id" db:"id"`
ModuleID string `json:"moduleId" db:"module_id"`
Version string `json:"version" db:"version"`
Changelog string `json:"changelog" db:"changelog"`
EntryURL *string `json:"entryUrl" db:"entry_url"`
Status string `json:"status" db:"status"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
}
type ModuleCategory struct {
Key string `json:"key" db:"key"`
DisplayName string `json:"displayName" db:"display_name"`
IconName string `json:"iconName" db:"icon_name"`
SortOrder int `json:"sortOrder" db:"sort_order"`
}
// 앱이 필요로 하는 간소화된 응답
type ModuleSummary struct {
ModuleKey string `json:"moduleKey"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Category string `json:"category"`
Type string `json:"type"`
IconURL *string `json:"iconUrl"`
EntryURL *string `json:"entryUrl"`
Manifest map[string]any `json:"manifest"`
PricingModel string `json:"pricingModel"`
InstallCount int `json:"installCount"`
RatingAvg float64 `json:"ratingAvg"`
PublisherName string `json:"publisherName"`
}
3.3 API 엔드포인트
// cmd/server/main.go 에 추가할 라우트
// Marketplace (공개 - 인증 불필요)
e.GET("/api/marketplace/modules", marketplaceHandler.ListModules)
e.GET("/api/marketplace/modules/:key", marketplaceHandler.GetModule)
e.GET("/api/marketplace/categories", marketplaceHandler.ListCategories)
e.GET("/api/marketplace/featured", marketplaceHandler.GetFeatured)
// Module Install (인증 필요)
api.POST("/api/families/:familyId/modules/:key/install", moduleInstallHandler.Install)
api.POST("/api/families/:familyId/modules/:key/uninstall", moduleInstallHandler.Uninstall)
api.GET("/api/families/:familyId/modules", moduleInstallHandler.ListInstalled)
3.4 주요 핸들러 로직
handler/marketplace.go
GET /api/marketplace/modules?category=lifestyle&q=여행&page=1&limit=20
Response:
{
"modules": [ModuleSummary],
"total": 42,
"page": 1,
"hasMore": true
}
status = 'published'인 모듈만 반환- category, keyword(display_name ILIKE), pricing_model 필터
- install_count, rating_avg 기반 정렬 옵션
handler/module_install.go
POST /api/families/:familyId/modules/:key/install
Authorization: Bearer <firebase-token>
→ module_installations에 INSERT
→ modules.install_count += 1
→ Firestore families/{familyId}/modules/{key} 에도 동기화 (기존 앱 호환)
3.5 Firestore 동기화 전략
Phase 1에서는 양방향 동기화가 아닌, 백엔드 → Firestore 단방향 쓰기로 호환성 유지:
설치 요청 → membloc-app-engine (PostgreSQL에 저장)
→ Firestore families/{id}/modules/{key} 에도 write
→ Flutter 앱은 기존 Firestore stream 그대로 사용
모듈 목록 → Flutter 앱이 /api/marketplace/modules 호출
→ NativeModuleRegistry + RemoteModuleRegistry merge
4. Flutter 앱 변경
4.1 RemoteModuleRegistry
// core/modules/remote_module_registry.dart
class RemoteModuleDescriptor {
final String moduleKey;
final String displayName;
final String description;
final String category;
final String type; // native, webview, server
final String? iconUrl;
final String? entryUrl;
final Map<String, dynamic> manifest;
final String pricingModel;
final int installCount;
final double ratingAvg;
final String publisherName;
}
class RemoteModuleRegistry {
// GET /api/marketplace/modules 호출
// 결과를 로컬 캐시 (SharedPreferences 또는 Hive)
// 오프라인 시 캐시된 데이터 사용
Future<List<RemoteModuleDescriptor>> fetchAll();
Future<List<RemoteModuleDescriptor>> search(String query);
Future<List<RemoteModuleDescriptor>> byCategory(String category);
}
4.2 MergedModuleRegistry
// core/modules/merged_module_registry.dart
class MergedModuleRegistry {
final ModuleRegistry nativeRegistry; // 기존 (하드코딩)
final RemoteModuleRegistry remoteRegistry; // 신규 (백엔드)
// native 모듈이 우선, remote에서 추가 모듈 merge
// native에 없는 webview/server 타입 모듈이 여기서 추가됨
List<ModuleDescriptor> get all { ... }
}
4.3 ModuleHubScreen 확장
현재:
ModuleHubScreen
└── 하드코딩 모듈 카드 목록
변경:
ModuleHubScreen
├── Tab 1: 내 모듈 (설치됨) - 기존과 동일
└── Tab 2: 마켓플레이스 탐색
├── 검색바
├── 카테고리 필터 칩
├── 추천 모듈 배너
└── 모듈 카드 그리드
└── 탭 → ModuleDetailScreen
├── 스크린샷
├── 설명
├── 퍼블리셔 정보
├── 권한 목록
├── 리뷰 (Phase 4)
└── [설치] 버튼
4.4 API 클라이언트
// core/services/marketplace_api.dart
class MarketplaceApi {
final String baseUrl; // membloc-app-engine 주소
Future<List<RemoteModuleDescriptor>> listModules({
String? category,
String? query,
int page = 1,
});
Future<RemoteModuleDescriptor> getModule(String moduleKey);
Future<List<ModuleCategory>> listCategories();
Future<List<RemoteModuleDescriptor>> getFeatured();
Future<void> installModule(String familyId, String moduleKey);
Future<void> uninstallModule(String familyId, String moduleKey);
}
5. 작업 목록
5.1 백엔드 (membloc-app-engine)
| # | 작업 | 의존성 | 예상 |
|---|---|---|---|
| B1 | 002_marketplace.up.sql 마이그레이션 작성 | - | 0.5d |
| B2 | 003_seed_modules.up.sql 시드 데이터 작성 | B1 | 0.5d |
| B3 | model/module.go, model/publisher.go 작성 | - | 0.5d |
| B4 | repository/module.go - 모듈 CRUD, 검색, 페이징 | B1, B3 | 1d |
| B5 | service/marketplace.go - 목록, 검색, 카테고리 | B4 | 0.5d |
| B6 | handler/marketplace.go - GET 엔드포인트 | B5 | 0.5d |
| B7 | repository/module_installation.go - 설치 CRUD | B1, B3 | 0.5d |
| B8 | service/module_install.go - 설치 로직 + Firestore 동기화 | B7 | 1d |
| B9 | handler/module_install.go - POST 엔드포인트 | B8 | 0.5d |
| B10 | main.go 라우트 등록 | B6, B9 | 0.5d |
| B11 | Firebase Admin SDK 연동 (Firestore 쓰기용) | - | 1d |
| B12 | 테스트 작성 | B10 | 1d |
5.2 Flutter (membloc-app)
| # | 작업 | 의존성 | 예상 |
|---|---|---|---|
| F1 | MarketplaceApi HTTP 클라이언트 | B6 완료 | 0.5d |
| F2 | RemoteModuleDescriptor 모델 | - | 0.5d |
| F3 | RemoteModuleRegistry - fetch + 캐시 | F1, F2 | 1d |
| F4 | MergedModuleRegistry - native + remote merge | F3 | 0.5d |
| F5 | ModuleHubScreen 탭 분리 (내 모듈 / 마켓플레이스) | F4 | 1d |
| F6 | ModuleDetailScreen 신규 | F1 | 1d |
| F7 | 설치 플로우 (기존 Cloud Functions → 백엔드 API 전환) | F1, B9 | 1d |
| F8 | 오프라인 캐시 및 에러 처리 | F3 | 0.5d |
6. 호환성 전략
Firestore ↔ PostgreSQL 이중화
Phase 1에서는 두 곳 모두에 데이터를 유지한다:
[기존 흐름 - 유지]
Flutter → Firestore families/{id}/modules/{key} (실시간 stream)
[신규 흐름 - 추가]
Flutter → membloc-app-engine API → PostgreSQL (마켓플레이스 데이터)
→ Firestore (설치 시 동기화)
- 기존
ModuleService.installsStream()- Firestore 기반, 그대로 유지 - 마켓플레이스 탐색 - 백엔드 API 사용
- 설치 요청 - 백엔드 API → 백엔드가 Firestore에도 write
기존 모듈 동작
- Core 4개 + Plugin 5개는
default_module_registry.dart에 계속 존재 - 백엔드에도 동일 데이터 시드
MergedModuleRegistry에서 native 우선, 중복 제거
7. 완료 기준
-
/api/marketplace/modules호출 시 9개 기존 모듈 반환 -
/api/marketplace/modules?category=lifestyle필터 동작 -
/api/families/:id/modules/:key/install호출 시 PostgreSQL + Firestore 양쪽 저장 - Flutter 앱에서 마켓플레이스 탭에 모듈 목록 표시
- Flutter 앱에서 모듈 상세 화면 진입
- 오프라인 상태에서 캐시된 모듈 목록 표시
- 기존 모듈 설치/해제 플로우 정상 동작 (regression 없음)
SDK assets