← 모듈 센터

01. 모듈 표준 — 모든 프로그램과 모듈 사이트가 함께 지키는 약속

표준 버전 1 ("moduleStandard": 1) · 작성 2026-10-04 이 문서는 계약입니다. 모듈 사이트(02)는 이 문서와 맞지 않는 모듈을 받지 않고, 각 프로그램(03)은 이 문서대로 모듈을 가져오고 고쳐서 올립니다.

쉬운 요약


1. 용어

용어뜻
모듈여러 프로그램이 함께 쓰려고 떼어 낸 기능 하나
사용처그 모듈을 쓰는 프로그램. 라이센스 서버의 제품 코드로 부릅니다(예: nahonza-work)
원본 저장소모듈 소스가 있는 단 하나의 장소. 기본은 hnbbos/isolo_modules
연결부(어댑터)모듈이 프로그램에 요구하는 작은 약속(예: 「사용자 찾기」, 「파일 저장」). 사용처가 자기 방식으로 채웁니다
호스트 연결 파일사용처 안에서 연결부를 채우는 파일. 사용처가 소유하고 고치는 유일한 모듈 관련 파일입니다
발행검사를 통과한 버전을 「써도 된다」고 확정하고 서명하는 것. 모듈 사이트 관리자가 합니다
회수발행한 버전에 문제가 있어 「쓰지 말라」고 표시하는 것. 번호는 다시 쓰지 않습니다
묶음고객 서버에 보낼 「프로그램 버전 + 모듈 버전들 + 이미지 지문」 세트. 서명해서 보냅니다
modctl모듈 도구(명령줄). 만들기·검사·가져오기·새 버전 받기·보고를 합니다 (02 §11)

2. 모듈의 3종류

2.1 부품 (part)

2.2 서비스 (service)

2.3 시작 틀 (starter)

2.4 어느 종류로 만들까 — 판단 순서

  1. 프로그램마다 사용자·회사·권한 구조 깊숙이 섞여야 하나? → 시작 틀
  2. 자기 화면이나 자기 자료(DB 표 여러 개)를 가졌나? → 서비스
  3. 함수나 미들웨어 수준이고, 사용처마다 작은 연결부만 있으면 되나? → 부품
  4. 애매하면 서비스를 고릅니다. 한 벌만 유지하면 되기 때문입니다.

2.5 기본 모듈 — 모든 프로그램이 반드시 넣는 것

라이센스 보호·설치·업데이트는 프로그램마다 따로 만들면 안 됩니다. 보호 장치 하나에 구멍이 있으면 그 프로그램만 뚫리고, 고쳐도 다른 프로그램에 전해지지 않습니다. 그래서 이 셋은 모듈 사이트에 올라가는 「기본 모듈」로 정하고, 모든 프로그램이 같은 것을 씁니다.

id종류하는 일지금 상태 (2026-10-04)
license-guard부품 (Node·Python)서명된 라이센스(.lic) 확인 → 정상·유예·잠금 판정, 제품 코드 확인두 벌로 갈라져 있음(제로라벨 268줄 / 라이센스 서버 181줄) → 합쳐서 1.0.0
installer부품 (shell: install.sh·install.ps1)한 줄 설치: Docker 확인 → 묶음 받기 → 비밀값 생성 → .lic 받기 → 실행 → 「설치 완료」 보고없음. 나혼자워크 deploy/standalone/run.sh가 원형
update-agent서비스 (곁붙이)묶음 확인 → 서명·typ 확인 → 백업 → 지문으로 교체 → 상태 확인 → 실패 시 되돌림 → 보고없음. 데스크톱(.exe)용 업데이트 부품만 있음

어느 프로그램이 필수인가

라이센스 서버도 사용처입니다. license-guard는 프로그램 쪽(확인하는 쪽)뿐 아니라 라이센스 서버(서명하는 쪽)도 씁니다. 그래서 이 모듈의 약속 검사(§11.2)는 서명·확인 양쪽의 정규화가 바이트 단위로 같은지를 반드시 봅니다.

모듈이 아닌 것


3. 원본 저장소

기본 원본은 GitHub 비공개 저장소 hnbbos/isolo_modules 하나입니다.

