Rename service, UI, storage, and target-path configuration; clarify expiration cleanup behavior.
Last Signal
개인 NAS 사용자를 위한 초경량 생존 확인 웹앱입니다.
- 현재 버전:
v1.0.0 - Git 저장소:
https://git.sori.studio/zenn/collection.sori.studio.git - 상세 설치 문서:
docs/DEPLOY.md
가끔 방문해서 중앙 버튼을 누르면 확인 시간이 갱신되고, 일정 기간 동안 아무도 누르지 않으면 사용자가 지정한 스크립트가 실행됩니다.
마지막 생존 확인 후 설정된 스크립트를 실행합니다. 기본 scripts/cleanup.sh는 TARGET_PATH로 지정한 폴더의 내용 전체를 삭제하며, 폴더 자체는 남깁니다. 기본 대상은 /home/zenn/Collection입니다.
주요 기능
- 중앙 버튼 한 개로 생존 확인
- SQLite 기반 통계 및 방문 흔적 저장
- 1초 쿨다운 스팸 방지
- 랜덤 이모지 클릭 효과
- 배경 이미지 교체 및 랜덤 선택
- threshold 초과 시 사용자 스크립트 실행
로컬 실행 가이드
개발하거나 먼저 동작을 확인할 때는 로컬에서 실행합니다.
필요한 것:
- Node.js 20 이상
- npm
설치:
npm install
실행:
npm start
브라우저에서 접속:
http://localhost:40100
개발 중 파일 변경을 감지해서 다시 실행하려면:
npm run dev
로컬 실행 시 SQLite 파일은 기본적으로 data/last-signal.sqlite에 생성됩니다.
Docker 실행 가이드
Docker로 한 번에 빌드하고 실행합니다.
필요한 것:
- Docker
- Docker Compose
실행:
docker compose up -d --build
브라우저에서 접속:
http://localhost:44092
상태 확인:
docker compose ps
로그 확인:
docker compose logs -f
중지:
docker compose down
다시 시작:
docker compose restart
컨테이너와 네트워크를 중지해도 last-signal-data 볼륨에 SQLite 데이터가 남습니다.
상용/NAS 설치 가이드
NAS에서 오래 켜둘 때는 Docker Compose 실행을 권장합니다.
- 프로젝트 폴더를 NAS에 둡니다.
cd /volume1/docker/last-signal
- 설정을 확인합니다.
vi config/default.json
자주 바꾸는 항목:
threshold_days: 며칠 동안 버튼을 누르지 않으면 스크립트를 실행할지cooldown_seconds: 생존 확인으로 인정되는 최소 클릭 간격trigger_script: 실행할 사용자 스크립트 경로background:none, 배경 파일명, 또는random
- 사용자 스크립트를 수정합니다.
vi scripts/cleanup.sh
chmod +x scripts/*.sh
docker-compose.yml의 NAS 경로를 확인합니다.
기본값은 Synology 스타일 경로입니다.
- /home/zenn/Collection:/home/zenn/Collection
다른 경로를 쓴다면 왼쪽 경로를 실제 Collection 폴더로 바꿉니다.
- 실행합니다.
docker compose up -d --build
- 로그를 확인합니다.
docker compose logs -f last-signal
- NAS 주소로 접속합니다.
http://NAS_IP:44092
예시:
http://192.168.0.10:44092
업데이트 가이드
코드를 새 버전으로 바꾼 뒤 다시 빌드합니다.
docker compose down --remove-orphans
docker compose up -d --build
--remove-orphans는 이전 서비스 이름으로 남은 컨테이너를 정리합니다. 데이터를 유지하려면 docker compose down -v는 사용하지 마세요. -v 옵션은 SQLite 데이터가 들어 있는 Docker 볼륨까지 삭제합니다.
백업 가이드
Docker Compose 기본 설정에서는 SQLite DB가 last-signal-data 볼륨에 저장됩니다.
볼륨 이름 확인:
docker volume ls | grep last-signal
로컬 실행에서는 data/last-signal.sqlite 파일을 백업하면 됩니다.
cp data/last-signal.sqlite last-signal.backup.sqlite
멀티 아키텍처 이미지 빌드
ARM64와 x86_64 이미지를 함께 빌드하려면 Docker Buildx를 사용합니다.
docker buildx build --platform linux/amd64,linux/arm64 -t last-signal:latest .
문제 해결
EADDRINUSE: address already in use :::40100
이미 40100 포트를 쓰는 서버가 떠 있을 때 발생합니다.
포트를 쓰는 프로세스 확인:
lsof -nP -iTCP:40100 -sTCP:LISTEN
예시 출력에서 PID가 30383이라면 종료:
kill 30383
다시 실행:
npm start
기존 서버를 끄고 싶지 않다면 다른 포트로 실행할 수도 있습니다.
PORT=40101 npm start
이 경우 브라우저에서는 다음 주소로 접속합니다.
http://localhost:40101
설정
기본 설정은 config/default.json 에 있습니다.
| 항목 | 기본값 | 설명 |
|---|---|---|
threshold_days |
90 |
생존 확인 허용 기간 |
threshold_seconds |
null |
테스트용 초 단위 생존 확인 기간. 값이 있으면 threshold_days보다 우선합니다. |
cooldown_seconds |
1 |
생존 확인 인정 쿨다운 |
check_interval_seconds |
60 |
스크립트 검사 주기 |
database_path |
./data/last-signal.sqlite |
SQLite 경로 |
trigger_script |
./scripts/cleanup.sh |
조건 충족 시 실행할 스크립트 |
background |
random |
none, 배경 파일명, 또는 random |
target_path |
/home/zenn/Collection |
만료 시 스크립트가 처리할 대상 폴더 |
recent_traces_limit |
5 |
최근 흔적 저장 조회 개수 |
reset_token |
"" |
초기화 API 보호용 토큰. 비어 있으면 토큰 없이 초기화 가능합니다. |
public_base_url |
https://collection.sori.studio |
메일 확인 링크에 사용할 공개 URL |
mail_enabled |
false |
Gmail 알림 메일 사용 여부 |
mail_smtp_user |
"" |
Gmail SMTP 계정 |
mail_smtp_pass |
"" |
Gmail 앱 비밀번호 |
mail_to |
zenn.message@gmail.com |
알림 수신 주소 |
mail_confirm_token |
"" |
메일 확인 링크 보호 토큰 |
환경 변수로도 덮어쓸 수 있습니다.
THRESHOLD_DAYSCOOLDOWN_SECONDSCHECK_INTERVAL_SECONDSDATABASE_PATHTRIGGER_SCRIPTTARGET_PATHBACKGROUNDPORTTHRESHOLD_SECONDSRESET_TOKENPUBLIC_BASE_URLMAIL_ENABLEDMAIL_SMTP_USERMAIL_SMTP_PASSMAIL_TOMAIL_CONFIRM_TOKEN
이전 설정과의 호환을 위해 COLLECTION_PATH와 collection_path도 읽습니다. 새 설정에는 TARGET_PATH와 target_path를 사용하세요.
화면 문구 커스터마이징
화면에 보이는 글, 클릭 이모지, 클릭 메시지는 public/custom.js 한 파일에서 바꿉니다.
코드를 몰라도 이 파일의 따옴표 안 문장과 이모지만 수정하면 됩니다.
window.LAST_SIGNAL_CUSTOM = {
keeperTextLines: [
'가끔은 생각해.',
'',
'누군가 버튼을 누른다는 건',
'이곳을 기억하고 있다는 뜻일까,',
],
farewellTextLines: [
'안녕.',
'',
'고마웠어.',
],
emojis: ['🐱', '🐰', '🌸', '✨'],
clickMessages: [
'기록은 남겨둘게.',
'아직 이곳을 기억하고 있어.',
],
};
keeperTextLines: 오른쪽 아래에 표시되는 글입니다. 빈 줄은''로 넣습니다.farewellTextLines: 남은 기간이 0이 되었을 때 표시되는 작별 인사입니다.emojis: 클릭할 때 기록되고 표시되는 이모지입니다.clickMessages: 클릭할 때 이모지 뒤에 붙는 랜덤 문장입니다.
만료 테스트와 초기화
만료 화면을 빠르게 확인하려면 초 단위 타이머를 사용합니다.
THRESHOLD_SECONDS=10 npm start
10초가 지나면 화면이 작별 인사로 전환되고, 더 이상 클릭해도 확인 시간이 갱신되지 않습니다.
다시 처음 상태로 초기화:
curl -X POST http://localhost:40100/api/reset
초기화 API에 토큰을 걸고 싶으면 RESET_TOKEN을 설정합니다. Docker 운영 환경에서는 Git에 올리지 않는 .env 파일을 사용하세요.
RESET_TOKEN=my-secret THRESHOLD_SECONDS=10 npm start
Docker Compose:
cat > .env <<'EOF'
RESET_TOKEN=my-secret
EOF
docker compose up -d --build
Gmail 알림 메일
마지막 확인 이후 60일, 75일, 89일이 지나면 Gmail SMTP로 알림 메일을 보낼 수 있습니다.
.env 예시:
PUBLIC_BASE_URL=https://collection.sori.studio
MAIL_ENABLED=true
MAIL_SMTP_USER=zenn.message@gmail.com
MAIL_SMTP_PASS=구글-앱-비밀번호
MAIL_TO=zenn.message@gmail.com
MAIL_CONFIRM_TOKEN=긴-랜덤-확인-토큰
메일의 [ ❤️ 생존 확인하기 ] 버튼은 사이트를 다시 열 필요 없이 /confirm?token=... 링크로 바로 생존 확인을 갱신합니다.
Google 계정에서는 일반 비밀번호가 아니라 앱 비밀번호를 사용해야 합니다.
토큰이 있을 때 초기화:
curl -X POST http://localhost:40100/api/reset \
-H 'Content-Type: application/json' \
-d '{"token":"my-secret"}'
배경 이미지
assets/backgrounds/ 폴더에 이미지를 넣고 설정에서 지정합니다. 기본값은 random입니다.
예시:
{
"background": "bg01.jpg"
}
랜덤 사용:
{
"background": "random"
}
배경 없이 사용:
{
"background": "none"
}
사용자 스크립트
scripts/ 폴더에 실행할 스크립트를 둡니다.
예시:
scripts/warning.shscripts/encrypt.shscripts/cleanup.sh
스크립트에는 다음 환경 변수가 전달됩니다.
TARGET_PATHTRIGGER_SCRIPT
기존 스크립트 호환을 위해 COLLECTION_PATH와 KEEPER_SCRIPT도 같은 값으로 전달됩니다. 새 스크립트에는 새 이름을 사용하세요.
안전 원칙
Last Signal은 다음 역할을 수행합니다.
- 마지막 확인 시간 관리
- 통계 관리
- 버튼 이벤트 처리
- 조건 충족 여부 판단
- 사용자 지정 스크립트 실행
파일 처리 동작은 실행 스크립트에 달려 있습니다. 기본 scripts/cleanup.sh는 TARGET_PATH 폴더의 내용을 전부 삭제합니다. 다른 작업을 원하면 해당 스크립트를 수정하고, 실제 데이터가 아닌 임시 폴더에서 먼저 검증하세요.
API
GET /api/statusPOST /api/check
버튼 클릭은 반드시 POST /api/check 로 처리됩니다. 단순 방문만으로는 생존 확인이 갱신되지 않습니다.
NAS 볼륨 예시
volumes:
- last-signal-data:/data
- ./scripts:/scripts:ro
- /home/zenn/Collection:/home/zenn/Collection