PubliraGitHub

만화를 자사 브랜드로 서비스하는 오픈 소스 플랫폼.

Publira는 IT 인력이 부족한 출판사도 만화 디지털 유통 플랫폼을 자사 브랜드로 운영할 수 있게 해 주는 멀티테넌트 SaaS입니다. 편집자가 작가에게서 받은 작품을 서지 정보와 함께 등록하면, 독자는 웹이나 모바일 앱에서 읽을 수 있습니다.

Next.js 앱
3
Go 프로세스
2
모바일 클라이언트
Flutter
독자 화면 지원 언어
5
라이선스
Apache-2.0

아직 개발 중입니다

Publira는 활발히 개발되고 있으며, 최신 릴리스인 v0.3.2도 아직 프로덕션 환경에서 쓸 수 있는 단계는 아닙니다. 이 페이지에서 소개하는 기능은 모두 지금 실제로 동작하고, 릴리스마다 공개되는 이미지와 Compose 파일 하나로 설치 환경을 구축할 수도 있습니다. 다만 실제 출판사의 서비스로 운영하려면 코드를 읽고 자신의 환경에 맞게 고쳐야 하며, 0.x 마이너 릴리스마다 생길 수 있는 변경에도 대응해야 합니다. 완성된 제품이라기보다는 직접 검증하고, 확장하고, 기여할 수 있는 프로젝트로 봐 주세요.

publisher.example / catalog
테넌트가 추천하는 작품을 맨 앞에 내세운 Publira 독자용 카탈로그
개발용 시드 데이터로 실행한 테넌트 사이트의 카탈로그.

출판사가 처음부터 쓸 수 있는 기능

출판사에는 독자용 사이트와 앱, 편집용 콘솔이 제공됩니다. 설치 환경 전체를 운영하는 사람에게는 이들을 한꺼번에 관리하는 콘솔이 따로 있습니다.

  • 처음부터 멀티테넌트를 고려한 설계

    출판사마다 자체 브랜드와 도메인으로 사이트를 운영할 수 있습니다. 그 상위에 있는 플랫폼 콘솔에서는 테넌트와 그 독자, 설치 환경 전체에 공통으로 적용되는 설정을 관리합니다. 플랫폼 콘솔을 두지 않는 설치 환경에서는 publiractl로 관리합니다.

  • 카탈로그 관리를 하나의 콘솔에서

    작품 카탈로그와 페이지, 공지사항을 하나의 콘솔에서 편집하고, 공개를 앞둔 항목은 공개 대기열에서 한눈에 볼 수 있습니다. 독자 문의에도 여기서 답하며, 스태프에게는 각자의 역할로 쓸 수 있는 기능만 표시됩니다.

  • 실제 화면을 보면서 편집하는 테마

    색상, 서체, 로고, 아이콘은 공개 사이트와 같은 컴포넌트로 만든 미리보기에서 실제 화면을 확인하면서 편집합니다. 저장하기 전까지 공개 사이트의 모습은 바뀌지 않습니다.

  • 원하는 결제 서비스로 유료 에피소드 판매

    테넌트는 웹에서는 Stripe나 PAY.JP로, 앱에서는 App Store와 Google Play로 에피소드를 판매합니다. 구매는 리디렉션 시점이 아니라 결제 서비스의 서명된 알림을 받은 시점에 기록되며, 독자에게는 구매 완료 메일이 발송됩니다. 웹 구매는 스태프가 콘솔에서 환불할 수 있고, 전액 환불되거나 결제 분쟁이 접수되면 독자는 그 에피소드를 더 이상 읽을 수 없습니다.

  • 테넌트가 정하는 무료 공개

    유료 에피소드도 무료 공개 기간에는 모든 독자가 읽을 수 있습니다. 기간은 에피소드마다 설정할 수도, 시리즈의 여러 에피소드에 한꺼번에 설정할 수도 있습니다. 시리즈에서는 ‘기다리면 무료’도 제공할 수 있어, 로그인한 독자가 이용권으로 유료 에피소드를 한 화씩 무료로 읽을 수 있습니다. 사용한 이용권은 일정 시간을 기다리면 다시 쓸 수 있습니다.

  • 패스키, Apple, Google로 로그인

    독자는 이메일 주소 외에도 사이트와 앱 모두에서 쓸 수 있는 패스키, 테넌트마다 설정한 Apple과 Google 계정으로 로그인할 수 있습니다. 테넌트 스태프와 플랫폼 운영자는 2단계 인증을 켤 수 있고(스태프는 패스키로도 인증할 수 있습니다), 플랫폼은 모든 테넌트 관리자와 운영자에게 2단계 인증을 의무화할 수 있습니다.

  • 웹과 앱의 표시 위치 나누기

    작품은 웹에만, 앱에만, 또는 양쪽 모두에 표시할 수 있고, 에피소드 단위로 더 좁힐 수 있습니다. 서버는 웹사이트와 앱에서 오는 요청에 각각 그곳에 표시할 수 있는 작품과 에피소드만 돌려줍니다. 판매 위치도 같은 방식으로 테넌트 기본값부터 에피소드 단위까지 설정할 수 있습니다.

  • 인세는 월별로 마감해 명세서로

    크레딧에 오른 작가마다 에피소드 매출에 대한 배분율을 정합니다. 월 마감은 직접 하거나 테넌트가 정한 날에 자동으로 할 수 있으며, 마감한 달의 명세서는 CSV로 내려받을 수 있습니다. 독자 계정과 연결된 작가는 자신의 에피소드를 구매하지 않고 읽을 수 있지만, 평가는 할 수 없습니다.

  • 테넌트 규칙에 따른 연령 등급

    R15와 R18 작품은 어느 목록에서든 표시가 붙고, 독자가 확인하기 전까지 가려집니다. 열람은 테넌트가 고른 확인 방식에 따라 계정의 생년월일로 제한됩니다. 랭킹도 등급별로 따로 집계되며, 열람할 수 있는 독자만 볼 수 있습니다.

