독자용 사이트
apps/web-storefront독자가 방문하는 사이트로, 출판사 자체 브랜드로 공개됩니다. 작품 둘러보기, 검색, 랭킹, 팔로우, 로그인, 그리고 페이지를 넘기며 읽는 뷰어를 갖추고 있습니다.
Publira는 IT 인력이 부족한 출판사도 만화 디지털 유통 플랫폼을 자사 브랜드로 운영할 수 있게 해 주는 멀티테넌트 SaaS입니다. 편집자가 작가에게서 받은 작품을 서지 정보와 함께 등록하면, 독자는 웹이나 모바일 앱에서 읽을 수 있습니다.
아직 개발 중입니다
Publira는 활발히 개발되고 있으며, 최신 릴리스인 v0.3.2도 아직 프로덕션 환경에서 쓸 수 있는 단계는 아닙니다. 이 페이지에서 소개하는 기능은 모두 지금 실제로 동작하고, 릴리스마다 공개되는 이미지와 Compose 파일 하나로 설치 환경을 구축할 수도 있습니다. 다만 실제 출판사의 서비스로 운영하려면 코드를 읽고 자신의 환경에 맞게 고쳐야 하며, 0.x 마이너 릴리스마다 생길 수 있는 변경에도 대응해야 합니다. 완성된 제품이라기보다는 직접 검증하고, 확장하고, 기여할 수 있는 프로젝트로 봐 주세요.

