PHP 판 설치
PHP 판은 웹호스팅에 그대로 올려놓으면 됩니다. Composer도, 패키지 관리자도, 계속 떠 있어야 하는 서비스도 없습니다. 디렉터리 하나, 설정 파일 하나, cron 하나. 이 안내서는 빈 디렉터리에서 실제 운영까지를 처음부터 끝까지 다룹니다. 대부분의 안내서가 건너뛰는 부분, 즉 권한, TLS, cron, 백업, 갱신까지 포함해서요.
필요한 것#
| 항목 | 버전 | 왜 필요한가 |
|---|---|---|
| PHP | 8.1 이상 | 유니온 타입, enum, readonly, never. 코드가 이것들을 씁니다. |
pdo_sqlite | — | 데이터베이스 그 자체. 이게 없으면 아무것도 돌지 않습니다. |
mbstring | — | 열여섯 언어에서 문자열을 올바르게 다루기 위해. |
openssl | 권장 | 데이터베이스 속 비밀값 암호화(SMTP 비밀번호, 외부 보관소 토큰). |
curl | 권장 | 웹훅 발송, 그리고 백업을 외부로 보내기. |
intl | 권장 | Node 판과 같은 날짜 표기. 없으면 달 이름은 사전에서 가져옵니다. |
설치 마법사는 이것들을 하나씩 확인해 화면에 보여준 뒤에야 다음으로 넘어가게 합니다. 필수 항목이 빠지면 설치가 멈추고, 선택 항목은 알려주기만 하고 막지는 않습니다.
composer로 설치할 것은 하나도 없습니다. IMAP 읽기, tar.gz 압축,
PDF, CSV, QR 코드, HTTP 클라이언트가 모두 제품 안에 들어 있습니다. 그래서 명령줄조차 쓸 수
없는 서버에도 올릴 수 있습니다.
1. 파일 올리기#
PHP 판 압축 파일을 받아 올립니다. 구성은 이렇습니다.
toutadmin/
├── app/ 코드(웹 서버가 절대 내보내지 않습니다)
│ ├── Core/ 커널: 라우팅, DB, 세션, 보안
│ ├── Modules/ 업무: 인사, 회계, 프로젝트, 품질……
│ ├── Controllers/
│ ├── views/ 템플릿
│ ├── locales/ 열여섯 개의 사전
│ └── schema.sql Node 판과 똑같은 DB 구조
├── public/ ← 문서 루트는 여기, 다른 어디도 아닙니다
│ ├── index.php 프런트 컨트롤러
│ ├── css/ js/
├── data/ DB, 금고, 서명철, 받은 서류, 백업
├── tools/ cron.php, check-keys.php
├── tests/
├── config.sample.php
└── VERSION
public/이지 디렉터리 전체가 아닙니다
이 쪽에서 가장 중요한 한 가지입니다. 서버가 디렉터리 전체를 내보내면
data/app.sqlite를 누구나 내려받을 수 있게 됩니다. 비밀번호 해시와 금고까지 포함한
데이터베이스 전체를요. data/가 public/ 바깥에 있는 건 그래서입니다.
웹호스팅에서#
두 가지 경우가 있습니다. 도메인의 루트를 고를 수 있는 업체라면(cPanel, Plesk, 그리고 '루트
디렉터리'를 지정할 수 있는 대부분의 호스팅) 디렉터리 전체를 www/ 바깥에 두고
도메인이 toutadmin/public을 가리키게 하세요.
루트가 고정돼 있다면 — 보통 www/나 public_html/ —
public/의 내용물을 그 안에 넣고 나머지는 그 한 단계 위에 둔 다음,
public/index.php 맨 앞의 경로를 고치세요.
www/ ← 고정된 루트
├── index.php
├── css/ js/
toutadmin/ ← 나머지, 웹이 닿지 않는 곳
├── app/
├── data/
└── config.php
// www/index.php —— 고쳐야 할 한 줄
require_once __DIR__ . '/../toutadmin/app/bootstrap.php';
정 안 되어 루트 바깥에 아무것도 둘 수 없다면, data/에 함께 들어 있는
.htaccess가 접근을 막습니다. 그건 안전벨트이지 해결책이 아닙니다. 가능하면 언제나
루트 바깥에 두세요.
2. 설정 쓰기#
cp config.sample.php config.php
그리고 config.php를 엽니다.
<?php
return [
// SQLite DB와 데이터 디렉터리. 문서 루트 바깥에 둡니다.
'db_path' => __DIR__ . '/data/app.sqlite',
'data_dir' => __DIR__ . '/data',
// 이 인스턴스만의 열쇠. 한 번만 만들면 됩니다:
// php -r "echo bin2hex(random_bytes(32));"
'session_secret' => '…',
// 설치 토큰. 다음 단계를 보세요.
'install_token' => '',
// 메일에 들어가는 링크에 쓰는 공개 주소.
'base_url' => 'https://intranet.hoesa.co.kr',
// 믿을 수 있는 프록시 뒤에 있을 때만 켭니다.
'trust_proxy' => false,
// 상한: 주소당 15분 동안의 로그인 시도 횟수,
// 그리고 주소당 1분 동안의 요청 수.
'login_rate_limit' => 10,
'global_rate_limit' => 300,
// 만료: 아무 조작 없는 시간, 그리고 무엇을 해도 늘어나지 않는 절대 시간.
'session_idle_minutes' => 60,
'session_max_hours' => 12,
];
서명철의 서명을 봉인하고 데이터베이스 속 비밀값을 암호화합니다. 잃어버리면 이미 있는 서명을
확인할 수 없게 됩니다. 예시 값을 그대로 두면 제품이 data/.instance-key에 직접
하나 만듭니다(권한 0600). 그때는 이 파일도 백업에 넣는 걸 잊지 마세요.
설정은 다른 곳에 둘 수도 있습니다. 환경 변수 TOUTADMIN_CONFIG로 다른 파일을 가리키면
됩니다.
3. 설치 토큰 정하기#
파일을 올린 순간부터 당신이 설치 마법사를 열기까지, 이 인스턴스는 먼저 찾아낸 사람의 것입니다. 먼저 도착한 사람이 관리자 계정을 만듭니다. 짧은 틈이지만 그걸로 충분합니다. 그러니 공개하기 전에 무작위 값을 넣어 두세요.
php -r "echo bin2hex(random_bytes(16));"
# 그리고 config.php에:
'install_token' => 'c3f1…',
마법사는 이 값을 요구하고 일정한 시간으로 비교하며 — 순진한 비교는 토큰을 한 글자씩 알아맞히게 해 줍니다 — 거절할 때마다 기록을 남깁니다. 토큰이 없으면 아무것도 만들어지지 않습니다. 설치가 끝나면 마법사는 스스로 닫힙니다. 토큰은 그대로 둬도 됩니다. 이제 아무 쓸모가 없으니까요.
4. 권한#
| 경로 | 권한 | 누가 쓰는가 |
|---|---|---|
data/ | 0770 | 웹 서버, 그리고 그것만 |
data/app.sqlite | 0660 | 설치할 때 만들어집니다 |
config.php | 0640 | 아무도 — 서버는 읽기만 |
app/, public/ | 0755 | 아무도 — 읽기만 |
# PHP가 www-data로 도는 서버의 예
sudo chown -R nin:www-data /var/www/toutadmin
sudo find /var/www/toutadmin -type d -exec chmod 750 {} \;
sudo find /var/www/toutadmin -type f -exec chmod 640 {} \;
sudo chmod 770 /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/public
SQLite는 DB 옆에 딸린 파일(-wal, -shm)도 씁니다. 쓸 수 있어야 하는 건
디렉터리이지 DB 파일 하나가 아닙니다.
5. 웹 서버#
Nginx#
server {
listen 443 ssl http2;
server_name intranet.hoesa.co.kr;
root /var/www/toutadmin/public; # 다른 어디도 아닙니다
index index.php;
ssl_certificate /etc/letsencrypt/live/intranet.hoesa.co.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.hoesa.co.kr/privkey.pem;
client_max_body_size 20M; # 금고에 넣는 파일, 받은 서류
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# 올려진 파일은 절대 직접 내보내지 않습니다. 세션과 파일
# 해시를 확인하는 경로를 반드시 거칩니다.
location ~ ^/(data|app|tools|tests)/ { deny all; }
}
server {
listen 80;
server_name intranet.hoesa.co.kr;
return 301 https://$host$request_uri;
}
Apache#
<VirtualHost *:443>
ServerName intranet.hoesa.co.kr
DocumentRoot /var/www/toutadmin/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/intranet.hoesa.co.kr/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/intranet.hoesa.co.kr/privkey.pem
<Directory /var/www/toutadmin/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
함께 들어 있는 public/.htaccess가 모든 주소를 index.php로 돌립니다.
Apache 가상 호스트라면 그것으로 충분합니다.
TLS#
연결이 암호화되어 있을 때만 제품은 Strict-Transport-Security 헤더를 붙이고,
평문일 때는 절대 붙이지 않습니다. 암호화되지 않은 쪽에서 그렇게 선언해 봐야 브라우저는 받아들이지
않고, 오히려 집에서 잠깐 해 보는 시험을 반년 동안 https에 묶어 둘 수 있기
때문입니다. 직접 관리하는 서버라면:
sudo certbot --nginx -d intranet.hoesa.co.kr
앞단의 프록시나 로드 밸런서가 TLS를 끝내고 있다면 'trust_proxy' => true로 하세요
— 그럴 때만요. 그러지 않으면 누구든 헤더에 원하는 IP와 프로토콜을 적어 낼 수 있습니다.
6. cron#
PHP 사이트는 요청이 있을 때만 돕니다. Node 판이 매시간 순회에서 하는 여덟 가지 일을, 여기서는 서버의 cron이 맡습니다.
* * * * * /usr/bin/php /var/www/toutadmin/tools/cron.php >> /var/log/toutadmin-cron.log 2>&1
| cron이 하는 일 | 주기 |
|---|---|
| 계약이 끝난 계정을 닫습니다 | 매번 |
| 기한이 된 정기 계약을 청구합니다 | 매번, 중복 청구는 없습니다 |
| 기한을 알림으로 바꿉니다 | 매번, 중복은 걸러서 |
| 읽은 알림과 감사 기록을 정리합니다 | 정해 둔 보관 기간에 따라 |
| 회계 메일함을 읽습니다(IMAP) | 가져오기를 설정해 두었다면 |
| 웹훅 대기열을 재시도까지 비웁니다 | 매번 |
| 자동 백업을 만들어 외부로 보냅니다 | 정해 둔 간격이 되었을 때 |
정해 둔 간격보다 스크립트를 자주 부른다고 백업이 늘지는 않습니다. 때가 안 되었으면 아무 일도
일어나지 않습니다. 호스팅이 cron을 15분에 한 번으로 제한한다면 * * * * *를
*/15 * * * *로 바꾸세요. 웹훅이 그만큼 늦게 나갈 뿐입니다.
입문용 호스팅에는 없을 수 있습니다. 외부 서비스(가동 감시 같은)에서 보내는 HTTP 호출은 쓸 수
없습니다. cron.php는 명령줄 밖에서의 실행을 거부합니다. 공개 주소가 백업을
일으키지 못하게 하려고요. 그럴 때는 백업 화면에서 손으로 실행하세요.
7. 설치 마법사#
도메인을 엽니다. 계정이 하나도 없는 동안에는 모든 주소가 /installation으로
향합니다. 전부 한 쪽에 들어 있습니다.
- 사전 조건을 하나씩 확인해 보여줍니다 — 필수인 것은 분명히 표시됩니다.
- 정해 두었다면 설치 토큰.
- 회사: 곳곳에 나올 이름, 그리고 (프리랜서가 아닌) 새 직원이 받는 연차 일수.
- 인스턴스의 기본 언어. 16개 중에서 고릅니다.
- 관리자 계정: 메일 주소, 그리고 열두 자 이상의 비밀번호.
모든 것이 하나의 트랜잭션으로 기록됩니다. 계정이 생기면
/installation은 로그인 화면으로 향합니다. 마법사가 스스로 닫히니 지울 파일은
없습니다.
8. 잘 도는지 확인하기#
# 테스트. 내 컴퓨터에서 돌립니다(당신의 DB에 쓰지 않습니다)
php tests/run.php
# 번역: 사전에 없는 열쇠를 쓰는 화면이 하나도 없는지
php tools/check-keys.php
그다음은 화면에서. 구성원을 하나 만들고, 그 계정으로 로그인하고, 첨부 파일을 하나 넣고, 백업을 손으로 실행한 뒤 백업 화면에서 검증하세요. 이 네 가지가 DB, 파일, 권한, 압축을 건드립니다. 서버에 요구되는 것 전부입니다.
9. 갱신하기#
# 1. 먼저 백업. 언제나
php tools/cron.php # 또는 '백업' 화면에서
# 2. 데이터가 아니라 코드를 바꿉니다
# app/ public/ tools/ tests/ VERSION ← 교체
# data/ config.php ← 그대로
# 3. DB는 첫 요청에서 스스로 옮겨 갑니다
DB 구조는 멱등한 이행으로 나아갑니다. 갱신을 두 번 돌려도 아무것도 망가지지 않습니다. Node 판 인스턴스와 PHP 판 인스턴스는 같은 구조(142개 표, 1311개 열)와 같은 비밀번호를 함께 씁니다. 데이터베이스는 변환 없이 어느 쪽으로든 옮길 수 있습니다.
10. 제대로 백업하기#
tar.gz 압축에는 데이터베이스(VACUUM INTO로 복사하므로 쓰는 도중에도
일관됩니다)와 파일 디렉터리 다섯 개가 들어갑니다. 사진, 받은 이력서, 금고, 서명철, 받은
서류입니다. 압축 속 파일마다 SHA-256이 붙고, 복원할 때 하나씩 대조됩니다.
지켜야 할 서버에 그대로 놓아둔 압축 파일은 아무것도 지키지 못합니다. 백업 화면에서 외부 보관소(FTPS 또는 Google 드라이브)를 설정하세요. 자세한 내용은 백업과 복원 쪽으로.
먼저 내 컴퓨터에서 해 보기#
cp config.sample.php config.php
php -S localhost:8000 -t public public/index.php
PHP에 딸린 서버로 제품을 둘러볼 수는 있습니다. 운영에는 맞지 않습니다. 프로세스 하나, TLS 없음, 유량 제한 없음.
Toutadmin 문서 —— 2026-09-13에 생성. 소프트웨어와 분리된 독립 사이트입니다.