독자, 편집자, 운영자를 위한 앱

Next.js로 만든 웹 앱 3개와 Flutter 앱, 그리고 이들이 보내는 메일입니다. 각각 쓰는 사람이 다릅니다.

독자용 사이트

apps/web-storefront

독자가 방문하는 사이트로, 출판사 자체 브랜드로 공개됩니다. 작품 둘러보기, 검색, 랭킹, 팔로우, 로그인, 그리고 페이지를 넘기며 읽는 뷰어를 갖추고 있습니다.

장르각 장르는 그 장르에서 순위가 높은 작품의 표지나 편집자가 올린 아트워크로 표시되고, 작품 수가 함께 나옵니다. 장르를 열면 이번 주 인기 작품 아래에 그 장르의 모든 작품이 나오며, 전체 목록과 같은 방식으로 정렬하고 필터링할 수 있습니다. 장르는 정해진 목록이 아니라 테넌트가 직접 정한 분류입니다.
시리즈 상세표지, 줄거리, 레이블, 장르, 태그와 함께 모든 작가가 역할별로 표시되고, 그 옆에 팔로우와 공유 버튼이 있습니다. 아래에는 에피소드가 읽는 순서대로 나열되고 무료인지 유료인지 표시되므로, 로그인하기 전에도 지금 무엇을 읽을 수 있는지 알 수 있습니다.
에피소드 뷰어페이지는 canvas에 그려지며, 편집자가 지정한 펼침면 시작 위치부터 오른쪽에서 왼쪽으로 두 쪽씩 넘어갑니다. 도구 모음의 슬라이더는 펼침면 단위로 움직이고, 마지막 페이지를 넘기면 에피소드에 반응을 남기거나 다음 에피소드로 넘어갈 수 있습니다. 뷰어는 npm에 공개한 패키지 그대로이므로, 페이지 넘김과 펼침면, 읽는 방향은 라이브러리가 처리합니다.

테넌트 콘솔

apps/web-admin

편집자가 작업하는 콘솔입니다. 카탈로그와 공개 대기열, 결제와 로그인 서비스, 공개 사이트의 테마와 페이지, 독자 문의, 독자의 구매와 환불, 작가별 지급액을 다루며, 이 모든 변경 내역이 기록됩니다.