isolo_modules/
├─ modules/                 # 부품·서비스
│  ├─ license-guard/
│  └─ messenger/
├─ templates/               # 시작 틀
│  └─ saas-onprem-node/
├─ tools/modctl/            # 모듈 도구 (02 §11)
├─ schemas/module-1.json    # module.json 형식 검사용 JSON Schema
└─ .github/workflows/       # 검사·발행 자동화

4. module.json — 이름표

4.1 공통 칸

칸필수설명
moduleStandard✔표준 버전. 지금은 1
id✔소문자·숫자·하이픈, 2~40자. 한 번 정하면 바꾸지 않음
name✔한글 이름
kind✔part · service · starter
version✔버전 번호(§7). 예: 1.4.0, 시험판 1.5.0-rc.1
summary✔한 줄 설명
owner✔담당(GitHub 사용자 이름)
source✔{ "repo": "hnbbos/isolo_modules", "path": "modules/license-guard" }
adapters연결부 목록 [{ "name", "required", "summary" }] (§6)
config설정 칸 [{ "key", "required", "default", "summary" }]. 보안 판단에 쓰는 값은 넣지 않음(§10-1)
requires{ "modules": { "<id>": "1.x" }, "host": { "node": ">=20", "python": ">=3.11", "db": ["mariadb","postgres"] } }
provides사용처에 생기는 것 { "routes": [], "tables": [], "events": [], "ui": [] }
network이 모듈이 바깥으로 접속하는 주소 목록. 예: ["lic.isolo.co.kr"]
tenancy✔multi(회사별 격리 지원) · single(한 회사 전제) · none(회사와 무관)
docs✔{ "integration": "INTEGRATION.md", "changelog": "CHANGELOG.md" }
catalog고객 패키지 센터 노출(서비스만). { "visible": true, "category": "업무", "icon": "message-circle", "tier": "licensed", "featureKey": "messenger", "baseUrl": "…" }. tier는 free 또는 licensed

4.2 부품 전용 — runtimes

"runtimes": {
  "node":   { "package": "@hnbbos/license-guard", "path": "node", "format": "esm+cjs", "engines": ">=20" },
  "python": { "package": "isolo-license-guard",  "path": "python", "requires": ">=3.11" }
}

format이 esm+cjs이면 import와 require를 모두 지원해야 합니다. 나혼자워크는 ESM, 제로라벨·라이센스 서버는 CommonJS이기 때문입니다.

설치기처럼 고객 서버에서 도는 스크립트는 shell 판을 둡니다. 리눅스·NAS와 윈도우를 모두 지원해야 하므로 두 파일이 필요합니다.

"runtimes": {
  "shell": { "path": "shell", "files": ["install.sh", "install.ps1"] }
}

4.3 서비스 전용 — service

"service": {
  "placement": "central",
  "image": "ghcr.io/hnbbos/messenger",
  "port": 8080,
  "health": "/health",
  "compose": "service/compose.fragment.yml",
  "api": "service/openapi.yaml",
  "auth": "oauth2",
  "embed": { "iframe": "/embed/messenger", "script": "/widget.js", "react": "@hnbbos/messenger-react" },
  "data": "own-db"
}

placement는 central(중앙) 또는 sidecar(곁붙이)입니다(§2.2). data는 own-db(서비스가 자기 DB를 가짐) 또는 host-db(사용처 DB에 표를 만듦, §9) 중 하나입니다. 서비스는 own-db를 권합니다.

4.4 시작 틀 전용 — starter

"starter": {
  "stack": "node-ts-express-prisma-react",
  "path": "template",
  "variables": ["PRODUCT_CODE", "PRODUCT_NAME", "PORT"],
  "includes": { "license-guard": "1.x" }
}

4.5 예시 — 라이센스 보호 장치

