diff --git a/Dockerfile b/Dockerfile index fd54062..b08172a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 WORKDIR /app - -# 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 - -# 백엔드 파일 복사 -COPY backend/ ./ - -# 프론트엔드 빌드 결과물 복사 -COPY --from=frontend-builder /frontend/dist ./dist - -EXPOSE 80 -CMD ["npm", "start"] +# ============================================ +# 배포 모드 (사용 시 위 개발 모드를 주석처리) +# ============================================ +# 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 +# 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"] diff --git a/backend/package-lock.json b/backend/package-lock.json index b88218a..07a4010 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,7 @@ "inko": "^1.1.1", "ioredis": "^5.4.2", "kiwi-nlp": "^0.22.1", + "meilisearch": "^0.44.0", "mysql2": "^3.12.0", "node-cron": "^3.0.3", "sharp": "^0.34.5" @@ -229,32 +230,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.970.0.tgz", - "integrity": "sha512-mDC792KkFzLZG9PS1Fv9b18lEzmSNBjAdweLJ83D2CZu6ved9+Pr/Dr+FRs0kSxqY+sUUUuIBmvDYHXY8E8EzA==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz", + "integrity": "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.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-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-location-constraint": "3.969.0", "@aws-sdk/middleware-logger": "3.969.0", "@aws-sdk/middleware-recursion-detection": "3.969.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/region-config-resolver": "3.969.0", "@aws-sdk/signature-v4-multi-region": "3.970.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.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/core": "^3.20.6", "@smithy/eventstream-serde-browser": "^4.2.8", @@ -295,9 +296,9 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.970.0.tgz", - "integrity": "sha512-ArmgnOsSCXN5VyIvZb4kSP5hpqlRRHolrMtKQ/0N8Hw4MTb7/IeYHSZzVPNzzkuX6gn5Aj8txoUnDPM8O7pc9g==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz", + "integrity": "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -311,7 +312,7 @@ "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.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/core": "^3.20.6", "@smithy/fetch-http-handler": "^5.3.9", @@ -418,19 +419,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.970.0.tgz", - "integrity": "sha512-L5R1hN1FY/xCmH65DOYMXl8zqCFiAq0bAq8tJZU32mGjIl1GzGeOkeDa9c461d81o7gsQeYzXyqFD3vXEbJ+kQ==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz", + "integrity": "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/credential-provider-env": "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-sso": "3.970.0", - "@aws-sdk/credential-provider-web-identity": "3.970.0", - "@aws-sdk/nested-clients": "3.970.0", + "@aws-sdk/credential-provider-sso": "3.971.0", + "@aws-sdk/credential-provider-web-identity": "3.971.0", + "@aws-sdk/nested-clients": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -443,13 +444,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.970.0.tgz", - "integrity": "sha512-C+1dcLr+p2E+9hbHyvrQTZ46Kj4vC2RoP6N935GEukHQa637ZjXs8VlyHJ2xTvbvwwLZQNiu56Cx7o/OFOqw1A==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz", + "integrity": "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==", "license": "Apache-2.0", "dependencies": { "@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", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", @@ -462,17 +463,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.970.0.tgz", - "integrity": "sha512-nMM0eeVuiLtw1taLRQ+H/H5Qp11rva8ILrzAQXSvlbDeVmbc7d8EeW5Q2xnCJu+3U+2JNZ1uxqIL22pB2sLEMA==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz", + "integrity": "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "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-sso": "3.970.0", - "@aws-sdk/credential-provider-web-identity": "3.970.0", + "@aws-sdk/credential-provider-sso": "3.971.0", + "@aws-sdk/credential-provider-web-identity": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -502,14 +503,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.970.0.tgz", - "integrity": "sha512-ROb+Aijw8nzkB14Nh2XRH861++SeTZykUzk427y8YtgTLxjAOjgDTchDUFW2Fx6GFWkSjqJ3sY7SZyb33IqyFw==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz", + "integrity": "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.970.0", + "@aws-sdk/client-sso": "3.971.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", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -521,13 +522,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.970.0.tgz", - "integrity": "sha512-r7tnYJJg+B6QvnsRHSW5vDol+ks6n+5jBZdCFdGyK63hjcMRMqHx59zEH8O47UR1PFv5hS2Q3uGz6HXvVtP40Q==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz", + "integrity": "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==", "license": "Apache-2.0", "dependencies": { "@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", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -572,9 +573,9 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.970.0.tgz", - "integrity": "sha512-mlKLwX0jWa5EwIvMjJAvVFL/zLAxB/fNLOg4hQCNCUf1qi+XxD+brDopXNPWeA8bSCnpvWfZrQd5yNksG6Fzqg==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.971.0.tgz", + "integrity": "sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -681,9 +682,9 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.969.0.tgz", - "integrity": "sha512-9wUYtd5ye4exygKHyl02lPVHUoAFlxxXoqvlw7u2sycfkK6uHLlwdsPru3MkMwj47ZSZs+lkyP/sVKXVMhuaAg==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.971.0.tgz", + "integrity": "sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.969.0", @@ -713,9 +714,9 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.970.0.tgz", - "integrity": "sha512-RIl8s4DCa31MXtRFw23iU90OqEoWuwQxiZOZshzsPtjyrunhHFjyZJEqb+vuQcYd1o22SMaYa3lPJRp64OH35Q==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz", + "integrity": "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -729,7 +730,7 @@ "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.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/core": "^3.20.6", "@smithy/fetch-http-handler": "^5.3.9", @@ -795,13 +796,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.970.0.tgz", - "integrity": "sha512-YO8KgJecxHIFMhfoP880q51VXFL9V1ELywK5yzVEqzyrwqoG93IUmnTygBUylQrfkbH+QqS0FxEdgwpP3fcwoQ==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz", + "integrity": "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==", "license": "Apache-2.0", "dependencies": { "@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", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -878,9 +879,9 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.970.0.tgz", - "integrity": "sha512-TNQpwIVD6SxMwkD+QKnaujKVyXy5ljN3O3jrI7nCHJ3GlJu5xJrd8yuBnanYCcrn3e2zwdfOh4d4zJAZvvIvVw==", + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz", + "integrity": "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "3.970.0", @@ -1894,9 +1895,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.6", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.6.tgz", - "integrity": "sha512-BpAffW1mIyRZongoKBbh3RgHG+JDHJek/8hjA/9LnPunM+ejorO6axkxCgwxCe4K//g/JdPeR9vROHDYr/hfnQ==", + "version": "3.20.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz", + "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -2114,12 +2115,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.7.tgz", - "integrity": "sha512-SCmhUG1UwtnEhF5Sxd8qk7bJwkj1BpFzFlHkXqKCEmDPLrRjJyTGM0EhqT7XBtDaDJjCfjRJQodgZcKDR843qg==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz", + "integrity": "sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.6", + "@smithy/core": "^3.20.7", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -2133,15 +2134,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.23", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.23.tgz", - "integrity": "sha512-lLEmkQj7I7oKfvZ1wsnToGJouLOtfkMXDKRA1Hi6F+mMp5O1N8GcVWmVeNgTtgZtd0OTXDTI2vpVQmeutydGew==", + "version": "4.4.24", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz", + "integrity": "sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.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/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -2308,13 +2309,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.8.tgz", - "integrity": "sha512-wcr3UEL26k7lLoyf9eVDZoD1nNY3Fa1gbNuOXvfxvVWLGkOVW+RYZgUUp/bXHryJfycIOQnBq9o1JAE00ax8HQ==", + "version": "4.10.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.9.tgz", + "integrity": "sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.6", - "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/core": "^3.20.7", + "@smithy/middleware-endpoint": "^4.4.8", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -2415,13 +2416,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.22", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.22.tgz", - "integrity": "sha512-O2WXr6ZRqPnbyoepb7pKcLt1QL6uRfFzGYJ9sGb5hMJQi7v/4RjRmCQa9mNjA0YiXqsc5lBmLXqJPhjM1Vjv5A==", + "version": "4.3.23", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz", + "integrity": "sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.10.9", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -2430,16 +2431,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.25", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.25.tgz", - "integrity": "sha512-7uMhppVNRbgNIpyUBVRfjGHxygP85wpXalRvn9DvUlCx4qgy1AB/uxOPSiDx/jFyrwD3/BypQhx1JK7f3yxrAw==", + "version": "4.2.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz", + "integrity": "sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.10.9", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -3484,6 +3485,12 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 2effe91..1723d67 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,7 @@ "inko": "^1.1.1", "ioredis": "^5.4.2", "kiwi-nlp": "^0.22.1", + "meilisearch": "^0.44.0", "mysql2": "^3.12.0", "node-cron": "^3.0.3", "sharp": "^0.34.5" diff --git a/backend/src/app.js b/backend/src/app.js index 0702a96..bec8bcc 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -12,6 +12,7 @@ import config from './config/index.js'; import dbPlugin from './plugins/db.js'; import redisPlugin from './plugins/redis.js'; import authPlugin from './plugins/auth.js'; +import meilisearchPlugin from './plugins/meilisearch.js'; import youtubeBotPlugin from './services/youtube/index.js'; import xBotPlugin from './services/x/index.js'; import schedulerPlugin from './plugins/scheduler.js'; @@ -44,6 +45,7 @@ export async function buildApp(opts = {}) { await fastify.register(dbPlugin); await fastify.register(redisPlugin); await fastify.register(authPlugin); + await fastify.register(meilisearchPlugin); await fastify.register(youtubeBotPlugin); await fastify.register(xBotPlugin); await fastify.register(schedulerPlugin); diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 2e50798..eaa007e 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -30,4 +30,8 @@ export default { bucket: process.env.RUSTFS_BUCKET || 'fromis-9', publicUrl: process.env.RUSTFS_PUBLIC_URL, }, + meilisearch: { + host: process.env.MEILI_HOST || 'http://fromis9-meilisearch:7700', + apiKey: process.env.MEILI_MASTER_KEY, + }, }; diff --git a/backend/src/plugins/meilisearch.js b/backend/src/plugins/meilisearch.js new file mode 100644 index 0000000..b758700 --- /dev/null +++ b/backend/src/plugins/meilisearch.js @@ -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'], +}); diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index 0b9add8..25007ed 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -3,103 +3,64 @@ * GET: 공개, POST/PUT/DELETE: 인증 필요 */ import suggestionsRoutes from './suggestions.js'; +import { searchSchedules, syncAllSchedules } from '../../services/meilisearch/index.js'; export default async function schedulesRoutes(fastify) { - const { db } = fastify; + const { db, meilisearch, redis } = fastify; // 추천 검색어 라우트 등록 fastify.register(suggestionsRoutes, { prefix: '/suggestions' }); /** * GET /api/schedules - * 월별 일정 목록 조회 - * @query year - 년도 (필수) - * @query month - 월 (필수) + * 검색 모드: search 파라미터가 있으면 Meilisearch 검색 + * 월별 조회 모드: year, month 파라미터로 월별 조회 */ fastify.get('/', { schema: { tags: ['schedules'], - summary: '월별 일정 목록 조회', + summary: '일정 조회 (검색 또는 월별)', querystring: { type: 'object', - required: ['year', 'month'], properties: { + search: { type: 'string', description: '검색어' }, year: { type: 'integer', description: '년도' }, month: { type: 'integer', minimum: 1, maximum: 12, description: '월' }, + offset: { type: 'integer', default: 0, description: '페이지 오프셋' }, + limit: { type: 'integer', default: 100, description: '결과 개수' }, }, }, }, }, 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) { - 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`; - const endDate = new Date(year, month, 0).toISOString().split('T')[0]; + return await handleMonthlySchedules(db, parseInt(year), parseInt(month)); + }); - 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; }); } + +/** + * 검색 처리 + */ +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; +} diff --git a/backend/src/services/meilisearch/index.js b/backend/src/services/meilisearch/index.js new file mode 100644 index 0000000..98cdfdf --- /dev/null +++ b/backend/src/services/meilisearch/index.js @@ -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; + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 54bf68f..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index a862e80..e71d5af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,19 @@ services: - fromis9-web: + fromis9-frontend: build: . container_name: fromis9-frontend labels: - "com.centurylinklabs.watchtower.enable=false" env_file: - .env + # 개발 모드 + volumes: + - ./backend:/app/backend + - ./frontend:/app/frontend + - backend_modules:/app/backend/node_modules + - frontend_modules:/app/frontend/node_modules + # 배포 모드 (사용 시 위 volumes를 주석처리) + # volumes: [] networks: - app - db @@ -25,10 +33,16 @@ services: redis: image: redis:7-alpine container_name: fromis9-redis + volumes: + - ./redis_data:/data networks: - app restart: unless-stopped +volumes: + backend_modules: + frontend_modules: + networks: app: external: true diff --git a/download_photos.sh b/download_photos.sh deleted file mode 100755 index 6bee747..0000000 --- a/download_photos.sh +++ /dev/null @@ -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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f67c5d4..56e43f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,13 +52,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -67,9 +67,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -77,21 +77,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -108,14 +108,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -125,13 +125,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -152,29 +152,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -184,9 +184,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -224,27 +224,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -286,33 +286,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -320,9 +320,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -813,9 +813,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -829,9 +829,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -843,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -857,9 +857,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -871,9 +871,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -885,9 +885,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -899,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -913,9 +913,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -927,9 +927,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -941,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -955,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -969,9 +969,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "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": [ "loong64" ], @@ -983,9 +997,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "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": [ "ppc64" ], @@ -997,9 +1025,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "cpu": [ "riscv64" ], @@ -1011,9 +1039,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -1025,9 +1053,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -1039,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -1053,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -1066,10 +1094,24 @@ "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": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "cpu": [ "arm64" ], @@ -1081,9 +1123,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -1095,9 +1137,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -1109,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ "x64" ], @@ -1123,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -1137,9 +1179,9 @@ ] }, "node_modules/@tanstack/query-core": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", - "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==", + "version": "5.90.19", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.19.tgz", + "integrity": "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==", "license": "MIT", "funding": { "type": "github", @@ -1147,12 +1189,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", - "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", + "version": "5.90.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.19.tgz", + "integrity": "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.16" + "@tanstack/query-core": "5.90.19" }, "funding": { "type": "github", @@ -1365,9 +1407,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1445,9 +1487,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "dev": true, "funding": [ { @@ -2443,9 +2485,9 @@ } }, "node_modules/react-intersection-observer": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.0.tgz", - "integrity": "sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.2.tgz", + "integrity": "sha512-lAMzxVWrBko6SLd1jx6l84fVrzJu91hpxHlvD2as2Wec9mDCjdYXwc5xNOFBchpeBir0Y7AGBW+C/AYMa7CSFg==", "license": "MIT", "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -2521,12 +2563,12 @@ } }, "node_modules/react-router": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -2536,13 +2578,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1", - "react-router": "6.30.2" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -2553,9 +2595,9 @@ } }, "node_modules/react-window": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.3.tgz", - "integrity": "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.5.tgz", + "integrity": "sha512-6viWvPSZvVuMIe9hrl4IIZoVfO/npiqOb03m4Z9w+VihmVzBbiudUrtUqDpsWdKvd/Ai31TCR25CBcFFAUm28w==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -2618,9 +2660,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2634,28 +2676,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@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" } }, @@ -3052,9 +3097,9 @@ "license": "ISC" }, "node_modules/zustand": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", - "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/frontend/src/pages/pc/public/Schedule.jsx b/frontend/src/pages/pc/public/Schedule.jsx index 10fff83..b092379 100644 --- a/frontend/src/pages/pc/public/Schedule.jsx +++ b/frontend/src/pages/pc/public/Schedule.jsx @@ -254,10 +254,30 @@ function Schedule() { enabled: !!searchTerm && isSearchMode, }); - // Flatten search results + // Flatten search results and normalize format to match monthly data const searchResults = useMemo(() => { 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]); const searchTotal = searchData?.pages?.[0]?.total || 0; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 0f7834e..35b6f0b 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,13 +5,27 @@ export default defineConfig({ plugins: [react()], server: { host: true, - port: 5173, + port: 80, allowedHosts: true, proxy: { + // 개발 모드 (localhost:3000) "/api": { - target: "http://fromis9-backend:3000", + target: "http://localhost:3000", 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, + // }, }, }, }); diff --git a/update_descriptions.sql b/update_descriptions.sql deleted file mode 100644 index ed0d48d..0000000 --- a/update_descriptions.sql +++ /dev/null @@ -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;