대시보드공개 중인 작품, 초안 에피소드, 공개 예정 건수 아래에 처리가 필요한 항목을 모은 대기열이 있습니다. 대기열에는 초안과 공개 예정 항목이 공개 일시와 함께 모이므로, 편집자는 콘솔을 열자마자 해야 할 일을 확인할 수 있습니다.
에피소드시리즈의 에피소드를 읽는 순서대로 나열하며, 번호를 일일이 고치지 않고 끌어서 놓거나 화살표 키로 순서를 바꿉니다. 에피소드 공개는 테넌트 시간대 기준 일시로 예약합니다. 페이지는 이미지, ZIP, EPUB으로 올리고 같은 방식으로 순서를 바꾸며, 공개한 뒤에도 페이지마다 교체하거나 삭제할 수 있습니다.
테마브랜드 색상과 그 위에 놓이는 글자 색, 독자에게 우선 적용할 서체를 공개 사이트의 로고, 아이콘과 함께 설정하고, 공개 사이트와 같은 컴포넌트로 만든 미리보기에서 확인합니다. 미리보기는 편집할 때마다 바로 바뀌지만 공개 사이트는 기존 색상을 유지하므로, 테마를 저장하기 전까지 독자에게는 아무것도 바뀌지 않습니다.

플랫폼 콘솔

apps/web-platform

테넌트보다 한 단계 위에서 설치 환경을 운영하는 사람을 위한 콘솔입니다. 설치 환경에 아직 필요한 설정, 테넌트 상태, 여러 테넌트에 걸친 이벤트, 모든 테넌트의 독자, 그리고 오브젝트 스토리지부터 보안 정책까지 모든 테넌트에 공통으로 적용되는 설정을 다룹니다.

대시보드설치 환경에 아직 필요한 설정과 각 설정 화면으로 가는 링크, 그 아래로 테넌트 수와 테넌트 생성, 역할 부여, 계정 추가 같은 여러 테넌트에 걸친 최근 이벤트가 표시됩니다. 운영자는 콘솔을 열면 설치 환경이 준비되었는지부터 확인할 수 있습니다.
테넌트등록된 모든 테넌트를 상태와 생성일과 함께 보여 주며, 이름이나 ID로 검색할 수 있습니다. 테넌트마다 프로필, 도메인, 멤버를 확인할 수 있습니다. 이 콘솔에서는 테넌트의 상태와 관리자만 관리하고, 카탈로그는 각 테넌트가 자체 콘솔에서 운영합니다.
설치 환경 설정모든 테넌트의 업로드 파일이 저장되는 S3 호환 버킷, 설치 환경이 메일을 보낼 때 쓰는 SMTP 릴레이, 로그인과 메일 발송을 보호하는 제한, Web Push 서명에 쓰는 키 쌍을 설정합니다. 저장한 변경 사항은 재시작하지 않아도 실행 중인 모든 서버에 반영됩니다.

모바일 앱

mobile

같은 카탈로그를 휴대폰에서 볼 수 있습니다. Android와 iOS용 앱을 하나의 Flutter 코드베이스로 만들며, 테넌트의 이름과 테마로 표시됩니다. 사이트를 거치지 않고 공개 API를 직접 호출하고, 이메일 주소나 패스키, Apple, Google로 로그인할 수 있습니다. 에피소드는 스토어나 사이트 결제로 구매하며, 독자가 열었거나 저장한 에피소드는 기기에 보관됩니다.

카탈로그앱 바에는 테넌트의 이름과 테마가 적용되고, 콘솔에서 고정한 공지와 독자가 읽다 만 에피소드 아래로 장르와 이번 주 랭킹이 이어집니다. 뷰어를 제외한 모든 화면에서 하단 바를 통해 홈, 검색, 보관함, 알림, 계정으로 이동할 수 있습니다.
작품과 작가표지, 테넌트가 정한 역할별 크레딧, 레이블, 장르, 태그와 함께 작품과 각 작가를 팔로우하는 버튼, 사이트 링크를 공유하는 버튼이 있습니다. 작가 페이지에서도 팔로우할 수 있으며, 팔로우한 작가의 새 에피소드가 나오면 푸시 알림이 오고, 알림을 누르면 바로 뷰어에서 열립니다.
에피소드 뷰어사이트와 같은 에피소드를 편집자가 지정한 펼침면과 읽는 방향에 맞춰 표시하며, 슬라이더로 페이지를 이동할 수 있습니다. 유료 에피소드는 테넌트가 정한 방식에 따라 스토어의 결제 화면이나 사이트 결제로 구매합니다. 페이지를 캡처하면 페이지 이용에 관한 테넌트의 안내가 표시됩니다.

