diff --git a/.github/workflows/devfactory-homepage.yml b/.github/workflows/devfactory-homepage.yml new file mode 100644 index 0000000..b7b99a9 --- /dev/null +++ b/.github/workflows/devfactory-homepage.yml @@ -0,0 +1,43 @@ +name: ๐Ÿš€ DevFactory Homepage Deploy +run-name: ๐Ÿš€ Deploying to Production by @${{ github.actor }} + +on: + push: + branches: + - main + paths: + - 'platform/**' + - '.github/workflows/devfactory-homepage.yml' + workflow_dispatch: + +# ๊ฐ™์€ ๋ธŒ๋žœ์น˜ ๋™์‹œ ์‹คํ–‰ ์‹œ ์ด์ „ ์žก ์ทจ์†Œ(๊ฒฝ์Ÿ ๋ฐฐํฌ ๋ฐฉ์ง€) +concurrency: + group: DevFactory-homepage-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy-prod: + if: github.ref_name == 'main' + name: ๐Ÿš€ Deploy DF-platform (Production) + runs-on: oracle + environment: platform + defaults: + run: + working-directory: ./platform + steps: + - uses: actions/checkout@v4 + + - name: Write .env (prod) + run: | + cat > .env <<'EOF' + APP_HOST=${{ vars.APP_HOST }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + EOF + + - name: Build & up (prod) + run: | + set -euxo pipefail + docker compose -p df-platform-main config -q + docker compose -p df-platform-main down --remove-orphans + docker compose -p df-platform-main up -d --build --remove-orphans + docker image prune -f --filter "label=org.pseudolab.project=devfactory-platform" diff --git a/README.en.md b/README.en.md index a76e116..ff0f94a 100644 --- a/README.en.md +++ b/README.en.md @@ -65,7 +65,7 @@ Highlight activities of DevFactory ๐Ÿค— Soohyun Kim
-
+

@@ -76,7 +76,7 @@ Highlight activities of DevFactory ๐Ÿค— Yesin Kim
-
+

@@ -86,7 +86,7 @@ Highlight activities of DevFactory ๐Ÿค— Seungkyu Kim
-
+

diff --git a/README.md b/README.md index 05a8142..ea391f9 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ ## ๐ŸŒŸ ํ”„๋กœ์ ํŠธ DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค ๐Ÿค— * **๐Ÿณ ๊ธฐ์ˆ  ํŠœํ† ๋ฆฌ์–ผ (Tutorials)** - * Docker, Git, LLM ๋“ฑ ์‹ค์Šต ์ค‘์‹ฌ์˜ ์ฝ˜ํ…์ธ ์™€ ์˜คํ”„๋ผ์ธ ์›Œํฌ์ˆ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค. + * Docker, Git, LLM ๋“ฑ ์‹ค์Šต ์ค‘์‹ฌ์˜ ์˜จ๋ผ์ธ ์ฝ˜ํ…์ธ ์™€ ์˜คํ”„๋ผ์ธ ์›Œํฌ์ˆ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค. * [๐Ÿ”— ํŠœํ† ๋ฆฌ์–ผ ๋ณด๊ธฐ](https://pseudo-lab.github.io/DevFactory/intro.html) * **๐ŸŽฎ ๋„คํŠธ์›Œํ‚น ํ”„๋กœ๊ทธ๋žจ (BINGO)** @@ -60,7 +60,7 @@ DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค ๐Ÿค— ๊น€์ˆ˜ํ˜„
-
+

@@ -71,7 +71,7 @@ DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค ๐Ÿค— ๊น€์˜ˆ์‹ 
-
+

@@ -81,7 +81,7 @@ DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค ๐Ÿค— ๊น€์Šน๊ทœ
-
+

