Communitygithub.com

AxelBeary/Inkglean

멀티 테넌트 커미션/마켓플레이스 플랫폼을 위한 아키텍처 패턴 — 템플릿 사용자 페이지, 임베드 가능 위젯 및 맞춤 페이지 호스팅. 호스팅 템플릿 제공과 외부 사이트 통합을 모두 가능하게 하는 3계층 페이지 시스템을 다룹니다.

Inkglean란 무엇인가요?

이 스킬은 아티스트, 크리에이터, 프리랜서를 위한 멀티 테넌트 커미션 플랫폼의 아키텍처 패턴을 다룹니다. 핵심은 다양한 기술 수준의 사용자를 지원하는 3계층 페이지 시스템으로, 내장 템플릿(Layer 1), 임베드 가능 위젯(Layer 2), 맞춤 페이지 호스팅(Layer 3)을 제공합니다. 레이아웃과 팔레트를 직교 축으로 분리하여 4가지 레이아웃과 4가지 팔레트로 16가지 외형을 8개의 정의로 구현하는 효율적인 템플릿 시스템을 제안합니다. 또한 주문 폼 로직을 공유 컴포저블로 추출하여 다중 템플릿 주문 페이지를 쉽게 구축할 수 있는 패턴, 아티스트 악센트 색상 재정의, URL 파라미터 기반 미리보기 모드 등 실용적인 구현 패턴을 제공합니다. 주로 Claude Code와 같은 AI 어시스턴트와 협업하여 Vue.js 기반의 중간 규모 웹 애플리케이션을 개발할 때 유용합니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/AxelBeary/Inkglean/tree/HEAD/docs/comms/archive/skills/software-development/commission-platform-architecture

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

커미션 플랫폼 아키텍처

멀티 테넌트 커미션 플랫폼으로, 각 사용자(아티스트, 크리에이터, 프리랜서)는 주문 기능이 있는 공개 페이지를 갖습니다. 이 스킬은 내장 호스팅 템플릿과 자체 사이트를 보유한 크리에이터의 통합을 모두 제공하기 위한 아키텍처 결정을 다룹니다.

3계층 페이지 시스템

중앙 아키텍처 패턴: 기술 수준과 독립성에 따라 세 가지 수준으로 크리에이터를 지원합니다.

┌─────────────────────────────────────────────────┐
│  레이어 3: 맞춤 페이지 호스팅                   │
│  자신의 HTML/CSS/JS 업로드 → /artist/alice에서   │
│  제공 (uploads/ 디렉토리의 정적 파일)            │
├─────────────────────────────────────────────────┤
│  레이어 2: 임베드 가능 위젯                     │
│  기존 사이트에 <script> 태그 하나만 붙이면       │
│  → "커미션 의뢰" 버튼 + 주문 양식              │
├─────────────────────────────────────────────────┤
│  레이어 1: 내장 템플릿                          │
│  레이아웃 변형 선택, 설정 불필요                │
│  → /artist/alice 전체 페이지                    │
└─────────────────────────────────────────────────┘

레이어 1: 내장 템플릿

대상: 자체 웹사이트가 없거나 유지 관리하고 싶지 않은 크리에이터.

구현: CSS 변수 테마를 사용한 Vue 컴포넌트.

// 라우트: /artist/:subdomain
routes.get('/artist/:subdomain', async (req, reply) => {
  const artist = await artistService.getBySubdomain(req.params.subdomain)
  
  if (artist.page_type === 'default') {
    // 내장 Vue SPA (또는 SSR) 렌더링
    // page_theme 필드가 템플릿 변형을 제어함
    return reply.sendFile('index.html')
  }
  // ... 다른 곳에서 레이어 2-3 처리로 넘어감
})

템플릿 = 레이아웃 × 팔레트 (직교 축) — 가장 중요한 모델링 결정입니다. 두 "템플릿"이 주로 색상만 다르다면, 그것은 두 개의 템플릿이 아니라 두 가지 색상 구성의 하나의 레이아웃입니다. 색상을 템플릿 차원에 묶으면 중복된 구조 코드로 템플릿 수가 급증합니다(사용자들이 지적할 것입니다: "이 페이지들은 색상만 다르고 같은 것 같아요"). 아티스트가 독립적으로 선택할 수 있는 축으로 분할하세요:

