설정 보강 및 메시지 동작 정리
RESET_TOKEN을 .env에서 읽도록 바꾸고 한국 시간 표시와 시간대별 클릭 메시지 동작을 정리합니다.
This commit is contained in:
@@ -2,6 +2,7 @@ node_modules
|
||||
data
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
README.md
|
||||
.cursor
|
||||
*.sqlite
|
||||
|
||||
12
README.md
12
README.md
@@ -317,12 +317,22 @@ THRESHOLD_SECONDS=10 npm start
|
||||
curl -X POST http://localhost:40100/api/reset
|
||||
```
|
||||
|
||||
초기화 API에 토큰을 걸고 싶으면 `RESET_TOKEN`을 설정합니다.
|
||||
초기화 API에 토큰을 걸고 싶으면 `RESET_TOKEN`을 설정합니다. Docker 운영 환경에서는 Git에 올리지 않는 `.env` 파일을 사용하세요.
|
||||
|
||||
```bash
|
||||
RESET_TOKEN=my-secret THRESHOLD_SECONDS=10 npm start
|
||||
```
|
||||
|
||||
Docker Compose:
|
||||
|
||||
```bash
|
||||
cat > .env <<'EOF'
|
||||
RESET_TOKEN=my-secret
|
||||
EOF
|
||||
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
토큰이 있을 때 초기화:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,6 +7,7 @@ services:
|
||||
ports:
|
||||
- "40100:40100"
|
||||
environment:
|
||||
TZ: Asia/Seoul
|
||||
THRESHOLD_DAYS: 90
|
||||
# 만료 테스트가 필요하면 아래 값을 켜세요. 예: 10초 후 만료
|
||||
# THRESHOLD_SECONDS: 10
|
||||
@@ -17,8 +18,7 @@ services:
|
||||
TRIGGER_SCRIPT: /scripts/cleanup.sh
|
||||
COLLECTION_PATH: /home/zenn/Collection
|
||||
BACKGROUND: random
|
||||
# 초기화 API 보호가 필요하면 토큰을 지정하세요.
|
||||
# RESET_TOKEN: change-me
|
||||
RESET_TOKEN: ${RESET_TOKEN:?Set RESET_TOKEN in .env}
|
||||
volumes:
|
||||
- keeper-data:/data
|
||||
- ./config/default.json:/app/config/default.json:ro
|
||||
|
||||
@@ -40,6 +40,8 @@ Collection Keeper는 웹페이지에 들어와서 가끔 클릭해 주는 작은
|
||||
- Git
|
||||
- NAS에 `/home/zenn/Collection` 폴더
|
||||
|
||||
NAS 본체에는 Node.js나 npm이 없어도 됩니다. Docker 이미지 빌드 과정에서 컨테이너 안에서 필요한 패키지를 설치합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Git이란?
|
||||
@@ -177,6 +179,16 @@ config/default.json
|
||||
|
||||
Docker에서는 `docker-compose.yml`도 함께 확인합니다.
|
||||
|
||||
초기화 API 보호 토큰은 Git에 올리지 않는 `.env` 파일에 넣습니다.
|
||||
|
||||
```bash
|
||||
cat > .env <<'EOF'
|
||||
RESET_TOKEN=여기에-긴-비밀-토큰
|
||||
EOF
|
||||
```
|
||||
|
||||
`.env` 파일이 없으면 Docker Compose 실행 시 `RESET_TOKEN` 설정 오류가 납니다.
|
||||
|
||||
### 5-7. Docker 실행
|
||||
|
||||
```bash
|
||||
@@ -241,6 +253,7 @@ http://NAS_IP:40100
|
||||
- `COLLECTION_PATH`
|
||||
- `BACKGROUND`
|
||||
- `RESET_TOKEN`
|
||||
- `TZ`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const custom = window.COLLECTION_KEEPER_CUSTOM ?? {};
|
||||
const activeEmojis = custom.emojis?.length ? custom.emojis : ['✨'];
|
||||
const activeClickMessages = custom.clickMessages?.length ? custom.clickMessages : ['기록은 남겨둘게.'];
|
||||
const timeMessageGroups = {
|
||||
dawn: custom.dawnMessages ?? [],
|
||||
morning: custom.morningMessages ?? [],
|
||||
afternoon: custom.afternoonMessages ?? [],
|
||||
evening: custom.eveningMessages ?? [],
|
||||
};
|
||||
const SEOUL_TIME_ZONE = 'Asia/Seoul';
|
||||
const KEEPER_TEXT_LINES = custom.keeperTextLines?.length
|
||||
? custom.keeperTextLines
|
||||
: ['Collection Keeper'];
|
||||
@@ -52,11 +59,61 @@ function renderFarewellText() {
|
||||
}
|
||||
|
||||
function randomEmoji() {
|
||||
return activeEmojis[Math.floor(Math.random() * activeEmojis.length)];
|
||||
return randomFrom(activeEmojis);
|
||||
}
|
||||
|
||||
function randomMessage() {
|
||||
return activeClickMessages[Math.floor(Math.random() * activeClickMessages.length)];
|
||||
const candidates = activeClickMessages.map((message) => ({
|
||||
text: message,
|
||||
type: 'normal',
|
||||
}));
|
||||
addWeightedMessages(candidates, getTimeMessages(), 3);
|
||||
|
||||
return randomFrom(candidates);
|
||||
}
|
||||
|
||||
function randomFrom(items) {
|
||||
return items[Math.floor(Math.random() * items.length)];
|
||||
}
|
||||
|
||||
function addWeightedMessages(candidates, messages, weight, type = 'normal') {
|
||||
if (!messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let count = 0; count < weight; count += 1) {
|
||||
candidates.push(...messages.map((message) => ({ text: message, type })));
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeMessages(date = new Date()) {
|
||||
const hour = getSeoulHour(date);
|
||||
|
||||
if (hour < 6) {
|
||||
return timeMessageGroups.dawn;
|
||||
}
|
||||
|
||||
if (hour < 12) {
|
||||
return timeMessageGroups.morning;
|
||||
}
|
||||
|
||||
if (hour < 18) {
|
||||
return timeMessageGroups.afternoon;
|
||||
}
|
||||
|
||||
return timeMessageGroups.evening;
|
||||
}
|
||||
|
||||
function getSeoulHour(date) {
|
||||
const hour = new Intl.DateTimeFormat('ko-KR', {
|
||||
timeZone: SEOUL_TIME_ZONE,
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.formatToParts(date)
|
||||
.find((part) => part.type === 'hour')?.value ?? '0';
|
||||
|
||||
return Number(hour);
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
@@ -295,7 +352,8 @@ function renderTraces(traces) {
|
||||
function spawnClickMessage(emoji, message) {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'click-message';
|
||||
node.textContent = `${emoji} ${message}`;
|
||||
node.classList.add(`is-${message.type}`);
|
||||
node.textContent = `${emoji} ${message.text}`;
|
||||
elements.effectLayer.appendChild(node);
|
||||
|
||||
const { x, y } = getRandomMessagePosition(node);
|
||||
|
||||
163
public/custom.js
163
public/custom.js
@@ -26,7 +26,6 @@ window.COLLECTION_KEEPER_CUSTOM = {
|
||||
// 클릭할 때 기록되고 화면에 표시될 이모지입니다.
|
||||
emojis: ['🐱', '🐰', '🐥', '🦊', '🍀', '🌸', '✨', '🎉', '💫', '🌈', '🍓', '🌙'],
|
||||
|
||||
// 클릭할 때 이모지 뒤에 함께 나올 문장입니다.
|
||||
clickMessages: [
|
||||
'오늘은 조금 조용하네.',
|
||||
'버튼 누르는 거 잊지 마.',
|
||||
@@ -79,4 +78,166 @@ window.COLLECTION_KEEPER_CUSTOM = {
|
||||
'여기는 늘 같은 자리에 있어.',
|
||||
'생각보다 오래 함께하고 있네.'
|
||||
],
|
||||
|
||||
dawnMessages: [
|
||||
'아직 안 자고 있었구나.',
|
||||
'밤은 생각보다 길어.',
|
||||
'다들 잠든 시간이라 조용해.',
|
||||
'오늘은 조금 늦었네.',
|
||||
'잠들기 전에 버튼 하나만 눌러줘.',
|
||||
'이 시간에도 찾아왔네.',
|
||||
'가끔은 새벽이 가장 편안해.',
|
||||
'컬렉션은 오늘도 무사해.',
|
||||
'아직 아무 일도 없었어.',
|
||||
'조용한 시간은 싫지 않아.',
|
||||
'별들도 슬슬 졸린가 봐.',
|
||||
'오늘 하루는 어땠어?',
|
||||
'여기는 아직 깨어있어.',
|
||||
'새벽 공기는 조금 특별해.',
|
||||
'이 시간의 방문자는 드물어.',
|
||||
'밤과 아침의 사이에 왔네.',
|
||||
'지금은 아주 조용한 시간이야.',
|
||||
'가끔은 아무 생각도 하지 않아.',
|
||||
'버튼 하나면 충분해.',
|
||||
'다음에도 찾아와줘.'
|
||||
],
|
||||
|
||||
morningMessages: [
|
||||
'좋은 아침.',
|
||||
'오늘도 무사히 시작됐어.',
|
||||
'잠에서 깨자마자 여기까지 왔네.',
|
||||
'오늘도 평화로운 하루가 되길.',
|
||||
'햇살이 조금 들어오는 것 같아.',
|
||||
'새로운 하루가 시작됐어.',
|
||||
'아침에는 기분이 조금 가벼워.',
|
||||
'오늘도 컬렉션은 잘 있어.',
|
||||
'아직은 한적한 시간이야.',
|
||||
'일찍 찾아왔네.',
|
||||
'좋은 일이 있으면 좋겠다.',
|
||||
'버튼 누르는 건 잊지 않았네.',
|
||||
'오늘도 잘 부탁해.',
|
||||
'기억은 오늘도 그대로야.',
|
||||
'아직 아무 변화도 없어.',
|
||||
'조용히 하루를 시작해보자.',
|
||||
'여기까지 와줘서 고마워.',
|
||||
'아침 공기는 상쾌하네.',
|
||||
'천천히 둘러보다 가.',
|
||||
'오늘도 괜찮을 거야.'
|
||||
],
|
||||
|
||||
afternoonMessages: [
|
||||
'오늘 하루는 어때?',
|
||||
'시간이 꽤 흘렀네.',
|
||||
'컬렉션은 오늘도 잘 있어.',
|
||||
'잠시 쉬어가는 것도 괜찮아.',
|
||||
'지금쯤이면 바쁠 시간일까.',
|
||||
'잠깐 들른 거야?',
|
||||
'생각난 김에 와준 거라면 충분해.',
|
||||
'오늘도 평범하게 흘러가고 있어.',
|
||||
'여기는 늘 같은 모습이야.',
|
||||
'버튼 하나 부탁할게.',
|
||||
'기록은 천천히 쌓이고 있어.',
|
||||
'특별한 일은 없었어.',
|
||||
'그게 오히려 좋은 걸지도.',
|
||||
'조금 쉬었다 가도 돼.',
|
||||
'컬렉션은 잘 보관되고 있어.',
|
||||
'가끔은 멍하니 있어도 괜찮아.',
|
||||
'오늘도 무사한 하루였으면 좋겠어.',
|
||||
'방문해줘서 고마워.',
|
||||
'여기는 늘 기다리고 있어.',
|
||||
'잠깐이라도 들러줘서 기뻐.'
|
||||
],
|
||||
|
||||
eveningMessages: [
|
||||
'오늘도 수고했어.',
|
||||
'하루가 거의 끝나가네.',
|
||||
'조금 쉬었다 가.',
|
||||
'버튼 누르는 건 잊지 않았네.',
|
||||
'오늘 하루는 괜찮았어?',
|
||||
'여기는 변함없이 조용해.',
|
||||
'해가 저물고 있어.',
|
||||
'하루가 지나가는 소리가 들려.',
|
||||
'오늘도 무사히 지나갔네.',
|
||||
'가끔은 아무것도 하지 않아도 돼.',
|
||||
'컬렉션은 잘 지켜지고 있어.',
|
||||
'이제 조금 쉬어도 되겠다.',
|
||||
'오늘도 와줘서 고마워.',
|
||||
'생각보다 오래 함께하고 있네.',
|
||||
'오늘은 어떤 기억이 남았어?',
|
||||
'별일 없는 하루가 제일 좋아.',
|
||||
'여기까지 와준 것만으로 충분해.',
|
||||
'조용한 저녁이네.',
|
||||
'내일도 좋은 하루가 되길.',
|
||||
'다음에 또 보자.'
|
||||
],
|
||||
|
||||
// 아래 메시지들은 나중에 화면에 뿌려줄 랜덤 텍스트용입니다. 현재 클릭 메시지에는 사용하지 않습니다.
|
||||
recentVisitMessages: [
|
||||
'어제도 왔었지?',
|
||||
'꽤 자주 들러주네.',
|
||||
'덕분에 안심하고 있어.',
|
||||
'생각보다 부지런하네.',
|
||||
'오늘도 빠르게 찾아왔구나.'
|
||||
],
|
||||
|
||||
weekVisitMessages: [
|
||||
'오랜만이네.',
|
||||
'잘 지냈어?',
|
||||
'한 주 정도는 금방 지나가네.',
|
||||
'다시 와줘서 반가워.',
|
||||
'컬렉션은 여전히 잘 있어.'
|
||||
],
|
||||
|
||||
monthVisitMessages: [
|
||||
'한동안 보이지 않아서 조금 걱정했어.',
|
||||
'생각보다 오래 쉬었네.',
|
||||
'그래도 다시 와줘서 다행이야.',
|
||||
'시간이 꽤 흘렀구나.',
|
||||
'여기는 변함없이 기다리고 있었어.'
|
||||
],
|
||||
|
||||
longAbsentMessages: [
|
||||
'오랫동안 보이지 않았네.',
|
||||
'조금 걱정하고 있었어.',
|
||||
'컬렉션은 아직 무사해.',
|
||||
'혹시 잊어버린 건 아니지?',
|
||||
'가끔은 안부도 들려줘.',
|
||||
'생각보다 긴 시간이 흘렀어.',
|
||||
'여기는 계속 기다리고 있었어.',
|
||||
'아직은 괜찮지만 조금 외롭네.',
|
||||
'무슨 일은 없는 거지?',
|
||||
'버튼 하나만 눌러주면 안심할 수 있을 것 같아.'
|
||||
],
|
||||
|
||||
warningMessages: [
|
||||
'정말 괜찮은 거지?',
|
||||
'최근 소식이 없어서 걱정돼.',
|
||||
'이곳은 아직 그대로야.',
|
||||
'생각보다 많이 늦었네.',
|
||||
'혹시 길을 잃은 건 아니지?',
|
||||
'조용한 시간이 너무 길어지고 있어.',
|
||||
'컬렉션은 계속 기다리고 있어.',
|
||||
'아직은 늦지 않았어.',
|
||||
'한 번만 확인해주면 돼.',
|
||||
'조금만 더 늦으면 곤란해질지도 몰라.'
|
||||
],
|
||||
|
||||
finalWarningMessages: [
|
||||
'이제 정말 얼마 남지 않았어.',
|
||||
'마지막 확인이 필요해.',
|
||||
'이곳은 아직 기다리고 있어.',
|
||||
'하루 차이로 모든 것이 달라질 수도 있어.',
|
||||
'지금이라도 괜찮아.',
|
||||
'아직은 되돌릴 수 있어.',
|
||||
'정말 마지막 안내야.',
|
||||
'부디 잊지 말아줘.'
|
||||
],
|
||||
|
||||
rareMessages: [
|
||||
'오늘은 왠지 기다리고 있었어.',
|
||||
'이 메시지를 본 사람은 별로 없어.',
|
||||
'가끔은 내가 먼저 기억하는 건지 헷갈려.',
|
||||
'버튼이 눌리면 여기 시간도 조금 움직여.',
|
||||
'이 문장을 읽고 있다면 충분해.'
|
||||
],
|
||||
};
|
||||
|
||||
@@ -55,18 +55,13 @@ body.is-expiring {
|
||||
right: 1.5rem;
|
||||
z-index: 4;
|
||||
max-width: min(24rem, calc(100vw - 3rem));
|
||||
padding: 0.75rem 0.95rem;
|
||||
border-radius: 999px;
|
||||
color: var(--strong);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(-0.4rem);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.toast.is-visible {
|
||||
|
||||
37
src/db.js
37
src/db.js
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const FALLBACK_EMOJIS = ['🐱', '🐰', '🐥', '🦊', '🍀', '🌸', '✨', '🎉', '💫', '🌈', '🍓', '🌙'];
|
||||
const DISPLAY_TIME_ZONE = 'Asia/Seoul';
|
||||
|
||||
function ensureDirectory(filePath) {
|
||||
const directory = path.dirname(filePath);
|
||||
@@ -129,21 +130,41 @@ function buildStatus(db, config) {
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const parts = getDateTimeParts(date);
|
||||
const year = parts.year;
|
||||
const month = parts.month;
|
||||
const day = parts.day;
|
||||
const hours = parts.hour;
|
||||
const minutes = parts.minute;
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
const parts = getDateTimeParts(date);
|
||||
const hours = parts.hour;
|
||||
const minutes = parts.minute;
|
||||
const seconds = parts.second;
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function getDateTimeParts(date) {
|
||||
return Object.fromEntries(
|
||||
new Intl.DateTimeFormat('ko-KR', {
|
||||
timeZone: DISPLAY_TIME_ZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.formatToParts(date)
|
||||
.filter((part) => part.type !== 'literal')
|
||||
.map((part) => [part.type, part.value])
|
||||
);
|
||||
}
|
||||
|
||||
function recordCheck(db, config, requestedEmoji) {
|
||||
const emoji =
|
||||
typeof requestedEmoji === 'string' && requestedEmoji.trim()
|
||||
|
||||
Reference in New Issue
Block a user