diff --git a/platform/.env.example b/platform/.env.example new file mode 100644 index 0000000..8b0e869 --- /dev/null +++ b/platform/.env.example @@ -0,0 +1,5 @@ +# Domain Setting +APP_HOST=your-domain.com + +# Database Setting +DATABASE_URL=postgresql://user:pass@devfactory-postgres:5432/dbname diff --git a/platform/.gitignore b/platform/.gitignore new file mode 100644 index 0000000..a7fce31 --- /dev/null +++ b/platform/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules/ +dist/ +build/ +.DS_Store \ No newline at end of file diff --git a/platform/PLATFORM.md b/platform/PLATFORM.md new file mode 100644 index 0000000..29eddad --- /dev/null +++ b/platform/PLATFORM.md @@ -0,0 +1,66 @@ +# DevFactory Unified Platform + +์†Œ๊ฐœ ํŽ˜์ด์ง€์™€ ๋ฐฑ์—”๋“œ API๊ฐ€ ํ†ตํ•ฉ๋œ DevFactory์˜ ๋ฉ”์ธ ํ”Œ๋žซํผ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. +๋ณธ ์„ค์ •์€ **Traefik ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ**๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์„ ์ „์ œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + +### 1. Production Build +๋กœ์ปฌ ๋˜๋Š” ์„œ๋ฒ„์—์„œ ํ”„๋ก ํŠธ์—”๋“œ ์—์…‹์„ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค. +```bash +cd platform/frontend +npm install +npm run build +``` +๊ฒฐ๊ณผ๋ฌผ์€ `platform/frontend/dist` ๋””๋ ‰ํ† ๋ฆฌ์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. + +### 2. Deployment Options + +#### Option A: Static Hosting (GitHub Pages / Vercel) +๋‹จ์ˆœ ์›น ํŽ˜์ด์ง€(Frontend)๋งŒ ๋ฐฐํฌํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +1. `platform/frontend`์—์„œ `npm run build`๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. +2. `dist` ํด๋” ๋‚ด์˜ ํŒŒ์ผ๋“ค์„ ๋ฐฐํฌ ์„œ๋น„์Šค์— ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. +> **์ฐธ๊ณ **: ์ด ๋ฐฉ์‹์œผ๋กœ๋Š” ์ž์ฒด DB ๋ฐฉ๋ฌธ์ž ์ถ”์  ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + +#### Option B: Unified Platform (Full-Stack / Docker) +๋ฐฉ๋ฌธ์ž ์ถ”์  ๊ธฐ๋Šฅ์„ ํฌํ•จํ•œ ์ „์ฒด ํ”Œ๋žซํผ ๋ฐฐํฌ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. **(๊ถŒ์žฅ)** + +### 3. Environment Setup +`platform` ๋””๋ ‰ํ† ๋ฆฌ ๋ฃจํŠธ์— `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ์„œ๋ฒ„ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. +```bash +# .env ํŒŒ์ผ ์˜ˆ์‹œ +APP_HOST=intro.pseudolab-devfactory.com + +# Database +DATABASE_URL=postgresql://user:pass@devfactory-postgres:5432/dbname +``` + +### 3. Traefik ๊ธฐ๋ฐ˜ ๋ฐฐํฌ +Docker Compose๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋น„์Šค๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. +```bash +cd platform +# ์ „์ฒด ์„œ๋น„์Šค ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ +docker-compose up -d --build +``` + +#### ๋ฐฐํฌ ํ™•์ธ +- **Frontend (Web)**: `https://` +- **Backend (API)**: `https:///api/health` + +### 4. Database Schema +์‚ฌ์šฉ ์ค‘์ธ `logging.access_log` ํ…Œ์ด๋ธ”์˜ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. +```sql +CREATE TABLE logging.access_log ( + id SERIAL PRIMARY KEY, + ts TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + path TEXT NOT NULL, + method TEXT NOT NULL, + status INTEGER NOT NULL, + latency_ms INTEGER, + ip_hash TEXT, + user_agent TEXT, + referrer TEXT +); +``` + +### 5. Verification +- Traefik ๋Œ€์‹œ๋ณด๋“œ์—์„œ `df-platform` ๊ด€๋ จ ๋ผ์šฐํ„ฐ๊ฐ€ ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. +- ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์ดํŠธ ์ ‘์† ์‹œ HTTPS ์ƒ์‹œ ์—ฐ๊ฒฐ ๋ฐ ๋ฐฉ๋ฌธ ๊ธฐ๋ก ์ ์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜์„ธ์š”. diff --git a/platform/docker-compose.dev.yml b/platform/docker-compose.dev.yml new file mode 100644 index 0000000..8213292 --- /dev/null +++ b/platform/docker-compose.dev.yml @@ -0,0 +1,43 @@ +services: + # Development Overrides with Traefik Support + frontend: + build: + context: ./frontend + target: build-stage + image: devfactory-frontend:dev + command: npm run dev -- --host 0.0.0.0 + volumes: + - ./frontend:/app + - /app/node_modules + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.df-platform-web.rule=Host(`${APP_HOST}`) + - traefik.http.routers.df-platform-web.entrypoints=websecure + - traefik.http.routers.df-platform-web.tls=true + - traefik.http.routers.df-platform-web.tls.certresolver=le + - traefik.http.services.df-platform-web.loadbalancer.server.port=5173 + # HTTP โ†’ HTTPS redirect (re-adding to ensure full functionality) + - traefik.http.routers.df-platform-web-http.rule=Host(`${APP_HOST}`) + - traefik.http.routers.df-platform-web-http.entrypoints=web + - traefik.http.routers.df-platform-web-http.middlewares=redirect-to-https@file + - traefik.http.routers.df-platform-web-http.service=df-platform-web + + server: + command: npm run dev + volumes: + - ./server:/app + - /app/node_modules + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.df-platform-api.rule=Host(`${APP_HOST}`) && PathPrefix(`/api`) + - traefik.http.routers.df-platform-api.entrypoints=websecure + - traefik.http.routers.df-platform-api.tls=true + - traefik.http.routers.df-platform-api.tls.certresolver=le + - traefik.http.services.df-platform-api.loadbalancer.server.port=3000 + # HTTP โ†’ HTTPS redirect + - traefik.http.routers.df-platform-api-http.rule=Host(`${APP_HOST}`) && PathPrefix(`/api`) + - traefik.http.routers.df-platform-api-http.entrypoints=web + - traefik.http.routers.df-platform-api-http.middlewares=redirect-to-https@file + - traefik.http.routers.df-platform-api-http.service=df-platform-api diff --git a/platform/docker-compose.yml b/platform/docker-compose.yml new file mode 100644 index 0000000..96d4834 --- /dev/null +++ b/platform/docker-compose.yml @@ -0,0 +1,61 @@ +services: + # Frontend static server (Nginx) + frontend: + build: + context: ./frontend + labels: + - "org.pseudolab.project=devfactory-platform" + container_name: devfactory-frontend + restart: unless-stopped + networks: + - traefik + - internal + labels: + - traefik.enable=true + - traefik.docker.network=traefik + # --- Traefik Router (frontend) --- + - traefik.http.routers.df-platform-web.rule=Host(`${APP_HOST}`) + - traefik.http.routers.df-platform-web.entrypoints=websecure + - traefik.http.routers.df-platform-web.tls=true + - traefik.http.routers.df-platform-web.tls.certresolver=le + - traefik.http.services.df-platform-web.loadbalancer.server.port=80 + # HTTP โ†’ HTTPS redirect + - traefik.http.routers.df-platform-web-http.rule=Host(`${APP_HOST}`) + - traefik.http.routers.df-platform-web-http.entrypoints=web + - traefik.http.routers.df-platform-web-http.middlewares=redirect-to-https@file + - traefik.http.routers.df-platform-web-http.service=df-platform-web + + # Backend API server + server: + build: + context: ./server + labels: + - "org.pseudolab.project=devfactory-platform" + container_name: devfactory-api + restart: unless-stopped + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=3000 + networks: + - traefik + - internal + labels: + - traefik.enable=true + - traefik.docker.network=traefik + # --- Traefik Router (backend) --- + - traefik.http.routers.df-platform-api.rule=Host(`${APP_HOST}`) && PathPrefix(`/api`) + - traefik.http.routers.df-platform-api.entrypoints=websecure + - traefik.http.routers.df-platform-api.tls=true + - traefik.http.routers.df-platform-api.tls.certresolver=le + - traefik.http.services.df-platform-api.loadbalancer.server.port=3000 + # HTTP โ†’ HTTPS redirect + - traefik.http.routers.df-platform-api-http.rule=Host(`${APP_HOST}`) && PathPrefix(`/api`) + - traefik.http.routers.df-platform-api-http.entrypoints=web + - traefik.http.routers.df-platform-api-http.middlewares=redirect-to-https@file + - traefik.http.routers.df-platform-api-http.service=df-platform-api + +networks: + traefik: + external: true + internal: + driver: bridge diff --git a/platform/frontend/Dockerfile b/platform/frontend/Dockerfile new file mode 100644 index 0000000..236c2e9 --- /dev/null +++ b/platform/frontend/Dockerfile @@ -0,0 +1,23 @@ +# Stage 1: Build stage +FROM node:20-alpine as build-stage + +WORKDIR /app + +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install + +# Copy project files and build +COPY . . +RUN npm run build + +# Stage 2: Serve stage +FROM nginx:stable-alpine as production-stage + +# Copy built assets from build-stage to nginx +COPY --from=build-stage /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/platform/frontend/index.html b/platform/frontend/index.html new file mode 100644 index 0000000..accb9d4 --- /dev/null +++ b/platform/frontend/index.html @@ -0,0 +1,237 @@ + + + + + + + DevFactory | Team Members + + + + + +
+
+ + + DevFactory + + + +
+
+ + +
+ + +
+ +
+
+ +
+ +
+
+

Bridging AI Research
through Engineering

+

+ ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ์˜ ๋‹ค์–‘ํ•œ AI ์—ฐ๊ตฌ๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ๊ฐ€์ง„ ๊ฐœ๋ฐœ ์—ญ๋Ÿ‰์„ ๋ฐ”ํƒ•์œผ๋กœ
+ ์„ธ์ƒ๊ณผ ์—ฐ๊ฒฐํ•˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +

+ +
+
+
โ†“
+ Scroll Down +
+
+ + +
+
+

Projects

+

DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค

+
+
+
+
๐Ÿณ
+

๊ธฐ์ˆ  ํŠœํ† ๋ฆฌ์–ผ

+

Docker, Git, LLM ๋“ฑ ์‹ค์Šต ์ค‘์‹ฌ์˜ ์˜จ๋ผ์ธ ์ฝ˜ํ…์ธ ์™€ ์˜คํ”„๋ผ์ธ ์›Œํฌ์ˆ

+ ํŠœํ† ๋ฆฌ์–ผ ๋ณด๊ธฐ โ†’ +
+
+
๐ŸŽฎ
+

