- **1.8.0** **직접 편집(시각 편집기)** — 초안이 만들어져 미리보기가 보이면 오른쪽 도구 줄의 "직접 편집" 으로 글을 바로 고치고, 블록을 끌어 옮기고, 구성 요소(섹션·제목·문단·버튼·이미지·구분선·여백)를 팔레트에서 끌어다 놓는다(버튼·키보드 대안 포함). 속성 창(글·링크·이미지·정렬·색·배경·여백), 실행 취소/다시 실행, 저장 상태 표시, Ctrl+S 저장, 페이지·사이트 전환·창 닫기 전 저장하지 않은 변경 보호. 저장은 선택한 HTML 파일을 갱신하고 버전 `visual-edit` 로 남기며 AI 캐시를 쓰지 않는다. 캔버스는 sandbox(출처 null)+CSP nonce 로 페이지 스크립트를 실행하지 않고, 서버는 원본에 없던 스크립트·이벤트 속성·위험 주소·iframe 등을 거절하며 머리·꼬리는 글자 그대로 보존한다(`lib/visual-editor.js`, `public/visual-editor-core.js`, `public/wm-ve-canvas.js`, `public/visual-editor-ui.js`). 독립 검토 차단 항목 12건 수정(캔버스 MessagePort 연결 규약, 이름 엔티티 주소 정규화, 구조 스캐너 기반 CSP 삽입, 완성 문서 전체 비교, 사이트별 변경 잠금, 저장 트랜잭션·되돌리기, 전환 보호, 복제 id, 끌기 정리 — CHANGELOG §8) + 종결 검토 3건(SVG SMIL 애니메이션 요소 거절, Git 저장 트랜잭션 재설계 — 로컬 커밋·기록 뒤 main + 변경 브랜치 원자적 푸시·검증된 되돌리기, Suite 릴리스 승인 규칙 — CHANGELOG §9) + 최종 검토 5건(Suite 승인 불변, 사이트 변경 요청의 잠금 안 재적재, 트랜잭션 준비 단계·모호한 푸시 판정, 문서, 배경 작업 저장이 짧은 변경을 덮어쓰지 않는 잎 단위 저장소 합치기와 같은 잎 동시 변경의 fail-closed 거절 — CHANGELOG §10) + 4차 검토 4건(Suite artifact 설치 불변, 읽기 요청 부작용 제거, 폐기 잠금·표식 규약, svg/math 안 title 파서 차이 — CHANGELOG §11). 테스트 115개(원본 52 + 직접 편집 63). 실제 브라우저 게이트(Playwright, Chrome) 61/61.
- **1.7.3** 도메인 연결 오류 수정(배포 서버에 `log_format wmstats` 가 이미 있으면 중복 정의하지 않고, 중복으로 nginx -t 가 실패하면 우리 파일을 지워 자가 복구 · nginx 1.19.4 미만은 ssl_reject_handshake 대신 자체 서명 인증서 기본 서버) + **메일 사용량 연동**(AriMail 1.0.9 사용량 API: 관리자 콘솔에서 키 발급·보관(보관소 `arimail-usage`), 도메인별 메일함 용량·메일 수·발송/수신 표, 사용자 메일 화면 사용량 행, 사이트 정보 "메일 데이터" = 서버 메일함 용량 + 메일 사용량 구역(30일 막대)).
- **1.7.2** 도메인 관리 역할 분리 — 도메인 화면은 구매·갱신·**네임서버 이전 4단계 띠**, DNS 화면은 **사이트 ↔ 도메인 연결 표**(하위 이름 선택·연결 끊기)가 먼저이고 레코드 관리는 그 아래, 메일 화면은 계정 형식·접속 주소·초기 관리자를 한눈에; **사이트 정보 페이지**를 오른쪽 캔버스로 옮겨 방문자 막대·송수신 선 그래프·7일/월간(3개월) 막대·디스크(웹/DB/메일) 그래프를 SVG 로 표시. 두 서버 로그 0건. bear 배포 중단.
- **1.6.40** 사용자 화면 전면 재구성 — 작업 흐름 중심의 세 칸 구조(레일: 홈 / 만들기·수정·배포·운영 / 도메인·DNS·메일 / 캐시·충전·설정, 상태 점·배지), **홈**(단계별 상태·다음 할 일·의존 관계 지도), **배포** 화면(준비 점검표 + 발행·원본 서버 반영·검색 등록), **DNS**·**메일** 화면 분리, **캐시·충전** 화면(선불 충전 상품 → 결제 진행은 PG 미연동이라 결제 대기 주문만 → 관리자 "입금 확인 → 적립", 적립 = 결제 금액 + 인센티브), 관리자 콘솔 선불 충전 상품·주문 관리(`lib/prepaid.js`). 로그 점검 두 서버 0건.
- **1.6.39** 메일 서버 비밀번호 보호(덮어쓰기 확인 창·**이전 비밀번호로 되돌리기**·Arimail 로그인 잠금 중 "연결 확인" 비활성·credentials.json/config.json/mail-servers.json `.bak`), **사이트 관리자 비밀번호 ↔ 메일(Arimail) 비밀번호 동기화**(로그인 때 자동으로 맞춤, 켤 때 본인 비밀번호 확인, 동기화 상태 열), 연동 관리자 계정 **비밀번호 보기/숨기기·복사·초기화(다른 관리자 계정으로도)·삭제 보호**, Route 53 접속 정보 누락 문구가 빠진 항목을 말함(webmaker.ibit.kr 사례는 1.6.38 배포로 이미 해결 확인); 1.6.38 배포 이후 로그 점검 0건.
- **1.6.38** AI 연결 탭에서 키 없이 저장한 제공자가 설정 탭 등급 목록에서 빠지는 이유를 표·저장 문구·빨간 안내로 바로 보여 주고 키 없는 "모델 조회" 는 거절(webmaker.ibit.kr OpenRouter 사례); **메일 서버(Arimail) 등록부** — 다른 서버에 설치된 Arimail 을 여러 대 등록(닉네임·IP 로 구분, 비밀번호는 보관소)하고 사이트마다 고르거나 메일 도메인으로 자동 선택, 고른 서버 호스트가 MX 기본값; 도메인 탭 외부 도메인에 **"네임서버 정보"·"DNS 레코드 정보" 팝업**(어디를 누르는지 단계 안내, A·MX·SPF·DMARC·DKIM 값 클릭 선택·복사 버튼); Route 53 설정 칸 `ACCESS_KEY` 별칭 허용; 로그 점검 0건. 1.6.36·1.6.37 건너뜀.
- **1.6.35** 도메인 관리 프로세스 개편: 관리자는 접속 정보·플랫폼 도메인·**서브 도메인 등록 허용/금지** 만 정하고, 도메인 검색·구매(**장바구니** → 최종 선택 구매)·**이미 가진 도메인 등록**(네임서버 이전: 레코드 옮김 → 네임서버 안내 → "네임서버 변경했습니다" 확인 → 전파 중 "DNS 변경 진행 중" 레코드 잠금 / 직접 DNS 관리: 필요한 레코드 안내 → "설정 완료 — 확인" → 미반영이면 배포 불가 안내)·**도메인 상태**(배포·메일이 안 되는 이유 단계별)는 사용자 "도메인" 탭에서 스스로; 관리자 DNS 영역 화면은 조회·정리만; 로그 점검 0건.
- **1.6.33** DNS 영역 관리 API 404 회귀 수정(1.5.36 부터 `dns` 경로 누락), "이전된 영역 받기" 가 실패 원인(미등록·위임 없음·네임서버 무응답)을 설명하고 빈 영역은 "zone 파일 구성을 찾을 수 없습니다" 로 안내, 배포 직후 접속 확인 재시도 + `vhost-wait` 단계, 발행 카드 "다시 확인"(해결된 오류 갱신); 로그 점검.
- **1.6.32** 등록 업체에 **Amazon Route 53 Domains** 추가(검색·가격·등록·갱신·네임서버, 비동기 등록은 PENDING → 완료 확인 뒤 DNS 영역 자동, 운영 복구 "등록 상태 확인"), 보관소 Route 53 연결 확인에 도메인 등록 권한 표시, 모의 서버 검증 6개; 로그 점검 0건.
- **1.6.31** 보안·안정성 개선판: Claude 가드 훅 실효화(서버 절대 경로 설정 생성, Node 가드)·AI 환경 변수 허용 목록·작업 시간/동시 상한·codex fail-closed, 내부 주소 차단 + 허용 목록, 생성 사이트 페이지의 세션 무시 + CSRF 토큰, 고정 관리자 admin/admin 폐지(토큰 1회성·1회용 비밀번호), 로그인 속도 제한·잠금, 원자적 JSON 저장·손상 표시, 캐시 중복 차감 금지, Arimail 계정 등록부, 배포 단계(dns-wait·cert-wait…)·배포본 보관·이전 배포본으로, 도메인 구매 원장, 관리자 **운영 복구**·**작업 기록** 탭, 업데이트 전 백업, `npm test` 자동 검증. 설치: 최초 관리자는 설정 페이지(토큰)에서 만든다 (docs/FIRST-INSTALL.md).
- **1.5.36** 좌측 위·아래 영역 구분(배경·경계), 모듈 위젯 다국어(data-title·data-button 등 속성 번역, 파일 선택 버튼 문구표), 등급 표 특징 칸 2줄, **도메인 등록 업체 모듈**(Openprovider·Porkbun) 과 **DNS 공급자 모듈**(Route 53·Google Cloud DNS·Porkbun·Openprovider — 영역·레코드·MX·ACME TXT·이전 영역·감사 기록, 관리자 DNS 영역 관리) — [docs/DOMAIN-DNS-PROVIDERS.md](docs/DOMAIN-DNS-PROVIDERS.md).
- **1.5.31** 빌더 위쪽 진행 상자 **한 줄**(상태 + 오른쪽 보조 문구), 배포 결과 상자를 없애고 메시지창으로, 재배포 접속 확인의 301 오판 수정(HTTP→HTTPS 리디렉션 정상 판정, SNI), 서비스 유닛 권한 점검·보정 도구 `scripts/unit-check.js --fix`(드롭인) 와 갱신 스크립트 연동.
- **1.5.27** 빌더 위쪽 **메시지창**(결과 메시지 누적·최신 위·삭제), 재배포 때 vhost 가 이미 같으면 sudo 없이 유지(sudo 가 막힌 서버의 "도메인 연결 실패" 경고 제거), 사이트 도메인 vhost 에도 **발행 취소 안내 규칙**(`.unpublished` → 503), 안내 페이지 다국어(`lib/maintpage.js`), 와일드카드 인증서 감지를 nginx 참조 파일·/etc/ssl(Cloudflare 원본 인증서)까지, **회원가입 규칙**(가입 ON/OFF · 관리자 승인 후 이용, 승인/거절), 문의 메일 발신 주소를 검증된 상위 도메인으로(Resend 403 수정).
- **1.5.19** 생성 탭 등급 특징 문구(기본 정적 웹페이지 / 중급 UI·UX 심미성 / 고급 복잡한 프런트엔드 아키텍처), 배포 오류 안내 개선 — sudo 가 막힌 **원인**(유닛 `NoNewPrivileges`·`ProtectSystem=strict`·sudoers)을 구분해 안내(`sudoHint`), 종류 local 배포 서버의 DNS 비교를 127.0.0.1 이 아니라 서버 공인 IP 로(`localServerIps`), 설치 점검표 "배포 서버 도메인 연결" 항목, 갱신 스크립트의 유닛 경고.
- **1.5.17** 서버에 **미리 발급된 와일드카드 인증서**(`*.<플랫폼 도메인>`, 이름 무관)를 인지해 서브 도메인 사이트에 자동 적용 — SAN 기반 감지(`deploy.findWildcardCert`), 기동 때 HTTP 로 배포돼 있던 사이트 재구성(`ssl.autoApply`), 설치 점검표 항목, `scripts/ssl-wildcard.js`(설치·갱신 스크립트가 호출).
- **1.5.16** 관리자 콘솔 — **AI 연결** 탭 신설(Claude 인증·연결 테스트·**사용할 Claude 모델**·AI 제공자를 설정 탭에서 분리; 여기서 켠 모델만 등급·내부 호출에 지정 가능, 서버도 저장 때 검사), "Claude 기본 모델" 을 등급 지정과 같은 목록에서 고르는 **내부 호출 모델**(`internalRef`, 사용자 문의 AI 분석 등)로 대체, 시스템 설정 › 설치 점검표 글자 색을 배경의 보색 계열로 고정(어두운 테마에서 읽을 수 없던 문제).
- **1.5.13** 관리자 콘솔 › 배포 서버 — 공개키를 **별도 창에서 버튼으로 복사**(키 생성·재생성·보기), 추가·수정 폼을 **대화상자**로 분리("배포 서버 추가"·"수정" 때만 열림).
- **1.5.11** 첫 설치 — **기본 관리자 `admin / admin`**(사용자가 없으면 기동 때 생성, 첫 로그인 뒤 비밀번호 변경 강제), **설치 점검표**(관리자 비밀번호·AI 엔진 인증·공개 주소 — 끝나기 전에는 첫 화면 `/`·`/login` 이 `/setup` 으로 넘어감), `/setup` 을 토큰으로도 열기, 설정 페이지 **기본 포트 8795 · 모든 인터페이스**(겹치면 다음 포트 자동), 설정 페이지에 관리자 계정·AI 엔진 인증 카드, `remote-install.sh` FQDN 없이 IP 로 처음 설치, 회원가입 설정(`service.allowSignup`), 생성 사이트 런타임 `wm-modules.js` 캐시 `no-cache`+ETag ([docs/FIRST-INSTALL.md](docs/FIRST-INSTALL.md)).
- **1.5.7** 운영 구조 — **설정 파일 분리**(`data/config.json`, 유닛의 환경 변수를 첫 기동 때 자동 이전), **초기 설정 페이지**(별도 포트 3081 `webmaker-setup`, 토큰/관리자 로그인; `/setup`·관리자 콘솔 시스템 설정 탭과 같은 화면), **연동 모듈**(`lib/connectors/` — Gitea 소스 보관, **Arimail** 메일 서버: 사이트 관리자 계정의 메일함 생성·웹메일 연결), **연동 서비스 설치 스크립트**(`install-services.sh`, `install-arimail.sh`, `install-gitea.sh` 설정 파일 등록, 설치·갱신 스크립트에 포함). 오류 수정: codex CLI 0.159+ 의 `--full-auto` 제거로 OpenAI 서버 로그인 작업이 모두 실패하던 문제, 모듈 키워드(일정관리 등) 수정 지시에 표준 위젯 코드가 빠져 AI 가 가짜 모듈(`schedule`)을 만들던 문제(런타임 별칭으로 기존 페이지도 복구). [docs/CONFIG.md](docs/CONFIG.md) · [docs/ARIMAIL.md](docs/ARIMAIL.md)
- **1.4.18** 기본 제공 모듈에 **일정관리(캘린더)** 추가 — 월간·주간·일간 보기, 시·분/일/주 단위 일정, 반복(매일·매주·매월·매년), **만세력 기준 음력 날짜·사주식 간지(병오년 무술월 신유일)·24절기 표시 옵션**, 음력 기준 일정(음력 매년·매월). 음력·간지는 자료표 없이 천문 계산(`lib/lunar.js`, [docs/CALENDAR.md](docs/CALENDAR.md)).
- **1.4.17** 미리보기 안에서 페이지를 옮기면 위 도구 줄의 페이지 선택과 소스 편집기가 따라간다 (생성 페이지의 `wm-stats.js` 가 부모 창에 현재 경로를 알림, sandbox iframe 이라 직접 읽을 수 없음).
- **1.4.15** 1.4.14 회귀 수정 — 사이트를 고르지 않은 상태의 상태 줄 클래스(`empty`)가 미리보기 빈 화면의 전체 덮개 규칙과 겹쳐 빌더가 클릭에 반응하지 않던 문제. "사용자 첫 화면 페이지" 는 빌더 오른쪽 빈 창에만 보이는 페이지임을 관리자 콘솔 문구와 동작(전용 상자)으로 분명히 했다.
- **1.4.14** 빌더 탭을 **생성/수정/관리/도메인/설정** 으로 — 새 "수정" 탭(수정 지시·발행·다국어·검색 최적화·버전·첨부), "관리" 탭은 사이트 관리자 기능·현황 전담이며 오른쪽 창이 사이트 관리 창(사이트 관리자 페이지 + 현황)으로 바뀜, 상태·운영 주소 줄 높이 고정(탭 들썩임 제거). 관리자: OpenAI 서버 로그인 모델 전체 조회(codex 모델 목록 파일), 등급 지정에 추가 모델이 바로 보이도록(제공자별 묶음·빠진 이유 표시). 모델이 바뀌어도 맥락 유지("엔진 변경 안내" 섹션). 사이트 소스 저장소를 서비스 서버의 **로컬 Gitea**(계정 webmaker, 저장소 webmaker/sites)로 — `deploy/install-gitea.sh`·`scripts/migrate-gitstore.sh`, [docs/GITEA-LOCAL.md](docs/GITEA-LOCAL.md). 플랫폼 도메인 **와일드카드 인증서**(`*.ibit.kr`)로 서브 도메인 사이트 HTTPS, 고객 도메인은 사이트별 인증서로 분리 — [docs/SSL-WILDCARD.md](docs/SSL-WILDCARD.md).
- **1.4.5** 관리자 지정 사용자 첫 화면 페이지, 숫자 입력·표시 3자리 쉼표, Claude 세션 누적 비용을 작업마다 청구하던 오류 수정(이번 실행 비용 = 누적 차이 또는 토큰 × 단가, 캐시 쓰기 단가·보정 배율 수정, [점검 보고서](reports/token-billing-2026-10-10.md)).
- **1.4.3** 도메인 서비스 준비 목록·만드는 방법 문서 [docs/DOMAIN-SETUP.md](docs/DOMAIN-SETUP.md) (Openprovider 계정·접속 정보, `*.ibit.kr` DNS, SSL, 메일, 점검 순서).
- **1.4.1** 사이트 복사가 같은 서비스의 다른 사이트를 복사할 때 로그인 화면만 복사되던 문제 수정(시작 폴더 범위만 복사), 관리자 빌더의 "캐시 충전 요청" 안내 제거, 캐시 표시 단위 설정(원·크레딧·D 등), 게시판 [글쓰기] 버튼 한 줄.
- **1.4.0** 빌더 "도메인" 탭(Openprovider 도메인 검색·구매·갱신, 가격 = 원가 × 요율 250% 를 사용 전에 표시·캐시 차감, DNS 레코드 관리, 사이트 연결), 계정 종류(서브 도메인 전용 / 자체 도메인 — 서브 도메인은 `이름.ibit.kr` 등록·메일 `admin@이름.ibit.kr`·`/mail`), 메일(MX)·Resend 레코드 자동 설정과 DNS 전파 대기 안내, 블로그 모듈(검색·댓글), 자료실 모듈(파일 내려받기), 다국어 선택 기본 드롭다운, 생성 탭 언어 상자 여백, API 키 재입력·비노출 점검, 다국어 언어 선택 링크 오류(1.3.16) 수정.
- **1.3.17** 코드 점검 보고서(2026-10-07) 46건 조치 — 세션 위조(S01)·세션 무효화·미리보기 sandbox·바깥 주소 가져오기 보호(netguard)·가져오기 경로 탈출·번역 마크업 차단 등 보안 14건, 지도 모듈·발행·폐기·청구·편집기 등 기능 32건. 관리자 콘솔 비밀번호 규칙(기본 6자 이상). 대응표 `reports/code-review-2026-10-07-response.md`.
- **1.3.16** 관리자 설정 값마다 실행 버튼(청구 마진 저장 안 됨 수정·빈 값 0 저장 방지·저장 안 된 변경 표시), 다국어 로케일 코드 URL 원칙·x-default 지정, 로케일별 메타·구조화 데이터 관리, 생성 사이트 시스템 문구 카탈로그(위젯·폼·오류·동의문·예약·문의·메일 알림 하드코딩 제거), 번역 품질 게이트(누락률·잔존율·링크 대응·검수), 현지 형식(날짜·시간대·통화·주소·전화·이름 순서·법적 고지·RTL), 사이트 관리자 다국어와 서비스 기본 언어 강제(사용자 선택 우선).
- **1.3.10** 구글 검색 등록 완전 자동(서비스 계정 키)·네이버 수동 단계 최소화·배포 후 IndexNow 자동, 관리자 사이트 템플릿(디렉터리·해시태그·사용 허용, 생성 탭 "템플릿에서 시작"), 다국어 사이트 생성(/ko/ /en/ 로케일 폴더·같은 구조 번역·RTL·언어별 날짜와 SEO/AEO/GEO·hreflang), 빌더 번역 개선(남은 한국어 제거·언어 파일 형식 2·속도), 관리자 화면 다국어, 좁은 화면 번역문 넘침 수정, SEO 초안 JSON 파싱 오류 수정.
- **1.3.5** 생성 옵션 SEO·AEO·GEO(AI 초안 → 관리 탭 검토 후 반영), 관리 탭 검색사이트 등록(구글·네이버 단계·IndexNow, 관리자 검색 등록 탭), 관리자 언어 탭 실제 상태·서비스 버전 표시와 버전 감시, 문의 메일 발송 칸 삭제.
- **1.3.4** 생성 탭에서 "사이트 관리하기(서버에서 가져오기)" 삭제, "사이트 복사하기" 만 남기고 상세 설명(기본 HTML 페이지만 복사·기능 미복사·기존 모양을 살린 새 사이트 제작용).
- **1.3.3** 가져온·복사한 사이트 미리보기의 "페이지를 찾을 수 없습니다" 수정(루트 기준 주소를 Referer 로 사이트 폴더에 연결), 미리보기 없음 이유 표시.
- **1.3.2** 메일 시스템 사이트 단위 켜기(관리자 메일 시스템 탭, "메일시스템 설정중"→"사용 가능"), 키 파일 올리기, 화면 설정 탭(실제 화면 미리보기), 로그 보기(종류·기간·회사·사이트·검색), 사용자 문의(문의 종류·종류별 AI 1차 분석·답변 초안), 관리자 새로고침 수정, 템플릿 문장 번역(패턴).
- **1.3.1** 외부 연동 접속 정보 암호화 보관소(`data/credentials.json`, 키 파일 가져오기, 기존 메일·Claude 키 자동 이전), 웹메일 보내기(메일 발송 키·Resend 도메인 등록·확인).
- **1.3.0** 사이트 메일 시스템(운영자 켜기/끄기, 아이디@사이트도메인, DNS 안내·확인·자동 재확인, 사이트 관리자 계정과 공유하는 메일 계정, 메일 사용 끄기 시 재확인·비밀번호, `사이트/webmail` 웹메일 — 송수신 백엔드는 mail-gateway 연동 대기), 언어 파일 업로드 "JSON 파싱 실패" 수정.
- **1.2.0** 생성 탭 "기존 사이트 관리" 분리(사이트 복사하기 신규·사이트 관리하기), 가져오기·복사 후 사이트 현황 분석, 웹서버 설정(nginx·Apache)에서 서비스 중인 사이트 찾기·선택, 크기 제한을 서버 용량 10% 기준으로, 위쪽 사이트 선택·잔여 캐시만 표시, 제안·오류 3단계 흐름(답변 시 처리 완료), 생성 코드 주석(화면 언어), AI 입력창 강조, 관리자 화면 외형(색·구성) 설정, 캐시 상세 가로 스크롤 제거(사용자 제안 반영).
- **1.1.6** 캐시 소진 즉시 중지(모델별 비용 보정·부분 스트리밍 반영·97% 기준)와 파이프라인·기획 단계 이어하기, 캐시 상세 폭 조정, 관리자 충전 폼 위치, 제안·오류 신고 환경 정보 수집(사전 고지), 관리자 "다음 작업 반영" 결정과 작업 서버 수집(`scripts/collect-backlog.js`).
- **1.1.4** 좌측 창 상하 분할(상태 창 위쪽), 발행 카드 간소화, 발행 취소 403 수정(.unpublished 표식·작업 폴더 보호), 수정 완료 배너 간소화, 검증 작업 표시, 캐시 내역 5행 스크롤 + 상세 보기(일·주·월), 사이트 관리자 계정 생성 갱신·직접 주소 표시, ? 풍선 화면 안 배치.
- **1.1.2** 다국어(원본 카탈로그 내려받기 → 번역 파일·ZIP 드래그앤드롭 업로드, 언어 켜기/끄기, 구버전 번역 자동 숨김), 등급 표 간결화, 페이지 수 옵션, 재배포 버튼 안전장치, 수정 완료 배너·상세 내역, 작업 단위 캐시 합계, 사용자 제안·오류 신고와 AI 자동 분석(관리자 탭).
- **1.1.0** 오류 점검(엔진 준비 확인·안내), 버전 표시, 발행 취소 시 "수정 중" 안내 페이지, 수정마다 모델 등급 선택과 맥락 유지, ? 도움말, 생성 탭 독립·중단 후 새로 만들기, 수정 진행 단계 표시, 제품명 AI Webmaker, 사이트 폐기, 큰 수정 지시 입력창, 재배포 버튼 정리, 모바일 미리보기, 모델 등급 비용 가이드.
- **1.0.3** 배포 후 운영 주소 표시, 공통 기능 모듈(회원·게시판·문의·설문·예약·갤러리·지도·후기·FAQ + 사이트 관리자 페이지, `modules/`), AI에게 맡기기 단순화(추가 입력만 받고 끝까지 자동 생성, 수정 횟수 한도·발행 대기), 생성/관리/설정 탭, 내가 고르기 속도 개선·사실정보 단순화·버튼 정리, 수정 요청 오류 수정.
- **1.0.2** 신규 회원 기본 캐시, 사이트별 배포 위치 지정(관리자 전용), 문의 메일 실제 발송 검증·재발송, 사이트 접속 통계(서버 로그 측정 + 브라우저 비컨 추정), 사용자 화면의 "사이트 정보" 카드(접속·네트워크 인/아웃·디스크·DB 사용량).
- **1.0.1** 캐시(크레딧) 도입: 관리자가 사용자별 캐시를 설정·추가하고 작업 비용이 캐시에서 차감됨. 사용자 화면의 토큰·예상 금액 표시를 캐시 사용액/잔액으로 대체. 캐시 소진 시 작업을 멈추고(지금까지의 작업 저장) 충전되면 자동으로 이어서 진행. 모델은 관리자가 지정하는 등급(기본·중급·고급)으로만 선택하며 모델명은 사용자에게 보이지 않음. 사용자 자신의 LLM 연결 기능 삭제. 외부 서비스용 캐시 조정 API(`X-Service-Key`).
- UI: `http://192.168.20.66/` (로그인 필요, 계정이 없으면 회원가입: 아이디·비밀번호·이름(회사명))
- 생성된 사이트: `http://192.168.20.66/<32자리 16진 ID>/` (메인 `index.html`), 하위 페이지는 `/<ID>/page.html`
## 구조
```
브라우저 ──> nginx:80 ─┬─ / ──> Node 서버 127.0.0.1:3080 (UI + API + SSE)
└─ /<32hex>/... ──> /var/www/sites/<32hex>/ 정적 파일
Node 서버 ──> claude -p --model claude-opus-5-5 (작업 폴더 = /var/www/sites/<ID>)
```
| 경로 | 역할 |
|---|---|
| `server.js` | HTTP 서버. 사이트 생성/수정 API, SSE 진행 스트림, 정적 UI |
| `public/` | 좌측 탭(생성·수정·관리·도메인·설정, v1.4.14)·진행 상태 / 우측 미리보기 iframe(관리 탭에서는 사이트 관리 창 `manage-ui.js`). 상세 진행 로그는 고객에게 보이지 않고 스피너(진행 중)/체크(완료)/경고(오류) 표시기만 노출 |
| `lib/config.js` · `scripts/config.js` | v1.5.7 설정 파일(`data/config.json`) — 항목표·우선순위·환경 변수 이전, 명령줄 도구 ([docs/CONFIG.md](docs/CONFIG.md)) |
| `setup-server.js` · `public/setup.*` · `lib/setupapi.js` | v1.5.7 초기 설정 페이지(별도 포트, systemd `webmaker-setup`; v1.5.11 기본 8795·0.0.0.0, 겹치면 다음 포트)와 설정·연동 상태 API (서비스 `/setup`·관리자 콘솔 시스템 설정 탭과 공용) |
| `lib/setupstate.js` · `lib/setupauth.js` | v1.5.11 설치 점검표(관리자 비밀번호·AI 인증·공개 주소 → 첫 화면 포워딩)와 설정 페이지 인증(접속 토큰·세션·시도 제한, 두 서버 공용) ([docs/FIRST-INSTALL.md](docs/FIRST-INSTALL.md)) |
| `lib/connectors/` | v1.5.7 외부 서비스 연동 모듈 — `gitea.js`(소스 보관), `arimail.js`(메일 서버), `index.js`(등록소) ([docs/ARIMAIL.md](docs/ARIMAIL.md)) |
-`/login` 에서 로그인하거나 회원가입한다. 회원가입은 아이디(영문 소문자·숫자 3~32자), 비밀번호(6자 이상), 이름(회사명)만 받는다.
- 비밀번호는 scrypt 로 해시해 `data/users.json` 에 저장하고, 세션은 `data/session.secret` 로 서명한 HttpOnly 쿠키(30일)다.
- 사이트에는 `ownerId` 가 기록되어 목록·조회·수정 모두 본인 것만 가능하다. 회원의 이름(회사명)은 생성 프롬프트에 "운영 주체"로 전달된다.
- 생성된 페이지 자체(`/<ID>/`)는 공개 URL 이므로 로그인 없이 열린다.
## Gitea 소스 보관 — v1.4.14 서비스 서버의 로컬 Gitea
사이트마다 로컬 git 저장소(`data/git/<ID>`, work-tree 는 `/var/www/sites/<ID>`, 사이트 폴더에 `.git` 없음)를 두고, 변경이 생길 때마다 커밋해 Gitea 로 푸시한다.
- 원격(`SITES_GIT_REMOTE`): **v1.4.14 부터 서비스 서버 안의 전용 Gitea** — Docker 컨테이너 `webmaker-gitea`(HTTP `<LAN IP>:3030`, SSH `127.0.0.1:2230`), 서비스 전용 계정 `webmaker`, 비공개 저장소 `webmaker/sites`, ssh 별칭 `webmaker-gitea`. `deploy/install-gitea.sh` 가 컨테이너·계정·저장소·키 등록·서비스 유닛 설정을 한 번에 하고, `--migrate-from <옛 원격>` 으로 1.4.5 까지 회사 소스 저장소(`source.ibit.kr``OZ-Group/WebHosting_AI_agent`)에 섞여 있던 `sites/*` 브랜치를 옮긴다(`scripts/migrate-gitstore.sh`, 옛 브랜치는 지우지 않음). 구성·확인·백업: [docs/GITEA-LOCAL.md](docs/GITEA-LOCAL.md). 관리자 콘솔 › 설정 › 소스 저장소에서 연결 상태와 이 서버의 공개키를 본다.
- 브랜치 구조(사이트 랜덤코드로 분리):
-`sites/<ID>/main` — 항상 최신
-`sites/<ID>/changes/0001-create`, `0002-ai-update`, `0003-manual-edit` … — 생성, AI 수정 지시, 소스 편집기 저장 등 변경마다 하나
- 커밋 작성자는 회원 이름(회사명)과 `<아이디>@webhosting.local`. 브랜치·푸시 상태는 사용자에게 보이지 않고, UI 에는 "이전 버전으로 되돌리기" 목록만 표시된다. 되돌리기(`POST /api/sites/:id/versions/:no/restore`)는 그 시점의 파일(과 Brief 스냅샷)을 복원하고 복원 자체를 새 버전(`restore`)으로 기록한다. 푸시 실패분은 다음 변경 때 자동 재시도한다.
## 관리자 콘솔 (`/admin`)
v1.3.2 탭: 사이트 관리 · 로그 보기(종류·기간·회사·사이트·검색) · 회원·청구 · 배포 서버 · 사용자 문의(문의/제안/오류/요구, AI 1차 분석·답변 초안) · 메일 시스템(사이트 단위) · 언어 · 화면 설정(실제 화면 미리보기) · 설정. "새로고침" 은 지금 탭과 개요를 다시 불러오며, 열린 탭은 주소 #탭 으로 유지된다.
관리자 계정은 로그인 후 곧바로 `/admin` 으로 이동하며 사이트 생성 화면은 쓰지 않는다(사이트 관리 전용). 일반 화면(밝은 배경·파란 포인트)과 구분되도록 짙은 남색 배경·호박색 포인트의 다크 테마다.
- **사이트 관리**: 모든 회원의 사이트 목록·상태·오류·토큰·비용, 열기/중단/삭제, 상세(명령 이력·모델·상세 로그·프롬프트·버전·기획 상태·검증 리포트).
- **제안 · 오류**(v1.2.0 흐름): 새 글(접수) → AI 분석이 붙으면 검토 중 → 관리자가 답변을 저장하면 처리 완료(반려 없음). 답변은 사용자 설정 탭 "내 제안 · 오류 신고" 에 펼쳐 보인다. "다음 수정에 반영" 으로 결정한 글은 작업 서버가 수집하고, 릴리스에 반영하면 `node scripts/collect-backlog.js --implemented <id,...> --version <v>` 로 반영 완료 표시한다.
- **설정 › Webmaker 화면 외형**(v1.2.0): 색 구성 미리 설정·색 10가지·왼쪽 패널 폭·글자 크기·모서리·간격·패널 위치·위쪽 영역 스타일. `/theme.css` 로 사용자 화면·로그인·사이트 관리자 페이지에 적용(저장 전에는 기존 화면 그대로).
- **AI 연결**(v1.5.16, 설정 탭에서 분리): Claude 모델 연결 방식(서버 로그인 / `claude setup-token` 으로 만든 OAuth 토큰 / Anthropic API 키) 저장과 연결 테스트, **사용할 Claude 모델** 체크(`settings.claudeModels`), AI 제공자(OpenAI·Gemini·Groq·OpenRouter·호환 서버) 키·모델 조회·제공 모델 선택. 여기서 켠 모델만 설정 탭의 등급·내부 호출 모델에 지정할 수 있다(서버도 저장 때 검사).
- **설정 › 회원가입 규칙**(v1.5.27): 회원가입 허용 ON/OFF(`service.allowSignup`)와 가입 후 이용 방식(`settings.signupApproval` — 즉시 / 관리자 승인). 승인 방식이면 회원 · 청구 탭에서 "가입 승인"·"거절(삭제)", 탭 배지에 대기 수.
- **설정**: 모델 등급 지정과 **내부 호출 모델**(v1.5.16 — 등급과 무관한 서비스 내부 호출(사용자 문의 AI 분석 등)에 쓰는 모델, 등급 지정과 같은 목록에서 선택; 1.5.13 까지의 "Claude 기본 모델" 대체), 환율 모드(자동: ECB·시장 중간환율 + 스프레드 % / 직접 입력: 당일 USD 구입 가격)와 마진 %.
청구 계산: 명령별 비용(USD, Claude Code 보고값) × 그 명령 실행일(KST)의 USD 구입 가격(원) × (1 + 마진%). 환율은 `data/fx.json` 에 날짜별로 캐시된다(자동 모드: frankfurter.dev → open.er-api 순, 조회 실패 시 직전 값). 선결제는 `data/billing.json` 에 충전 내역으로 기록된다. 설정은 `data/settings.json`(0600)에 저장되며 토큰/키는 화면에 마스킹된다.
- 회원에는 `role`(`user`|`admin`)이 있다. 관리자는 `ADMIN_USERS` 환경변수(아이디 쉼표 구분)나 `node scripts/admin.js grant <id>` 로 지정한다. 관리자 계정은 메인 화면 상단의 "관리자" 링크로 `/admin` 에 들어간다.
- 관리자 화면: 모든 회원의 사이트 목록(방식·단계·상태·명령 수·오류·토큰·비용), 사이트별 명령 이력과 **상세 로그**(도구 호출, 도구 오류, 프롬프트, 실시간 스트림), 전체 오류 로그, 회원 역할 변경.
- 사용자 화면에는 상세 로그와 원인 메시지가 보이지 않는다. 사용자에게는 "작업을 완료하지 못했습니다. 다시 시도해 주세요." 같은 일반 안내만 표시되고, 원인(`도구 오류: self signed certificate`, 종료 코드 등)은 `debug` 레벨로 명령 이력(`commands[].log`)에 남아 관리자만 본다. SSE 도 역할에 따라 필터링된다.
## 참고 사이트 수집
참고 사이트와 기존 사이트 URL 은 AI 가 WebFetch 로 직접 열지 않고, 서버가 먼저 본문을 가져와 사이트 폴더의 `.refs/ref-N.txt` 에 저장한 뒤 AI 가 Read 로 읽는다(`lib/refs.js`). 인증서 검증 실패(자체 서명·만료), EUC-KR 인코딩, 리디렉션을 처리하며 실패하면 사용자가 적은 참고 포인트만 반영한다. WebFetch 는 보조 수단이며, 국내 사이트의 인증서 문제로 실패하지 않도록 Claude 실행 환경에 `NODE_TLS_REJECT_UNAUTHORIZED=0` 을 준다(`CLAUDE_ALLOW_INSECURE_TLS=0` 으로 끌 수 있음). `.refs/` 는 nginx 가 서비스하지 않고 git 보관에서도 제외된다.
## 문의 접수 위젯 (사이트 안에서 메일 발송)
생성 사이트의 문의·예약 폼은 `mailto:` 대신 **표준 위젯**(`templates/contact-widget.html`)을 그대로 삽입한다. 생성·수정 프롬프트에 사이트별 엔드포인트(`<폼 접수 서버>/api/forms/<사이트ID>/contact`)가 채워진 위젯 코드가 포함되고, AI 는 data-* 속성과 script 를 바꾸지 않고 CSS 로만 꾸민다. 검증은 `mailto:` 사용과 위젯 존재 여부를 검사하며, 자동 수정은 mailto 를 위젯으로 대체한다.
접수 흐름: 방문자 제출 → 서비스의 공개 엔드포인트(CORS: 서비스·사이트 도메인·배포 서버 공개 URL 출처만 허용, 허니팟, IP/사이트별 속도 제한, 길이 검증) → Resend API 로 메일 발송(발신 `webmaster@사이트도메인`이 Resend 검증 도메인이면 그것, 아니면 기본 발신 도메인; `reply_to` 는 문의자) → `data/inquiries/<사이트ID>.json` 에 기록. 수신 주소는 사이트의 "문의 수신 이메일"(기본 사항 / Brief 사실정보 `email`)이며 HTML 에는 노출되지 않는다. 사용자 화면의 "문의 접수 내역" 카드에서 접수·발송 여부를 본다.
관리자 콘솔 › 설정 › 메일 발송: 메일 에이전트 키 파일 경로(.key 의 `RESEND_API_KEY`) 또는 키 직접 입력(암호화 저장), 기본 발신 도메인, 발신자 이름, 폼 접수 서버 주소(공개 URL; 사설 IP 서비스에서는 공개 인스턴스 주소를 지정), 테스트 메일 발송.
## 오류 페이지
존재하지 않는 페이지·숨김 파일·서버 오류는 서비스 서버 nginx, 생성되는 호스팅 vhost, Node 앱 모두 "페이지를 찾을 수 없습니다" / "일시적인 오류가 발생했습니다" 라는 일반 안내 페이지(`public/404.html` 과 동일 내용)로 응답하고 `server_tokens off` 로 서버 버전을 숨긴다. 다른 서비스의 내용이나 서버 종류가 드러나지 않는다.
## 배포 서버 (여러 웹호스팅 서버 관리)
관리자 콘솔 › 배포 서버에서 사이트를 실제로 호스팅할 서버를 여러 개 등록하고 기본 배포 서버를 지정한다. 종류는 두 가지다.
- **로컬**: 이 서비스 서버의 디렉터리(예: `/var/www/sites`, 즉 지금 서비스되는 위치 — 같은 경로면 이미 배포된 것으로 처리).
- **SSH 원격**: 주소·포트·사용자·사이트 저장 경로(공개 URL 은 선택)를 입력하고, "키 생성"으로 이 서버가 만든 전용 ed25519 키의 공개키를 원격 서버 `~/.ssh/authorized_keys` 에 등록하거나 기존 개인키를 붙여넣는다. "연결 테스트"는 접속 → 사용자 → 저장 경로 존재/생성 → 쓰기 권한 → tar 순으로 진단하고, 실패 시 원인과 서버에서 실행할 명령(예: `sudo chown <user> <경로>`)을 알려 준다. 배포는 `tar | ssh tar` 로 전송한 뒤 임시 디렉터리를 원자적으로 교체한다(`.git`, `.claude`, `.refs` 제외). 원격 서버에는 `tar` 만 있으면 된다.
사용자가 "최종 확정(발행)"하면 사이트의 지정 서버(없으면 기본 서버)로 자동 배포되고 결과 URL 이 표시된다. 확정 전에 **도메인을 여러 개**(예: `www.ibit.kr`, `ibit.kr`) 입력하면 배포 서버의 nginx 에 `server_name` 으로 모두 등록한 vhost(`/etc/nginx/sites-available/wh-<ID>.conf` 또는 `conf.d/`)를 만들어 어느 도메인으로 접속해도 열리게 한다(대상 서버에 nginx 와 비밀번호 없는 `sudo` 필요, 배포 서버 설정의 "vhost 자동 설정"으로 끌 수 있음). DNS 는 운영자가 배포 서버로 가리켜야 한다.
**기본 오류 서버**: 매 배포마다(sudo 허용·vhost 자동 설정인 서버) 배포 서버 nginx 에 `default_server` 가 있는지 점검하고, 없으면 `wh-default.conf` 로 80 포트는 "페이지를 찾을 수 없습니다" 안내 페이지(404)를, 443 포트는 `ssl_reject_handshake` 로 미등록 호스트의 HTTPS 요청을 거부하도록 설정한 뒤, 미등록 호스트 요청이 실제로 안내/거부되는지 검증해 결과를 배포 결과에 표시한다. 기존 default_server 가 있으면 유지하되 다른 내용이 노출되면 경고한다. 관리자 배포 서버 탭의 "오류 서버 점검"으로 수동 점검도 가능하다.
**첫 배포와 재배포**: 같은 서버에 성공한 배포가 있으면 재배포로 처리해 파일 갱신·기본 오류 서버 점검·vhost 재적용만 하고, 대상 서버에 이미 사이트 인증서가 있고 모든 도메인을 포함하면 HTTPS 설정을 유지한 채 인증서 재발급을 건너뛴다(도메인이 추가되었거나 인증서가 없을 때만 발급). 도메인 연결이 끝나면 **Let's Encrypt SSL** 을 자동 발급한다(배포 서버 설정 "SSL 자동 발급", 기본 켜짐): DNS 가 배포 서버를 가리키는 도메인만 골라 `certbot --nginx --redirect` 로 인증서를 받고 HTTPS 서버 블록과 HTTP→HTTPS 리디렉션을 만든다(certbot 이 없으면 apt 로 설치 시도, 갱신은 certbot 타이머). 알림 이메일은 설정 탭에서 지정한다. nginx 설정 쓰기와 certbot 은 **sudo** 가 필요하므로 배포 서버마다 "sudo 사용 허용"을 운영자가 켜야 하며, 관리자 배포 시 허용되지 않은 서버면 확인 대화상자에서 허용 여부를 묻는다. 허용하지 않으면 파일만 배포된다.
배포 후에는 접속 확인을 자동으로 수행한다: 도메인별 DNS(A 레코드가 배포 서버를 가리키는지)와 배포 서버의 vhost 응답, 공개 URL 응답을 검사해 관리자 배포 결과 패널과 사용자 발행 상자에 ✓/✗ 와 조치 방법(예: A 레코드 추가)을 표시한다. 관리자 사이트 상세의 "접속 확인"으로 언제든 다시 검사할 수 있다. 배포 서버의 공개 URL 은 그 서버의 웹서버가 저장 경로를 `공개URL/<ID>/` 로 직접 서비스할 때만 설정한다.
배포가 완료되면 되돌리기용 이전 버전(로컬·Gitea 의 `sites/<ID>/changes/*` 브랜치)을 모두 삭제하고 현재 배포본 하나만 남긴다(`sites/<ID>/main` 강제 갱신). 이후 수정 과정의 버전은 다음 배포 때 다시 정리된다. 관리자는 사이트 상세에서 서버를 바꾸거나 즉시 배포할 수 있으며, 사이트를 삭제하면 배포본도 제거한다. 설정은 `data/deploy-targets.json`, 키는 `data/deploy-keys/`(0600).
원격 호스팅 서버의 nginx 예시(저장 경로 `/var/www/sites`, 공개 URL `https://host.example.com`):
- **고객 도메인**(example.com, www.example.com): 발행(배포) 때 DNS 가 배포 서버를 가리키는 도메인에 Let's Encrypt 인증서(`wh-<사이트ID>`, webroot)를 발급하고 HTTP→HTTPS 로 돌린다(1.0.x 부터).
- **플랫폼 서브 도메인**(이름.ibit.kr): 사이트마다 받지 않고 **와일드카드 인증서 `*.ibit.kr`**(`wh-wild-ibit.kr`) 하나로 HTTPS. 관리자 콘솔 › 설정 › SSL 에서 운영자가 발급·갱신한다(DNS 인증 — 플랫폼 DNS 가 Openprovider 면 자동, 수동이면 화면에 뜨는 TXT 를 DNS 에 넣음). 와일드카드가 없으면 서브 도메인은 HTTP 로 열리고 발급되면 재배포 없이 HTTPS 가 적용된다. **v1.5.17**: 서버에 다른 절차로 받아 둔 `*.<도메인>` 인증서(이름 무관)도 SAN 으로 찾아 같은 방식으로 쓰며, 서비스 기동 때 HTTP 로 배포돼 있던 서브 도메인 사이트를 자동으로 HTTPS 로 바꾼다(`node scripts/ssl-wildcard.js --apply`). nginx 설정은 두 묶음을 별도 server 블록으로 쓴다(`lib/deploy.js vhostConf`, `lib/ssl.js`, `scripts/acme-dns-hook.js`). 절차·메시지: [docs/SSL-WILDCARD.md](docs/SSL-WILDCARD.md).
## 여러 AI 제공자와 사용자 자기 모델
- 관리자 콘솔 ›**AI 연결** 탭(v1.5.16, 이전 설정 탭) ›**AI 제공자**: OpenAI(ChatGPT), Google Gemini, Groq, OpenRouter, 기타 OpenAI 호환 서버의 API 키를 저장하고 "모델 조회"로 목록을 불러온 뒤 이 서비스에서 쓸 모델과 단가($/1M)를 고른다. 키가 있고 활성화된 제공자의 선택 모델만 등급·내부 호출 모델 목록에 나타난다(Claude 는 같은 탭의 "Claude 모델 연결"·"사용할 Claude 모델" 을 따름). 키는 AES-256-GCM 으로 암호화해 `data/settings.json` 에 저장한다.
- 사용자 화면 상단 **AI 모델** 선택: 사이트마다 사용할 모델을 고르며(`modelRef`, 예: `openai:gpt-5.2`), 생성·수정·기획 단계 모두 그 모델로 실행된다.
- **내 AI 연결**: 사용자가 자기 API 키(OpenAI·Gemini·Groq·OpenRouter·호환 서버) 또는 Claude 구독의 `claude setup-token` 토큰을 직접 등록한다. "연결 확인"으로 모델 목록을 받아 쓸 모델을 고르고 저장하면 `data/user-connections.json`(암호화, 0600)에 보관되어 다음에도 재사용된다. 내 연결 모델로 실행한 작업은 사용자 키로 과금되므로 청구액이 0 원으로 기록된다.
- OpenAI 는 API 키 대신 **서버 로그인**(OpenAI Codex CLI 의 ChatGPT 계정 로그인)으로도 쓸 수 있다. 서버에서 `npm i -g @openai/codex` 후 `codex login --device-auth` 로 로그인하고, 관리자 콘솔의 OpenAI 제공자에서 "서버 로그인"을 고르면 정적 모델 목록(gpt-5.2-codex, gpt-5.2, gpt-5.1 등)이 제공되며 `lib/engine-codex.js` 가 `codex exec --json` 으로 실행한다(파일 변경 사용량은 turn.completed 의 토큰으로 집계).
- 서버 로그인 **모델 조회**(v1.4.14): codex CLI 가 백엔드에서 받아 두는 모델 목록 파일(`~/.codex/models_cache.json`)을 읽어 그 계정에 보이는 모델 전체(이름·설명·숨김·종료 예정)를 한 번에 보인다. 파일이 없으면 기본 모델로 한 번 실행해 만든다.
- 실행 엔진: Anthropic 은 Claude Code CLI(파일 도구), 그 외는 `lib/engine-openai.js` 의 OpenAI 호환 Chat Completions 도구 호출 에이전트(list/read/write/edit/delete_file, 사이트 폴더 밖 접근 차단)로 같은 프롬프트·진행 이벤트·사용량 집계를 제공한다. Gemini 는 OpenAI 호환 엔드포인트를 사용한다.
## 인증(Claude)과 모델
API 키 대신 서버에 로그인된 Claude Code CLI(구독 계정)를 그대로 사용한다. 모델은 `CLAUDE_MODEL`(기본 `claude-opus-5-5`).
- v1.3.2: 값이 들어가는 문장(템플릿 문자열)은 카탈로그에 `현재 사이트: {1} · 화면: {2}` 같은 패턴(pattern: true)으로 들어간다. 번역문에도 같은 {숫자}를 넣어야 하며(순서 변경 가능), 다르면 업로드 때 제외된다. 런타임은 정확한 문장이 없으면 패턴으로 맞춰 값을 끼워 넣는다.
- 번역 키 대신 **원문(한국어) 자체**를 키로 쓴다. `lib/i18n.js` 가 `public/index.html`, `login.html`, `site-admin.html`, `app.js`, `wizard.js`, `import.js`, `modules-ui.js`, `site-admin.js` 에서 한글이 든 텍스트·속성(placeholder/title/data-tip/alt)·문자열 리터럴을 추출해 카탈로그를 만든다(변수가 든 템플릿 문자열은 제외). `node scripts/i18n-extract.js` 로 `locales/ko.json` 을 갱신해 소스에 함께 둔다.
- 관리자 콘솔 › 언어에서 원본 파일을 내려받아 `translation` 을 채우고 `lang`/`name` 을 적어 올린다(JSON 여러 개·ZIP, 드래그앤드롭). 저장 위치 `data/locales/<lang>.json`. `sourceVersion` 이 서비스 버전보다 낮으면 구버전으로 숨겨진다.
- 화면은 `public/i18n.js` 가 사전으로 치환한다(텍스트 노드·속성, MutationObserver). 언어 선택은 헤더 `#langSelect`(언어가 둘 이상일 때만 표시), `localStorage.wm_lang`, `?lang=`.
- v1.3.10: 관리자 콘솔(`admin.html`·`admin.js`)·사이트 관리자도 번역 대상(관리자 콘솔에 언어 선택). 서버 목록·메시지 파일(`SERVER_FILES`)과 백틱·HTML 템플릿까지 추출해 사이트 안 모든 한국어가 `locales/ko.json` 에 들어간다. AI 프롬프트·사이트 파일 내용은 `// i18n-ignore-start` … `// i18n-ignore-end` 로 제외.
- v1.3.10 언어 파일 형식 2: `guide`(번역 방법)·`stats`, 항목마다 `status`("번역 필요"/"번역됨")·`group`·`placeholders`. 화면 코드는 값과 문구를 나눠 `T('문구')`, `TF('{1}원', 값)`, 날짜·숫자는 `uiLocale()` 로 쓰고, 언어가 바뀌면 `wm:i18n` 이벤트로 다시 그린다. 서버가 쿠키 `wm_lang` 으로 첫 HTML 을 미리 번역해 보내고, 사전은 브라우저에 버전 표시와 함께 저장한다.
## 접속 통계와 사이트 정보 — v1.0.2
- 측정(서버 로그): `deploy/nginx-wmstats.conf` 의 `wmstats` 형식을 `/etc/nginx/conf.d/` 에 두고, 서비스 서버의 사이트 경로 블록은 `/var/log/nginx/webmaker-sites.log`, 원격 배포 서버의 사이트 vhost 는 `/var/log/nginx/wh-<id>.access.log` 로 남긴다. `lib/stats.js` 가 로컬 5분·원격 15분(SSH, sudo 허용 서버만) 주기로 새 부분만 읽어 `data/stats/<id>.json` 에 일자별 요청·페이지뷰·방문자(일자+IP+UA 해시)·수신/송신 바이트·상위 페이지·유입 경로를 집계한다. 봇 UA 는 따로 센다. 서비스 사용자는 `adm` 그룹이어야 로그를 읽을 수 있다(Ubuntu 기본 nginx 로그 권한 0640 www-data:adm).
- 추정(비컨): 모든 HTML 에 `<script src="<서비스 URL>/wm-stats.js" data-wm-stats data-site="<id>" async>` 가 들어간다(AI 작업 뒤와 배포·원본 반영 직전에 `stats.ensureBeacon` 이 보장). 페이지를 열 때 `POST /api/stats/beacon` 으로 경로·리퍼러·기기·전송량을 보고한다. 로그를 못 읽는 서버용 보조 통계이며 수신량은 측정하지 못한다.
- 사용자 화면 "사이트 정보" 카드(`GET /api/sites/:id/stats`): 오늘/어제/7일/이번 달/누적 방문자·페이지뷰, 14일 막대, 네트워크 인/아웃(오늘·월간·누적), 디스크(사이트 파일·첨부)·DB(사이트 정보·문의·통계·버전 저장소) 현재 사용량, 많이 본 페이지·유입 경로. 로그가 있으면 측정값, 없으면 비컨 추정값을 쓰고 출처를 표시한다.
- v1.4.1 표시 단위 `settings.cashUnit`(기본 "원", 관리자 콘솔 › 설정 › 캐시 표시 단위). 서버는 `credits.fmt(n)`, 빌더 `krwFmt`, 관리자 `krw` 로만 금액을 쓴다. 단위만 바뀌고 1 단위 = 1원.
- 단위는 원. `data/credits.json` 에 사용자별 잔액과 원장(설정/추가/조정/사용/환불/이관)을 보관한다. 이전 "선결제 충전" 내역은 첫 기동 때 초기 잔액으로 1회 이관된다.
- 관리자 콘솔 › 회원·청구: 사용자별 캐시 잔액·누적 사용·충전 대기 건수. 상세에서 원장을 보고 "캐시 추가"(음수면 차감) 또는 "이 값으로 설정"을 한다. API: `GET /api/admin/credits`, `GET|POST /api/admin/credits/:userId` (`{type: set|add, amount, memo}`).
- 외부 서비스 연동: 관리자 콘솔 › 설정 › 외부 서비스 캐시 연동 키를 저장하면 `GET|POST /api/service/credits/:userId` 를 `X-Service-Key` 헤더로 호출할 수 있다(본문 `{type: set|add|adjust|refund, amount, memo, source, ref}`). 로그인 세션 없이 동작하므로 다른 시스템(결제·정산)이 잔액을 자동 조정하는 용도.
- 차감: 각 작업(생성·수정·기획 단계)이 끝날 때 확정 청구액(비용 × 환율 × (1+마진))을 잔액에서 뺀다. 사용자 화면은 토큰을 보여 주지 않고 "이번 회차 캐시", "잔여 캐시", 사이트별 사용 캐시만 표시한다.
- 한도: 잔액이 0 이하이면 새 작업을 시작할 수 없다(402). 진행 중 추정 사용액이 잔액에 이르면 작업을 멈추고 지금까지의 파일을 버전으로 저장한 뒤 `waiting-credit` 상태로 대기한다. 캐시가 충전되면(관리자/외부 API) 같은 세션을 이어받아 "원래 지시 중 아직 완료되지 않은 부분만" 계속 진행한다. 기획 단계(이해 확인·구조·디자인·검증)는 짧아서 자동 재개 대신 충전 후 다시 실행하도록 안내한다. 관리자 계정은 한도를 적용하지 않는다(사용은 기록).
## 모델 등급 — v1.0.1
- 사용자는 **기본 / 중급 / 고급** 만 고른다(헤더의 "모델 등급", 사이트별 저장, `PUT /api/sites/:id/model {tier}`). 모델 제공사·모델명은 사용자 화면·API 응답에서 제거된다.
- 관리자 콘솔 › 설정 › 모델 등급 지정에서 각 등급에 실제 모델(제공자:모델, AI 연결 탭에서 켠 제공자·모델 목록에서 **제공자별 묶음**으로 선택 — v1.5.16: 목록에 없는 모델은 서버가 저장을 거부하고, 내부 호출 모델도 같은 목록에서 고른다 — v1.4.14: AI 제공자를 저장하면 바로 갱신되고, 켜 두었지만 목록에 못 들어간 제공자는 이유를 보인다)을 지정하고 새 사이트의 기본 등급을 정한다. 기본값: 기본=Claude Haiku 4.5, 중급=Claude Sonnet 5.5, 고급=Claude Opus 5.5. 등급 변경은 다음 작업부터 적용된다.
- 사용자가 자신의 API 키로 모델을 연결하던 "내 AI 연결" 기능은 제거되었다(`/api/models`, `/api/me/connections` 는 410).
### 모델이 바뀌어도 맥락 유지 — v1.4.14
수정마다 등급(모델)을 바꿀 수 있다. Claude 는 사이트별 세션(`--resume`)을 이어 가고, OpenAI·codex 엔진은 실행마다 새로 시작하므로 모든 수정 프롬프트에 "[작업 맥락]"(사이트 정보·최근 지시 8건·결과·바뀐 파일 수)을 넣는다. 여기에 더해 이번 모델이 직전 작업과 다르면 **"[엔진 변경 안내 — 맥락 유지]"**(이 모델이 이 사이트를 마지막으로 다룬 뒤 다른 모델이 한 작업의 지시·결과·바뀐 파일, 최대 6건)를 넣어 Claude↔OpenAI 를 오가도 이전 수정을 알고 이어 간다(`lib/prompt.js engineSwitchSection`). 작업 로그에 "모델 변경 A → B — 맥락 전달" 이 남는다.
## 토큰 집계
- v1.4.5: Claude CLI 의 `total_cost_usd`·`modelUsage` 는 세션 누적값이므로 그대로 쓰지 않는다. `lib/claude.js runCost` 가 세션 장부(`data/claude-sessions.json`)로 이번 실행 비용(누적 차이)을 구하고, 장부가 없거나 지난 실행이 중단되었으면 이번 실행 `usage`× 단가(`settings.estimateUsd`, 1시간 캐시 쓰기 = 입력 × 2)로 계산한다. 작업 기록 `costSource` = single | delta | priced | sum.
각 명령의 결과 이벤트 `usage` 를 그대로 기록한다.
- 입력 / 출력(사고 토큰 포함) / 캐시 생성 / 캐시 읽기, 합계 = 네 값의 합
- 비용은 Claude Code 가 보고하는 `total_cost_usd`
- 사이트별 누적 합계는 `totals` 로 저장되고 UI 상단 카드와 표 하단에 표시
- 사용자 화면에는 토큰 총량과 마진 포함 원화 금액(회차별 총액, 전체 합계)만 표시한다. 입력/출력/캐시 구분, USD, 단가는 관리자 콘솔에서만 본다. 명령 완료 시 `chargeKrw`(실행일 환율 × 마진)를 명령에 확정 저장하고, 실행 중에는 모델 단가로 추정한 금액을 `usage` 이벤트의 `krwEstimate` 로 보낸다
- 진행 중에는 `progress` 이벤트로 "파일 작성 중: index.html" 같은 친화적 메시지가 한 줄씩 갱신되어 표시된다(목록이 아닌 최신 한 줄)
- 실행 중에는 진행 표시기 옆에 이번 회차 토큰이 실시간으로 올라간다(스트림의 assistant 메시지 usage 를 메시지 ID 로 중복 제거해 합산, 사고 토큰은 추정치 포함). 완료 시 결과 이벤트의 확정 값으로 교체된다
## 제작 방식 선택: AI에게 맡기기 / 내가 고르기
새 사이트를 만들 때 두 방식 중 하나를 고른다. 두 방식 모두 참고 사이트(URL + 참고·반영할 점)를 최대 10개까지 입력할 수 있고, 기획이 확정되기 전에는 페이지를 생성하지 않는다. 확정된 **Site Brief(JSON)** 가 생성 에이전트의 유일한 입력이다.
| | AI에게 맡기기 | 내가 고르기 |
|---|---|---|
| 입력 | 자유 서술 + 첨부(소개서·로고) + 기존 사이트 URL | 상호명, 업종 대분류→소분류, 지역 시·도→시·군·구 (업종 기본값 사전 선택, 단계 건너뛰기 가능) |
| 기획 | AI 이해 확인 카드(사업·고객·목표·기능·추천 스타일·사실정보, 사용자 명시/AI 추론 구분, 규제 업종 플래그) → 구조 제안(페이지 트리·섹션·목표/기능 연결, 정합성 경고, [이 구조로 진행]/[페이지 수정]) → 디자인 3안(추천안 기본 선택) | 목적(목표→기능 자동 체크, 기능 on/off, 규제 업종 제한 자동 비활성) → 고객(업종별 칩·연령대) → 페이지(추천 체크, 드래그 순서, 섹션 선택) → 디자인(톤 5종→팔레트 3안→폰트 3안→밀도→이미지 유형) → 문구(어조·길이·언어) → 사실정보 전용 폼([지금 입력]/[나중에]) → 자료(로고·사진 또는 스톡) → 요약(항목 클릭 시 해당 단계로) |
| 미리보기 | 구조/디자인 단계에서 와이어프레임 | 선택 즉시 우측 와이어프레임 반영 |
| 생성 | 골격→콘텐츠→스타일 순. 사실정보는 Brief 값만 사용. 항목별 처리: 내용유지(기본, 입력·수정한 내용 반영) / 빈항목유지(값 없이 "[입력 필요]" 자리만) / 항목삭제(페이지에서 제외). 가짜 후기 금지, 창작은 문구·헤드라인만 | 동일 |
| 검증 | 결정적 규칙(페이지 파일, "[입력 필요]" 수, Brief 외 전화·주소·가격 패턴, 규제 표현) + AI 대조(CTA·기능·페이지·스타일·어조) 리포트, [검증 결과 자동 수정] | 동일 |
| 수정 | 채팅 지시 → Brief 선갱신 → 해당 부분만 재생성 | 채팅 또는 [선택값 수정] → Brief 갱신 → 재생성 |
| 발행 | "[입력 필요]"·누락 페이지·규제 표현이 남아 있으면 보류(경고 후 강제 가능). 도메인·결제·광고는 발행 후에만 표시 | 동일 |
관련 코드: `lib/brief.js`(Brief 스키마, 업종·지역·디자인 카탈로그, 규제 규칙, 정합성 검사, 결과 검증), `lib/planner.js`(AI 기획 단계 프롬프트/JSON), `public/wizard.js`(위저드 UI·와이어프레임). 기획 단계의 AI 호출도 명령 이력에 기록되어 토큰·비용이 표시된다.
모든 기획 단계 화면 아래에는 항상 "다음 단계", "끝까지 자동 진행(추천안으로 생성까지)", "현재 설정으로 바로 생성/재생성" 버튼이 있어, 확정하지 않은 항목이나 점검 항목이 남아 있어도 언제든 생성할 수 있다(`POST plan/generate {force:true}` 는 빠진 항목을 기본값으로 채운다). 입력 칸에 포커스가 있는 동안에는 화면을 다시 그리지 않고, 편집 중에는 서버의 plan 이벤트가 로컬 편집을 덮어쓰지 않는다.
- 생성 옵션(`optimize`: {seo, aeo, geo}) → 생성·검증 후 `draftOptimize`(AI, 기본 등급, `runPlanStep('seo-draft')`) → `site.optimize.draft` → 관리 탭에서 검토 → `POST /api/sites/:id/optimize/apply` → `site.optimize.final` + `seo.materialize`.
-`materialize(site, siteDir, baseUrl)`: 페이지 `<head>` 의 `<!-- wm-seo:start -->…<!-- wm-seo:end -->` 구역과 `<!-- wm-aeo-faq:start -->…` FAQ 영역, sitemap.xml·robots.txt·llms.txt·IndexNow 키 파일을 멱등으로 쓴다(사용자가 만든 파일은 해시로 구분해 덮어쓰지 않음). 배포 직전마다 배포 주소로 다시 쓴다(`deployBaseFor`).
- 검색사이트 등록: `site.searchReg` {google, naver, indexnow}. 단계(`seo.steps`)는 자동/직접으로 나뉘고, 직접 단계는 관리자 콘솔 › 검색 등록 탭에서도 진행. 배포가 끝나고 작업 중이 아닌 사이트만(`regAvailability`). IndexNow 는 `api.indexnow.org`(시험 시 `INDEXNOW_ENDPOINT`).
- v1.3.10 구글 자동 등록: 연동 키 `google-search`(서비스 계정 키 JSON 전체, 설정 `OWNER_EMAIL`)가 있으면 `runGoogleAuto` 가 `lib/google.js`(RS256 JWT → OAuth 토큰, Site Verification API 토큰·확인, Search Console API 속성 추가·사이트맵 제출)로 끝까지 진행하고 상태를 `site.searchReg.google.auto` 에 남긴다. 네이버는 코드 저장 시 태그·재배포·운영 확인 자동(`applyVerifyTag`), 사이트맵 제출은 선택. 검색 등록을 시작한 사이트는 배포 후 IndexNow 자동 요청(`autoIndexNow`). 시험용 `GOOGLE_API_BASE`, `GOOGLE_TOKEN_URL`.
- 관리자는 `/builder`(관리자만, 빌더 화면)에서 사이트를 만들고 관리 탭 "템플릿으로 등록" 또는 관리자 콘솔 › 템플릿에서 관리자 소유 사이트를 등록한다. 저장 `data/templates/index.json` + `data/templates/<id>/files/`(사이트별 정보 — 통계 스크립트·SEO 관리 구역·sitemap/robots/llms·IndexNow 키·웹메일 — 는 빼고 복사).
- 디렉터리: 기본 `ind:<업종 대분류>/<세부 업종>`(사이트 만들기 업종 목록), 관리자가 만드는 별도 디렉터리 `c:<id>`(하위 가능). 템플릿 항목: 제목·설명·해시태그·디렉터리·`enabled`(사용자 사용 허용).
- 사용자: 생성 탭 "템플릿에서 시작" → `GET /api/templates?q=#태그 낱말&dir=` (허용된 것만) → 미리보기 `GET /api/templates/:id/preview/` → `POST /api/sites/template`(파일 복사, 사이트 ID 치환, 업종 설정, 모드 `template`).
## 다국어 사이트 — v1.3.10 (v1.3.16 원칙·현지 형식·품질 게이트)
**URL 원칙 (v1.3.16 고정):** 다국어 사이트의 주소는 로케일 코드로 구분한다 — `/<로케일 코드>/<페이지>`. 로케일 코드는 BCP 47 언어 태그(ISO 639-1 언어 코드, 필요할 때만 지역: `zh-CN`, `zh-TW`, `en-GB`, `pt-PT`, `es-MX`)이고 기본 언어도 폴더를 쓴다(`/ko/`). 사이트 맨 위(`/`)는 언어 선택 페이지. **x-default** 는 사이트마다 정한다(`site.langs.xDefault` — 언어 코드 또는 `root`=언어 선택 페이지, 기본 = 기본 언어): hreflang `x-default`, 언어 선택 페이지의 자동 이동, sitemap 대체 주소에 같은 값을 쓴다. 생성 화면의 사이트 언어 옵션과 관리 탭 다국어 카드에서 고른다.
- v1.3.16 **로케일별 메타·구조화 데이터**: 언어 폴더마다 title·description·canonical·og:locale(+alternate)·og:url·hreflang, 사업체(LocalBusiness 등) JSON-LD(같은 `@id`, `knowsLanguage`, `currenciesAccepted`, 그 언어 값 — 없으면 기본 언어 값으로 대체하고 현황에 표시), sitemap 언어별 주소+`xhtml:link`. 검색 값을 반영하지 않은 언어도 canonical·og:locale 은 항상 넣는다. 현황 표: `GET /api/sites/:id/langs/quality` 의 `matrix`(`seo.localeMatrix`).
- v1.3.16 **현지 형식**(`multilang.renderFormats`): `data-wm-date`, `data-wm-datetime`(사업장 시간대 + 시간대 이름), `data-wm-number`, `data-wm-price`(+`data-currency`, 기본 사업장 통화), `data-wm-tel`(같은 나라면 국내식, 아니면 국제식, `tel:` 은 국제 번호), `data-wm-address`(JSON 부분 값 → 동아시아는 큰 단위부터, 그 외는 도로명부터), `data-wm-name`(성·이름 순서), `data-wm-legal`(copyright·privacy·terms·cookie-notice·consent·business-info → 시스템 문구), RTL 보정(`data-wm-mirror`). 사업장 현지 설정 `site.locale = { timeZone, currency, country }` (`PUT /api/sites/:id/langs/settings`).
- v1.3.16 **번역 품질 게이트**(`multilang.quality`): 번역 누락률 ≤ 1%(시스템 문구 누락 0), 원문 잔존율 ≤ 2%, 페이지별 사이트 안 링크가 기본 언어와 같은 짝·페이지 누락 없음, 언어별 검수 완료(`site.langs.review[언어]` — 페이지 내용이 바뀌면 `pending`). 검증 결과(`plan.verification.i18n`)·검증 리포트·배포 로그 경고·관리 탭 카드(검수 완료/수정 필요 버튼, `POST /api/sites/:id/langs/review`).
`lib/multilang.js`, 관리 탭 다국어 카드(`public/optimize.js` LangUI).
-`site.langs = { default, list }`(생성 옵션 `langs` 또는 `PUT /api/sites/:id/langs`). 주소는 `/<언어>/…`, 공용 파일 `assets/`, 맨 위 `index.html` 은 언어 선택 페이지(`data-wm-lang-root`).
- AI 는 기본 언어 폴더만 만든다(`promptRules`). 생성·수정이 끝나면 `syncLanguages` → `multilang.sync`: 기본 언어 HTML 의 글자만 번역해 다른 언어 폴더에 같은 구조로 쓴다(번역 저장소 `data/site-i18n/<사이트ID>/<언어>.json`, 바뀐 문장만 번역, 작업 기록 `site-translate`). `<html lang dir data-wm-lang>`, 언어 선택 `<nav data-wm-langs>`, `data-wm-date`/`data-wm-number` 언어별 형식, 위젯 번역표 `<script type="application/json" data-wm-i18n>`(public/wm-modules.js `L()`, 문의 위젯).
- 언어별 SEO/AEO/GEO: 기본 언어 초안(`site.optimize.draft`) + 언어별 현지화 초안(`langDrafts`) → 언어 탭 검토 → `final` + `langFinals`. `seo.materialize` 가 언어 폴더마다 og:locale·hreflang(+x-default)·FAQ 제목, sitemap `xhtml:link` 대체 주소를 쓴다.
- 한 언어 사이트 → 다국어: AI 수정 1회로 기본 언어 폴더로 옮기고 `cleanupAfterConvert` 로 옛 파일 정리. 가져온 사이트(원본 서버 반영)는 지원하지 않음.
## 생성 사이트 시스템 문구 — v1.3.16
`lib/sitemsg.js`, `locales/site/ko.json`(원문 172개, id·묶음), `locales/site/<언어>.json`(씨앗 번역 — 영어 전부, 그 밖은 일부), `scripts/site-msg-catalog.py`(원본 목록 → JSON).
- 생성 사이트에 서비스가 넣는 글자(모듈 위젯·폼 라벨·내비게이션 검색·오류·동의문·쿠키·법적 고지·예약·문의 안내·소유자 알림 메일)는 코드·템플릿에 쓰지 않고 id 로만 쓴다: 템플릿 `{{t:id}}`·`{{msgs:id,…}}`, 런타임 `L('id', {값})`(서비스가 `/wm-modules.js` 를 보낼 때 원문 표를 넣음), API 오류 `sitemsg.err('err.…')` → 응답은 요청 언어(`X-WM-Lang` = 페이지 `<html lang>`, 폼 `lang`), 소유자 메일은 사이트 관리 언어(`site.adminLang` → 소유자 화면 언어).
- 번역 찾는 순서: 씨앗 → `data/site-i18n/_runtime/<언어>.json`(다국어 사이트가 그 언어를 처음 쓸 때 AI 가 채움, 서비스 공용) → `<언어>.override.json`(관리자 콘솔 › 언어 › 생성 사이트 시스템 문구에서 고친 값). 다른 언어 페이지에는 그 언어 표를 `data-wm-i18n` 로 넣고, 관리자가 정한 모듈 설정 값(문의 유형·분류)은 `v:값` 키로 함께 번역한다.
## 화면 언어 기본값 (서비스·사이트 관리자) — v1.3.16
- 관리자 콘솔 › 언어 ›**서비스 화면 기본 언어**: 기본 언어를 정하고 "모든 사용자에게 지금 적용(강제)" 으로 적용하면 `settings.uiLang.forcedAt` 이 남는다. 화면 언어는 서버가 정해 `<meta name="wm-ui-lang">` 로 알린다(`effectiveUiLang`): 강제 시각 이후 사용자가 고른 언어(`users[].uiLang/uiLangAt`, `POST /api/me/ui-lang`) → 강제 기본 언어 → 브라우저 쿠키 → 사이트 관리 언어(사이트 관리자 화면) → 서비스 기본 언어. 표시 중인 언어만.
- 사이트 관리자 화면(`/site-admin/<id>/`)에 언어 선택, 기본값은 사이트 관리 언어.
## 도메인 · 계정 종류 — v1.4.0
> 운영 준비(계정·DNS·설정 값)는 [docs/DOMAIN-SETUP.md](docs/DOMAIN-SETUP.md) 를 따른다.
`lib/openprovider.js`(Openprovider API v1beta 클라이언트), `lib/domains.js`, 빌더 `public/domains-ui.js`(도메인 탭), 데이터 `data/domains.json`, 설정 `settings.domains`(관리자 콘솔 › 설정 › 도메인 서비스).
- 접속 정보: 관리자 콘솔 › 설정 › 외부 연동 접속 정보 › 종류 **Openprovider** — 주소(기본 `https://api.openprovider.eu/v1beta`), 설정 `USERNAME=…`(선택 `NS_GROUP=dns-openprovider`), 키 = 비밀번호. 시험용 `OPENPROVIDER_API_BASE`.
- 계정 종류 `users[].accountType`: `subdomain`(서브 도메인 전용) / `domain`(자체 도메인). 관리자 콘솔 › 회원·청구에서 회원마다 바꾸고, 새 가입자 기본값은 `settings.domains.defaultAccountType`(기본 subdomain). 값이 없는 1.4.0 이전 회원·관리자는 domain.
- 서브 도메인: `<이름>.<플랫폼 도메인>`(플랫폼 도메인 기본값 = 서비스 주소에서 첫 이름을 뺀 것, webmaker.ibit.kr → ibit.kr). 플랫폼 DNS `manual`(운영자가 `*.ibit.kr` 와일드카드 A 레코드를 한 번 넣음) / `openprovider`(이름별 A·메일 레코드 자동). 메일 `admin@<이름>.ibit.kr`, 웹메일 `https://<이름>.ibit.kr/mail`(또는 `/webmail`).
- 자체 도메인: 검색(`settings.domains.tlds`) → 사용자 가격 = 리셀러 가격 × `marginPct`(기본 250) ÷ 100 을 그날 환율로 원화(100원 올림) → 확인 창 → 구매(화면 금액 `expectedKrw` 보다 비싸지면 409 + 새 견적, 잔액 부족 402) → 캐시 차감 → DNS 영역(@·www A). 갱신도 같은 순서. 레코드 A·AAAA·CNAME·MX·TXT·CAA·SRV 추가·삭제.
- 메일(MX) 자동 설정: `mailsys.dnsRecords`(받기 MX·SPF·DMARC + Resend DKIM·반송) 를 그 영역에 추가하고, `settings.domains.userMail` 이면 사이트 메일 시스템을 그 도메인으로 켠다.
- DNS 전파: 바꾼 레코드를 공용 DNS(1.1.1.1·8.8.8.8)로 확인, 반영 전에는 도메인 탭·발행(409 `dnsPending`)·웹메일(보내기 막음)에 "DNS 연결이 진행 중입니다 … 약 N분 후에 다시 이용해 주세요"(위임 240분, 레코드 20분 기준).
## 처음 설치 · 기본 관리자 · 설치 점검표 — v1.5.11
- **설치는 한 줄, 설정은 웹에서**: `deploy/remote-install.sh <ssh-host>` (FQDN 생략 → 서버 IP 로 처음 설치, DB 복사 없음). 끝나면 `http://<서버 IP>:8795/?token=…` 를 알려 주고, 서비스 첫 화면도 설치 점검표가 끝나기 전에는 `/setup` 으로 넘어간다. 절차는 [docs/FIRST-INSTALL.md](docs/FIRST-INSTALL.md).
- **최초 관리자**(`lib/auth.js`, v1.6.31): 고정 비밀번호 `admin / admin` 은 더 이상 만들지 않는다. 사용자가 없으면 기동 로그에 안내만 남기고, 설치 스크립트가 알려 주는 접속 토큰으로 설정 페이지에 들어가 "관리자 계정" 카드에서 새 관리자를 만들거나 "admin 을 1회용 비밀번호로 만들기"(무작위 비밀번호 1회 표시, 첫 로그인 때 변경 강제 `mustChangePassword`) 를 쓴다. `node scripts/admin.js bootstrap` 도 같은 1회용 비밀번호를 출력한다. 쓸 수 있는 관리자가 생기면 토큰 로그인은 거절된다.
- **설치 점검표**(`lib/setupstate.js`): 관리자 비밀번호 · AI 엔진 인증(Claude 서버 로그인 `claude auth status`/OAuth 토큰/API 키 또는 OpenAI) · 서비스 공개 주소. 하나라도 안 되면 `/`·`/login` → `/setup`(끄기 `service.setupRedirect=off`, 우회 `/login?direct=1`). `/setup` 은 로그인 없이 열리고 안에서 접속 토큰 또는 관리자 계정으로 들어온다(`lib/setupauth.js`, API `/api/setup/*`).
- 설정 페이지 상단 카드: 설치 점검표 · 관리자 계정(만들기·재설정·새 관리자) · AI 엔진 인증(Claude/OpenAI 저장·확인). 설정 페이지 포트 기본 **8795**, 바인드 **0.0.0.0**, 겹치면 8796… 자동 기록. 회원가입은 `service.allowSignup` 으로 닫는다.
## 설정 파일 · 초기 설정 페이지 · 연동 모듈 — v1.5.7
- **설정 파일** `data/config.json`(`lib/config.js`): 서비스 공개 주소, AI 실행 파일·턴 상한, 소스 보관(Gitea) 주소·계정, 메일 연동(Arimail) 주소·도메인·관리자, SSL·복사 한도·외부 API 주소. 우선순위 설정 파일 > 환경 변수 > 기본값, 배포 때 정해지는 포트·경로는 읽기 전용. 비밀값은 암호화 보관소. 1.5.7 첫 기동 때 유닛의 환경 변수를 자동으로 옮겨 기존 운영 값이 보존된다. [docs/CONFIG.md](docs/CONFIG.md)
- **초기 설정 페이지** `setup-server.js`(systemd `webmaker-setup`, 기본 포트 3081, 토큰 `data/setup.token` 또는 관리자 로그인) — 설치가 끝나면 접속 주소를 알려 주고, 같은 화면이 `/setup` 과 관리자 콘솔 › 시스템 설정 탭에도 있다. 상태 카드(연결 확인)·그룹별 저장·서비스 재시작·연동 서비스 설치 명령 안내.
- **연동 모듈** `lib/connectors/` — 종류마다 모듈 하나: `gitea.js`(소스 보관: 주소·링크·오류 해석·연결 확인·API 로 저장소 만들기/공개키 등록), `arimail.js`(메일 서버: 관리자 API 로 사이트 관리자 계정의 메일함 생성·삭제, 웹메일 연결, 메일 프로그램 설정값). 다른 제품으로 바꾸려면 같은 모양의 모듈을 더하고 설정 파일의 provider 를 바꾼다. [docs/ARIMAIL.md](docs/ARIMAIL.md)
- **설치 스크립트** `deploy/install-services.sh`(Gitea·Arimail 한 번에, `--check` 로 설치된 것 등록), `deploy/install-arimail.sh`(Arimail 표준 절차 자동화 + 웹메이커 등록), `deploy/install-gitea.sh`(설정 파일 등록·API 토큰). `install.sh`·`remote-install.sh`·`remote-update.sh` 가 부른다.
## 일정관리 모듈 · 만세력 — v1.4.18
-`calendar`: 일정(`events`: 제목·설명·장소·분류·색·단위 time/day/week·양력/음력 시작일·시각·기간·반복·공개). 설정: 기본 보기(월간/주간/일간), 음력·간지·24절기 표시, 주 시작 요일, 주간·일간 시간 범위, 분류. 공개 API `GET /api/m/<사이트>/calendar/range?from&to[&cat]`(최대 70일, 날짜별 음력·간지·절기 + 발생 일정), `…/settings`, `…/day?date`, `…/lunar?year&month&day&leap`. 위젯 `modules/calendar/widget.html`(`data-view`·`data-lunar`·`data-ganji`·`data-category`).
-`blog`: 글(`blogPosts`: 제목·요약·본문·대표 이미지·분류·태그·글쓴이·공개)과 댓글(`blogComments`). 설정: 한 페이지 글 수, 분류, 댓글(all/member/none), 승인 후 공개, 글쓴이 표시. 공개 API `GET /api/m/<사이트>/blog/posts?q=&cat=&tag=&page=`, `GET …/blog/posts/:id`(조회 +1), `POST …/blog/posts/:id/comments`. 위젯 `modules/blog/widget.html`(글 보기 `#post-<id>`).
-`library`: 자료(`libraryFiles`). 설정: 내려받기(all/member), 분류, 파일 크기 상한(MB, 기본 100), 내려받은 횟수 표시. 파일은 `data/modules/<사이트>/library/` 에 무작위 이름으로 보관. 공개 API `GET /api/m/<사이트>/library/files`, `GET …/library/files/:id/download`(회원 전용이면 토큰 `?t=`·헤더), 관리 `POST /api/site-admin/<사이트>/library`(multipart `files[]`, `category`).
## 보안 기본 — v1.3.17
- 세션 쿠키 `wh_session` = base64url(JSON {v:2, u, e, g}) + HMAC. `g` 는 사용자 `sessionGen` — 비밀번호 변경·재설정 때 증가해 그 전 세션이 무효(`lib/auth.js`).
- 비밀번호 규칙 `lib/passpolicy.js` (`settings.passwordPolicy`, 관리자 콘솔 › 설정, 기본 6자 이상). 화면 안내는 `public/pwrule.js` 가 `GET /api/auth/password-policy` 로 채운다.
- 바깥 주소 가져오기는 `lib/netguard.js safeFetch`(단계별 공개 주소 확인·IP 고정·리다이렉트 직접 처리·본문 상한)만 쓴다.
- 빌더 미리보기 iframe 은 `sandbox`(allow-same-origin 없음). 서비스 주소의 생성 사이트 페이지가 Referer 인 요청에는 빌더 세션을 쓰지 않는다(`fromTenantPage`). 완전한 분리는 생성 사이트 전용 도메인.
## 외부 연동 접속 정보 (암호화 보관소) — v1.3.1
`lib/vault.js`. 다른 서비스에 접속하는 주소·설정·API 키/토큰은 이곳에만 둔다(관리자 콘솔 › 설정 › 외부 연동 접속 정보).
- 파일: `data/credentials.json`(0600). 키는 AES-256-GCM(`session.secret` 파생, 또는 `VAULT_KEY`). 응답에는 hint 만.
- 종류: `resend`(메일 발송), `mail-gateway`, `anthropic-api-key`, `anthropic-oauth`, `google-search`, `openprovider`(v1.4.0 도메인), `http`(기타, 식별자 지정). 키는 언제든 다시 넣어 바꿀 수 있고(빈 칸 저장 = 유지), 화면·응답에는 앞뒤 몇 글자 hint 만 나온다.
- 키 파일 가져오기: `POST /api/admin/credentials/import {kind, file}` — 서버가 한 번 읽어 암호화 보관, 찾은 항목 이름만 응답.
- 서버 코드에서 쓰기: `vault.resolve(id)` → `{url, settings, secret}`. 새 연동을 만들 때 키를 settings.json·평문 파일에 두지 말고 여기에 종류를 추가한다.
- 기동 시 `vault.migrate()` 가 1.3.0 이전 settings.json 의 메일 발송 키·키 파일, Claude 키·토큰을 옮긴다.
## 사이트 메일 시스템 (웹메일) — v1.3.0
`lib/mailsys.js`, `public/webmail.html|js`, 사이트 관리자 페이지 "메일" 탭.
- 운영자(v1.3.2): 관리자 콘솔 › 메일 시스템 탭에서 **사이트마다** 켠다(`site.webmail`: enabled, domain, mxHost, spfInclude, dmarc, configuredAt/By). 전역 `settings.mailSystem` 은 기본값(mxHost·spfInclude·dmarc)만. 켜지 않은 사이트는 사이트 관리자 페이지에 메일 탭이 없고 웹메일 로그인도 막힌다.
- 사이트 관리자: 메일 탭에서 상태·DNS 안내를 보고 계정별 메일 사용과 DNS 다시 확인만 한다(켜기·끄기는 운영자). DNS 안내: MX `@ → mxHost`(10), TXT `@ → v=spf1 mx [include:…] ~all`, TXT `_dmarc → DMARC 값`. 확인은 실제 DNS 조회(MX 에 mxHost 포함, SPF 에 mx/a:mxHost/include 포함), 자동 재확인 10분 주기 검사(미완료 30분·완료 6시간 간격).
- 상태(`mailsys.status`, v1.3.2): off(사용 안 함) → setting-up("메일시스템 설정중": DNS 전파·보내기 인증 대기) → send-only("사용 가능 (보내기)") → ready("사용 가능"). 빌더 관리 탭·사이트 관리자 메일 탭·웹메일에 같은 표시.
- 계정: 사이트 관리자 계정(회원 role=admin, loginId)마다 `mail` 표시. 웹메일 로그인은 같은 계정·비밀번호(사이트 관리자 토큰). 메일 사용 끄기는 "삭제" 입력 + 작업 중인 관리자 비밀번호 재확인 후 메일함(`data/mail/<site>/<member>.json`)과 백엔드 메일함 삭제.
- 접속: 사이트 폴더 `webmail/index.html` 진입 파일(웹메일 켜짐 동안 유지, 배포 시 동기화) → `<서비스주소>/webmail/<사이트ID>/`.
- 백엔드: `backendFor()` 가 송수신 어댑터. 보내기는 메일 발송 키(Resend): 웹메일을 켜면 도메인을 Resend 에 등록(`ensureResendDomain`)하고 그 레코드를 DNS 안내에 추가, verified 가 되면 발송(`sendAs`, 보낸 편지함 사본). 받기는 미연결 — `OZ-Group/mail-gateway` 연동 시 createMailbox/deleteMailbox/send 와 메일함 채우기를 구현한다. 연동용 배포 키: 이 서버 `~/.ssh/id_ed25519_gitea_mailgw.pub` (ssh 별칭 `source-mailgw`).
## 미리보기의 루트 기준 주소 — v1.3.3
미리보기·같은 서버 배포는 `/<사이트ID>/` 하위 경로에서 열린다. 운영 사이트의 루트 기준 주소(`/ko/`, `/assets/…`, `location.replace("/")`)는 서비스 루트로 오는데, 요청의 Referer 가 같은 서비스의 `/<사이트ID>/…` 이면 서비스가 `/<사이트ID><경로>` 로 302 이동시킨다(`server.js previewRedirect`). `/api/`, `/wm-*.js`, `/webmail/`, `/site-admin/`, `/theme.css`, `/i18n.js` 는 서비스 경로라 이동하지 않는다. 원본 파일은 바꾸지 않는다.
## 사이트 복사하기 — v1.2.0 (v1.3.4 설명 보강)
- v1.4.1 복사 범위: 시작 주소가 폴더 아래(`https://호스트/<폴더>/`)이면 그 폴더 안 페이지만 복사하고 시작 페이지를 복사본 `index.html` 로 둔다(범위는 첫 페이지의 최종 주소로 결정). 범위 밖 페이지 링크는 절대 주소로, 범위 밖 자원은 `_root/` 로. 이 서비스(PUBLIC_BASE_URL 호스트)의 `/wm-*.js`·`/api/` 는 복사하지 않는다.
생성 탭 "2. 기존 사이트 모양으로 시작" › **사이트 복사하기**. 기존 사이트의 모양은 최대한 유지하면서 새로운 사이트를 만들 때 쓰는 기능으로, 기본 HTML 페이지(와 CSS·JS·이미지·글꼴)만 복사하고 로그인·게시판·신청·결제 같은 서버 기능과 데이터는 복사하지 않는다. 웹 주소만 입력하면 서버가 브라우저처럼 접속해 화면 코드를 복사한다(`lib/crawler.js`).
- 같은 도메인(www 유무 무시)의 페이지를 따라가며 HTML(최대 300개)과 CSS·JS·이미지·글꼴·PDF 등(최대 5,000개)을 내려받는다. 다른 도메인(CDN 등) 자원은 원래 주소를 그대로 쓴다.
- 저장할 때 `href`·`src`·`srcset`·`poster`·`data-src`·`url()`·`@import` 의 같은 사이트 주소를 상대 경로로 바꾸고 `<base>` 를 지운다. 쿼리가 붙은 주소는 이름에 해시를 붙이고, HTML 로 받은 서버 실행 페이지는 `이름.php.html` 처럼 `.html` 로 저장한다.
- 내부(사설·루프백·링크로컬) 주소는 관리자만 복사할 수 있다(일반 사용자 요청은 DNS 확인 후 거부, 복사 중에도 30초마다 재확인). 리다이렉트는 같은 사이트 안에서만 최대 5번 직접 따라간다. 크기는 아래 "가져오기 크기 기준" 을 따른다.
- 결과는 "생성 완료" 상태의 일반 사이트(`mode: copy`)로, 수정 지시·발행·배포를 그대로 쓴다. 서버 프로그램(문의 저장·로그인·결제)은 복사되지 않는다.
가져오기·복사가 끝나면 자동으로, 또는 관리 탭 "사이트 현황 분석 › 다시 분석" 으로 실행한다(`POST /api/sites/:id/analyze`). 파일 통계(개수·크기·종류·페이지·최근 수정·감지된 기술: PHP, WordPress, 그누보드, Laravel 등)와 웹서버 정보(가져오기: 웹서버 종류·도메인·설정 파일·경로, 복사: 페이지·파일 수)를 모으고, 기본 등급 AI 가 구성·사용 기술·수정 난이도·문제점·개선 제안을 JSON 으로 요약한다(캐시 차감, 작업 내역 "사이트 분석"). 결과는 `site.analysis`.
## 가져오기 크기 기준 — v1.2.0
고정 제한(300MB·4,000개)은 없앴다. 한 사이트가 서비스 서버의 사이트 저장 디스크(`SITES_ROOT`) 전체 용량의 10% — 남은 공간이 적으면 남은 공간의 90% — 를 넘으면 "서버 한계 초과" 로 중지한다(`remote.capacity()`/`capacityError()`, HTTP 413).
## 기존 사이트 가져오기 (사이트 관리하기) — v1.3.4 에서 새로 시작하기 종료
v1.3.4 부터 생성 탭에서 삭제되었고 새로 시작하는 API 는 410 을 돌려준다. 아래는 이미 가져온 사이트의 동작(관리 탭 원본 반영·접속 정보 변경·다시 가져오기는 계속 동작)이다.
생성 탭 "2. 기존 사이트 관리" ›**사이트 관리하기**(1.1.x 의 "기존 사이트 가져오기"). 이미 운영 중인 사이트의 주소와 서버 접속 정보를 입력하면 파일을 가져와 같은 편집기(수정 지시·소스 편집기·버전 되돌리기)로 고친 뒤, **원본 서버의 같은 경로에 되돌려 반영**한다. 배포 서버(관리자 등록)와는 무관하며 vhost/SSL 을 건드리지 않는다.
- 접근 방식은 두 가지만 지원: **ID/비밀번호**(SSH·SFTP 22 또는 FTP 21) / **SSH 키**(서비스가 키를 생성해 공개키를 서버 `~/.ssh/authorized_keys` 에 등록하거나, 암호 없는 개인키 붙여넣기).
- 연결 확인 → 사이트 찾기(v1.2.0): SSH 셸이 있으면 실행 중인 웹서버(nginx·Apache·Caddy·LiteSpeed 등)를 확인하고, 접속 계정이 읽을 수 있는 설정 파일(`/etc/nginx/…`, `/etc/apache2/sites-enabled/*`, `/etc/httpd/…`)의 `server` 블록·`VirtualHost` 에서 도메인과 문서 루트를 찾는다(`remote.discoverSites`, sudo 는 쓰지 않음). 같은 문서 루트는 합치고 프록시 전용 사이트는 제외. 흔한 경로도 함께 훑어 첫 페이지 파일을 실제 사이트 첫 화면과 대조(일치/제목 일치/다름)한다. 서비스 중인 사이트가 하나면 자동 선택, 여러 개면 목록에서 고른다(입력한 주소의 도메인과 일치하는 사이트를 미리 선택). 고른 사이트의 도메인·웹서버·설정 파일은 `source.vhost`·`source.webServers` 로 남는다. 경로를 직접 입력하면 그 경로만 확인한다.
- 가져오기: 셸이 있는 SSH 는 `tar` 스트림, FTP·셸 없는 SFTP 계정은 `curl`(ftp://, sftp://) 파일 단위 전송. `.git`, `node_modules`, 캐시 폴더 제외, 크기는 위 "가져오기 크기 기준". 가져온 원본은 버전 #1(가져옴)로 기록된다.
- 수정: 프롬프트에 "운영 중인 기존 사이트" 규칙(구조·경로 유지, 지시된 부분만 최소 수정, 서버 스크립트는 건드리지 않음)이 붙는다. PHP 등이 있으면 미리보기는 정적 파일만 보여 준다.
- 반영(배포): 변경된 파일만 올리고, 가져온 뒤 삭제한 파일은 서버에서도 지운다. 서버에만 있는(가져오지 않은) 파일은 건드리지 않는다. SSH 방식은 반영 전에 서버 `~/.webmaker-backups/` 에 tgz 백업(최근 3개)을 만든다. 반영 후 사이트 주소를 열어 첫 화면이 반영본과 같은지 확인하고, 이전 버전은 정리되어 배포본 하나만 남는다(다른 사이트와 동일).
- 접속 정보: 비밀번호는 AES-256-GCM 으로 암호화(`data/session.secret` 유래 키), SSH 키는 `data/site-keys/<id>` 에 0600 으로 보관. 비밀번호는 명령행에 올리지 않는다(ssh 는 `hooks/askpass.sh` + `SSH_ASKPASS_REQUIRE=force`, curl 은 표준입력 설정). 사이트 카드의 "접속 정보 변경"으로 계정·비밀번호·경로·주소를 바꿀 수 있다(연결 확인 후 저장).
- 관리자 콘솔의 사이트 목록에는 도메인 대신 `원본: user@host:경로` 가 표시되고, 배포 버튼은 원본 서버 반영으로 동작한다. 사이트 삭제 시 키 파일도 지운다.
사이트를 연 뒤 "홈페이지 기본 사항 보기 / 수정"을 펼치면 생성 때 입력한 제목·목적·주요 컨텐츠·디자인 방향·기타 요구사항이 보이고 바로 고쳐 저장할 수 있다. 수정 지시 프롬프트에는 항상 현재 기본 사항이 참고로 포함되며, 저장 후 아직 지시를 보내지 않은 변경 항목은 "최근 변경됨"으로 표시되어 AI 가 페이지 전반에 반영하도록 요청한다.
## 첨부 파일
이미지·사진·텍스트 문서를 첨부하면 사이트의 `uploads/` 폴더에 저장되고, 프롬프트에 파일 목록과 활용 지침이 포함된다.
- 이미지(png, jpg, jpeg, gif, webp, svg, bmp, avif): AI 가 Read 로 내용을 본 뒤 `<img src="uploads/파일명">` 로 페이지에 삽입
- 문서(txt, md, csv, tsv, json, html, pdf, docx, xml, yaml, rtf): AI 가 Read 로 읽어 본문에 반영. docx 는 `python3-docx` 로 텍스트 추출본(`*.docx.txt`)을 함께 생성
- 파일당 최대 25MB, 요청당 최대 20개. 파일명은 경로 문자를 제거하고 중복 시 `-2`, `-3` 을 붙인다
- 첨부는 생성 폼, 수정 지시 폼, "파일만 먼저 올려두기" 세 곳의 드롭존에 끌어다 놓거나 클릭해서 선택한다. 화면 어디에 놓아도 되며, 사이트가 열려 있으면 즉시 업로드되고 아니면 생성 폼의 첨부 목록에 쌓인다. 아직 명령에 쓰이지 않은 파일은 다음 지시 프롬프트에 "새로 첨부된 파일"로 전달된다
## 소스 편집기
우측 툴바의 **소스 보기** 를 누르면 우측 화면이 위(미리보기)/아래(소스) 로 분할된다(구분선 드래그로 크기 조절, 설정은 브라우저에 기억).
- 파일 선택 상자에서 사이트의 텍스트 파일(html, css, js, json, md, svg 등)을 골라 편집
- "편집 즉시 미리보기 반영" 이 켜져 있으면 HTML 편집 내용이 저장 없이 미리보기에 바로 반영된다(`srcdoc` + `<base>`). css/js 는 저장 후 새로고침으로 반영
- **파일에 저장(Ctrl+S)** 으로 실제 파일(`/var/www/sites/<ID>/...`)에 기록되고, 이후 AI 수정 지시도 저장된 내용을 기준으로 이어진다
- AI 작업 실행 중에는 저장이 거부(409)되고, AI 가 파일을 바꾼 뒤에는 편집기가 자동으로 최신 내용을 다시 읽는다(미저장 변경이 있으면 경고만 표시)
| GET | `/api/sites/:id/events` | SSE 진행 이벤트 (`status`, `usage`(실시간 토큰), `plan`, `done`, `log`) |
| POST | `/api/sites/:id/cancel` | 실행 중인 AI 작업 중단. 명령은 `cancelled` 로 기록되고 토큰은 스트림 추정치(`usageEstimated`)로 남는다. 생성 중 중단 시 기획 단계는 다시 생성 가능한 상태로 복귀 |
## 다른 서버에 서비스 설치 (예: bear → webmaker.ibit.kr)
`deploy/remote-install.sh <ssh호스트> <FQDN> [포트] [사이트경로]` 가 앱 코드와 이 서버의 DB(회원·사이트·청구·환율·배포 서버 목록·버전 저장소·사이트 파일)를 대상 서버로 복사하고, node·claude CLI·systemd 서비스(`webmaker`)를 설치한 뒤 대상 서버의 웹서버(Caddy 또는 nginx)에 FQDN 전용 사이트 블록만 추가한다(기존 서비스는 그대로, 오류는 일반 안내 페이지). **비밀값은 복사하지 않는다**: Claude API 키/OAuth 토큰, 세션 비밀, 배포용 SSH 개인키는 새 서버의 관리자 콘솔에서 다시 설정한다. 공인 80/443 이 다른 프런트 서버(nginx)로 들어오는 환경에서는 `deploy/front-proxy-webmaker.conf` 를 그 서버에 추가하고 `certbot --nginx -d <FQDN> --redirect` 로 Let's Encrypt 인증서를 발급한다.
## 설치 / 운영
```bash
./deploy/install.sh # nginx 설정 + systemd 서비스 설치·기동
sudo systemctl restart webhosting-agent
journalctl -u webhosting-agent -f
```
**설정 파일 (v1.5.7)**: 서비스 공개 주소·AI 실행 파일·Gitea/Arimail 연동 정보 같은 환경 정보는 systemd 유닛이 아니라 `data/config.json` 에 있다(우선순위 설정 파일 > 환경 변수 > 기본값, 비밀값은 보관소). 유닛에는 배포 때 정해지는 `PORT`, `HOST`, `SITES_ROOT`, `DATA_ROOT`, `PATH`, `HOME` 만 둔다. 1.4.x 의 환경 변수(`PUBLIC_BASE_URL`, `CLAUDE_MODEL`, `CLAUDE_BIN`, `CLAUDE_MAX_TURNS`, `SITES_GIT_REMOTE/WEB/PREFIX`, `WM_ACME_*` …)는 첫 기동 때 설정 파일로 옮겨지며 그대로 동작한다. 항목표·CLI(`node scripts/config.js list|get|set|check|setup-url`)·기존 서버 올리기는 [docs/CONFIG.md](docs/CONFIG.md).
**초기 설정 페이지 (v1.5.7, v1.5.11 개정)**: 설치 스크립트가 systemd `webmaker-setup`(별도 포트, 기본 **8795**, 모든 인터페이스)을 띄우고 접속 주소 `http://<서버 IP>:8795/?token=…` 를 알려 준다. 설치 점검표(관리자 비밀번호·AI 인증·공개 주소)가 끝나기 전에는 서비스 첫 화면도 `/setup` 으로 넘어간다([docs/FIRST-INSTALL.md](docs/FIRST-INSTALL.md)). 서비스 안 `/setup`·관리자 콘솔 › 시스템 설정 탭도 같은 화면이다 — 상태 카드(서비스·Gitea·Arimail·Claude·Codex·Resend 연결 확인), 그룹별 값 저장, 서비스 재시작.
**연동 서비스 (v1.5.7)**: `deploy/install-services.sh --gitea | --arimail --host mail.<도메인> --mailbox admin@<도메인> | --check`. 사이트 버전 저장소(로컬 Gitea, v1.4.14): `deploy/install-gitea.sh --service webhosting-agent [--bind <LAN IP>] [--migrate-from <옛 원격>]` — [docs/GITEA-LOCAL.md](docs/GITEA-LOCAL.md). 메일 서버 Arimail: `deploy/install-arimail.sh` — [docs/ARIMAIL.md](docs/ARIMAIL.md). `remote-install.sh` 는 끝에 Gitea(와 `ARIMAIL_HOST`·`ARIMAIL_MAILBOX` 가 있으면 Arimail)를 설치하고, `remote-update.sh` 는 설치된 서비스를 점검·등록한다.
## 제한 사항 (초기 버전)
- 사설망(192.168.20.0/24) 전용을 전제로 한다(HTTP, 비밀번호 재설정 없음).