축선택자예시제어 요소
레이아웃 (구조)아티스트classic (2열 스튜디오) / gallery (전체화면 갤러리) / folio (단일 페이지 랜딩) / atelier (책 스타일 웜 스튜디오)오프닝 처리, 열 로직, 갤러리 모양, 탐색
팔레트 (분위기)아티스트paper / ink / dusk / moss — 각각 밝은색과 어두운색 변형으로 정의페이지 바닥/표면/텍스트 색상 (--pal-* CSS 변수)
악센트 + 모드방문자5색 악센트 선택기 + 밝음/어두움 선호도--color-primary, html.dark

핵심 규칙:

  • 팔레트는 밝은색+어두운색 변형이 있는 분위기이지, "어두운 테마"가 아닙니다. 아티스트가 ink를 선택하면, 밝은 모드 방문자는 옅은 회색을, 어두운 모드 방문자는 차콜색을 봅니다 — 분위기는 일관되고 밝기는 방문자를 따릅니다.
  • 악센트 색상(버튼/링크/하이라이트)은 항상 방문자의 악센트 변수(--color-primary)를 사용하며, 팔레트 변수를 사용하지 않습니다 — 두 시스템은 직교 상태를 유지하며 모두 계속 작동합니다.
  • 4개 레이아웃 × 4개 팔레트 = 8개 정의로 16가지 개별 외형.
  • 레이아웃은 구조적으로 달라야 하며(다른 오프닝, 다른 열 로직), 그렇지 않으면 하나의 레이아웃으로 병합되어야 합니다.

작동하는 레이아웃 원형 — 각각 아티스트의 가장 독특한 자산(그들의 작품)으로 시작하며, 일반적인 중앙 정렬 이름+약력+버튼 히어로가 아닙니다:

레이아웃오프닝시그니처 동작
gallery전체 뷰포트 작품, 이름은 작은 코너 명판편집 스타일 대형/소형 교대 갤러리, 호버 확대, 클릭 미리보기
classic시그니처 작품 배너, 작품 위에 이름 오버레이데스크탑 2열, 고정 정보 카드와 항상 보이는 커미션 CTA
folio분할 화면: 왼쪽 텍스트, 오른쪽 작품 스택스크롤 추적 네비게이션 하이라이트, 모바일 햄버거 + 하단 고정 CTA
atelier따뜻한 책 스타일 확산, 세리프 서체 (Noto Serif SC)종이 질감 바닥, 편집 여백, 넉넉한 여백, 문학적 분위기의 작품 중심