발송되는 메일

apps/email-renderer

계정 관련 메일과 스태프가 받는 알림은 전용 렌더러가 React 컴포넌트로 HTML을 만들고, 일반 텍스트 파트와 함께 보냅니다. 테넌트의 독자와 스태프에게 가는 메일에는 Publira가 아니라 테넌트의 이름이 표시되며, 테넌트의 언어로 작성됩니다. 일본어, 한국어, 중국어에서 서로 다른 문자 사이의 간격은 사이트와 마찬가지로 메일 클라이언트의 텍스트 렌더링에 맡깁니다.

이메일 주소 확인새 독자가 처음 받는 메일입니다. 이메일 주소를 확인하는 버튼과 링크 만료 시각, 그리고 버튼이 표시되지 않는 메일 클라이언트를 위한 링크 주소가 들어 있습니다.
비밀번호 재설정새 비밀번호를 설정하는 링크가 담긴 메일로, 링크는 발송 후 하루가 지나면 만료됩니다. 테넌트의 다른 메일과 같은 레이아웃을 사용합니다.
스태프에게 가는 문의 알림독자가 문의 양식으로 보낸 내용과 함께 회신 주소와 독자가 로그인한 계정이 적혀 있어, 스태프는 콘솔의 문의 페이지에서도, 자신의 메일 클라이언트에서도 답할 수 있습니다.

전체 구성

앞단에는 Next.js가 있고, 그 뒤에서 Connect RPC로 통신하는 Go 서버와 워커가 동작합니다. 그 아래에는 직접 운영할 수 있는 범용 소프트웨어만 있습니다.

  1. 클라이언트브라우저Flutter 앱
  2. 엣지리버스 프록시
  3. Web (Next.js)web-storefrontweb-adminweb-platform
  4. 전송Connect RPC over Protocol Buffers
  5. 서버(Go)publira serverpublira worker
  6. 인프라PostgreSQLRedis 호환 캐시S3 호환 스토리지SMTP

publira server 프로세스 하나가 공개, 관리, 플랫폼 네임스페이스를 모두 처리하고, 독자용 오리진과 콘솔 오리진 양쪽에 이미지를 전송합니다. --role storefront로 두 번째 프로세스를 실행하면 테넌트 도메인만 맡기고 독자용 데이터베이스 로그인만 갖게 할 수 있습니다. 같은 바이너리로 실행하는 publira worker는 메일과 푸시 알림을 보내고, 예약된 에피소드 공개, 무료 공개 기간의 시작과 종료 처리, 테넌트별 날짜 전환, 랭킹 재집계, 인세 월 마감, 보존 기간이 지난 데이터 삭제 등 모든 정기 작업을 실행합니다. 워커만 실행하면 별도의 스케줄러는 필요 없으며, 호스트 한 대에서는 publira server --with-worker로 두 프로세스를 하나로 합쳐 실행할 수도 있습니다. 선택 사항인 email-renderer는 각 메일의 HTML 파트를 만들고, publiractl은 데이터베이스 마이그레이션, 역할 생성, 설치 환경 초기 설정을 담당하며 워커의 작업을 수동으로 실행할 수도 있습니다.

모든 계층이 OpenTelemetry로 계측되어 있어, 브라우저 요청 하나를 하나의 트레이스로 추적할 수 있습니다. Next.js 루트 스팬, SSR 중에 호출한 Connect RPC, Go 쪽의 같은 이름의 스팬, 그 아래의 db.query 자식 스팬까지 이어집니다. 이미 운영 중인 collector로 보낼 수도, 트레이싱을 끈 채로 둘 수도 있으며, 기본값은 꺼짐입니다. 두 Go 프로세스는 OpenTelemetry 메트릭도 내보내며, 이것도 같은 방법으로 켜기 전까지는 꺼져 있습니다.