BINGO

+

ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜์˜ ๋น™๊ณ  ๊ฒŒ์ž„์„ ํ†ตํ•œ ๋„คํŠธ์›Œํ‚น ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

+ ์›น์‚ฌ์ดํŠธ ํ™•์ธ โ†’ +
+
+
๐ŸŽฎ
+

์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ

+

๊ฐ„๋‹จํ•œ ํ€ด์ฆˆ๋ฅผ ํ†ตํ•ด ์„œ๋กœ๋ฅผ ์•Œ์•„๊ฐ€๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

+ ์›น์‚ฌ์ดํŠธ ํ™•์ธ + โ†’ +
+
+
๐Ÿ“œ
+

์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ

+

๊ฐ€์งœ์—ฐ๊ตฌ์†Œ์˜ ์„ฑ์žฅ์„ ๊ธฐ๋กํ•˜๋Š” ํ™œ๋™ ์ˆ˜๋ฃŒ์ฆ ๊ด€๋ฆฌ ์„œ๋น„์Šค

+ ์›น์‚ฌ์ดํŠธ ํ™•์ธ โ†’ +
+
+
๐Ÿค–
+

JobPT

+

AI ๊ธฐ๋ฐ˜ ๊ฐœ์ธํ™” ์ทจ์—… ์ง€์› ์†”๋ฃจ์…˜ - ์ด๋ ฅ์„œ ๋ถ„์„๋ถ€ํ„ฐ ๋ฉด์ ‘ ํ”ผ๋“œ๋ฐฑ๊นŒ์ง€

+ ์„œ๋น„์Šค ๋ฐ”๋กœ๊ฐ€๊ธฐ โ†’ +
+
+
+
โ†“
+ Team Members +
+
+ + +
+
+
DEVFACTORY
+
+ +
+

Team Members

+

DevFactory๋ฅผ ํ•จ๊ป˜ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ํŒ€์›๋“ค์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค

+
+ + +
+ +
+ + +
+ +
+ +
+
PROFILE
+
BUILDER
+

NAME

+

+ DevFactory๋ฅผ ์ด๋Œ์–ด๊ฐ€๋Š” ํ•ต์‹ฌ ๋ฉค๋ฒ„์˜ ์†Œ๊ฐœ๊ฐ€ ์ด๊ณณ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +

+
+
+
Position
+
-
+
+
+
Contribution
+
-
+
+
+
Generation
+
-
+
+
+ +
+ + +
+
+ Member Visual +
+
+ +
+
+ +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/platform/frontend/main.js b/platform/frontend/main.js new file mode 100644 index 0000000..b51e8c7 --- /dev/null +++ b/platform/frontend/main.js @@ -0,0 +1,347 @@ +// Mobile Menu Toggle +const menuToggle = document.getElementById('menu-toggle'); +const closeBtn = document.getElementById('close-menu'); +const menuOverlay = document.getElementById('mobile-menu-overlay'); +const mobileLinks = document.querySelectorAll('.mobile-nav-link'); + +function openMenu() { + menuOverlay.classList.add('active'); + document.body.style.overflow = 'hidden'; // Prevent scroll +} + +function closeMenu() { + menuOverlay.classList.remove('active'); + document.body.style.overflow = ''; // Restore scroll +} + +if (menuToggle) menuToggle.addEventListener('click', openMenu); +if (closeBtn) closeBtn.addEventListener('click', closeMenu); + +mobileLinks.forEach(link => { + link.addEventListener('click', closeMenu); +}); + +// Member Data & Core Functions +const teamMembers = [ + { + name: "๊น€์ˆ˜ํ˜„", + enName: "KIM SOOHYUN", + role: "BUILDER", + position: "TPM / INFRA", + status: "Active", + contribution: "Builder", + gen: "10th, 11th", + desc: "DevFactory์˜ ๋นŒ๋”๋กœ์„œ ํด๋ผ์šฐ๋“œ ์ธํ”„๋ผ์™€ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ๋ฅผ ์ด๊ด„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ์˜ ๊ฐœ๋ฐœ ๋ฌธํ™”๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์šด์˜ํ•ฉ๋‹ˆ๋‹ค.", + image: "/members/soohyun.png", + github: "https://github.com/soohyunme", + linkedin: "https://www.linkedin.com/in/soohyun-dev" + }, + { + name: "๊น€์˜ˆ์‹ ", + enName: "KIM YESIN", + role: "BUILDER", + position: "Backend Developer", + status: "Active", + contribution: "Builder", + gen: "10th, 11th", + desc: "๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ๋‹ด๋‹นํ•˜๋ฉฐ ๊ฒฌ๊ณ ํ•œ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. 10๊ธฐ์™€ 11๊ธฐ ๋นŒ๋”๋กœ์„œ ํ™œ๋™ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.", + image: "/members/yesin.jpg", + github: "https://github.com/yesinkim", + linkedin: "https://www.linkedin.com/in/bailando/" + }, + { + name: "๊น€์Šน๊ทœ", + enName: "KIM SEUNGKYU", + role: "BUILDER", + position: "Frontend Developer", + status: "Active", + contribution: "Builder", + gen: "10th, 11th", + desc: "์‚ฌ์šฉ์ž ์ค‘์‹ฌ์˜ ํ”„๋ก ํŠธ์—”๋“œ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. DevFactory์˜ ๋‹ค์–‘ํ•œ ์›น ์„œ๋น„์Šค๋ฅผ ๋งค๋ ฅ์ ์œผ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.", + image: "/members/seungkyu.jpg", + github: "https://github.com/ed-kyu", + linkedin: "https://www.linkedin.com/in/seungkyu-kim-9088a21b1/" + }, + { + name: "ํ™ฉ์œคํฌ", + enName: "HWANG YUNHEE", + role: "BUILDER", + position: "Product Owner", + status: "Active", + contribution: "Builder", + gen: "11th", + desc: "ํ”„๋กœ๋•ํŠธ์˜ ๋ฐฉํ–ฅ์„ฑ์„ ์ œ์‹œํ•˜๊ณ  ๊ธฐํš์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. 11๊ธฐ ๋นŒ๋”๋กœ์„œ DevFactory์˜ ์„ฑ์žฅ์„ ์ด๋•๋‹ˆ๋‹ค.", + image: "https://github.com/yunhee1.png", + github: "https://github.com/yunhee1", + linkedin: "https://www.linkedin.com/in/uni-po/" + }, + { + name: "์ตœ์œ ์ง„", + enName: "CHOI YUJIN", + role: "RUNNER", + position: "Product Owner", + status: "Active", + contribution: "Runner", + gen: "11th", + desc: "11๊ธฐ ๋Ÿฌ๋„ˆ๋กœ์„œ ํ”„๋กœ๋•ํŠธ ์˜ค๋„ˆ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ ์ƒˆ๋กœ์šด ์•„์ด๋””์–ด๋ฅผ ๊ตฌ์ฒดํ™”ํ•ฉ๋‹ˆ๋‹ค.", + image: "https://github.com/yujin37.png", + github: "https://github.com/yujin37", + linkedin: "https://www.linkedin.com/in/yujin37/" + }, + { + name: "์„์ข…์ผ", + enName: "SEOK JONGIL", + role: "RUNNER", + position: "Frontend Developer", + status: "Active", + contribution: "Runner", + gen: "11th", + desc: "์›น ๊ธฐ์ˆ ์— ์—ด์ •์„ ๊ฐ€์ง„ 11๊ธฐ ๋Ÿฌ๋„ˆ์ž…๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์„œ ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.", + image: "https://github.com/daclouds.png", + github: "https://github.com/daclouds", + linkedin: "https://www.linkedin.com/in/daclouds/" + }, + { + name: "์ •ํ˜„์ค€", + enName: "JUNG HYUNJUN", + role: "RUNNER", + position: "Backend Developer", + status: "Active", + contribution: "Runner", + gen: "11th", + desc: "์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋กœ์ง๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋‹ค๋ฃจ๋Š” 11๊ธฐ ๋Ÿฌ๋„ˆ์ž…๋‹ˆ๋‹ค. ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค ์ œ๊ณต์— ํž˜์”๋‹ˆ๋‹ค.", + image: "https://github.com/hu6r1s.png", + github: "https://github.com/hu6r1s", + linkedin: "https://www.linkedin.com/in/hu6r1s/" + }, + { + name: "ํ•œ๋‚˜์—ฐ", + enName: "HAN NAYEON", + role: "RUNNER", + position: "Backend Developer", + status: "Active", + contribution: "Runner", + gen: "11th", + desc: "11๊ธฐ ๋Ÿฌ๋„ˆ๋กœ์„œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์— ์ฐธ์—ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํšจ์œจ์ ์ธ ์ฝ”๋“œ์™€ ํ˜‘์—…์„ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.", + image: "https://github.com/HanNayeoniee.png", + github: "https://github.com/HanNayeoniee", + linkedin: "https://www.linkedin.com/in/nayeon-han/" + }, +]; + +let currentIndex = 0; + +function updateDisplay(index) { + const member = teamMembers[index]; + + // Select elements + const visual = document.getElementById('member-visual'); + const name = document.getElementById('member-name'); + const bgName = document.getElementById('member-bg-name'); + const role = document.getElementById('role-tag'); + const desc = document.getElementById('member-desc'); + const pos = document.getElementById('stat-position'); + const contr = document.getElementById('stat-contribution'); + const gen = document.getElementById('stat-gen'); + const socialContainer = document.getElementById('member-social'); + + // Add animation classes + const panel = document.getElementById('info-panel'); + const visualContainer = visual.parentElement; + + panel.classList.remove('fade-in'); + visualContainer.classList.remove('fade-in'); + if (bgName) bgName.classList.remove('fade-in'); + + // Trigger reflow to restart animation + void panel.offsetWidth; + void visualContainer.offsetWidth; + if (bgName) void bgName.offsetWidth; + + // Update content + visual.src = member.image; + name.textContent = member.name; + if (bgName) bgName.textContent = member.enName; + role.textContent = member.role; + + // Add runner class for styling (Builder keeps default cyan) + role.className = 'role-tag'; + if (member.role === 'RUNNER') { + role.classList.add('runner'); + } + + desc.textContent = member.desc; + pos.textContent = member.position; + contr.textContent = member.contribution; + gen.textContent = member.gen; + + // Update social links (restore bar buttons) + socialContainer.innerHTML = ''; + if (member.github) { + const githubLink = document.createElement('a'); + githubLink.href = member.github; + githubLink.target = '_blank'; + githubLink.className = 'social-btn'; + githubLink.title = 'GitHub Profile'; + githubLink.innerHTML = ` + + + + GitHub + `; + socialContainer.appendChild(githubLink); + } + if (member.linkedin) { + const linkedinLink = document.createElement('a'); + linkedinLink.href = member.linkedin; + linkedinLink.target = '_blank'; + linkedinLink.className = 'social-btn'; + linkedinLink.title = 'LinkedIn Profile'; + linkedinLink.innerHTML = ` + + + + LinkedIn + `; + socialContainer.appendChild(linkedinLink); + } + + panel.classList.add('fade-in'); + visualContainer.classList.add('fade-in'); + if (bgName) bgName.classList.add('fade-in'); + + // Highlight thumbnail + document.querySelectorAll('.thumb-btn').forEach((btn, i) => { + if (i === index) btn.classList.add('active'); + else btn.classList.remove('active'); + }); +} + +function initThumbnails() { + const container = document.getElementById('thumb-grid'); + teamMembers.forEach((member, index) => { + const btn = document.createElement('button'); + btn.className = 'thumb-btn'; + if (index === 0) btn.classList.add('active'); + + const img = document.createElement('img'); + img.src = member.image; + img.alt = member.name; + + btn.appendChild(img); + btn.addEventListener('click', () => { + if (currentIndex === index) return; + currentIndex = index; + updateDisplay(index); + }); + + container.appendChild(btn); + }); +} + +function updateTime() { + const timeEl = document.getElementById('system-time'); + if (timeEl) { + const now = new Date(); + timeEl.textContent = now.toLocaleTimeString('en-US', { hour12: false }); + } +} + +function nextMember() { + currentIndex = (currentIndex + 1) % teamMembers.length; + updateDisplay(currentIndex); +} + +function prevMember() { + currentIndex = (currentIndex - 1 + teamMembers.length) % teamMembers.length; + updateDisplay(currentIndex); +} + +// Initialize +initThumbnails(); +updateDisplay(0); +setInterval(updateTime, 1000); +updateTime(); + +// Visitor Tracking +async function logVisit() { + try { + await fetch('/api/stats/visit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: window.location.pathname + window.location.hash, + userAgent: navigator.userAgent + }) + }); + } catch (err) { + console.warn('Visitor tracking failed (server probably not running yet)'); + } +} + +logVisit(); + +// Prev/Next Navigation +document.getElementById('prev-member').addEventListener('click', prevMember); +document.getElementById('next-member').addEventListener('click', nextMember); + +// Cross-Platform Swipe Navigation (Touch & Mouse) +let startX = 0; +let isDragging = false; + +const teamSect = document.getElementById('team'); + +// Touch Events +teamSect.addEventListener('touchstart', e => { + startX = e.changedTouches[0].screenX; +}, { passive: true }); + +teamSect.addEventListener('touchend', e => { + const endX = e.changedTouches[0].screenX; + handleSwipe(startX, endX); +}, { passive: true }); + +// Mouse Events for PC +teamSect.addEventListener('mousedown', e => { + startX = e.screenX; + isDragging = true; +}); + +teamSect.addEventListener('mouseup', e => { + if (!isDragging) return; + const endX = e.screenX; + handleSwipe(startX, endX); + isDragging = false; +}); + +teamSect.addEventListener('mouseleave', () => { + isDragging = false; +}); + +function handleSwipe(sX, eX) { + const swipeThreshold = 50; + if (eX < sX - swipeThreshold) { + animateArrowFeedback('next-member'); + nextMember(); // Swipe Left -> Next + } else if (eX > sX + swipeThreshold) { + animateArrowFeedback('prev-member'); + prevMember(); // Swipe Right -> Prev + } +} + +function animateArrowFeedback(buttonId) { + const btn = document.getElementById(buttonId); + if (btn) { + btn.classList.add('active'); + setTimeout(() => btn.classList.remove('active'), 200); + } +} + +// Global Keyboard Navigation +document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') { + prevMember(); + } else if (e.key === 'ArrowRight') { + nextMember(); + } +}); diff --git a/platform/frontend/package-lock.json b/platform/frontend/package-lock.json new file mode 100644 index 0000000..fd1ab03 --- /dev/null +++ b/platform/frontend/package-lock.json @@ -0,0 +1,551 @@ +{ + "name": "intro_page", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "intro_page", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "vite": "^4.5.14" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/platform/frontend/package.json b/platform/frontend/package.json new file mode 100644 index 0000000..d549f18 --- /dev/null +++ b/platform/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "intro_page", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "vite": "^4.5.14" + } +} \ No newline at end of file diff --git a/platform/frontend/public/brand_logo.png b/platform/frontend/public/brand_logo.png new file mode 100644 index 0000000..585e7be Binary files /dev/null and b/platform/frontend/public/brand_logo.png differ diff --git a/platform/frontend/public/favicon.png b/platform/frontend/public/favicon.png new file mode 100644 index 0000000..b2fee67 Binary files /dev/null and b/platform/frontend/public/favicon.png differ diff --git a/docs/imgs/members/seungkyu.jpg b/platform/frontend/public/members/seungkyu.jpg similarity index 100% rename from docs/imgs/members/seungkyu.jpg rename to platform/frontend/public/members/seungkyu.jpg diff --git a/docs/imgs/members/soohyun.png b/platform/frontend/public/members/soohyun.png similarity index 100% rename from docs/imgs/members/soohyun.png rename to platform/frontend/public/members/soohyun.png diff --git a/docs/imgs/members/yesin.jpg b/platform/frontend/public/members/yesin.jpg similarity index 100% rename from docs/imgs/members/yesin.jpg rename to platform/frontend/public/members/yesin.jpg diff --git a/platform/frontend/public/pl_symbol.png b/platform/frontend/public/pl_symbol.png new file mode 100644 index 0000000..b2fee67 Binary files /dev/null and b/platform/frontend/public/pl_symbol.png differ diff --git a/platform/frontend/public/pl_symbol_white.png b/platform/frontend/public/pl_symbol_white.png new file mode 100644 index 0000000..f4262f7 Binary files /dev/null and b/platform/frontend/public/pl_symbol_white.png differ diff --git a/platform/frontend/style.css b/platform/frontend/style.css new file mode 100644 index 0000000..36dfaec --- /dev/null +++ b/platform/frontend/style.css @@ -0,0 +1,1273 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Space+Grotesk:wght@300;500;700&display=swap'); + +:root { + --bg-dark: #050b15; + --bg-card: rgba(10, 20, 35, 0.8); + --accent-cyan: #00f2ff; + --accent-magenta: #ff00ff; + --accent-yellow: #f8fb3c; + --text-main: #e0e6ed; + --text-dim: #94a3b8; + --glass-border: rgba(255, 255, 255, 0.1); + --neon-shadow: 0 0 15px rgba(0, 242, 255, 0.4); + --transition-smooth: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + background-color: var(--bg-dark); + color: var(--text-main); + font-family: 'Outfit', sans-serif; + overflow-x: hidden; + overflow-y: auto; +} + +/* Brand Header */ +.brand-header { + position: fixed; + top: 0; + left: 0; + width: 100%; + padding: 0.8rem 4rem; + /* Balanced padding */ + z-index: 1000; + background: rgba(5, 11, 21, 0.7); + /* Translucent dark background */ + backdrop-filter: blur(15px); + /* Premium blur effect */ + -webkit-backdrop-filter: blur(15px); + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + pointer-events: auto; +} + +.header-content { + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo-container { + display: inline-flex; + align-items: center; + gap: 1.2rem; + text-decoration: none; +} + +.brand-logo.mobile-logo { + display: none; + /* Hidden on desktop */ +} + +.brand-logo { + height: 40px; + width: auto; + filter: brightness(1.2); +} + +.team-label { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.6rem; + font-weight: 700; + color: #fff; + letter-spacing: -0.01rem; +} + +.header-nav { + display: flex; + gap: 2.5rem; +} + +.menu-toggle { + display: none; + /* Hidden on desktop */ + flex-direction: column; + gap: 5px; + background: transparent; + border: none; + cursor: pointer; + padding: 10px; + z-index: 1001; +} + +.menu-toggle .bar { + width: 25px; + height: 2px; + background: var(--accent-cyan); + border-radius: 2px; + transition: var(--transition-smooth); +} + +/* Mobile Menu Overlay */ +.mobile-menu-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(5, 10, 20, 0.95); + backdrop-filter: blur(15px); + z-index: 2000; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.mobile-menu-overlay.active { + opacity: 1; + pointer-events: auto; +} + +.close-menu { + position: absolute; + top: 2rem; + right: 2rem; + background: transparent; + border: none; + color: #fff; + font-size: 3rem; + cursor: pointer; +} + +.mobile-nav-links { + display: flex; + flex-direction: column; + gap: 2.5rem; + text-align: center; +} + +.mobile-nav-link { + font-family: 'Space Grotesk', sans-serif; + font-size: 2rem; + font-weight: 700; + color: #fff; + text-decoration: none; + text-transform: uppercase; + letter-spacing: 0.2rem; + transition: var(--transition-smooth); +} + +.mobile-nav-link:hover { + color: var(--accent-cyan); + text-shadow: 0 0 20px rgba(0, 242, 255, 0.5); +} + +.nav-link { + text-decoration: none; + font-family: 'Outfit', sans-serif; + font-size: 0.9rem; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.1rem; + transition: var(--transition-smooth); + position: relative; +} + +.nav-link:hover { + color: var(--accent-cyan); +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: var(--accent-cyan); + transition: var(--transition-smooth); +} + +.nav-link:hover::after { + width: 100%; +} + +/* Background Effects */ +.bg-grid { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(0, 242, 255, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 242, 255, 0.05) 1px, transparent 1px); + background-size: 50px 50px; + z-index: -1; + pointer-events: none; +} + +.bg-glow { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80vw; + height: 80vh; + background: radial-gradient(circle, rgba(0, 242, 255, 0.05) 0%, transparent 70%); + filter: blur(100px); + z-index: -1; + pointer-events: none; +} + +/* Full-page Section Layout */ +.full-section { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem; + position: relative; + overflow: hidden; +} + +/* Section 1: Intro/Vision */ +#intro { + text-align: center; +} + +.intro-content { + max-width: 850px; + /* Slightly wider for longer words */ +} + +.intro-badge { + display: inline-block; + padding: 0.5rem 1rem; + border: 1px solid var(--accent-cyan); + color: var(--accent-cyan); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.2rem; + margin-bottom: 2rem; + background: rgba(0, 242, 255, 0.05); +} + +.intro-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 4rem; + /* Increased from 3.8rem */ + font-weight: 800; + line-height: 1.1; + padding: 0.1em 0; + background: linear-gradient(to bottom, #00f2ff, #fff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 2rem; + text-shadow: 0 0 40px rgba(0, 242, 255, 0.3); +} + +.intro-desc { + font-size: 1.1rem; + color: var(--text-dim); + line-height: 1.8; + margin-bottom: 3rem; +} + +.intro-cta { + display: flex; + gap: 1.5rem; + justify-content: center; +} + +.btn-primary, +.btn-secondary { + padding: 1rem 2.5rem; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + border-radius: 4px; + transition: var(--transition-smooth); + text-transform: uppercase; + letter-spacing: 0.1rem; +} + +.btn-primary { + background: var(--accent-cyan); + color: var(--bg-dark); + box-shadow: var(--neon-shadow); +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 0 25px rgba(0, 242, 255, 0.6); +} + +.btn-secondary { + border: 2px solid var(--accent-magenta); + color: var(--accent-magenta); + background: transparent; +} + +.btn-secondary:hover { + background: rgba(255, 0, 255, 0.1); + transform: translateY(-3px); +} + +/* Section 2: Projects */ +#projects { + padding: 1rem 2rem 10rem 2rem; + /* Reduced top (from 3rem), increased bottom (from 8rem) */ +} + +.section-header { + text-align: center; + margin-bottom: 4rem; +} + +.section-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 3.5rem; + font-weight: 700; + background: linear-gradient(to right, var(--accent-cyan), var(--accent-magenta)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 1rem; +} + +.section-subtitle { + font-size: 1.2rem; + color: var(--text-dim); +} + +.projects-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + /* Fixed 3-col for scannability */ + gap: 1.5rem; + max-width: 1100px; + width: 100%; +} + +.project-card { + background: var(--bg-card); + border: 1px solid var(--glass-border); + padding: 1.5rem; + /* Reduced padding */ + border-radius: 8px; + transition: var(--transition-smooth); + display: flex; + flex-direction: column; + text-align: left; +} + +.project-card:hover { + border-color: var(--accent-cyan); + transform: translateY(-5px); + box-shadow: var(--neon-shadow); +} + +.project-icon { + font-size: 2rem; + /* Shrunk from 3rem */ + margin-bottom: 0.8rem; +} + +.project-name { + font-size: 1.2rem; + /* Shrunk from 1.5rem */ + font-weight: 700; + margin-bottom: 0.5rem; +} + +.project-desc { + font-size: 0.9rem; + /* Shrunk from 1rem */ + color: var(--text-dim); + line-height: 1.5; + margin-bottom: 1.2rem; + flex: 1; + /* Match heights naturally */ +} + +.project-link { + color: var(--accent-cyan); + text-decoration: none; + font-weight: 600; + font-size: 0.85rem; +} + +.btn-primary, +.btn-secondary { + white-space: nowrap; + /* Prevent team-member break */ +} + +/* Section 3: Team Members - Centralized & Gathered */ +#team { + padding: 6rem 2rem; + /* Increased top padding from 4rem */ +} + +/* Giant Background Name */ +.member-bg-name-container { + position: absolute; + bottom: 20%; + left: 0; + width: 100%; + display: flex; + justify-content: center; + z-index: 1; + pointer-events: none; +} + +.member-bg-name { + font-family: 'Space Grotesk', sans-serif; + font-size: 10vw; + /* Reduced from 12vw */ + font-weight: 800; + color: rgba(255, 255, 255, 0.02); + /* Made slightly subtler */ + -webkit-text-stroke: 1px rgba(0, 242, 255, 0.08); + white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.5rem; +} + +.team-navigation { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + max-width: 1200px; + /* Limits the "Whole area" width on huge screens */ + margin: 0 auto; + position: relative; + z-index: 10; + user-select: none; + /* Prevent text selection while swiping */ +} + +.team-navigation img { + pointer-events: none; + /* Prevent image dragging while swiping */ +} + +.team-container { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + gap: 4rem; + /* Restored original spacing */ +} + +/* Navigation Arrow Styling - Smart Placement */ +.arrows-wrapper { + position: absolute; + top: 50%; + left: 0; + right: 0; + transform: translateY(-50%); + width: 100%; + /* Spans the entire 1200px on PC */ + display: flex; + justify-content: space-between; + pointer-events: none; + z-index: 30; +} + +.nav-arrow { + pointer-events: auto; + /* Re-enable clicks for buttons */ + background: rgba(10, 20, 35, 0.4); + border: 1px solid rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 50px; + color: var(--text-dim); + padding: 1.2rem 1.5rem; + cursor: pointer; + transition: var(--transition-smooth); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.nav-arrow:hover, +.nav-arrow.active { + background: rgba(0, 242, 255, 0.2); + color: var(--accent-cyan); + box-shadow: 0 0 30px rgba(0, 242, 255, 0.3); + transform: scale(1.05); + /* Added slight pop for feedback */ +} + +.nav-arrow .nav-label { + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.2rem; +} + +.nav-arrow:hover { + background: rgba(0, 242, 255, 0.1); + color: var(--accent-cyan); + box-shadow: 0 0 30px rgba(0, 242, 255, 0.2); +} + +/* Info Panel (Left) */ +.hero-info { + max-width: 380px; + /* Reduced from 450px */ + text-align: left; + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.role-tag { + display: inline-block; + padding: 0.2rem 0.6rem; + background: rgba(0, 242, 255, 0.1); + border: 1px solid var(--accent-cyan); + color: var(--accent-cyan); + font-size: 0.75rem; + font-weight: 600; + align-self: flex-start; +} + +/* Runner - Subtle green variant */ +.role-tag.runner { + background: rgba(52, 211, 153, 0.15); + border: 1px solid rgba(52, 211, 153, 0.5); + color: #34d399; +} + +.member-name { + font-family: 'Space Grotesk', sans-serif; + font-size: 3.5rem; + /* Reduced from 4.5rem */ + font-weight: 700; + line-height: 1.1; + padding: 0.1em 0; + background: linear-gradient(to bottom, #fff, #94a3b8); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.member-desc { + font-size: 0.95rem; + /* Reduced from 1.1rem */ + color: var(--text-dim); + line-height: 1.7; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.2rem; + margin-top: 0.5rem; +} + +.stat-item { + border-left: 2px solid var(--accent-magenta); + padding-left: 1rem; +} + +.stat-label { + font-size: 0.7rem; + color: var(--accent-magenta); + text-transform: uppercase; +} + +.stat-value { + font-size: 1.1rem; + font-weight: 600; + color: #fff; + word-break: keep-all; + /* Keep Korean together if possible */ + overflow-wrap: break-word; + /* Break long English words like Backend... */ +} + +/* Member Social Links (Profile Panel) */ +.member-social-links { + display: flex; + gap: 1.2rem; + margin-top: 1.5rem; +} + +.social-btn { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 1.2rem; + background: rgba(0, 242, 255, 0.05); + border: 1px solid rgba(0, 242, 255, 0.2); + border-radius: 4px; + color: var(--accent-cyan); + text-decoration: none; + font-size: 0.85rem; + font-weight: 600; + transition: var(--transition-smooth); +} + +.social-btn:hover { + background: rgba(0, 242, 255, 0.1); + border-color: var(--accent-cyan); + transform: translateY(-2px); + box-shadow: 0 0 15px rgba(0, 242, 255, 0.3); +} + +/* Visual (Right) */ +.hero-visual { + position: relative; + width: 320px; + /* Reduced from 420px */ + height: 320px; + /* Reduced from 420px */ + display: flex; + justify-content: center; + align-items: center; +} + +.visual-orbit { + position: absolute; + width: 280px; + /* Reduced from 380px */ + height: 280px; + /* Reduced from 380px */ + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 50%; +} + +.member-image { + max-height: 100%; + max-width: 100%; + object-fit: contain; + z-index: 5; + filter: drop-shadow(0 0 50px rgba(0, 242, 255, 0.2)); +} + +/* Navigation - Thumbnails Grid at Bottom */ +.nav-footer { + display: grid; + grid-template-columns: repeat(4, 1fr); + /* Default 2 rows for 8 people */ + gap: 1.2rem; + margin-top: 3rem; + width: fit-content; + max-width: 1000px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 10; +} + +/* Wide Desktop: Show everyone in one line */ +@media (min-width: 1200px) { + .nav-footer { + grid-template-columns: repeat(8, 1fr); + } +} + +.thumb-btn { + width: 80px; + height: 80px; + border-radius: 50%; + border: none; + background: transparent; + cursor: pointer; + position: relative; + padding: 4px; + /* Space for the ring */ + transition: var(--transition-smooth); +} + +/* Slotted Circle Effect (Inspired by reference) */ +.thumb-btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + /* Create the "notches" using linear-gradients or clip-path */ + mask: radial-gradient(circle at 0 50%, transparent 10%, black 10.5%), + radial-gradient(circle at 100% 50%, transparent 10%, black 10.5%), + radial-gradient(circle at 50% 0, transparent 10%, black 10.5%), + radial-gradient(circle at 50% 100%, transparent 10%, black 10.5%); + -webkit-mask: radial-gradient(circle at 0 50%, transparent 10%, black 10.5%), + radial-gradient(circle at 100% 50%, transparent 10%, black 10.5%), + radial-gradient(circle at 50% 0, transparent 10%, black 10.5%), + radial-gradient(circle at 50% 100%, transparent 10%, black 10.5%); + transition: var(--transition-smooth); +} + +.thumb-btn img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; + opacity: 0.4; + transition: var(--transition-smooth); +} + +.thumb-btn:hover::before, +.thumb-btn.active::before { + border-color: var(--accent-cyan); + transform: rotate(45deg); + /* Subtle spin on active */ + box-shadow: 0 0 20px rgba(0, 242, 255, 0.4); +} + +.thumb-btn:hover img, +.thumb-btn.active img { + opacity: 1; + transform: scale(0.9); + /* Inset feel */ +} + +.thumb-btn.active { + transform: scale(1.1); +} + +@media (max-width: 480px) { + .nav-footer { + grid-template-columns: repeat(4, 1fr); + /* Keep 4 for small avatars */ + gap: 1rem; + max-width: 320px; + } + + .thumb-btn { + width: 65px; + height: 65px; + } +} + +/* Scroll Indicator */ +.scroll-indicator { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + color: var(--accent-cyan); + animation: bounce 2s infinite; +} + +.scroll-arrow { + font-size: 2rem; +} + +@keyframes bounce { + + 0%, + 100% { + transform: translateX(-50%) translateY(0); + } + + 50% { + transform: translateX(-50%) translateY(-10px); + } +} + +.fade-in { + animation: fadeInUp 0.6s forwards; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive */ +@media (max-width: 1024px) { + .team-container { + gap: 2rem; + } + + .member-name { + font-size: 3rem; + } + + .hero-visual { + width: 350px; + height: 350px; + } + + .visual-orbit { + width: 300px; + height: 300px; + } +} + +/* Site Footer */ +.site-footer { + background: #000; + padding: 3rem 3rem 1.5rem; + /* Reduced from 6rem 4rem 2rem */ + border-top: 1px solid rgba(0, 242, 255, 0.1); + position: relative; + z-index: 100; + margin-top: 8rem; + /* Add spacing between content and footer */ +} + +.footer-container { + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: flex-start; + align-items: flex-start; + margin-bottom: 2rem; + /* Reduced from 4rem */ +} + +.footer-logo { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.8rem; + /* Reduced from 2.5rem */ + font-weight: 800; + letter-spacing: -0.05rem; + color: #fff; + margin-bottom: 1.2rem; + /* Reduced from 2rem */ +} + +.footer-social-wrapper { + display: flex; + flex-direction: row; + gap: 3rem; +} + +.footer-social { + display: flex; + align-items: center; + gap: 1.2rem; +} + +.social-label { + font-size: 0.65rem; + /* Slightly smaller for multi-tier */ + min-width: 90px; + /* Ensure labels align */ + font-weight: 700; + letter-spacing: 0.15rem; + color: var(--text-dim); +} + +.social-link { + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + text-decoration: none; + color: #fff; + font-size: 0.7rem; + font-weight: 700; + transition: var(--transition-smooth); +} + +.social-link img { + width: 20px; + height: auto; + opacity: 0.8; + transition: var(--transition-smooth); +} + +.social-link:hover img { + opacity: 1; + filter: drop-shadow(0 0 8px var(--accent-cyan)); +} + +.footer-right { + display: flex; + gap: 6rem; + align-items: flex-start; +} + +.footer-nav { + display: flex; + gap: 2.5rem; +} + +.footer-nav-link { + writing-mode: vertical-rl; + text-decoration: none; + color: #fff; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.3rem; + transition: var(--transition-smooth); + padding-bottom: 1rem; + border-right: 1px solid transparent; +} + +.footer-nav-link:hover { + color: var(--accent-cyan); + border-right-color: var(--accent-cyan); +} + +.to-top-container { + position: relative; +} + +.to-top-btn { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; + gap: 1.5rem; +} + +.to-top-text { + writing-mode: vertical-rl; + color: #fff; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.2rem; +} + +.to-top-line { + width: 1px; + height: 80px; + background: linear-gradient(to bottom, var(--accent-cyan), transparent); + position: relative; +} + +.to-top-line::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, 50%); + width: 8px; + height: 8px; + background: var(--accent-magenta); + border-radius: 50%; + box-shadow: 0 0 10px var(--accent-magenta); +} + +.footer-bottom { + max-width: 1400px; + margin: 0 auto; + border-top: 1px solid rgba(255, 255, 255, 0.05); + padding-top: 0.8rem; + /* Reduced from 2rem */ + display: flex; + justify-content: space-between; + align-items: center; +} + +.copyright { + font-size: 0.8rem; + color: var(--text-dim); +} + +.footer-meta-links { + display: flex; + gap: 2rem; +} + +.footer-meta-links a { + text-decoration: none; + color: var(--text-dim); + font-size: 0.8rem; + transition: var(--transition-smooth); +} + +.footer-meta-links a:hover { + color: #fff; +} + +@media (max-width: 768px) { + .brand-header { + padding: 0.8rem 1.5rem; + /* Reduced from 4rem */ + } + + .full-section { + padding: 3rem 1.2rem; + /* Reduced horizontal padding for better flow */ + } + + /* Section Header Compactness */ + .section-header { + margin-bottom: 1.5rem; + } + + .section-title { + font-size: 2.2rem; + } + + .section-subtitle { + font-size: 0.9rem; + } + + /* Team Section Mobile Fixes (Photo on Top) */ + #team { + min-height: 100vh; + height: auto; + padding: 6rem 1.2rem 4rem 1.2rem; + display: flex; + flex-direction: column; + justify-content: center; + /* Better tall screen centering */ + gap: 2rem; + } + + .team-navigation { + flex-direction: column; + gap: 1.5rem; + align-items: center; + width: 100%; + margin: 0; + } + + .team-container { + flex-direction: column-reverse; + /* PHOTO ON TOP */ + text-align: center; + gap: 2.5rem; + width: 100%; + order: 1; + } + + /* Arrows tightly framing the photo on mobile */ + .arrows-wrapper { + position: absolute; + top: 130px; + /* Centered vertically on the 140px photo */ + left: 50%; + transform: translate(-50%, -50%); + width: 100vw; + /* Spans full screen width */ + display: flex; + justify-content: space-between; + padding: 0 0.4rem; + /* Tight gap from the wall */ + pointer-events: none; + z-index: 100; + } + + .nav-arrow { + pointer-events: auto; + display: flex; + padding: 0.6rem 0.8rem; + width: auto; + min-width: 45px; + background: rgba(10, 20, 35, 0.7); + backdrop-filter: blur(8px); + border-radius: 4px; + font-size: 0.9rem; + } + + .nav-prev { + order: 1; + } + + .nav-next { + order: 2; + } + + .hero-info { + max-width: 100%; + align-items: center; + text-align: center; + } + + .member-name { + font-size: 2rem; + } + + .stat-value { + font-size: 0.9rem; + /* Reduced from 1.1rem for mobile */ + } + + .member-desc { + font-size: 0.9rem; + line-height: 1.5; + margin-bottom: 2rem; + min-height: auto; + /* Allow content to dictate height */ + display: block; + /* Remove flex/clamp behavior */ + } + + .stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem 0.5rem; + width: 100%; + max-width: 260px; + /* Unified width to keep pink bars aligned */ + margin: 0 auto; + text-align: left; + } + + .stat-item { + min-height: 3rem; + /* Standardized height for all stat boxes */ + display: flex; + flex-direction: column; + justify-content: flex-start; + } + + .hero-visual { + width: 100%; + max-width: 140px; + /* Reduced further from 150px */ + height: 140px; + margin: 0 auto; + } + + .visual-orbit { + width: 130px; + height: 130px; + } + + .nav-footer { + margin-top: 1rem; + /* Heavily reduced margin */ + gap: 0.5rem; + } + + .thumb-btn { + width: 50px; + /* Shrunk from 65px/80px */ + height: 50px; + } + + .role-tag { + align-self: center; + font-size: 0.7rem; + padding: 0.2rem 0.6rem; + } + + .intro-title { + font-size: 2.4rem; + margin-bottom: 1.5rem; + } + + .intro-title br { + display: none; + /* Hide line break on mobile for better flow */ + } + + .intro-desc { + font-size: 1rem; + } + + .intro-cta { + flex-wrap: wrap; + /* Allow stacking if extremely narrow */ + gap: 1rem; + } + + .btn-primary, + .btn-secondary { + padding: 0.8rem 1.2rem; + /* Tighter padding for mobile */ + font-size: 0.85rem; + text-align: center; + min-width: 130px; + /* Uniform width for symmetry */ + } + + .projects-grid { + grid-template-columns: 1fr; + gap: 1.2rem; + } + + .project-card { + padding: 1.2rem; + } + + #projects { + padding: 6rem 1.2rem; + } + + .footer-container { + flex-direction: column; + gap: 2rem; + text-align: center; + } + + .footer-social-wrapper { + flex-direction: column; + gap: 1.5rem; + align-items: center; + } + + .footer-left { + align-items: center; + display: flex; + flex-direction: column; + } + + .footer-bottom { + flex-direction: column; + gap: 1.5rem; + } + + .logo-container { + gap: 0.8rem; + } + + .brand-logo.standard-logo { + display: block; + /* Show full logo again */ + height: 22px; + /* Adjusted size to fit menu button */ + } + + .brand-logo.mobile-logo { + display: none; + } + + .team-label { + display: block !important; + /* Restore 'DevFactory' text */ + font-size: 1.1rem; + /* Smaller for mobile header */ + } + + .header-nav { + display: none; + /* Hide direct links on mobile */ + } + + .menu-toggle { + display: flex; + /* Show menu button */ + } + + .nav-link { + font-size: 0.75rem; + } +} \ No newline at end of file diff --git a/platform/frontend/vite.config.js b/platform/frontend/vite.config.js new file mode 100644 index 0000000..c587c08 --- /dev/null +++ b/platform/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + allowedHosts: [ + 'private.pseudolab-devfactory.com' + ], + host: '0.0.0.0', + port: 5173, + // Add HMR port if needed for Traefik + hmr: { + clientPort: 443 + } + } +}); diff --git a/platform/server/.env.example b/platform/server/.env.example new file mode 100644 index 0000000..78b89a2 --- /dev/null +++ b/platform/server/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL=postgresql://username:password@localhost:5432/devfactory +PORT=3000 diff --git a/platform/server/Dockerfile b/platform/server/Dockerfile new file mode 100644 index 0000000..270de5c --- /dev/null +++ b/platform/server/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install + +# Copy source files +COPY . . + +# Expose port +EXPOSE 3000 + +# Run the server +CMD ["npm", "start"] diff --git a/platform/server/package-lock.json b/platform/server/package-lock.json new file mode 100644 index 0000000..519b62f --- /dev/null +++ b/platform/server/package-lock.json @@ -0,0 +1,1255 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "pg": "^8.17.2" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", + "dependencies": { + "pg-connection-string": "^2.10.1", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/platform/server/package.json b/platform/server/package.json new file mode 100644 index 0000000..7a5a43e --- /dev/null +++ b/platform/server/package.json @@ -0,0 +1,23 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "pg": "^8.17.2" + }, + "devDependencies": { + "nodemon": "^3.1.11" + } +} \ No newline at end of file diff --git a/platform/server/src/index.js b/platform/server/src/index.js new file mode 100644 index 0000000..504b6c2 --- /dev/null +++ b/platform/server/src/index.js @@ -0,0 +1,72 @@ +require('dotenv').config(); +const express = require('express'); +const { Pool } = require('pg'); +const cors = require('cors'); + +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// PostgreSQL Connection +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +// Test DB Connection +pool.query('SELECT NOW()', (err, res) => { + if (err) { + console.error('Error connecting to the database:', err); + } else { + console.log('Connected to PostgreSQL successfully'); + } +}); + +// API Routes +app.get('/api/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +// Register a visit +app.post('/api/stats/visit', async (req, res) => { + try { + const { path, userAgent } = req.body; + // ๊ธฐ์กด ๋กœ๊ทธ ํฌ๋งท์— ๋งž์ถฐ method๋Š” 'PAGEVIEW'๋กœ, referrer๋Š” ํ˜„์žฌ ํ˜ธ์ŠคํŠธ๋กœ ๊ธฐ๋ก + const referrer = req.headers.referer || ''; + + await pool.query( + 'INSERT INTO logging.access_log (path, method, status, user_agent, referrer, ts) VALUES ($1, $2, $3, $4, $5, NOW())', + [path || '/', 'PAGEVIEW', 200, userAgent, referrer] + ); + res.status(201).json({ message: 'Visit logged successfully' }); + } catch (err) { + console.error('Error logging visit:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Get total visit count (filtered by current site if requested) +app.get('/api/stats/count', async (req, res) => { + try { + const { site } = req.query; + let query = 'SELECT COUNT(*) FROM logging.access_log'; + let params = []; + + if (site) { + query += ' WHERE referrer LIKE $1'; + params.push(`%${site}%`); + } + + const result = await pool.query(query, params); + res.json({ count: parseInt(result.rows[0].count) }); + } catch (err) { + console.error('Error fetching count:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.listen(port, () => { + console.log(`Server running on port ${port}`); +});