공유 키트 아키텍처(검증됨): 템플릿은 백엔드 필드 이름을 직접 읽지 않습니다 — 하나의 어댑터 컴포저블(useArtistData(props))이 imgUrl(), statusText() (i18n), socialLinks, heroArtwork를 제공합니다. 재사용 가능한 Tpl* 컴포넌트는 props를 통해 변형을 받습니다(TplHero variant="banner|fullscreen|split", TplGallery layout="grid|editorial|masonry") (티어 그리드의 #addons 슬롯을 확장 포인트로 사용). 전체 패턴(비동기 컴포넌트 타이밍 수정 및 레거시 ID 매핑 포함)은 references/template-system.md를 참조하세요.

아티스트 설정 악센트 색상 재정의 (R49 패턴): 아티스트는 공개 페이지에서 방문자의 ThemePicker 선택을 재정의하는 영구 악센트 색상(5색 화이트리스트, artists.accent_color TEXT로 저장)을 설정할 수 있습니다. ArtistHome.vue 구현:

const ACCENT_INDEX = { '#34dbcb': '1', '#34c2db': '2', '#3498db': '3', '#346edb': '4', '#3445db': '5' }
const accentOverride = computed(() => {
  const raw = previewAccent.value || artist.value?.accentColor
  return raw ? (ACCENT_INDEX[String(raw).toLowerCase()] || null) : null
})
// watch(immediate) + onUnmounted: save/restore document.documentElement.dataset.accent

핵심 결정: (1) 전역 data-accent 속성 삽입 — 템플릿별 CSS 변수 변경 불필요 (theme.css는 이미 data-accent="1"~"5" 선택자와 밝은색+어두운색 변형을 가지고 있음). 4개 템플릿 모두에 악센트 지원 추가 = 템플릿 파일 변경 0개. (2) 아티스트 페이지에 있는 동안 아티스트 악센트가 방문자 ThemePicker보다 우선함. 방문자 선택은 onUnmounted에서 복원됨. (3) 백엔드는 5색 화이트리스트 + null(지우기)에 대해 유효성을 검사함. 쓰기 시 toLowerCase() 정규화. (4) 설정 UI: 원형 스와치 버튼(32px) + 활성화 시 ✓ 체크 + "默认" 지우기 버튼. el-color-picker 아님(자유 형식 색상은 화이트리스트를 깨뜨림). (5) C61 결정: 악센트는 클라이언트 직면 페이지만 영향을 주며, 아티스트 대시보드에는 영향을 주지 않음.

URL 파라미터를 통한 미리보기 모드 (R50 패턴): 설정 "预览主页" 버튼이 새 창에서 /artist/:subdomain?_tpl=X&_pal=Y&_accent=Z를 엽니다. ArtistHome.vue는 route.query._tpl/_pal/_accent를 렌더링 레이어 재정의로 읽습니다(데이터 레이어에 쓰지 않음). 미리보기 파라미터가 있는 경우 고정 경고 배너("预览模式 — 修改尚未保存")가 표시됩니다. 템플릿/팔레트/악센트 계산 속성은 previewParam || artist.field || default로 대체됩니다. 공개 액세스에 안전함 — 미리보기 파라미터는 렌더링에만 영향을 주며, 모든 데이터는 공개 API에서 가져옴.

플랫 메모 목록을 대체하는 활동 타임라인 (R40 패턴): 상태 변경 기록(시스템 메모)과 아티스트 메모를 단일 연대기 보기로 병합할 때, 유형/중공 구분과 함께 el-timeline을 사용하세요:

<el-timeline-item :type="note.created_by === 'system' ? 'info' : (note.image_path ? 'success' : 'primary')"
  :hollow="note.created_by === 'system'" :timestamp="formatDate(note.created_at)" placement="top">

유형 표시기: 🔄 상태 변경 / 📝 메모 / 🖼 이미지 포함 메모. 시스템 메모는 tl-item--system 클래스(희미한 색상, 작은 글꼴)를 얻습니다. 삭제 버튼(R46)은 시스템 메모가 아닌 경우에만 표시되며, @media (hover: none) 터치 폴백과 함께 호버 시 나타납니다. 입력 상자는 타임라인 하단으로 이동합니다. 백엔드 변경 0개 — 시스템 메모는 이미 order_notes에 created_by='system'으로 존재합니다. 작업 표시줄(진행/되돌리기/취소)은 타임라인 카드 외부에 유지됩니다.

컨텍스트 종속 확인 동작이 있는 드래그 교체 (R53 대 R55): 동일한 앱의 두 드래그-투-리플레이스 기능은 다른 확인 요구사항을 가집니다 — 통합하지 마세요:

R53 큐 보드 초점 이미지R55 티어 예시 이미지
덮어쓰기 확인아니요 (이전 이미지는 주문 갤러리에 남음)예 — ElMessageBox.confirm (이전 이미지 손실)
이유초점 교체 ≠ 이미지 삭제예시 덮어쓰기 = 이전 이미지 복구 불가
구현: 공유 드래그 인프라(dragover.prevent/dragleave with contains(relatedTarget) 안티-플리커/drop.prevent + 숨겨진 파일 입력)이지만, 업로드 함수는 R55에 대해서만 if (row.example_image) { await ElMessageBox.confirm(...) }를 확인합니다. 둘 다 기존 업로드 API를 재사용합니다. R53의 초점 이미지에서 el-image preview-src-list를 제거하여 클릭 이벤트를 삼키지 않도록 해야 합니다(R18과 동일한 함정).

주문 폼 컴포저블 추출 (R58-1 패턴): 주문 폼이 ~400줄(데이터 로딩 + 가격 책정 + 유효성 검사 + 업로드 + 제출 + 초안 복구)을 초과하면, 모든 비즈니스 로직을 공유 컴포저블(useOrderForm.js)로 추출하세요. 페이지 컴포넌트는 순수 레이아웃이 됩니다(~300줄의 템플릿 + 스타일, 스크립트는 const of = useOrderForm(subdomain, formRef) + 구조 분해만). 컴포저블은 관심사별로 그룹화된 refs/reactives/methods의 평면 객체를 반환합니다: 데이터 로딩(artist, tiers, rulesContent, loading), 폼 상태 + 유효성 검사 규칙, 제출 상태(submitting, showSuccess, resultNo, submit), 참조 이미지(refFileList, handleRefUpload, handleRefRemove), 가격 책정(addonSelections, pricePreview, pricingExpanded, selectedTier, onTierChange), 정리된 규칙 HTML. 다중 템플릿에 중요한 이유: 주문 페이지가 여러 레이아웃 템플릿(단계별, 영수증 스타일, 잡지 스타일)을 지원할 때, 각 템플릿은 동일한 컴포저블을 호출하는 얇은 레이아웃 셸입니다 — 로직 중복 0. 컴포저블은 "코어", 템플릿은 "스킨"입니다. 이는 홈페이지의 useArtistData와 동일한 어댑터 패턴이며, 주문 폼에 적용된 것입니다. 핵심 구현 세부사항: 컴포저블은 formRef (템플릿의 el-form ref)를 파라미터로 받습니다. 왜냐하면 유효성 검사(formRef.value.validate())는 템플릿의 폼 요소를 호출해야 하기 때문입니다 — 컴포저블은 로직을 소유하지만 DOM은 소유하지 않습니다.

단계별 주문 폼 레이아웃 (R58-2 패턴, 검증됨): 기본 주문 레이아웃은 useOrderForm() 위에 완전히 구축된 3단계 가이드 흐름입니다(R58-1에서 컴포저블 변경 0 — 추출을 검증함):

  • 단계 표시기: 3개의 번호가 매겨진 점 + 연결선. 현재: 확대 + 기본. 완료: ✓ + 기본 채우기. 보류 중: 회색 중공. 모바일 (≤860px): 점만.
  • 1단계 — 티어 선택: el-select를 대체하는 반응형 카드 그리드(repeat(auto-fill, minmax(240px, 1fr))). 선택됨: --ease-bounce 전환 + 회전 스탬프 ✓ (@keyframes tier-stamp-in: scale 0→1.2→1, rotate -15°→0°). R14 점진적 공개는 1단계 내에 유지.
  • 2단계 — 설명 + 업로드: 텍스트 영역 + 영감 태그 (R58-4: 하드코딩된 기본 칩, 클릭 시 스마트 쉼표 + 2000자 잘림으로 추가) + 참조 업로드 (파일 + 붙여넣기 변경 없음).
  • 3단계 — 연락처: QQ + 이름 + 규칙 동의.
  • 고정 요약 카드: 와이드 스크린 오른쪽 열(280px 고정), 실시간 티어 + 내역 + 합계 + 할부. 모바일: 활성 단계 하단.
  • 영수증 확인 (R58-3): 제출 → 유효성 검사 → 카페 영수증 el-dialog (본문으로 텔레포트, templates.css의 전역 CSS receipt-* 네임스페이스). 종이 #fdfbf5, Courier 고정폭, 지그재그 가장자리(repeating-conic-gradient), 점선 구분선, 바코드 장식, 할부 칩. 고정 라이트 테마(종이 메타포). 취소는 3단계로 돌아감. 확인은 submit() 호출.
  • 주문 정보 복사 (R58-5): 성공 대화상자 "复制约稿信息" → 여러 줄 클립보드(주문 번호 + 티어 + 애드온 + 합계).
  • 단계 탐색: "下一步"는 진행 전에 현재 단계의 유효성을 검사함. "上一步"는 항상 허용됨. 건너뛰기 없음.

다중 템플릿 주문 페이지 (R58 아키텍처): 주문 페이지는 홈페이지 템플릿과 독립적으로 아티스트가 선택 가능한 레이아웃 템플릿을 지원합니다. 백엔드 (검증됨, 마이그레이션 v16): artists.order_template_id TEXT DEFAULT 'default' — 서비스 계층 화이트리스트 유효성 검사(ORDER_TEMPLATES = ['default'], 템플릿 추가 시 배열 확장). GET /api/artists/:subdomain는 orderTemplateId: artist.order_template_id || 'default'를 반환합니다. PUT /api/artist/profile는 orderTemplateId를 허용합니다(keyMap을 통해 order_template_id에 매핑). 유효하지 않은 값은 INVALID_ORDER_TEMPLATE(409)를 발생시킵니다. 빈 문자열은 'default'로 대체됩니다. 프론트엔드: 아티스트 설정 페이지에는 카드 스타일 템플릿 선택기가 있습니다(홈페이지 템플릿 선택과 동일한 UI 패턴). 클라이언트 측 라우팅: OrderForm.vue는 artist.orderTemplateId를 읽고 <component :is="..."> 또는 템플릿 레지스트리 맵을 통해 해당 레이아웃 컴포넌트를 렌더링합니다. 모든 레이아웃 컴포넌트는 useOrderForm()을 공유합니다 — 시각적 배열(단계 표시기, 카드 레이아웃, 확인 대화상자 스타일)에서만 다릅니다. 새 주문 템플릿 추가 = 레이아웃 셸이 있는 하나의 새 .vue 파일 + 백엔드 화이트리스트 배열에 하나의 항목, 로직 변경 0. 미리보기: 홈페이지 미리보기의 _tpl URL 파라미터 메커니즘을 재사용합니다.

QQ 연락처 패턴 (R58-6): QQ 연락을 위한 두 가지 방법: "跳转QQ" (window.open('tencent://message/?uin=' + encodeURIComponent(qq), '_self')) + "复制QQ" (navigator.clipboard.writeText(qq) with toast). 클립보드 폴백: 실패 시(권한 거부, 비-HTTPS), ElMessage.warning(qq) 표시 — 경고 메시지 자체로 QQ 번호를 제공하여 사용자가 토스트에서 수동으로 복사할 수 있도록 함. 두 곳에 적용: 클라이언트 측 성공 대화상자(아티스트의 contactQq) 및 아티스트 측 주문 세부 정보(클라이언트의 qq). i18n: 컨텍스트별로 별도 키(orderForm.jumpQq 대 orderDetail.jumpQq).

CSS 제작 업그레이드 패턴 (v0.16 배치1, 검증됨):

  • 전역 바운스 이징: :root에 --ease-bounce: cubic-bezier(.34, 1.56, .64, 1). transform/box-shadow 전환에만 사용(절대 opacity에 사용하지 마세요 — 1.0을 초과할 수 없음). 소비 코드의 폴백: var(--ease-bounce, cubic-bezier(0.22, 1, 0.36, 1)).
  • 버튼 3상태 물리적 모델: .el-button:hover:not(:disabled):not(.is-loading) { transform: translateY(-3px); box-shadow: deeper } + :active { transform: translateY(-1px) scale(0.98) }. :not() 제외는 비활성화/로딩 버튼이 애니메이션되는 것을 방지합니다.
  • clamp() 유동 타이포그래피: font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem) — 값이 clamp()로 단순해질 때까지 calc()가 있는 중첩 min()/max() 호출을 제거합니다.