{
  "moduleStandard": 1,
  "id": "license-guard",
  "name": "라이센스 보호 장치",
  "kind": "part",
  "version": "1.0.0",
  "summary": "서명된 라이센스(.lic)를 확인해 정상·유예·잠금 상태를 정한다",
  "owner": "hnbbos",
  "source": { "repo": "hnbbos/isolo_modules", "path": "modules/license-guard" },
  "tenancy": "none",
  "runtimes": {
    "node":   { "package": "@hnbbos/license-guard", "path": "node", "format": "esm+cjs", "engines": ">=20" },
    "python": { "package": "isolo-license-guard", "path": "python", "requires": ">=3.11" }
  },
  "adapters": [
    { "name": "SettingsStore", "required": true,  "summary": "설치된 .lic 와 마지막 확인 시각 저장" },
    { "name": "AuditLog",      "required": false, "summary": "상태가 바뀔 때 기록" },
    { "name": "Clock",         "required": false, "summary": "현재 시각(검사에서 고정용)" }
  ],
  "config": [
    { "key": "productCode", "required": true, "summary": "이 프로그램의 제품 코드(코드에 고정해 넘김 — 환경변수 금지)" },
    { "key": "graceDays", "required": false, "default": 14, "summary": "만료 전 경고 시작 일수" }
  ],
  "network": ["lic.isolo.co.kr"],
  "docs": { "integration": "INTEGRATION.md", "changelog": "CHANGELOG.md" }
}

5. 폴더 모양

5.1 부품

modules/license-guard/
├─ module.json
├─ INTEGRATION.md            # 붙이기 안내서 (§12)
├─ CHANGELOG.md              # 변경 기록 (§13)
├─ node/
│  ├─ package.json           # name = @hnbbos/license-guard, version = module.json 과 같게
│  ├─ src/index.ts           # 바깥에 내보내는 것은 여기서만
│  ├─ src/adapters.ts        # 연결부 약속(interface)
│  └─ test/
├─ python/
│  ├─ pyproject.toml         # version = module.json 과 같게
│  ├─ isolo_license_guard/__init__.py
│  ├─ isolo_license_guard/adapters.py   # 연결부 약속(Protocol)
│  └─ tests/
├─ contract/cases.json       # 두 판이 같은 답을 내는지 보는 공통 시험 자료 (§11.2)
└─ examples/                 # 가장 작은 사용 예 (Express 하나, FastAPI 하나)

5.2 서비스

modules/messenger/
├─ module.json · INTEGRATION.md · CHANGELOG.md
├─ service/
│  ├─ Dockerfile
│  ├─ compose.fragment.yml   # 단독 설치 묶음에 끼울 조각
│  ├─ openapi.yaml           # 바깥에 약속한 API
│  └─ src/
├─ widget/                   # iframe 진입 · 스크립트 위젯 · React 컴포넌트
└─ test/                     # API 약속 검사 · 회사 격리 검사

5.3 시작 틀

templates/saas-onprem-node/
├─ module.json · INTEGRATION.md(새 프로그램 만들기 안내) · CHANGELOG.md
└─ template/                 # {{PRODUCT_CODE}} 같은 자리표시자 포함

버전 번호는 module.json이 기준입니다. package.json·pyproject.toml의 번호가 다르면 modctl lint가 실패합니다.


6. 연결부(어댑터) 규칙

  1. 모듈은 사용처 파일을 import하지 않습니다. 필요한 것은 모두 연결부로 받습니다.
  2. 진입점은 하나입니다. configure({ config, adapters })가 모듈 객체(함수·라우터·미들웨어)를 돌려줍니다. 전역 상태로 설정하지 않습니다.
  3. 연결부는 작게 만듭니다. 메서드 3~7개. 이름은 하는 일로 짓습니다.
  4. 표준 연결부 이름: 여러 모듈이 같은 이름을 쓰면, 사용처는 한 번 만든 연결부를 다른 모듈에도 재사용합니다.
이름하는 일비고
TenantResolver요청 → 회사 ID한 회사용 프로그램은 상수를 돌려줌
UserProvider사용자 조회·목록·시스템 사용자바코드 메신저와 같은 이름
FileStorage저장·읽기·삭제·존재 확인바코드 메신저와 같은 이름. 나혼자워크 StorageProvider와 맞춤
Notifier사용자에게 알림바코드 메신저와 같은 이름
SettingsStore키-값 설정(회사별 또는 배포 전체)
AuditLog중요한 일 기록
Clock현재 시각검사에서 시간을 고정할 때
HttpGuard바깥 주소 접속 전 사설망·금지 주소 검사사용자가 입력한 주소로 접속하는 모듈은 필수
  1. 연결부 약속이 바뀌면 MAJOR입니다(§7).
  2. 연결부 메서드가 실패하면 모듈은 삼키지 않고 오류를 위로 올립니다. 「없음」과 「물어보지 못함」을 구분해야 합니다. 나혼자워크 저장소 작업에서 이 둘을 섞어 자료 유실 위험이 생겼던 일이 있습니다.

