docs.membloc.com

membloc docs

제품, 플랫폼, 운영 문서를 한곳에서 읽는 Membloc 공식 문서 포털입니다.

Phase 1: Foundation — 동적 모듈 레지스트리

하드코딩된 모듈 레지스트리를 백엔드 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)

#작업의존성예상
B1002_marketplace.up.sql 마이그레이션 작성-0.5d
B2003_seed_modules.up.sql 시드 데이터 작성B10.5d
B3model/module.go, model/publisher.go 작성-0.5d
B4repository/module.go — 모듈 CRUD, 검색, 페이징B1, B31d
B5service/marketplace.go — 목록, 검색, 카테고리B40.5d
B6handler/marketplace.go — GET 엔드포인트B50.5d
B7repository/module_installation.go — 설치 CRUDB1, B30.5d
B8service/module_install.go — 설치 로직 + Firestore 동기화B71d
B9handler/module_install.go — POST 엔드포인트B80.5d
B10main.go 라우트 등록B6, B90.5d
B11Firebase Admin SDK 연동 (Firestore 쓰기용)-1d
B12테스트 작성B101d

5.2 Flutter (membloc-app)

#작업의존성예상
F1MarketplaceApi HTTP 클라이언트B6 완료0.5d
F2RemoteModuleDescriptor 모델-0.5d
F3RemoteModuleRegistry — fetch + 캐시F1, F21d
F4MergedModuleRegistry — native + remote mergeF30.5d
F5ModuleHubScreen 탭 분리 (내 모듈 / 마켓플레이스)F41d
F6ModuleDetailScreen 신규F11d
F7설치 플로우 (기존 Cloud Functions → 백엔드 API 전환)F1, B91d
F8오프라인 캐시 및 에러 처리F30.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 없음)