실행에 필요한 것관리형 서비스를 전제로 하지 않는 일반적인 인프라입니다. 직접 준비해도 되고, Compose 파일이 실행하는 것을 그대로 써도 됩니다.
구성 요소용도
리버스 프록시필수테넌트의 호스트를 해당 사이트와 콘솔로, 공개 API와 이미지 요청을 서버로 라우팅합니다. Traefik, nginx, Caddy용 설정이 저장소에 포함되어 있습니다.
PostgreSQL필수카탈로그, 테넌트, 사용자, 구매, 감사 로그를 저장합니다. 각 서버는 전용 역할로 접속합니다.
Redis 호환 캐시필수Next.js 공유 캐시로, 로드 밸런서 뒤에서 인스턴스를 여러 개 실행할 수 있게 합니다.
S3 호환 오브젝트 스토리지필수에피소드 페이지, 아이캐치 등 업로드 파일을 저장합니다. 버킷은 프로세스마다 환경 변수로 지정하지 않고 설치 환경 설정에 저장합니다.
SMTP 릴레이필수계정 관련 메일, 알림, 스태프에게 가는 알림을 보냅니다. 플랫폼과 테넌트별로 설정합니다.
OpenSearch 또는 Elasticsearch선택카탈로그 검색 결과를 관련도순으로 정렬하고 싶은 설치 환경에서 사용합니다. 모든 노드에 analysis-kuromoji와 analysis-icu 플러그인이 필요합니다. 쓰지 않으면 PostgreSQL로 검색합니다.
결제 서비스선택유료 에피소드를 판매할 때만 필요하며, 테넌트마다 계정이 하나씩 필요합니다. 웹에서는 Stripe 또는 PAY.JP, 앱에서는 App Store와 Google Play를 사용합니다. 구매는 결제 서비스의 서명된 알림으로 확정됩니다.
메일 수신 서비스선택SendGrid, Resend, Mailgun, Amazon SES, 또는 받은 메일을 HTTPS로 전달하는 메일 서버를 테넌트별로 사용합니다. 답변에 대한 독자의 회신을 콘솔의 해당 문의 아래에 기록하려는 테넌트만 필요합니다.
Firebase 프로젝트선택테넌트 앱마다 하나씩 필요합니다. 모바일 앱의 푸시 알림을 Firebase Cloud Messaging으로 보내며, iOS에는 APNs를 거쳐 전달됩니다.
OpenTelemetry collector선택Next.js 앱과 Go 서버의 트레이스, Go 서버의 메트릭을 수집합니다. 켜기 전까지는 꺼져 있습니다.

설치 환경 구축하기

설치 환경은 컨테이너 이미지와 Compose 파일 하나로 실행되며, publiractl로 데이터베이스와 첫 테넌트를 사용할 수 있는 상태로 만듭니다. 호스트에는 Docker만 있으면 됩니다.

릴리스 다운로드
PUBLIRA_VERSION=0.3.2
curl -fsSLO \
  https://github.com/publira/publira/releases/download/v$PUBLIRA_VERSION/publira-deploy-$PUBLIRA_VERSION.tar.gz
tar -xzf publira-deploy-$PUBLIRA_VERSION.tar.gz
cd publira-deploy-$PUBLIRA_VERSION

# Write .env from .env.example, with every secret generated
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/work -w /work \
  ghcr.io/publira/publiractl:$PUBLIRA_VERSION env generate

아카이브에는 해당 릴리스의 Compose 파일, 프록시용 Caddyfile, .env.example이 들어 있습니다. publiractl env generate를 실행하면 .env.example을 바탕으로 .env를 만들고 모든 비밀 값을 생성해 채웁니다.

릴리스마다 이미지가 ghcr.io/publira/publiractl:0.3.2처럼 해당 버전 태그로 GitHub Container Registry에 공개됩니다. linux/amd64와 linux/arm64를 지원하며 로그인 없이 받을 수 있으므로, 호스트에서 직접 빌드할 필요가 없습니다.

.env의 PUBLIRA_IMAGE_TAG에는 이미 해당 릴리스의 버전이 들어 있으니 바꾸지 마세요. 이미지는 .env와 같은 디렉터리에 있는 파일과 같은 릴리스여야 합니다. publiractl에는 그 릴리스의 마이그레이션이 들어 있고, 각 프로세스는 그 릴리스의 스키마를 전제로 하며, 프록시는 아카이브에 든 Caddyfile의 라우팅대로 동작하기 때문입니다.

코드를 수정하거나 다른 플랫폼에서 실행하려면 저장소를 체크아웃해 이미지를 직접 빌드하고, 그 이미지를 지정하세요.