6.1 예 — Node(TypeScript)

// modules/license-guard/node/src/adapters.ts
export interface SettingsStore {
  get(key: string): Promise<string | null>;   // 없으면 null, 못 읽으면 throw
  set(key: string, value: string): Promise<void>;
}
export interface AuditLog { record(action: string, detail: Record<string, unknown>): Promise<void>; }
export interface GuardAdapters { settings: SettingsStore; audit?: AuditLog; clock?: () => Date; }
export interface GuardConfig { productCode: string; graceDays?: number; }

// modules/license-guard/node/src/index.ts
export function configure(opts: { config: GuardConfig; adapters: GuardAdapters }): LicenseGuard { /* … */ }

6.2 예 — Python

# modules/license-guard/python/isolo_license_guard/adapters.py
from typing import Protocol

class SettingsStore(Protocol):
    def get(self, key: str) -> str | None: ...   # 없으면 None, 못 읽으면 예외
    def set(self, key: str, value: str) -> None: ...

6.3 사용처 쪽 — 호스트 연결 파일

// (나혼자워크) server/src/modules/license-guard.host.ts — 사용처가 소유하는 유일한 파일
import { configure } from '@hnbbos/license-guard';
// 예시: 「배포 전체」 설정 저장 함수. 회사별·사용자별 표(나혼자워크 AppSetting 은 사용자별)에 두면 안 된다.
// 나혼자워크라면 LicenseSetting 같은 한 줄짜리(singleton) 표가 맞는 자리다.
import { readDeploymentSetting, writeDeploymentSetting } from '../services/deployment-settings.js';

export const guard = configure({
  config: { productCode: 'nahonza-work' },          // 코드에 고정 — 환경변수로 받지 않음
  adapters: {
    settings: {
      get: (k) => readDeploymentSetting(`license-guard:${k}`),     // 없으면 null, DB 오류는 그대로 throw
      set: (k, v) => writeDeploymentSetting(`license-guard:${k}`, v),
    },
  },
});

7. 버전 규칙

바뀐 것번호사용처는
버그·보안 고침. 겉으로 보이는 동작은 같음PATCH 1.4.0 → 1.4.1자동으로 받아도 안전
기능 추가. 기존 사용법은 그대로 동작MINOR 1.4.1 → 1.5.0골라서 받기
아래 「MAJOR 목록」 중 하나라도 해당MAJOR 1.5.0 → 2.0.0안내서대로 손보고 받기

MAJOR 목록

규칙


8. 사용처 기록 — modules.lock.json

각 프로그램 저장소의 최상위에 둡니다. modctl이 쓰고 읽으며, 모듈 사이트에 보고됩니다.

{
  "lockVersion": 1,
  "product": "nahonza-work",
  "modules": {
    "license-guard": {
      "kind": "part", "version": "1.4.0", "runtime": "node",
      "host": "server/src/modules/license-guard.host.ts"
    },
    "messenger": {
      "kind": "service", "version": "2.4.0",
      "image": "ghcr.io/hnbbos/messenger@sha256:…", "embed": "widget"
    },
    "saas-onprem-node": {
      "kind": "starter", "version": "1.0.0", "appliedAt": "2026-10-20",
      "pending": ["1.1.0 설치 마법사 개선 — 아직 반영 안 함"]
    }
  },
  "patches": [
    {
      "module": "license-guard",
      "reason": "만료일 계산 오류 긴급 우회",
      "issue": "https://github.com/hnbbos/isolo_modules/issues/12",
      "removeBy": "2026-11-01"
    }
  ]
}

