feat: Meilisearch 검색 기능 및 개발환경 통합

- Meilisearch 기반 일정 검색 API 구현
- 멤버 별명으로 검색 지원 (하냥 → 송하영)
- 영문 자판 → 한글 변환 검색 지원
- 검색 응답 구조 개선 (category 객체, datetime 통합, members 배열)
- 개발/배포 환경 Dockerfile 통합 (주석 전환 방식)
- docker-compose.yml 단일 파일로 통합

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-18 18:53:57 +09:00
parent 5521c44fa9
commit abe9687cc8
15 changed files with 946 additions and 517 deletions

View file

@ -1,27 +1,27 @@
# 빌드 스테이지 - 프론트엔드 빌드 # ============================================
FROM node:20-alpine AS frontend-builder # 개발 모드
WORKDIR /frontend # ============================================
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
# 프로덕션 스테이지
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app WORKDIR /app
# ffmpeg 설치 (비디오 썸네일 추출용)
RUN apk add --no-cache ffmpeg RUN apk add --no-cache ffmpeg
CMD ["sh", "-c", "cd /app/backend && npm install && cd /app/frontend && npm install --include=dev && (cd /app/backend && PORT=3000 npm run dev &) && sleep 3 && cd /app/frontend && npm run dev -- --host 0.0.0.0"]
# 백엔드 의존성 설치 # ============================================
COPY backend/package*.json ./ # 배포 모드 (사용 시 위 개발 모드를 주석처리)
RUN npm install --production # ============================================
# FROM node:20-alpine AS frontend-builder
# 백엔드 파일 복사 # WORKDIR /frontend
COPY backend/ ./ # COPY frontend/package*.json ./
# RUN npm install
# 프론트엔드 빌드 결과물 복사 # COPY frontend/ ./
COPY --from=frontend-builder /frontend/dist ./dist # RUN npm run build
#
EXPOSE 80 # FROM node:20-alpine
CMD ["npm", "start"] # WORKDIR /app
# RUN apk add --no-cache ffmpeg
# COPY backend/package*.json ./
# RUN npm install --production
# COPY backend/ ./
# COPY --from=frontend-builder /frontend/dist ./dist
# EXPOSE 80
# CMD ["npm", "start"]

View file