레이어 2: 임베드 가능 위젯

대상: 자신의 웹사이트가 있고 단일 주문 진입점을 추가하려는 크리에이터.

구현: 간단한 <script> 태그를 로드하는 JavaScript 스니펫. 가장 간단한 형태:

<script src="https://platform.example.com/widget.js" data-artist="alice"></script>

위젯은 다음을 수행합니다:

  • DOMContentLoaded 또는 스크립트가 비동기로 로드될 때까지 기다린 후에 페이지에 주문 양식을 삽입합니다.
  • iframe을 사용하여 상위 페이지 스타일과 격리하거나 또는 섀도우 DOM에 렌더링합니다.
  • 백엔드와 통신하기 위해 공개 API를 호출합니다.

아키텍처 고려 사항:

  • CORS: 백엔드는 Access-Control-Allow-Origin: * 또는 특정 출처를 허용해야 합니다.
  • 보안: 위젯은 신뢰할 수 없는 사이트에 삽입될 수 있으므로 CSRF 보호 및 속도 제한이 중요합니다.
  • 성능: 위젯 스크립트는 작고(10-20KB minified+gzip) 동기적으로 로드되어서는 안 됩니다.

레이어 3: 맞춤 페이지 호스팅

대상: 자신의 사이트를 완전히 제어하고 싶지만 플랫폼의 주문 처리 및 결제 인프라가 필요한 크리에이터.