9. 자료(DB)를 가진 모듈

  1. 서비스는 자기 DB를 갖는 것(own-db)이 기본입니다. 사용처 DB에 표를 만드는 것(host-db)은 부품이나 작은 서비스에만 씁니다.
  2. 새 모듈의 표 이름에는 접두사를 붙입니다(<id 줄임>_…). 이미 있는 모듈(예: 바코드 메신저 chat_*)은 그대로 두고 provides.tables에 적습니다.
  3. tenancy: "multi"인 모듈의 표는 모두 회사 칸(tenant_id)을 가지며, 모든 조회에 회사 조건이 붙습니다. 한 회사용 프로그램은 TenantResolver가 상수를 돌려줍니다.
  4. DB 변경 파일은 data/migrations/0001_<이름>.sql(MariaDB/MySQL)로 둡니다. PostgreSQL을 지원하면 .pg.sql도 둡니다.
  5. 여러 번 돌려도 결과가 같아야 합니다(IF NOT EXISTS).
  6. 앞으로만 갑니다. 되돌리기는 「이전 번호의 코드가 새 표에서도 도는 것」으로 보장합니다.
  7. 지우기·이름 바꾸기는 확장 → 옮기기 → 축소의 세 번 발행으로 나누고, 축소는 MAJOR입니다.
  8. 사용처의 ORM 모델은 사용처가 소유합니다. 모듈은 붙여 넣을 조각을 제공합니다.
  9. Prisma(나혼자워크): data/prisma.fragment.prisma
  10. SQLAlchemy(바코드 WMS): data/sqlalchemy_models.py
  11. 모듈은 사용처의 표를 고치지 않습니다(ALTER·직접 조회 금지). 사용처 자료가 필요하면 연결부로 받습니다.

10. 보안 규칙 — 반드시

  1. 보안 판단을 환경변수로 바꿀 수 없게 합니다. 검증용 공개키, 강제 여부, 제품 코드, 권한 판정이 여기에 해당합니다.
  2. 단독 설치에서는 환경변수를 고객이 쥐고 있습니다. 실제로 기존 라이센스 보호 장치는 공개키·강제 여부를 환경변수(VENDOR_LICENSE_ENFORCE 등)로 받아 우회될 수 있었습니다.
  3. 환경변수로 받아도 되는 것: 주소, 포트, 성능 값, 기록 수준.
  4. 비밀값을 모듈에 넣지 않습니다. 예제·시험 자료에도 진짜 값은 넣지 않습니다.
  5. 바깥 접속은 network에 적은 곳만 합니다. 사용자가 입력한 주소로 접속하는 모듈은 HttpGuard로 사설망·금지 주소를 막습니다(SSRF).
  6. 회사 격리: 모든 조회에 회사 조건을 붙이고, 검사에 「다른 회사 자료가 보이지 않는다」 항목을 반드시 넣습니다.
  7. 사용자가 입력한 HTML은 정화합니다. 파일 경로는 FileStorage 연결부로만 다룹니다(직접 경로 조립 금지).
  8. 기록(로그)에 비밀값·토큰·개인정보를 남기지 않습니다.
  9. 의존 패키지는 잠금 파일로 고정하고, 발행 전에 취약점 검사(npm audit, pip-audit)를 통과합니다.
  10. 화면 모듈은 사용처의 전역 스타일(Tailwind 등)에 기대지 않습니다. 접두사를 붙인 CSS나 CSS 변수를 씁니다.
  11. iframe 끼워 넣기는 반드시 다른 원본(주소)에서 엽니다. 같은 원본에 allow-scripts allow-same-origin을 주면 끼워 넣은 화면이 부모 창의 로그인 정보에 닿습니다(나혼자워크 integration.controller.ts 주석에 기록된 문제).
  12. 서비스의 공개 경로(인증 없이 열리는 주소)는 openapi.yaml에 security: []로 명시하고, 검사에 「인증 없이 막혀야 할 곳이 막히는지」를 넣습니다.

11. 검사 규칙

11.1 세 겹의 검사

검사누가무엇
단위 검사모듈 저장소가짜 연결부로 모듈 혼자 돌림
약속 검사모듈 저장소두 판(Node·Python)이 같은 답을 내는지(§11.2), 서비스는 openapi.yaml대로 답하는지
사용처 검사각 프로그램 저장소자기 실제 연결부로 새 번호를 넣고 원래 검사를 돌림 → 결과를 모듈 사이트에 보고

11.2 공통 시험 자료 — contract/cases.json

두 판이 같은 입력에 같은 출력을 내야 하는 곳(서명 정규화, 상태 판정, 날짜 계산)은 시험 자료를 공유합니다. 특히 서명용 JSON 정규화(canonical)는 서명하는 쪽(라이센스 서버)과 확인하는 쪽이 바이트 단위로 같아야 합니다.