@ -21,6 +21,7 @@
"inko": "^1.1.1", "inko": "^1.1.1",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
"kiwi-nlp": "^0.22.1", "kiwi-nlp": "^0.22.1",
"meilisearch": "^0.44.0",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"sharp": "^0.34.5" "sharp": "^0.34.5"
@ -229,32 +230,32 @@
} }
}, },
"node_modules/@aws-sdk/client-s3": { "node_modules/@aws-sdk/client-s3": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz",
"integrity": "sha512-mDC792KkFzLZG9PS1Fv9b18lEzmSNBjAdweLJ83D2CZu6ved9+Pr/Dr+FRs0kSxqY+sUUUuIBmvDYHXY8E8EzA==", "integrity": "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/credential-provider-node": "3.970.0", "@aws-sdk/credential-provider-node": "3.971.0",
"@aws-sdk/middleware-bucket-endpoint": "3.969.0", "@aws-sdk/middleware-bucket-endpoint": "3.969.0",
"@aws-sdk/middleware-expect-continue": "3.969.0", "@aws-sdk/middleware-expect-continue": "3.969.0",
"@aws-sdk/middleware-flexible-checksums": "3.970.0", "@aws-sdk/middleware-flexible-checksums": "3.971.0",
"@aws-sdk/middleware-host-header": "3.969.0", "@aws-sdk/middleware-host-header": "3.969.0",
"@aws-sdk/middleware-location-constraint": "3.969.0", "@aws-sdk/middleware-location-constraint": "3.969.0",
"@aws-sdk/middleware-logger": "3.969.0", "@aws-sdk/middleware-logger": "3.969.0",
"@aws-sdk/middleware-recursion-detection": "3.969.0", "@aws-sdk/middleware-recursion-detection": "3.969.0",
"@aws-sdk/middleware-sdk-s3": "3.970.0", "@aws-sdk/middleware-sdk-s3": "3.970.0",
"@aws-sdk/middleware-ssec": "3.969.0", "@aws-sdk/middleware-ssec": "3.971.0",
"@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/middleware-user-agent": "3.970.0",
"@aws-sdk/region-config-resolver": "3.969.0", "@aws-sdk/region-config-resolver": "3.969.0",
"@aws-sdk/signature-v4-multi-region": "3.970.0", "@aws-sdk/signature-v4-multi-region": "3.970.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-endpoints": "3.970.0",
"@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-browser": "3.969.0",
"@aws-sdk/util-user-agent-node": "3.970.0", "@aws-sdk/util-user-agent-node": "3.971.0",
"@smithy/config-resolver": "^4.4.6", "@smithy/config-resolver": "^4.4.6",
"@smithy/core": "^3.20.6", "@smithy/core": "^3.20.6",
"@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-browser": "^4.2.8",
@ -295,9 +296,9 @@
} }
}, },
"node_modules/@aws-sdk/client-sso": { "node_modules/@aws-sdk/client-sso": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz",
"integrity": "sha512-ArmgnOsSCXN5VyIvZb4kSP5hpqlRRHolrMtKQ/0N8Hw4MTb7/IeYHSZzVPNzzkuX6gn5Aj8txoUnDPM8O7pc9g==", "integrity": "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
@ -311,7 +312,7 @@
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-endpoints": "3.970.0",
"@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-browser": "3.969.0",
"@aws-sdk/util-user-agent-node": "3.970.0", "@aws-sdk/util-user-agent-node": "3.971.0",
"@smithy/config-resolver": "^4.4.6", "@smithy/config-resolver": "^4.4.6",
"@smithy/core": "^3.20.6", "@smithy/core": "^3.20.6",
"@smithy/fetch-http-handler": "^5.3.9", "@smithy/fetch-http-handler": "^5.3.9",
@ -418,19 +419,19 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-ini": { "node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz",
"integrity": "sha512-L5R1hN1FY/xCmH65DOYMXl8zqCFiAq0bAq8tJZU32mGjIl1GzGeOkeDa9c461d81o7gsQeYzXyqFD3vXEbJ+kQ==", "integrity": "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/credential-provider-env": "3.970.0", "@aws-sdk/credential-provider-env": "3.970.0",
"@aws-sdk/credential-provider-http": "3.970.0", "@aws-sdk/credential-provider-http": "3.970.0",
"@aws-sdk/credential-provider-login": "3.970.0", "@aws-sdk/credential-provider-login": "3.971.0",
"@aws-sdk/credential-provider-process": "3.970.0", "@aws-sdk/credential-provider-process": "3.970.0",
"@aws-sdk/credential-provider-sso": "3.970.0", "@aws-sdk/credential-provider-sso": "3.971.0",
"@aws-sdk/credential-provider-web-identity": "3.970.0", "@aws-sdk/credential-provider-web-identity": "3.971.0",
"@aws-sdk/nested-clients": "3.970.0", "@aws-sdk/nested-clients": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/credential-provider-imds": "^4.2.8", "@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
@ -443,13 +444,13 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-login": { "node_modules/@aws-sdk/credential-provider-login": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz",
"integrity": "sha512-C+1dcLr+p2E+9hbHyvrQTZ46Kj4vC2RoP6N935GEukHQa637ZjXs8VlyHJ2xTvbvwwLZQNiu56Cx7o/OFOqw1A==", "integrity": "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/nested-clients": "3.970.0", "@aws-sdk/nested-clients": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8", "@smithy/protocol-http": "^5.3.8",
@ -462,17 +463,17 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-node": { "node_modules/@aws-sdk/credential-provider-node": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz",
"integrity": "sha512-nMM0eeVuiLtw1taLRQ+H/H5Qp11rva8ILrzAQXSvlbDeVmbc7d8EeW5Q2xnCJu+3U+2JNZ1uxqIL22pB2sLEMA==", "integrity": "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/credential-provider-env": "3.970.0", "@aws-sdk/credential-provider-env": "3.970.0",
"@aws-sdk/credential-provider-http": "3.970.0", "@aws-sdk/credential-provider-http": "3.970.0",
"@aws-sdk/credential-provider-ini": "3.970.0", "@aws-sdk/credential-provider-ini": "3.971.0",
"@aws-sdk/credential-provider-process": "3.970.0", "@aws-sdk/credential-provider-process": "3.970.0",
"@aws-sdk/credential-provider-sso": "3.970.0", "@aws-sdk/credential-provider-sso": "3.971.0",
"@aws-sdk/credential-provider-web-identity": "3.970.0", "@aws-sdk/credential-provider-web-identity": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/credential-provider-imds": "^4.2.8", "@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
@ -502,14 +503,14 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-sso": { "node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz",
"integrity": "sha512-ROb+Aijw8nzkB14Nh2XRH861++SeTZykUzk427y8YtgTLxjAOjgDTchDUFW2Fx6GFWkSjqJ3sY7SZyb33IqyFw==", "integrity": "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/client-sso": "3.970.0", "@aws-sdk/client-sso": "3.971.0",
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/token-providers": "3.970.0", "@aws-sdk/token-providers": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/shared-ini-file-loader": "^4.4.3",
@ -521,13 +522,13 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-web-identity": { "node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz",
"integrity": "sha512-r7tnYJJg+B6QvnsRHSW5vDol+ks6n+5jBZdCFdGyK63hjcMRMqHx59zEH8O47UR1PFv5hS2Q3uGz6HXvVtP40Q==", "integrity": "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/nested-clients": "3.970.0", "@aws-sdk/nested-clients": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/shared-ini-file-loader": "^4.4.3",
@ -572,9 +573,9 @@
} }
}, },
"node_modules/@aws-sdk/middleware-flexible-checksums": { "node_modules/@aws-sdk/middleware-flexible-checksums": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.971.0.tgz",
"integrity": "sha512-mlKLwX0jWa5EwIvMjJAvVFL/zLAxB/fNLOg4hQCNCUf1qi+XxD+brDopXNPWeA8bSCnpvWfZrQd5yNksG6Fzqg==", "integrity": "sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32": "5.2.0",
@ -681,9 +682,9 @@
} }
}, },
"node_modules/@aws-sdk/middleware-ssec": { "node_modules/@aws-sdk/middleware-ssec": {
"version": "3.969.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.969.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.971.0.tgz",
"integrity": "sha512-9wUYtd5ye4exygKHyl02lPVHUoAFlxxXoqvlw7u2sycfkK6uHLlwdsPru3MkMwj47ZSZs+lkyP/sVKXVMhuaAg==", "integrity": "sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
@ -713,9 +714,9 @@
} }
}, },
"node_modules/@aws-sdk/nested-clients": { "node_modules/@aws-sdk/nested-clients": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz",
"integrity": "sha512-RIl8s4DCa31MXtRFw23iU90OqEoWuwQxiZOZshzsPtjyrunhHFjyZJEqb+vuQcYd1o22SMaYa3lPJRp64OH35Q==", "integrity": "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
@ -729,7 +730,7 @@
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-endpoints": "3.970.0",
"@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-browser": "3.969.0",
"@aws-sdk/util-user-agent-node": "3.970.0", "@aws-sdk/util-user-agent-node": "3.971.0",
"@smithy/config-resolver": "^4.4.6", "@smithy/config-resolver": "^4.4.6",
"@smithy/core": "^3.20.6", "@smithy/core": "^3.20.6",
"@smithy/fetch-http-handler": "^5.3.9", "@smithy/fetch-http-handler": "^5.3.9",
@ -795,13 +796,13 @@
} }
}, },
"node_modules/@aws-sdk/token-providers": { "node_modules/@aws-sdk/token-providers": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz",
"integrity": "sha512-YO8KgJecxHIFMhfoP880q51VXFL9V1ELywK5yzVEqzyrwqoG93IUmnTygBUylQrfkbH+QqS0FxEdgwpP3fcwoQ==", "integrity": "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.970.0", "@aws-sdk/core": "3.970.0",
"@aws-sdk/nested-clients": "3.970.0", "@aws-sdk/nested-clients": "3.971.0",
"@aws-sdk/types": "3.969.0", "@aws-sdk/types": "3.969.0",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/shared-ini-file-loader": "^4.4.3",
@ -878,9 +879,9 @@
} }
}, },
"node_modules/@aws-sdk/util-user-agent-node": { "node_modules/@aws-sdk/util-user-agent-node": {
"version": "3.970.0", "version": "3.971.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.970.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz",
"integrity": "sha512-TNQpwIVD6SxMwkD+QKnaujKVyXy5ljN3O3jrI7nCHJ3GlJu5xJrd8yuBnanYCcrn3e2zwdfOh4d4zJAZvvIvVw==", "integrity": "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/middleware-user-agent": "3.970.0",
@ -1894,9 +1895,9 @@
} }
}, },
"node_modules/@smithy/core": { "node_modules/@smithy/core": {
"version": "3.20.6", "version": "3.20.7",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.6.tgz", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz",
"integrity": "sha512-BpAffW1mIyRZongoKBbh3RgHG+JDHJek/8hjA/9LnPunM+ejorO6axkxCgwxCe4K//g/JdPeR9vROHDYr/hfnQ==", "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-serde": "^4.2.9",
@ -2114,12 +2115,12 @@
} }
}, },
"node_modules/@smithy/middleware-endpoint": { "node_modules/@smithy/middleware-endpoint": {
"version": "4.4.7", "version": "4.4.8",
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.7.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz",
"integrity": "sha512-SCmhUG1UwtnEhF5Sxd8qk7bJwkj1BpFzFlHkXqKCEmDPLrRjJyTGM0EhqT7XBtDaDJjCfjRJQodgZcKDR843qg==", "integrity": "sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/core": "^3.20.6", "@smithy/core": "^3.20.7",
"@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-serde": "^4.2.9",
"@smithy/node-config-provider": "^4.3.8", "@smithy/node-config-provider": "^4.3.8",
"@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/shared-ini-file-loader": "^4.4.3",
@ -2133,15 +2134,15 @@
} }
}, },
"node_modules/@smithy/middleware-retry": { "node_modules/@smithy/middleware-retry": {
"version": "4.4.23", "version": "4.4.24",
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.23.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz",
"integrity": "sha512-lLEmkQj7I7oKfvZ1wsnToGJouLOtfkMXDKRA1Hi6F+mMp5O1N8GcVWmVeNgTtgZtd0OTXDTI2vpVQmeutydGew==", "integrity": "sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/node-config-provider": "^4.3.8", "@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8", "@smithy/protocol-http": "^5.3.8",
"@smithy/service-error-classification": "^4.2.8", "@smithy/service-error-classification": "^4.2.8",
"@smithy/smithy-client": "^4.10.8", "@smithy/smithy-client": "^4.10.9",
"@smithy/types": "^4.12.0", "@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8", "@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8", "@smithy/util-retry": "^4.2.8",
@ -2308,13 +2309,13 @@
} }
}, },
"node_modules/@smithy/smithy-client": { "node_modules/@smithy/smithy-client": {
"version": "4.10.8", "version": "4.10.9",
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.8.tgz", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.9.tgz",
"integrity": "sha512-wcr3UEL26k7lLoyf9eVDZoD1nNY3Fa1gbNuOXvfxvVWLGkOVW+RYZgUUp/bXHryJfycIOQnBq9o1JAE00ax8HQ==", "integrity": "sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/core": "^3.20.6", "@smithy/core": "^3.20.7",
"@smithy/middleware-endpoint": "^4.4.7", "@smithy/middleware-endpoint": "^4.4.8",
"@smithy/middleware-stack": "^4.2.8", "@smithy/middleware-stack": "^4.2.8",
"@smithy/protocol-http": "^5.3.8", "@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0", "@smithy/types": "^4.12.0",
@ -2415,13 +2416,13 @@
} }
}, },
"node_modules/@smithy/util-defaults-mode-browser": { "node_modules/@smithy/util-defaults-mode-browser": {
"version": "4.3.22", "version": "4.3.23",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.22.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz",
"integrity": "sha512-O2WXr6ZRqPnbyoepb7pKcLt1QL6uRfFzGYJ9sGb5hMJQi7v/4RjRmCQa9mNjA0YiXqsc5lBmLXqJPhjM1Vjv5A==", "integrity": "sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/smithy-client": "^4.10.8", "@smithy/smithy-client": "^4.10.9",
"@smithy/types": "^4.12.0", "@smithy/types": "^4.12.0",
"tslib": "^2.6.2" "tslib": "^2.6.2"
}, },
@ -2430,16 +2431,16 @@
} }
}, },
"node_modules/@smithy/util-defaults-mode-node": { "node_modules/@smithy/util-defaults-mode-node": {
"version": "4.2.25", "version": "4.2.26",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.25.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz",
"integrity": "sha512-7uMhppVNRbgNIpyUBVRfjGHxygP85wpXalRvn9DvUlCx4qgy1AB/uxOPSiDx/jFyrwD3/BypQhx1JK7f3yxrAw==", "integrity": "sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/config-resolver": "^4.4.6", "@smithy/config-resolver": "^4.4.6",
"@smithy/credential-provider-imds": "^4.2.8", "@smithy/credential-provider-imds": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8", "@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8", "@smithy/property-provider": "^4.2.8",
"@smithy/smithy-client": "^4.10.8", "@smithy/smithy-client": "^4.10.9",
"@smithy/types": "^4.12.0", "@smithy/types": "^4.12.0",
"tslib": "^2.6.2" "tslib": "^2.6.2"
}, },
@ -3484,6 +3485,12 @@
"url": "https://github.com/sponsors/wellwelwel" "url": "https://github.com/sponsors/wellwelwel"
} }
}, },
"node_modules/meilisearch": {
"version": "0.44.1",
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.44.1.tgz",
"integrity": "sha512-ZTZYBmomtRwjaWbvU8U8ct04g/YnrNOlvchogJOPgHcQIQBfjdbAvMJ8mLhuZEzpioYXIT6Cv+FcE150pc2+nw==",
"license": "MIT"
},
"node_modules/mime": { "node_modules/mime": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",

View file

@ -20,6 +20,7 @@
"inko": "^1.1.1", "inko": "^1.1.1",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
"kiwi-nlp": "^0.22.1", "kiwi-nlp": "^0.22.1",
"meilisearch": "^0.44.0",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"sharp": "^0.34.5" "sharp": "^0.34.5"

View file

@ -12,6 +12,7 @@ import config from './config/index.js';
import dbPlugin from './plugins/db.js'; import dbPlugin from './plugins/db.js';
import redisPlugin from './plugins/redis.js'; import redisPlugin from './plugins/redis.js';
import authPlugin from './plugins/auth.js'; import authPlugin from './plugins/auth.js';
import meilisearchPlugin from './plugins/meilisearch.js';
import youtubeBotPlugin from './services/youtube/index.js'; import youtubeBotPlugin from './services/youtube/index.js';
import xBotPlugin from './services/x/index.js'; import xBotPlugin from './services/x/index.js';
import schedulerPlugin from './plugins/scheduler.js'; import schedulerPlugin from './plugins/scheduler.js';
@ -44,6 +45,7 @@ export async function buildApp(opts = {}) {
await fastify.register(dbPlugin); await fastify.register(dbPlugin);
await fastify.register(redisPlugin); await fastify.register(redisPlugin);
await fastify.register(authPlugin); await fastify.register(authPlugin);
await fastify.register(meilisearchPlugin);
await fastify.register(youtubeBotPlugin); await fastify.register(youtubeBotPlugin);
await fastify.register(xBotPlugin); await fastify.register(xBotPlugin);
await fastify.register(schedulerPlugin); await fastify.register(schedulerPlugin);

View file

@ -30,4 +30,8 @@ export default {
bucket: process.env.RUSTFS_BUCKET || 'fromis-9', bucket: process.env.RUSTFS_BUCKET || 'fromis-9',
publicUrl: process.env.RUSTFS_PUBLIC_URL, publicUrl: process.env.RUSTFS_PUBLIC_URL,
}, },
meilisearch: {
host: process.env.MEILI_HOST || 'http://fromis9-meilisearch:7700',
apiKey: process.env.MEILI_MASTER_KEY,
},
}; };