구현: 업로드된 정적 파일을 제공합니다.

uploads/
  artists/
    alice/
      index.html
      style.css
      script.js
      assets/
        logo.png

라우팅:

// /artist/:subdomain에 대한 모든 요청은 먼저 맞춤 파일을 확인합니다.
app.get('/artist/:subdomain', async (req, res) => {
  const customPath = path.join(UPLOAD_DIR, req.params.subdomain, 'index.html')
  if (fs.existsSync(customPath)) {
    return res.sendFile(customPath)
  }
  // 레이어 1 또는 2로 폴백
  next()
})

제한 사항 및 보안:

  • 업로드된 파일은 정적 에셋만 허용됩니다(서버 측 실행 금지).
  • 파일 크기 제한(예: 총 50MB).
  • Sanitize HTML(악성 스크립트 방지를 위해 선택적 CSP 헤더가 좋은 추가 사항).
  • 맞춤 페이지는 공개 API를 통해 주문 데이터에 접근할 수 있습니다.

아키텍처 요약

3계층 시스템은 확장성을 위한 명확한 분리를 제공합니다:

  1. 내장 템플릿: 낮은 진입 장벽, 플랫폼에서 완전히 호스팅.
  2. 임베드 가능 위젯: 유연성과 통합 제어 사이의 균형.
  3. 맞춤 페이지 호스팅: 최대 유연성, 개발 노력 필요.

각 계층은 동일한 백엔드 API와 인프라를 공유하므로 일관된 데이터 모델과 비즈니스 로직을 유지하면서 다양한 사용자 요구를 충족합니다.

관련 스킬