[
  { "name": "만료 10일 전 → 유예", "fn": "vendorState",
    "input": { "payload": { "expiresAt": "2026-10-14" }, "now": "2026-10-04T00:00:00Z" },
    "expect": { "state": "grace" } }
]

11.3 검사가 「정말 돌았는지」 확인

11.4 사용처 검사 결과 보고 형식

{
  "module": "license-guard", "version": "1.5.0",
  "product": "nahonza-work", "productVersion": "1.8.0", "runtime": "node",
  "passed": 42, "failed": 0, "ok": true,
  "logUrl": "https://github.com/hnbbos/alone_boss/actions/runs/…"
}

12. 붙이기 안내서(INTEGRATION.md) 목차 — 필수

  1. 이 모듈이 하는 일(3줄)
  2. 먼저 판단할 것: 소스를 가져와야 하나, 서비스 연결로 충분한가
  3. 준비물: 런타임, DB, 함께 필요한 모듈
  4. 가져오기: 언어별 명령(modctl add …)
  5. 연결부 채우기: 연결부마다 메서드 설명 + Node·Python 예제
  6. 설정 칸: 값과 기본값. 보안 판단은 왜 코드에 고정하는지
  7. 화면 붙이기(있으면): iframe / 스크립트 위젯 / React
  8. 자료(DB) 준비(있으면)
  9. 확인하는 법: 사용처 검사에 꼭 넣을 최소 항목
  10. 자주 막히는 곳
  11. Claude 요청문(복사용): 「이 프로그램에 ○○ 모듈을 붙여 줘 …」

13. 변경 기록(CHANGELOG.md) 형식

## 1.5.0 — 2026-10-20
### 새 기능
- 만료 전 경고 시작일을 설정으로 정할 수 있음(`graceDays`)
### 고침
- 윤년 2/29 만료일이 하루 일찍 잠기던 것
### 사용처가 할 일
- 없음 (graceDays 를 안 넣으면 지금처럼 14일)

「사용처가 할 일」은 빼지 않습니다. 할 일이 없으면 「없음」이라고 씁니다. modctl update가 이 부분을 그대로 보여 줍니다.


14. 서명 — 발행 기록과 묶음

모든 서명은 라이센스 서버의 Ed25519 키링으로 합니다. 개인키는 라이센스 서버 밖으로 나가지 않습니다(VENDOR-KEY-RUNBOOK.md). 정규화는 라이센스 보호 장치의 canonical()(정렬 키 JSON)을 그대로 씁니다.

14.1 발행 기록

{
  "typ": "isolo.module-release/1",
  "id": "license-guard",
  "version": "1.5.0",
  "manifestSha256": "…",
  "artifacts": {
    "npm":   { "name": "@hnbbos/license-guard", "version": "1.5.0", "integrity": "sha512-…" },
    "git":   { "repo": "hnbbos/isolo_modules", "tag": "license-guard-v1.5.0", "commit": "<40자리>" },
    "image": "ghcr.io/hnbbos/messenger@sha256:…"
  },
  "releasedAt": "2026-10-20T03:00:00Z"
}

14.2 묶음 — 고객 서버로 보내는 세트

{
  "typ": "isolo.bundle/1",
  "product": "nahonza-work",
  "bundleVersion": "1.8.0",
  "minFrom": "1.6.0",
  "images": {
    "server": "ghcr.io/hnbbos/nahonza-server@sha256:…",
    "client": "ghcr.io/hnbbos/nahonza-client@sha256:…"
  },
  "modules": { "license-guard": "1.5.0", "messenger": "2.4.0" },
  "createdAt": "2026-10-21T01:00:00Z"
}

14.3 확인하는 쪽의 규칙

  1. typ부터 확인합니다. 같은 키로 .lic도 서명하므로, 종류가 다른 서명본을 다른 용도로 쓰지 못하게 해야 합니다.
  2. 공개키는 코드에 내장합니다(환경변수 금지, §10-1). 키 교체는 키링에 새 키를 추가해 배포하는 방식입니다.
  3. 이미지는 이름표(:latest)가 아니라 지문(@sha256:)으로 받습니다.

15. 이 표준을 바꿀 때

표준날짜바뀐 점
12026-10-04처음 작성