View file

@ -0,0 +1,92 @@
import fp from 'fastify-plugin';
import { MeiliSearch } from 'meilisearch';
const INDEX_NAME = 'schedules';
async function meilisearchPlugin(fastify, opts) {
const { host, apiKey } = fastify.config.meilisearch;
const client = new MeiliSearch({
host,
apiKey,
});
// 연결 테스트 및 인덱스 초기화
try {
await client.health();
fastify.log.info('Meilisearch 연결 성공');
// 인덱스 초기화
await initIndex(client, fastify.log);
} catch (err) {
fastify.log.error('Meilisearch 연결 실패:', err.message);
// Meilisearch가 없어도 서버는 동작하도록 에러를 던지지 않음
}
fastify.decorate('meilisearch', client);
fastify.decorate('meilisearchIndex', INDEX_NAME);
}
/**
* 인덱스 초기화 설정
*/
async function initIndex(client, log) {
try {
// 인덱스 생성 (이미 존재하면 무시)
try {
await client.createIndex(INDEX_NAME, { primaryKey: 'id' });
} catch (err) {
// 이미 존재하는 경우 무시
}
const index = client.index(INDEX_NAME);
// 검색 가능한 필드 설정 (순서가 우선순위 결정)
await index.updateSearchableAttributes([
'title',
'member_names',
'description',
'source_name',
'category_name',
]);
// 필터링 가능한 필드 설정
await index.updateFilterableAttributes(['category_id', 'date']);
// 정렬 가능한 필드 설정
await index.updateSortableAttributes(['date', 'time']);
// 랭킹 규칙 설정 (동일 유사도일 때 최신 날짜 우선)
await index.updateRankingRules([
'words',
'typo',
'proximity',
'attribute',
'exactness',
'date:desc',
]);
// 오타 허용 설정
await index.updateTypoTolerance({
enabled: true,
minWordSizeForTypos: {
oneTypo: 2,
twoTypos: 4,
},
});
// 페이징 설정
await index.updatePagination({
maxTotalHits: 10000,
});
log.info('Meilisearch 인덱스 초기화 완료');
} catch (err) {
log.error('Meilisearch 인덱스 초기화 오류:', err.message);
}
}
export default fp(meilisearchPlugin, {
name: 'meilisearch',
dependencies: ['db'],
});