Publira의 중심은 플랫폼입니다. 개발하면서 필요했던 구성 요소 가운데 일부는 독립된 라이브러리로 분리해, 플랫폼 없이도 누구나 쓸 수 있도록 공개하고 있습니다.
출판사에는 독자용 사이트와 앱, 편집용 콘솔이 제공됩니다. 설치 환경 전체를 운영하는 사람에게는 이들을 한꺼번에 관리하는 콘솔이 따로 있습니다.
출판사마다 자체 브랜드와 도메인으로 사이트를 운영할 수 있습니다. 그 상위에 있는 플랫폼 콘솔에서는 테넌트와 그 독자, 설치 환경 전체에 공통으로 적용되는 설정을 관리합니다. 플랫폼 콘솔을 두지 않는 설치 환경에서는 publiractl로 관리합니다.
작품 카탈로그와 페이지, 공지사항을 하나의 콘솔에서 편집하고, 공개를 앞둔 항목은 공개 대기열에서 한눈에 볼 수 있습니다. 독자 문의에도 여기서 답하며, 스태프에게는 각자의 역할로 쓸 수 있는 기능만 표시됩니다.
색상, 서체, 로고, 아이콘은 공개 사이트와 같은 컴포넌트로 만든 미리보기에서 실제 화면을 확인하면서 편집합니다. 저장하기 전까지 공개 사이트의 모습은 바뀌지 않습니다.
테넌트는 웹에서는 Stripe나 PAY.JP로, 앱에서는 App Store와 Google Play로 에피소드를 판매합니다. 구매는 리디렉션 시점이 아니라 결제 서비스의 서명된 알림을 받은 시점에 기록되며, 독자에게는 구매 완료 메일이 발송됩니다. 웹 구매는 스태프가 콘솔에서 환불할 수 있고, 전액 환불되거나 결제 분쟁이 접수되면 독자는 그 에피소드를 더 이상 읽을 수 없습니다.
유료 에피소드도 무료 공개 기간에는 모든 독자가 읽을 수 있습니다. 기간은 에피소드마다 설정할 수도, 시리즈의 여러 에피소드에 한꺼번에 설정할 수도 있습니다. 시리즈에서는 ‘기다리면 무료’도 제공할 수 있어, 로그인한 독자가 이용권으로 유료 에피소드를 한 화씩 무료로 읽을 수 있습니다. 사용한 이용권은 일정 시간을 기다리면 다시 쓸 수 있습니다.
독자는 이메일 주소 외에도 사이트와 앱 모두에서 쓸 수 있는 패스키, 테넌트마다 설정한 Apple과 Google 계정으로 로그인할 수 있습니다. 테넌트 스태프와 플랫폼 운영자는 2단계 인증을 켤 수 있고(스태프는 패스키로도 인증할 수 있습니다), 플랫폼은 모든 테넌트 관리자와 운영자에게 2단계 인증을 의무화할 수 있습니다.
작품은 웹에만, 앱에만, 또는 양쪽 모두에 표시할 수 있고, 에피소드 단위로 더 좁힐 수 있습니다. 서버는 웹사이트와 앱에서 오는 요청에 각각 그곳에 표시할 수 있는 작품과 에피소드만 돌려줍니다. 판매 위치도 같은 방식으로 테넌트 기본값부터 에피소드 단위까지 설정할 수 있습니다.
크레딧에 오른 작가마다 에피소드 매출에 대한 배분율을 정합니다. 월 마감은 직접 하거나 테넌트가 정한 날에 자동으로 할 수 있으며, 마감한 달의 명세서는 CSV로 내려받을 수 있습니다. 독자 계정과 연결된 작가는 자신의 에피소드를 구매하지 않고 읽을 수 있지만, 평가는 할 수 없습니다.
R15와 R18 작품은 어느 목록에서든 표시가 붙고, 독자가 확인하기 전까지 가려집니다. 열람은 테넌트가 고른 확인 방식에 따라 계정의 생년월일로 제한됩니다. 랭킹도 등급별로 따로 집계되며, 열람할 수 있는 독자만 볼 수 있습니다.
Next.js로 만든 웹 앱 3개와 Flutter 앱, 그리고 이들이 보내는 메일입니다. 각각 쓰는 사람이 다릅니다.
독자가 방문하는 사이트로, 출판사 자체 브랜드로 공개됩니다. 작품 둘러보기, 검색, 랭킹, 팔로우, 로그인, 그리고 페이지를 넘기며 읽는 뷰어를 갖추고 있습니다.
편집자가 작업하는 콘솔입니다. 카탈로그와 공개 대기열, 결제와 로그인 서비스, 공개 사이트의 테마와 페이지, 독자 문의, 독자의 구매와 환불, 작가별 지급액을 다루며, 이 모든 변경 내역이 기록됩니다.
테넌트보다 한 단계 위에서 설치 환경을 운영하는 사람을 위한 콘솔입니다. 설치 환경에 아직 필요한 설정, 테넌트 상태, 여러 테넌트에 걸친 이벤트, 모든 테넌트의 독자, 그리고 오브젝트 스토리지부터 보안 정책까지 모든 테넌트에 공통으로 적용되는 설정을 다룹니다.
같은 카탈로그를 휴대폰에서 볼 수 있습니다. Android와 iOS용 앱을 하나의 Flutter 코드베이스로 만들며, 테넌트의 이름과 테마로 표시됩니다. 사이트를 거치지 않고 공개 API를 직접 호출하고, 이메일 주소나 패스키, Apple, Google로 로그인할 수 있습니다. 에피소드는 스토어나 사이트 결제로 구매하며, 독자가 열었거나 저장한 에피소드는 기기에 보관됩니다.
계정 관련 메일과 스태프가 받는 알림은 전용 렌더러가 React 컴포넌트로 HTML을 만들고, 일반 텍스트 파트와 함께 보냅니다. 테넌트의 독자와 스태프에게 가는 메일에는 Publira가 아니라 테넌트의 이름이 표시되며, 테넌트의 언어로 작성됩니다. 일본어, 한국어, 중국어에서 서로 다른 문자 사이의 간격은 사이트와 마찬가지로 메일 클라이언트의 텍스트 렌더링에 맡깁니다.
앞단에는 Next.js가 있고, 그 뒤에서 Connect RPC로 통신하는 Go 서버와 워커가 동작합니다. 그 아래에는 직접 운영할 수 있는 범용 소프트웨어만 있습니다.
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 메트릭도 내보내며, 이것도 같은 방법으로 켜기 전까지는 꺼져 있습니다.
| 구성 요소 | 용도 |
|---|---|
| 리버스 프록시필수 | 테넌트의 호스트를 해당 사이트와 콘솔로, 공개 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의 라우팅대로 동작하기 때문입니다.
코드를 수정하거나 다른 플랫폼에서 실행하려면 저장소를 체크아웃해 이미지를 직접 빌드하고, 그 이미지를 지정하세요.