View file

@ -3,103 +3,64 @@
* GET: 공개, POST/PUT/DELETE: 인증 필요 * GET: 공개, POST/PUT/DELETE: 인증 필요
*/ */
import suggestionsRoutes from './suggestions.js'; import suggestionsRoutes from './suggestions.js';
import { searchSchedules, syncAllSchedules } from '../../services/meilisearch/index.js';
export default async function schedulesRoutes(fastify) { export default async function schedulesRoutes(fastify) {
const { db } = fastify; const { db, meilisearch, redis } = fastify;
// 추천 검색어 라우트 등록 // 추천 검색어 라우트 등록
fastify.register(suggestionsRoutes, { prefix: '/suggestions' }); fastify.register(suggestionsRoutes, { prefix: '/suggestions' });
/** /**
* GET /api/schedules * GET /api/schedules
* 월별 일정 목록 조회 * 검색 모드: search 파라미터가 있으면 Meilisearch 검색
* @query year - 년도 (필수) * 월별 조회 모드: year, month 파라미터로 월별 조회
* @query month - (필수)
*/ */
fastify.get('/', { fastify.get('/', {
schema: { schema: {
tags: ['schedules'], tags: ['schedules'],
summary: '월별 일정 목록 조회', summary: '일정 조회 (검색 또는 월별)',
querystring: { querystring: {
type: 'object', type: 'object',
required: ['year', 'month'],
properties: { properties: {
search: { type: 'string', description: '검색어' },
year: { type: 'integer', description: '년도' }, year: { type: 'integer', description: '년도' },
month: { type: 'integer', minimum: 1, maximum: 12, description: '월' }, month: { type: 'integer', minimum: 1, maximum: 12, description: '월' },
offset: { type: 'integer', default: 0, description: '페이지 오프셋' },
limit: { type: 'integer', default: 100, description: '결과 개수' },
}, },
}, },
}, },
}, async (request, reply) => { }, async (request, reply) => {
const { year, month } = request.query; const { search, year, month, offset = 0, limit = 100 } = request.query;
// 검색 모드
if (search && search.trim()) {
return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit));
}
// 월별 조회 모드
if (!year || !month) { if (!year || !month) {
return reply.code(400).send({ error: 'year와 month는 필수입니다.' }); return reply.code(400).send({ error: 'search 또는 year/month는 필수입니다.' });
} }
const startDate = `${year}-${String(month).padStart(2, '0')}-01`; return await handleMonthlySchedules(db, parseInt(year), parseInt(month));
const endDate = new Date(year, month, 0).toISOString().split('T')[0];
const [schedules] = await db.query(`
SELECT
s.id,
s.title,
s.date,
s.time,
s.category_id,
c.name as category_name,
c.color as category_color,
sy.channel_name as source_name
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id
WHERE s.date BETWEEN ? AND ?
ORDER BY s.date ASC, s.time ASC
`, [startDate, endDate]);
// 날짜별로 그룹화
const grouped = {};
for (const s of schedules) {
const dateKey = s.date.toISOString().split('T')[0];
if (!grouped[dateKey]) {
grouped[dateKey] = {
categories: [],
schedules: [],
};
}
// 일정 추가
const schedule = {
id: s.id,
title: s.title,
time: s.time,
category: {
id: s.category_id,
name: s.category_name,
color: s.category_color,
},
};
if (s.source_name) {
schedule.source_name = s.source_name;
}
grouped[dateKey].schedules.push(schedule);
// 카테고리 카운트
const existingCategory = grouped[dateKey].categories.find(c => c.id === s.category_id);
if (existingCategory) {
existingCategory.count++;
} else {
grouped[dateKey].categories.push({
id: s.category_id,
name: s.category_name,
color: s.category_color,
count: 1,
}); });
}
}
return grouped; /**
* POST /api/schedules/sync-search
* Meilisearch 전체 동기화 (관리자 전용)
*/
fastify.post('/sync-search', {
schema: {
tags: ['schedules'],
summary: 'Meilisearch 전체 동기화',
security: [{ bearerAuth: [] }],
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const count = await syncAllSchedules(meilisearch, db);
return { success: true, synced: count };
}); });
/** /**
@ -150,3 +111,170 @@ export default async function schedulesRoutes(fastify) {
return result; return result;
}); });
} }
/**
* 검색 처리
*/
async function handleSearch(fastify, query, offset, limit) {
const { db, meilisearch, redis } = fastify;
// 첫 페이지 검색 시에만 검색어 저장 (bi-gram 학습)
if (offset === 0) {
// 비동기로 저장 (응답 지연 방지)
saveSearchQueryAsync(fastify, query);
}
// Meilisearch 검색
const results = await searchSchedules(meilisearch, db, query, { limit: 1000 });
// 페이징 적용
const paginatedHits = results.hits.slice(offset, offset + limit);
return {
schedules: paginatedHits,
total: results.total,
offset,
limit,
hasMore: offset + paginatedHits.length < results.total,
};
}
/**
* 검색어 비동기 저장
*/
async function saveSearchQueryAsync(fastify, query) {
try {
// suggestions 서비스의 saveSearchQuery 사용
const { SuggestionService } = await import('../../services/suggestions/index.js');
const service = new SuggestionService(fastify.db, fastify.redis);
await service.saveSearchQuery(query);
} catch (err) {
console.error('[Search] 검색어 저장 실패:', err.message);
}
}
/**
* 월별 일정 조회 (생일 포함)
*/
async function handleMonthlySchedules(db, year, month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = new Date(year, month, 0).toISOString().split('T')[0];
// 일정 조회
const [schedules] = await db.query(`
SELECT
s.id,
s.title,
s.date,
s.time,
s.category_id,
c.name as category_name,
c.color as category_color,
sy.channel_name as source_name
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id
WHERE s.date BETWEEN ? AND ?
ORDER BY s.date ASC, s.time ASC
`, [startDate, endDate]);
// 생일 조회
const [birthdays] = await db.query(`
SELECT m.id, m.name, m.name_en, m.birth_date,
i.thumb_url as image_url
FROM members m
LEFT JOIN images i ON m.image_id = i.id
WHERE m.is_former = 0 AND MONTH(m.birth_date) = ?
`, [month]);
// 날짜별로 그룹화
const grouped = {};
// 일정 추가
for (const s of schedules) {
const dateKey = s.date instanceof Date
? s.date.toISOString().split('T')[0]
: s.date;
if (!grouped[dateKey]) {
grouped[dateKey] = {
categories: [],
schedules: [],
};
}
const schedule = {
id: s.id,
title: s.title,
time: s.time,
category: {
id: s.category_id,
name: s.category_name,
color: s.category_color,
},
};
if (s.source_name) {
schedule.source_name = s.source_name;
}
grouped[dateKey].schedules.push(schedule);
// 카테고리 카운트
const existingCategory = grouped[dateKey].categories.find(c => c.id === s.category_id);
if (existingCategory) {
existingCategory.count++;
} else {
grouped[dateKey].categories.push({
id: s.category_id,
name: s.category_name,
color: s.category_color,
count: 1,
});
}
}
// 생일 일정 추가
for (const member of birthdays) {
const birthDate = new Date(member.birth_date);
const birthdayThisYear = new Date(year, birthDate.getMonth(), birthDate.getDate());
const dateKey = birthdayThisYear.toISOString().split('T')[0];
if (!grouped[dateKey]) {
grouped[dateKey] = {
categories: [],
schedules: [],
};
}
// 생일 카테고리 (id: 8)
const BIRTHDAY_CATEGORY = {
id: 8,
name: '생일',
color: '#f472b6',
};
const birthdaySchedule = {
id: `birthday-${member.id}`,
title: `HAPPY ${member.name_en} DAY`,
time: null,
category: BIRTHDAY_CATEGORY,
is_birthday: true,
member_name: member.name,
member_image: member.image_url,
};
grouped[dateKey].schedules.push(birthdaySchedule);
// 생일 카테고리 카운트
const existingBirthdayCategory = grouped[dateKey].categories.find(c => c.id === 8);
if (existingBirthdayCategory) {
existingBirthdayCategory.count++;
} else {
grouped[dateKey].categories.push({
...BIRTHDAY_CATEGORY,
count: 1,
});
}
}
return grouped;
}

View file

@ -0,0 +1,249 @@
/**
* Meilisearch 검색 서비스
* - 일정 검색 (멤버 별명 이름 변환)
* - 영문 자판 한글 변환
* - 유사도 필터링
* - 일정 동기화
*/
import Inko from 'inko';
const inko = new Inko();
const INDEX_NAME = 'schedules';
/**
* 영문 자판으로 입력된 검색어인지 확인
*/
function isEnglishKeyboard(text) {
const englishChars = text.match(/[a-zA-Z]/g) || [];
const koreanChars = text.match(/[가-힣ㄱ-ㅎㅏ-ㅣ]/g) || [];
return englishChars.length > 0 && koreanChars.length === 0;
}
/**
* 별명/이름으로 멤버 이름 조회
*/
export async function resolveMemberNames(db, query) {
const searchTerm = `%${query}%`;
const [members] = await db.query(`
SELECT DISTINCT m.name
FROM members m
LEFT JOIN member_nicknames mn ON m.id = mn.member_id
WHERE m.name LIKE ? OR mn.nickname LIKE ?
`, [searchTerm, searchTerm]);
return members.map(m => m.name);
}
/**
* 일정 검색
* @param {object} meilisearch - Meilisearch 클라이언트
* @param {object} db - DB 연결
* @param {string} query - 검색어
* @param {object} options - 검색 옵션
*/
export async function searchSchedules(meilisearch, db, query, options = {}) {
const { limit = 1000, offset = 0 } = options;
try {
const index = meilisearch.index(INDEX_NAME);
const searchOptions = {
limit,
offset: 0, // 내부적으로 전체 검색 후 필터링
attributesToRetrieve: ['*'],
showRankingScore: true,
};
// 검색어 목록 구성
const searchQueries = [query];
// 영문 자판 입력 → 한글 변환
if (isEnglishKeyboard(query)) {
const koreanQuery = inko.en2ko(query);
if (koreanQuery !== query) {
searchQueries.push(koreanQuery);
}
}
// 별명 → 멤버 이름 변환
const memberNames = await resolveMemberNames(db, query);
for (const name of memberNames) {
if (!searchQueries.includes(name)) {
searchQueries.push(name);
}
}
// 각 검색어로 검색 후 병합
const allHits = new Map(); // id 기준 중복 제거
for (const q of searchQueries) {
const results = await index.search(q, searchOptions);
for (const hit of results.hits) {
// 더 높은 점수로 업데이트
if (!allHits.has(hit.id) || allHits.get(hit.id)._rankingScore < hit._rankingScore) {
allHits.set(hit.id, hit);
}
}
}
// 유사도 0.5 미만 필터링
let filteredHits = Array.from(allHits.values())
.filter(hit => hit._rankingScore >= 0.5);
// 유사도 순 정렬
filteredHits.sort((a, b) => (b._rankingScore || 0) - (a._rankingScore || 0));
const total = filteredHits.length;
// 페이징 적용
const paginatedHits = filteredHits.slice(offset, offset + limit);
// 응답 형식 변환
const formattedHits = paginatedHits.map(formatScheduleResponse);
return {
hits: formattedHits,
total,
offset,
limit,
hasMore: offset + paginatedHits.length < total,
};
} catch (err) {
console.error('[Meilisearch] 검색 오류:', err.message);
return { hits: [], total: 0, offset: 0, limit, hasMore: false };
}
}
/**
* 검색 결과 응답 형식 변환
*/
function formatScheduleResponse(hit) {
// date + time 합치기
let datetime = null;
if (hit.date) {
const dateStr = hit.date instanceof Date
? hit.date.toISOString().split('T')[0]
: String(hit.date).split('T')[0];
if (hit.time) {
datetime = `${dateStr}T${hit.time}`;
} else {
datetime = dateStr;
}
}
// member_names를 배열로 변환
const members = hit.member_names
? hit.member_names.split(',').map(name => name.trim()).filter(Boolean)
: [];
return {
id: hit.id,
title: hit.title,
datetime,
category: {
id: hit.category_id,
name: hit.category_name,
color: hit.category_color,
},
source_name: hit.source_name || null,
members,
_rankingScore: hit._rankingScore,
};
}
/**
* 일정 추가/업데이트
*/
export async function addOrUpdateSchedule(meilisearch, schedule) {
try {
const index = meilisearch.index(INDEX_NAME);
const document = {
id: schedule.id,
title: schedule.title,
description: schedule.description || '',
date: schedule.date,
time: schedule.time || '',
category_id: schedule.category_id,
category_name: schedule.category_name || '',
category_color: schedule.category_color || '',
source_name: schedule.source_name || '',
member_names: schedule.member_names || '',
};
await index.addDocuments([document]);
console.log(`[Meilisearch] 일정 추가/업데이트: ${schedule.id}`);
} catch (err) {
console.error('[Meilisearch] 문서 추가 오류:', err.message);
}
}
/**
* 일정 삭제
*/
export async function deleteSchedule(meilisearch, scheduleId) {
try {
const index = meilisearch.index(INDEX_NAME);
await index.deleteDocument(scheduleId);
console.log(`[Meilisearch] 일정 삭제: ${scheduleId}`);
} catch (err) {
console.error('[Meilisearch] 문서 삭제 오류:', err.message);
}
}
/**
* 전체 일정 동기화
*/
export async function syncAllSchedules(meilisearch, db) {
try {
// DB에서 모든 일정 조회
const [schedules] = await db.query(`
SELECT
s.id,
s.title,
s.description,
s.date,
s.time,
s.category_id,
c.name as category_name,
c.color as category_color,
sy.channel_name as source_name,
GROUP_CONCAT(DISTINCT m.name ORDER BY m.id SEPARATOR ',') as member_names
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
LEFT JOIN members m ON sm.member_id = m.id
GROUP BY s.id
`);
const index = meilisearch.index(INDEX_NAME);
// 기존 문서 모두 삭제
await index.deleteAllDocuments();
// 문서 변환
const documents = schedules.map(s => ({
id: s.id,
title: s.title,
description: s.description || '',
date: s.date instanceof Date ? s.date.toISOString().split('T')[0] : s.date,
time: s.time || '',
category_id: s.category_id,
category_name: s.category_name || '',
category_color: s.category_color || '',
source_name: s.source_name || '',
member_names: s.member_names || '',
}));
// 일괄 추가
await index.addDocuments(documents);
console.log(`[Meilisearch] ${documents.length}개 일정 동기화 완료`);
return documents.length;
} catch (err) {
console.error('[Meilisearch] 동기화 오류:', err.message);
return 0;
}
}

View file

@ -1,60 +0,0 @@
services:
# 프론트엔드 - Vite 개발 서버
fromis9-frontend:
image: node:20-alpine
container_name: fromis9-frontend
labels:
- "com.centurylinklabs.watchtower.enable=false"
working_dir: /app
command: sh -c "npm install && npm run dev -- --host 0.0.0.0 --port 80"
volumes:
- ./frontend:/app
networks:
- app
- db
restart: unless-stopped
# 백엔드 - Fastify API 서버
fromis9-backend:
image: node:20-alpine
container_name: fromis9-backend
working_dir: /app
command: sh -c "apk add --no-cache ffmpeg && npm install && npm run dev"
env_file:
- .env
environment:
- PORT=3000
volumes:
- ./backend:/app
networks:
- app
- db
restart: unless-stopped
# Meilisearch - 검색 엔진
meilisearch:
image: getmeili/meilisearch:v1.6
container_name: fromis9-meilisearch
environment:
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
volumes:
- ./meilisearch_data:/meili_data
networks:
- app
restart: unless-stopped
# Redis - 추천 검색어 캐시
redis:
image: redis:7-alpine
container_name: fromis9-redis
volumes:
- ./redis_data:/data
networks:
- app
restart: unless-stopped
networks:
app:
external: true
db:
external: true

View file

@ -1,11 +1,19 @@
services: services:
fromis9-web: fromis9-frontend:
build: . build: .
container_name: fromis9-frontend container_name: fromis9-frontend
labels: labels:
- "com.centurylinklabs.watchtower.enable=false" - "com.centurylinklabs.watchtower.enable=false"
env_file: env_file:
- .env - .env
# 개발 모드
volumes:
- ./backend:/app/backend
- ./frontend:/app/frontend
- backend_modules:/app/backend/node_modules
- frontend_modules:/app/frontend/node_modules
# 배포 모드 (사용 시 위 volumes를 주석처리)
# volumes: []
networks: networks:
- app - app
- db - db
@ -25,10 +33,16 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: fromis9-redis container_name: fromis9-redis
volumes:
- ./redis_data:/data
networks: networks:
- app - app
restart: unless-stopped restart: unless-stopped
volumes:
backend_modules:
frontend_modules:
networks: networks:
app: app:
external: true external: true

View file

@ -1,53 +0,0 @@
#!/bin/bash
# fromis_9 Photos 테이블에서 이미지 다운로드 스크립트
# 앨범별로 폴더 분류하여 저장
OUTPUT_DIR="/docker/fromis_9/downloaded_photos"
mkdir -p "$OUTPUT_DIR"
# MariaDB에서 데이터 가져오기
docker exec mariadb mariadb -u admin -p'auddnek0403!' fromis_9 -N -e "SELECT photo_id, album_name, photo FROM Photos;" | while IFS=$'\t' read -r photo_id album_name photo_url; do
# 앨범명에서 특수문자 제거하여 폴더명 생성
folder_name=$(echo "$album_name" | sed 's/[^a-zA-Z0-9가-힣 ]/_/g' | sed 's/ */_/g')
# 폴더 생성
mkdir -p "$OUTPUT_DIR/$folder_name"
# 파일명 생성 (photo_id 기반)
filename="${photo_id}.jpg"
filepath="$OUTPUT_DIR/$folder_name/$filename"
# 이미 다운로드된 파일은 건너뛰기
if [ -f "$filepath" ]; then
echo "Skip: $filepath (already exists)"
continue
fi
# 다운로드
echo "Downloading: $album_name/$filename"
curl -s -L -o "$filepath" "$photo_url"
# 다운로드 실패 시 삭제
if [ ! -s "$filepath" ]; then
rm -f "$filepath"
echo "Failed: $filepath"
fi
# Rate limiting (0.2초 대기)
sleep 0.2
done
echo "Download complete!"
echo "Saved to: $OUTPUT_DIR"
# 결과 요약
echo ""
echo "=== Summary ==="
for dir in "$OUTPUT_DIR"/*/; do
if [ -d "$dir" ]; then
count=$(ls -1 "$dir" 2>/dev/null | wc -l)
dirname=$(basename "$dir")
echo "$dirname: $count files"
fi
done

View file

@ -52,13 +52,13 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
@ -67,9 +67,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -77,21 +77,21 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.6",
"@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.3", "@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.4", "@babel/helpers": "^7.28.6",
"@babel/parser": "^7.28.5", "@babel/parser": "^7.28.6",
"@babel/template": "^7.27.2", "@babel/template": "^7.28.6",
"@babel/traverse": "^7.28.5", "@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.5", "@babel/types": "^7.28.6",
"@jridgewell/remapping": "^2.3.5", "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
@ -108,14 +108,14 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.28.5", "@babel/parser": "^7.28.6",
"@babel/types": "^7.28.5", "@babel/types": "^7.28.6",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@ -125,13 +125,13 @@
} }
}, },
"node_modules/@babel/helper-compilation-targets": { "node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.27.2", "@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0", "browserslist": "^4.24.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
@ -152,29 +152,29 @@
} }
}, },
"node_modules/@babel/helper-module-imports": { "node_modules/@babel/helper-module-imports": {
"version": "7.27.1", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/traverse": "^7.27.1", "@babel/traverse": "^7.28.6",
"@babel/types": "^7.27.1" "@babel/types": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-module-transforms": { "node_modules/@babel/helper-module-transforms": {
"version": "7.28.3", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.27.1", "@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.3" "@babel/traverse": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -184,9 +184,9 @@
} }
}, },
"node_modules/@babel/helper-plugin-utils": { "node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -224,27 +224,27 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.28.4", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.27.2", "@babel/template": "^7.28.6",
"@babel/types": "^7.28.4" "@babel/types": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.28.5" "@babel/types": "^7.28.6"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -286,33 +286,33 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.27.2", "@babel/parser": "^7.28.6",
"@babel/types": "^7.27.1" "@babel/types": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.6",
"@babel/helper-globals": "^7.28.0", "@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.5", "@babel/parser": "^7.28.6",
"@babel/template": "^7.27.2", "@babel/template": "^7.28.6",
"@babel/types": "^7.28.5", "@babel/types": "^7.28.6",
"debug": "^4.3.1" "debug": "^4.3.1"
}, },
"engines": { "engines": {
@ -320,9 +320,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.28.5", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -813,9 +813,9 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.23.1", "version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -829,9 +829,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -843,9 +843,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -857,9 +857,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -871,9 +871,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -885,9 +885,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -899,9 +899,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -913,9 +913,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -927,9 +927,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -941,9 +941,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -955,9 +955,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -969,9 +969,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -983,9 +997,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -997,9 +1025,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1011,9 +1039,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1025,9 +1053,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1039,9 +1067,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1053,9 +1081,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1066,10 +1094,24 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1081,9 +1123,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1095,9 +1137,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1109,9 +1151,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1123,9 +1165,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1137,9 +1179,9 @@
] ]
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.90.16", "version": "5.90.19",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.19.tgz",
"integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==", "integrity": "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@ -1147,12 +1189,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.90.16", "version": "5.90.19",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.19.tgz",
"integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", "integrity": "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.90.16" "@tanstack/query-core": "5.90.19"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -1365,9 +1407,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.11", "version": "2.9.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@ -1445,9 +1487,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001762", "version": "1.0.30001764",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2443,9 +2485,9 @@
} }
}, },
"node_modules/react-intersection-observer": { "node_modules/react-intersection-observer": {
"version": "10.0.0", "version": "10.0.2",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.0.tgz", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.2.tgz",
"integrity": "sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==", "integrity": "sha512-lAMzxVWrBko6SLd1jx6l84fVrzJu91hpxHlvD2as2Wec9mDCjdYXwc5xNOFBchpeBir0Y7AGBW+C/AYMa7CSFg==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
@ -2521,12 +2563,12 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.30.2", "version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.1" "@remix-run/router": "1.23.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -2536,13 +2578,13 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.30.2", "version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.1", "@remix-run/router": "1.23.2",
"react-router": "6.30.2" "react-router": "6.30.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -2553,9 +2595,9 @@
} }
}, },
"node_modules/react-window": { "node_modules/react-window": {
"version": "2.2.3", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.5.tgz",
"integrity": "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ==", "integrity": "sha512-6viWvPSZvVuMIe9hrl4IIZoVfO/npiqOb03m4Z9w+VihmVzBbiudUrtUqDpsWdKvd/Ai31TCR25CBcFFAUm28w==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",
@ -2618,9 +2660,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.54.0", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2634,28 +2676,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm-eabi": "4.55.1",
"@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-android-arm64": "4.55.1",
"@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.55.1",
"@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-darwin-x64": "4.55.1",
"@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.55.1",
"@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.55.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
"@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.55.1",
"@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.55.1",
"@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.55.1",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-loong64-musl": "4.55.1",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
"@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-ppc64-musl": "4.55.1",
"@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
"@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.55.1",
"@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.55.1",
"@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.55.1",
"@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.55.1",
"@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-openbsd-x64": "4.55.1",
"@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.55.1",
"@rollup/rollup-win32-x64-msvc": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.55.1",
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
"@rollup/rollup-win32-x64-gnu": "4.55.1",
"@rollup/rollup-win32-x64-msvc": "4.55.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -3052,9 +3097,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/zustand": { "node_modules/zustand": {
"version": "5.0.9", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.20.0" "node": ">=12.20.0"

View file

@ -254,10 +254,30 @@ function Schedule() {
enabled: !!searchTerm && isSearchMode, enabled: !!searchTerm && isSearchMode,
}); });
// Flatten search results // Flatten search results and normalize format to match monthly data
const searchResults = useMemo(() => { const searchResults = useMemo(() => {
if (!searchData?.pages) return []; if (!searchData?.pages) return [];
return searchData.pages.flatMap(page => page.schedules); return searchData.pages.flatMap(page =>
page.schedules.map(s => {
// datetime date + time
const dateTime = s.datetime || '';
const [date, time] = dateTime.includes('T')
? dateTime.split('T')
: [dateTime, null];
return {
...s,
date,
time,
// category flat
category_id: s.category?.id,
category_name: s.category?.name,
category_color: s.category?.color,
// members ( )
member_names: Array.isArray(s.members) ? s.members.join(',') : s.member_names,
};
})
);
}, [searchData]); }, [searchData]);
const searchTotal = searchData?.pages?.[0]?.total || 0; const searchTotal = searchData?.pages?.[0]?.total || 0;

View file

@ -5,13 +5,27 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: true, host: true,
port: 5173, port: 80,
allowedHosts: true, allowedHosts: true,
proxy: { proxy: {
// 개발 모드 (localhost:3000)
"/api": { "/api": {
target: "http://fromis9-backend:3000", target: "http://localhost:3000",
changeOrigin: true, changeOrigin: true,
}, },
"/docs": {
target: "http://localhost:3000",
changeOrigin: true,
},
// 배포 모드 (사용 시 위를 주석처리)
// "/api": {
// target: "http://fromis9-backend:3000",
// changeOrigin: true,
// },
// "/docs": {
// target: "http://fromis9-backend:3000",
// changeOrigin: true,
// },
}, },
}, },
}); });

View file

@ -1,34 +0,0 @@
-- 하얀 그리움 앨범 소개글
UPDATE albums SET description = '하얗게 내리는 겨울, 그 안에 다시 피어난 따뜻한 기억.
"하얀 그리움" .
, .
, .
, "그리움" .
,
.
, .
, .' WHERE id = 2;
-- From Our 20's 앨범 소개글
UPDATE albums SET description = '[From Our 20''s] "어디로든 갈 수 있고 무엇이든 될 수 있는 아름다운 20대의 이야기"
"여전히 서툴지만, 그래서 더 아름다운 시간.
, ."
6 [From Our 20''s] "20대의 우리" .
"LIKE YOU BETTER" 6 , 20 "프로미스나인" .
"프로미스나인" , .
, "20대의 우리" .
"20대의 삶" .
, , , , 20 , 믿.
.' WHERE id = 1;