commit 301040973bffc2471f7a0040eac1547095a535f1 Author: qjarl5678 Date: Thu Jan 22 22:12:34 2026 +0900 design: Mobile Publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e18fe55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Local +*.local diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..30f8474 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,105 @@ +# Merk Korea PDA 배포 가이드 + +## 1. 로컬 빌드 및 실행 + +### 빌드 +```bash +npm install +npm run build +``` + +빌드 결과물은 `dist/` 폴더에 생성됩니다. + +### 로컬에서 빌드 결과 확인 +```bash +npm run preview +``` +http://localhost:4173 에서 확인 가능 + +--- + +## 2. Docker로 배포 + +### 이미지 빌드 +```bash +docker build -t merk-korea-pda . +``` + +### 컨테이너 실행 +```bash +docker run -d -p 80:80 --name merk-pda merk-korea-pda +``` + +http://localhost 에서 확인 가능 + +### 컨테이너 중지/삭제 +```bash +docker stop merk-pda +docker rm merk-pda +``` + +--- + +## 3. Docker Compose (선택) + +`docker-compose.yml` 파일 생성: +```yaml +version: '3.8' +services: + web: + build: . + ports: + - "80:80" + restart: unless-stopped +``` + +실행: +```bash +docker-compose up -d +``` + +--- + +## 4. 정적 파일 직접 배포 + +`dist/` 폴더를 웹 서버에 업로드하면 됩니다. + +### 주의사항 +React Router(SPA)를 사용하므로, 모든 경로를 `index.html`로 리다이렉트 설정 필요: + +**Nginx 예시:** +```nginx +location / { + try_files $uri $uri/ /index.html; +} +``` + +**Apache 예시 (.htaccess):** +```apache +RewriteEngine On +RewriteBase / +RewriteRule ^index\.html$ - [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule . /index.html [L] +``` + +--- + +## 5. 포트 변경 + +### Docker 포트 변경 +```bash +docker run -d -p 3000:80 --name merk-pda merk-korea-pda +``` +→ http://localhost:3000 에서 접근 + +### Vite 개발 서버 포트 변경 +`vite.config.ts`에서 설정: +```ts +export default defineConfig({ + server: { + port: 3000 + } +}) +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..13ac946 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Build stage +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine + +# SPA 라우팅을 위한 nginx 설정 복사 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 빌드된 정적 파일 복사 +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/PAGES.md b/PAGES.md new file mode 100644 index 0000000..c193b3c --- /dev/null +++ b/PAGES.md @@ -0,0 +1,37 @@ +# Merk Korea PDA 페이지 목록 + +## 기본 +| 페이지 | URL | +|--------|-----| +| 로그인 | http://localhost:5173/ | +| 대시보드 | http://localhost:5173/dashboard | + +## 출입기록 검색 +| 페이지 | URL | +|--------|-----| +| 시설별 현황 | http://localhost:5173/facility-status | +| 시설 상세 | http://localhost:5173/facility-status/{시설명} | +| 출입 로그 | http://localhost:5173/facility-status/{시설명}/log/{위치명} | +| 층별 로그 | http://localhost:5173/facility-status/{시설명}/floor-log/{층이름} | +| 전체 로그 | http://localhost:5173/facility-status/{시설명}/all-log | + +## 비상대응 관리 +| 페이지 | URL | +|--------|-----| +| 비상경보 발생 | http://localhost:5173/emergency-alert | +| 대피소 현황 | http://localhost:5173/emergency-fire | +| 대피자 목록 | http://localhost:5173/emergency-fire/evacuee-list | +| 미대피자 목록 | http://localhost:5173/emergency-fire/non-evacuee-list | +| 인원태깅 (QR스캔) | http://localhost:5173/qr-scan | +| QR스캔 완료 | http://localhost:5173/qr-scan/complete | + +## 태그설정 +| 페이지 | URL | +|--------|-----| +| 태그 배터리 | http://localhost:5173/tag-battery | +| 태그 배터리 목록 | http://localhost:5173/tag-battery/{type} | + +## 관리자설정 +| 페이지 | URL | +|--------|-----| +| 내 정보 설정 | http://localhost:5173/my-info | diff --git a/app-design/MK-001.png b/app-design/MK-001.png new file mode 100644 index 0000000..de342dc Binary files /dev/null and b/app-design/MK-001.png differ diff --git a/app-design/MK-001_CASE.png b/app-design/MK-001_CASE.png new file mode 100644 index 0000000..f8b5ede Binary files /dev/null and b/app-design/MK-001_CASE.png differ diff --git a/app-design/MK-001_MENU.png b/app-design/MK-001_MENU.png new file mode 100644 index 0000000..f6a7944 Binary files /dev/null and b/app-design/MK-001_MENU.png differ diff --git a/app-design/MK-002.png b/app-design/MK-002.png new file mode 100644 index 0000000..cbfe8ec Binary files /dev/null and b/app-design/MK-002.png differ diff --git a/app-design/MK-003-1.png b/app-design/MK-003-1.png new file mode 100644 index 0000000..84f7c98 Binary files /dev/null and b/app-design/MK-003-1.png differ diff --git a/app-design/MK-003-2.png b/app-design/MK-003-2.png new file mode 100644 index 0000000..6b3d06c Binary files /dev/null and b/app-design/MK-003-2.png differ diff --git a/app-design/MK-003-3.png b/app-design/MK-003-3.png new file mode 100644 index 0000000..7e60a56 Binary files /dev/null and b/app-design/MK-003-3.png differ diff --git a/app-design/MK-003-4.png b/app-design/MK-003-4.png new file mode 100644 index 0000000..2163594 Binary files /dev/null and b/app-design/MK-003-4.png differ diff --git a/app-design/MK-003.png b/app-design/MK-003.png new file mode 100644 index 0000000..3119243 Binary files /dev/null and b/app-design/MK-003.png differ diff --git a/app-design/MK-005.png b/app-design/MK-005.png new file mode 100644 index 0000000..0482ef9 Binary files /dev/null and b/app-design/MK-005.png differ diff --git a/app-design/MK-006-1.png b/app-design/MK-006-1.png new file mode 100644 index 0000000..c74094d Binary files /dev/null and b/app-design/MK-006-1.png differ diff --git a/app-design/MK-006-2.png b/app-design/MK-006-2.png new file mode 100644 index 0000000..edcc17c Binary files /dev/null and b/app-design/MK-006-2.png differ diff --git a/app-design/MK-006.png b/app-design/MK-006.png new file mode 100644 index 0000000..c8ced3d Binary files /dev/null and b/app-design/MK-006.png differ diff --git a/app-design/MK-007-1.png b/app-design/MK-007-1.png new file mode 100644 index 0000000..187efa4 Binary files /dev/null and b/app-design/MK-007-1.png differ diff --git a/app-design/MK-007-2.png b/app-design/MK-007-2.png new file mode 100644 index 0000000..2f74040 Binary files /dev/null and b/app-design/MK-007-2.png differ diff --git a/app-design/MK-010-1.png b/app-design/MK-010-1.png new file mode 100644 index 0000000..4f50bf9 Binary files /dev/null and b/app-design/MK-010-1.png differ diff --git a/app-design/MK-010-2.png b/app-design/MK-010-2.png new file mode 100644 index 0000000..d600c65 Binary files /dev/null and b/app-design/MK-010-2.png differ diff --git a/app-design/MK-010-3.png b/app-design/MK-010-3.png new file mode 100644 index 0000000..d1024e5 Binary files /dev/null and b/app-design/MK-010-3.png differ diff --git a/app-design/MK-011.png b/app-design/MK-011.png new file mode 100644 index 0000000..e9cdda0 Binary files /dev/null and b/app-design/MK-011.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..116d4a3 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 머크코리아 작업자 위치관제 시스템 + + +
+ + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..556149b Binary files /dev/null and b/logo.png differ diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c743362 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # gzip 압축 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # SPA 라우팅: 모든 경로를 index.html로 + location / { + try_files $uri $uri/ /index.html; + } + + # 정적 파일 캐싱 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0e0315d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1768 @@ +{ + "name": "merk-korea-pda", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "merk-korea-pda", + "version": "0.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "~5.6.2", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC" + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..11e0a59 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "merk-korea-pda", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "~5.6.2", + "vite": "^5.4.10" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..27f069b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,42 @@ +import { Routes, Route } from 'react-router-dom' +import LoginPage from './pages/LoginPage' +import DashboardPage from './pages/DashboardPage' +import FacilityStatusPage from './pages/FacilityStatusPage' +import FacilityDetailPage from './pages/FacilityDetailPage' +import AccessLogPage from './pages/AccessLogPage' +import FloorLogPage from './pages/FloorLogPage' +import AllLogPage from './pages/AllLogPage' +import EmergencyAlertPage from './pages/EmergencyAlertPage' +import EmergencyFirePage from './pages/EmergencyFirePage' +import EvacueeListPage from './pages/EvacueeListPage' +import NonEvacueeListPage from './pages/NonEvacueeListPage' +import QRScanPage from './pages/QRScanPage' +import QRScanCompletePage from './pages/QRScanCompletePage' +import TagBatteryPage from './pages/TagBatteryPage' +import TagBatteryListPage from './pages/TagBatteryListPage' +import MyInfoPage from './pages/MyInfoPage' + +function App() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +} + +export default App diff --git a/src/assets/arrow-up-right.svg b/src/assets/arrow-up-right.svg new file mode 100644 index 0000000..8ed8613 --- /dev/null +++ b/src/assets/arrow-up-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/battery_high.svg b/src/assets/battery_high.svg new file mode 100644 index 0000000..9aff725 --- /dev/null +++ b/src/assets/battery_high.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/battery_low.svg b/src/assets/battery_low.svg new file mode 100644 index 0000000..d7133bc --- /dev/null +++ b/src/assets/battery_low.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/bg-ellipse-blur.svg b/src/assets/bg-ellipse-blur.svg new file mode 100644 index 0000000..45c543a --- /dev/null +++ b/src/assets/bg-ellipse-blur.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/check-icon.svg b/src/assets/check-icon.svg new file mode 100644 index 0000000..443d43c --- /dev/null +++ b/src/assets/check-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/close-icon.svg b/src/assets/close-icon.svg new file mode 100644 index 0000000..68349dd --- /dev/null +++ b/src/assets/close-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/emergency-bell.png b/src/assets/emergency-bell.png new file mode 100644 index 0000000..65565bc Binary files /dev/null and b/src/assets/emergency-bell.png differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..556149b Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..95a34b3 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/mk005-bg.svg b/src/assets/mk005-bg.svg new file mode 100644 index 0000000..4770e41 --- /dev/null +++ b/src/assets/mk005-bg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/qr-illustration.png b/src/assets/qr-illustration.png new file mode 100644 index 0000000..2bb3047 Binary files /dev/null and b/src/assets/qr-illustration.png differ diff --git a/src/assets/red-ellipse-bg.svg b/src/assets/red-ellipse-bg.svg new file mode 100644 index 0000000..6120132 --- /dev/null +++ b/src/assets/red-ellipse-bg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/components/common/Button.module.css b/src/components/common/Button.module.css new file mode 100644 index 0000000..e731dc0 --- /dev/null +++ b/src/components/common/Button.module.css @@ -0,0 +1,43 @@ +/* Total Exits Container / Button */ +.button { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + gap: 16px; + height: 60px; + background: #000000; + border-radius: 8px; + cursor: pointer; + border: none; +} + +.fullWidth { + width: 320px; + align-self: stretch; +} + +.buttonText { + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 600; + font-size: 16px; + line-height: 100%; + display: flex; + align-items: center; + color: #FFFFFF; +} + +.button:hover { + background: #1a1a1a; +} + +.button:active { + background: #333333; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx new file mode 100644 index 0000000..361a103 --- /dev/null +++ b/src/components/common/Button.tsx @@ -0,0 +1,22 @@ +import { ButtonHTMLAttributes } from 'react' +import styles from './Button.module.css' + +interface ButtonProps extends ButtonHTMLAttributes { + fullWidth?: boolean +} + +export default function Button({ + children, + fullWidth = false, + className = '', + ...props +}: ButtonProps) { + return ( + + ) +} diff --git a/src/components/common/Input.module.css b/src/components/common/Input.module.css new file mode 100644 index 0000000..4381ed9 --- /dev/null +++ b/src/components/common/Input.module.css @@ -0,0 +1,117 @@ +/* Input Container */ +.inputContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 16px; + width: 320px; + position: relative; +} + +/* Label */ +.label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0px; + gap: 8px; +} + +.labelText { + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 600; + font-size: 16px; + line-height: 100%; + display: flex; + align-items: center; + color: #FFFFFF; +} + +/* Input Wrapper */ +.inputWrapper { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 8px; + width: 320px; +} + +/* Input Field */ +.input { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + padding: 22px 16px; + gap: 10px; + width: 320px; + height: 64px; + background: #1C2542; + border: none; + border-radius: 8px; + flex: none; + order: 0; + flex-grow: 1; + + /* Text style */ + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 27px; + letter-spacing: -0.02em; + color: #FFFFFF; +} + +.input::placeholder { + color: #D1D9FF; +} + +.input:focus { + outline: none; +} + +/* Password Toggle Button */ +.toggleButton { + position: absolute; + right: 16px; + background: transparent; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.inputWrapper { + position: relative; +} + +.inputWrapper .input { + padding-right: 50px; +} + +/* Error Text */ +.errorText { + position: absolute; + top: 100%; + left: 0; + margin-top: 8px; + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 27px; + letter-spacing: -0.02em; + color: #BF151B; + white-space: pre-line; +} + +/* Error state - add margin for error message space */ +.hasError { + margin-bottom: 35px; +} diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx new file mode 100644 index 0000000..49f733a --- /dev/null +++ b/src/components/common/Input.tsx @@ -0,0 +1,71 @@ +import { useState, InputHTMLAttributes } from 'react' +import styles from './Input.module.css' + +interface InputProps extends InputHTMLAttributes { + label: string + error?: string + showPasswordToggle?: boolean +} + +export default function Input({ + label, + error, + type = 'text', + showPasswordToggle = false, + ...props +}: InputProps) { + const [showPassword, setShowPassword] = useState(false) + + const inputType = showPasswordToggle + ? (showPassword ? 'text' : 'password') + : type + + return ( +
+
+ {label} +
+
+ + {showPasswordToggle && ( + + )} +
+ {error && {error}} +
+ ) +} + +function EyeIcon() { + return ( + + + + + ) +} diff --git a/src/components/common/Logo.module.css b/src/components/common/Logo.module.css new file mode 100644 index 0000000..5fa7dda --- /dev/null +++ b/src/components/common/Logo.module.css @@ -0,0 +1,47 @@ +/* Menu Icon / Logo */ +.logo { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 0px; + gap: 5.33px; + width: 104px; + height: 38px; +} + +.logoImage { + width: 99px; + height: 16px; + object-fit: contain; +} + +.subText { + width: 85px; + height: 12px; + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 100%; + display: flex; + align-items: center; + letter-spacing: -0.02em; +} + +/* Variants */ +.light { + color: #FFFFFF; +} + +.light .subText { + color: rgba(255, 255, 255, 0.8); +} + +.dark { + color: #000000; +} + +.dark .subText { + color: rgba(0, 0, 0, 0.8); +} diff --git a/src/components/common/Logo.tsx b/src/components/common/Logo.tsx new file mode 100644 index 0000000..8e5072f --- /dev/null +++ b/src/components/common/Logo.tsx @@ -0,0 +1,22 @@ +import styles from './Logo.module.css' + +interface LogoProps { + variant?: 'light' | 'dark' +} + +export default function Logo({ variant = 'light' }: LogoProps) { + return ( +
+ + Security System +
+ ) +} + +function MerckLogo({ className }: { className?: string }) { + return ( + + + + ) +} diff --git a/src/components/dashboard/BatteryHealthCard.module.css b/src/components/dashboard/BatteryHealthCard.module.css new file mode 100644 index 0000000..bdf2774 --- /dev/null +++ b/src/components/dashboard/BatteryHealthCard.module.css @@ -0,0 +1,32 @@ +.card { + flex: 1; + display: flex; + gap: 20px; + align-items: flex-start; + justify-content: center; + padding: 20px; + background: #121628; + border-radius: 16px; + backdrop-filter: blur(25px); +} + +.content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.value { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 18px; + color: #FFFFFF; + letter-spacing: -0.36px; +} + +.label { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 12px; + color: rgba(255, 255, 255, 0.4); +} diff --git a/src/components/dashboard/BatteryHealthCard.tsx b/src/components/dashboard/BatteryHealthCard.tsx new file mode 100644 index 0000000..a0f4b17 --- /dev/null +++ b/src/components/dashboard/BatteryHealthCard.tsx @@ -0,0 +1,40 @@ +import styles from './BatteryHealthCard.module.css' + +interface BatteryHealthCardProps { + level: 'high' | 'low' + value: number +} + +export default function BatteryHealthCard({ level, value }: BatteryHealthCardProps) { + return ( +
+ +
+ {value.toLocaleString()} + {level === 'high' ? 'High' : 'Low'} +
+
+ ) +} + +function BatteryIcon({ level }: { level: 'high' | 'low' }) { + const fillColor = level === 'high' ? '#4CAF50' : '#FF5252' + + return ( + + {/* Battery body */} + + {/* Battery cap */} + + {/* Battery fill */} + + + ) +} diff --git a/src/components/dashboard/BuildingCard.module.css b/src/components/dashboard/BuildingCard.module.css new file mode 100644 index 0000000..23325e9 --- /dev/null +++ b/src/components/dashboard/BuildingCard.module.css @@ -0,0 +1,106 @@ +.card { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 16px 0 12px; + background: #121628; + border-radius: 16px; + backdrop-filter: blur(12.66px); + cursor: pointer; + transition: background 0.2s ease; + overflow: hidden; +} + +.card:hover { + background: #1a1f38; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 16px; +} + +.name { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 1.2; + color: #FFFFFF; +} + +.workerCount { + display: flex; + align-items: center; + gap: 4px; + padding: 0 16px; + width: 100%; + justify-content: flex-end; +} + +.count { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 20px; + color: #FFFFFF; +} + +.anchorStatus { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background: #0B0C12; + border-radius: 8px; + height: 36px; + width: calc(100% - 14px); +} + +.anchorLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 400; + font-size: 12px; + color: #748CFF; + flex-shrink: 0; +} + +.anchorValues { + display: flex; + gap: 6px; + align-items: center; +} + +.anchorItem { + display: flex; + gap: 4px; + align-items: center; +} + +.anchorType { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 12px; + color: #888888; + white-space: nowrap; +} + +.anchorValue { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 13px; + color: #FFFFFF; + letter-spacing: -0.26px; + white-space: nowrap; +} + +.anchorValue.dimmed { + opacity: 0.8; +} + +.anchorValue.broken { + color: #FF020B; + font-size: 14px; +} diff --git a/src/components/dashboard/BuildingCard.tsx b/src/components/dashboard/BuildingCard.tsx new file mode 100644 index 0000000..628b353 --- /dev/null +++ b/src/components/dashboard/BuildingCard.tsx @@ -0,0 +1,59 @@ +import styles from './BuildingCard.module.css' + +interface BuildingCardProps { + name: string + workers: number + anchorNormal: number + anchorBroken: number + onClick?: () => void +} + +export default function BuildingCard({ + name, + workers, + anchorNormal, + anchorBroken, + onClick +}: BuildingCardProps) { + const hasBroken = anchorBroken > 0 + + return ( +
+
+ {name} +
+
+ + {workers.toLocaleString()} +
+
+ 앵커 +
+
+ 정상 + + {anchorNormal} + +
+
+ 고장 + + {anchorBroken} + +
+
+
+
+ ) +} + +function PersonIcon() { + return ( + + + + + + + ) +} diff --git a/src/components/facility/CategoryTab.module.css b/src/components/facility/CategoryTab.module.css new file mode 100644 index 0000000..69f64eb --- /dev/null +++ b/src/components/facility/CategoryTab.module.css @@ -0,0 +1,43 @@ +.tabContainer { + display: flex; + gap: 8px; + padding: 0 20px; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.tabContainer::-webkit-scrollbar { + display: none; +} + +.tab { + flex-shrink: 0; + padding: 12px 20px; + border-radius: 100px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 1; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + border: 1px solid transparent; + background: transparent; +} + +.tab.active { + background: #FFFFFF; + color: #000000; + font-weight: 700; +} + +.tab.inactive { + background: transparent; + border-color: rgba(209, 217, 255, 0.3); + color: #FFFFFF; +} + +.tab.inactive:hover { + border-color: rgba(209, 217, 255, 0.5); +} diff --git a/src/components/facility/CategoryTab.tsx b/src/components/facility/CategoryTab.tsx new file mode 100644 index 0000000..6852546 --- /dev/null +++ b/src/components/facility/CategoryTab.tsx @@ -0,0 +1,29 @@ +import styles from './CategoryTab.module.css' + +interface CategoryTabProps { + categories: string[] + activeCategory: string + onCategoryChange: (category: string) => void +} + +export default function CategoryTab({ + categories, + activeCategory, + onCategoryChange +}: CategoryTabProps) { + return ( +
+ {categories.map((category) => ( + + ))} +
+ ) +} diff --git a/src/components/facility/EntryCard.module.css b/src/components/facility/EntryCard.module.css new file mode 100644 index 0000000..1d69b16 --- /dev/null +++ b/src/components/facility/EntryCard.module.css @@ -0,0 +1,140 @@ +.card { + width: 100%; + background: rgba(15, 17, 26, 0.8); + backdrop-filter: blur(20.833px); + border-radius: 16px; + padding: 24px 0 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + overflow: hidden; +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + gap: 32px; +} + +.mainInfo { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 0 24px; +} + +.locationInfo { + display: flex; + flex-direction: column; +} + +.locationName { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 1.4; + color: #FFFFFF; +} + +.stats { + display: flex; + gap: 16px; + align-items: center; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.statLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: rgba(209, 217, 255, 0.5); +} + +.statValue { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 20px; + color: #FFFFFF; + letter-spacing: -0.4px; + text-align: right; +} + +.anchorInfo { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 24px; +} + +.anchorLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 12px; + color: rgba(209, 217, 255, 0.5); +} + +.anchorStatus { + display: flex; + align-items: center; + gap: 8px; +} + +.statusDot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.statusDot.normal { + background: #748CFF; +} + +.statusDot.broken { + background: #FF656A; +} + +.statusText { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 16px; +} + +.statusText.normal { + color: #CECECE; +} + +.statusText.broken { + color: #FF656A; +} + +.logButton { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: 260px; + padding: 16px; + background: #000000; + border-radius: 6px; + border: none; + cursor: pointer; +} + +.logButton:hover { + background: #1a1a1a; +} + +.logButtonText { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #FFFFFF; +} diff --git a/src/components/facility/EntryCard.tsx b/src/components/facility/EntryCard.tsx new file mode 100644 index 0000000..b78228c --- /dev/null +++ b/src/components/facility/EntryCard.tsx @@ -0,0 +1,64 @@ +import styles from './EntryCard.module.css' + +interface EntryCardProps { + floor: string + location: string + inCount: number + outCount: number + anchorStatus: 'normal' | 'broken' + onLogClick?: () => void +} + +export default function EntryCard({ + floor, + location, + inCount, + outCount, + anchorStatus, + onLogClick +}: EntryCardProps) { + return ( +
+
+
+
+ {floor} + {location} +
+
+
+ IN + {inCount} +
+
+ OUT + {outCount} +
+
+
+ +
+ 앵커 +
+
+ + {anchorStatus === 'normal' ? '정상' : '고장'} + +
+
+
+ + +
+ ) +} diff --git a/src/components/facility/FacilityCard.module.css b/src/components/facility/FacilityCard.module.css new file mode 100644 index 0000000..9d8e3ce --- /dev/null +++ b/src/components/facility/FacilityCard.module.css @@ -0,0 +1,70 @@ +.card { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + height: 128px; + padding: 16px; + background: linear-gradient(108.18deg, rgba(58, 69, 123, 0.64) 1.96%, rgba(36, 44, 85, 0.32) 94.63%); + border-radius: 12px; + backdrop-filter: blur(25px); + cursor: pointer; + transition: background 0.2s ease; + overflow: hidden; +} + +.card:hover { + background: linear-gradient(108.18deg, rgba(68, 79, 133, 0.74) 1.96%, rgba(46, 54, 95, 0.42) 94.63%); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.name { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 16px; + line-height: 1; + color: #FFFFFF; +} + +.arrowButton { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + background: rgba(21, 27, 52, 0.8); + border-radius: 50%; +} + +.arrowIcon { + width: 16px; + height: 16px; +} + +.workerInfo { + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-end; + width: 100%; +} + +.personIcon { + width: 12px; + height: 12px; +} + +.count { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + letter-spacing: -0.48px; + text-align: right; +} diff --git a/src/components/facility/FacilityCard.tsx b/src/components/facility/FacilityCard.tsx new file mode 100644 index 0000000..8cdc6ca --- /dev/null +++ b/src/components/facility/FacilityCard.tsx @@ -0,0 +1,47 @@ +import styles from './FacilityCard.module.css' + +interface FacilityCardProps { + name: string + workers: number + onClick?: () => void +} + +export default function FacilityCard({ name, workers, onClick }: FacilityCardProps) { + return ( +
+
+ {name} +
+ + + +
+
+
+ + + + {workers.toLocaleString()} +
+
+ ) +} diff --git a/src/components/facility/FloorSection.module.css b/src/components/facility/FloorSection.module.css new file mode 100644 index 0000000..4930b0d --- /dev/null +++ b/src/components/facility/FloorSection.module.css @@ -0,0 +1,83 @@ +.section { + background: linear-gradient(104deg, #171C30 1.96%, #172241 94.63%); + border-radius: 20px; + backdrop-filter: blur(25px); + padding: 24px 10px; + overflow: hidden; +} + +.headerWrapper { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 20px; + width: 320px; + margin: 0 auto; + padding-left: 4px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.floorName { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 28px; + color: #FFFFFF; + letter-spacing: -0.56px; +} + +.workerInfo { + display: flex; + align-items: center; +} + +.personIcon { + width: 16px; + height: 16px; +} + +.workerCount { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 24px; + color: #D3D3D3; + letter-spacing: -0.48px; + padding: 0 8px; +} + +.floorLogButton { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 50px; + padding: 16px; + background: #000000; + border-radius: 6px; + border: none; + cursor: pointer; +} + +.floorLogButton:hover { + background: #1a1a1a; +} + +.floorLogButtonText { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #FFFFFF; +} + +.entryList { + display: flex; + flex-direction: column; + gap: 20px; + width: 320px; + margin: 20px auto 0; +} diff --git a/src/components/facility/FloorSection.tsx b/src/components/facility/FloorSection.tsx new file mode 100644 index 0000000..e5f00d9 --- /dev/null +++ b/src/components/facility/FloorSection.tsx @@ -0,0 +1,45 @@ +import styles from './FloorSection.module.css' + +interface FloorSectionProps { + floorName: string + workerCount: number + children: React.ReactNode + onFloorLogClick?: () => void +} + +export default function FloorSection({ + floorName, + workerCount, + children, + onFloorLogClick +}: FloorSectionProps) { + return ( +
+
+
+ {floorName} +
+ + + + {workerCount} +
+
+ + +
+ +
{children}
+
+ ) +} diff --git a/src/components/layout/Header.module.css b/src/components/layout/Header.module.css new file mode 100644 index 0000000..48dab32 --- /dev/null +++ b/src/components/layout/Header.module.css @@ -0,0 +1,42 @@ +.header { + display: flex; + justify-content: space-between; + align-items: center; + position: absolute; + width: 100%; + top: 40px; + left: 0; + padding: 0 16px; + z-index: 10; +} + +.iconButton { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.iconButton:hover { + opacity: 0.8; +} + +.spacer { + width: 40px; + height: 40px; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 1.4; + color: #FFFFFF; + margin: 0; + text-align: center; +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..9548542 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,123 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import Logo from '../common/Logo' +import SideMenu from './SideMenu' +import styles from './Header.module.css' + +interface HeaderProps { + showMenu?: boolean + showBackButton?: boolean + logoPosition?: 'left' | 'center' + title?: string + onMenuClick?: () => void + onBackClick?: () => void +} + +export default function Header({ + showMenu = false, + showBackButton = false, + logoPosition = 'center', + title, + onMenuClick, + onBackClick +}: HeaderProps) { + const [isMenuOpen, setIsMenuOpen] = useState(false) + const navigate = useNavigate() + + const handleMenuClick = () => { + if (onMenuClick) { + onMenuClick() + } else { + setIsMenuOpen(true) + } + } + + const handleBackClick = () => { + if (onBackClick) { + onBackClick() + } else { + navigate(-1) + } + } + + // MK-002 layout: Logo left, Menu right + if (logoPosition === 'left') { + return ( + <> +
+ +
+ {showMenu && ( + + )} +
+ setIsMenuOpen(false)} /> + + ) + } + + // MK-003 layout: Back button left, Logo center, Menu right + return ( + <> +
+ {/* Left: Back button or spacer */} + {showBackButton ? ( + + ) : ( +
+ )} + + {/* Center: Logo or Title */} + {title ? ( +

{title}

+ ) : ( + + )} + + {/* Right: Menu button or spacer */} + {showMenu ? ( + + ) : ( +
+ )} +
+ setIsMenuOpen(false)} /> + + ) +} + +function HamburgerIcon() { + return ( + + + + + + ) +} + +function BackIcon() { + return ( + + + + ) +} diff --git a/src/components/layout/SideMenu.module.css b/src/components/layout/SideMenu.module.css new file mode 100644 index 0000000..86a3e6d --- /dev/null +++ b/src/components/layout/SideMenu.module.css @@ -0,0 +1,153 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 100; +} + +.menu { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 359px; + max-width: 100vw; + background: #FFFFFF; + z-index: 101; + display: flex; + flex-direction: column; + animation: slideIn 0.25s ease-out; + overflow-y: auto; +} + +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 16px 20px 24px; + min-height: 56px; +} + +.closeButton { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.closeButton:hover { + opacity: 0.7; +} + +/* Navigation */ +.nav { + flex: 1; + padding: 20px 20px 40px; + display: flex; + flex-direction: column; + gap: 30px; +} + +/* 대시보드 아이템 */ +.dashboardItem { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 0; +} + +.dashboardItem:hover { + opacity: 0.7; +} + +.dashboardText { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + line-height: 40px; + color: #000000; + letter-spacing: -0.48px; +} + +/* 구분선 */ +.divider { + width: 100%; + height: 1px; + background: #E5E5E5; +} + +.sectionDivider { + width: 100%; + height: 8px; + background: #EFEFEF; + margin: 0 -20px; + width: calc(100% + 40px); +} + +/* 섹션 */ +.section { + display: flex; + flex-direction: column; + gap: 24px; + padding-left: 4px; +} + +.sectionTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 12px; + line-height: 40px; + color: #999999; + letter-spacing: -0.24px; + margin: 0; +} + +/* 메뉴 아이템 */ +.menuItems { + display: flex; + flex-direction: column; + gap: 16px; +} + +.menuItemsGrid { + display: grid; + grid-template-columns: 120px 1fr; + gap: 16px 60px; +} + +.menuItem { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 16px; + line-height: 1; + color: #000000; + background: transparent; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + width: fit-content; +} + +.menuItem:hover { + opacity: 0.7; +} diff --git a/src/components/layout/SideMenu.tsx b/src/components/layout/SideMenu.tsx new file mode 100644 index 0000000..f9afccd --- /dev/null +++ b/src/components/layout/SideMenu.tsx @@ -0,0 +1,110 @@ +import { useNavigate } from 'react-router-dom' +import Logo from '../common/Logo' +import styles from './SideMenu.module.css' + +interface SideMenuProps { + isOpen: boolean + onClose: () => void +} + +export default function SideMenu({ isOpen, onClose }: SideMenuProps) { + const navigate = useNavigate() + + const handleNavigate = (path: string) => { + navigate(path) + onClose() + } + + if (!isOpen) return null + + return ( + <> +
+ + + ) +} + +function CloseIcon() { + return ( + + + + + ) +} + +function ChevronIcon() { + return ( + + + + ) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..66a12e0 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './styles/global.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/src/pages/AccessLogPage.module.css b/src/pages/AccessLogPage.module.css new file mode 100644 index 0000000..566b2e7 --- /dev/null +++ b/src/pages/AccessLogPage.module.css @@ -0,0 +1,104 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + overflow-x: hidden; + padding-bottom: 40px; +} + +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.closeButton:hover { + opacity: 0.7; +} + +.content { + display: flex; + flex-direction: column; + gap: 40px; + padding-top: 50px; + width: 320px; + margin: 0 auto; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + letter-spacing: -0.56px; + color: #000000; + margin: 0; +} + +.logTable { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.tableHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + height: 20px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #111111; + text-align: center; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 16px; +} + +.tableRow { + display: flex; + justify-content: space-between; + align-items: flex-start; + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 14px; + color: #444444; +} + +.colAccess { + width: 40px; + text-align: center; +} + +.colTagId { + width: 85px; + text-align: center; +} + +.colTime { + flex: 1; + text-align: right; + letter-spacing: -0.28px; + white-space: nowrap; +} + +.tableHeader .colTime { + text-align: center; +} diff --git a/src/pages/AccessLogPage.tsx b/src/pages/AccessLogPage.tsx new file mode 100644 index 0000000..55155dc --- /dev/null +++ b/src/pages/AccessLogPage.tsx @@ -0,0 +1,75 @@ +import { useNavigate, useParams } from 'react-router-dom' +import styles from './AccessLogPage.module.css' + +interface LogEntry { + id: number + type: 'IN' | 'OUT' + tagId: string + time: string +} + +// Mock data - 추후 API 연동 +const mockLogs: LogEntry[] = [ + { id: 1, type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 2, type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 3, type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 4, type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 5, type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 6, type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 7, type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 8, type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 9, type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 10, type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 11, type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 12, type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 13, type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 14, type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 15, type: 'OUT', tagId: 'MK0105', time: '2025-12-01 17:00:55' }, + { id: 16, type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, + { id: 17, type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, +] + +export default function AccessLogPage() { + const navigate = useNavigate() + const { locationName } = useParams<{ locationName: string }>() + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* Close Button */} + + + {/* Content */} +
+

{locationName || '옥상출입구'} 출입로그

+ +
+ {/* Table Header */} +
+ 출입 + TagID + 시간 +
+ + {/* Table Body */} +
+ {mockLogs.map((log) => ( +
+ {log.type} + {log.tagId} + {log.time} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/AllLogPage.module.css b/src/pages/AllLogPage.module.css new file mode 100644 index 0000000..f52c047 --- /dev/null +++ b/src/pages/AllLogPage.module.css @@ -0,0 +1,106 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + overflow-x: hidden; + padding-bottom: 40px; +} + +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.closeButton:hover { + opacity: 0.7; +} + +.content { + display: flex; + flex-direction: column; + gap: 40px; + padding-top: 50px; + width: 320px; + margin: 0 auto; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + letter-spacing: -0.56px; + color: #000000; + margin: 0; +} + +.logTable { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.tableHeader { + display: flex; + align-items: flex-start; + height: 20px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #111111; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 16px; +} + +.tableRow { + display: flex; + align-items: flex-start; + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 14px; + color: #444444; +} + +.colDoor { + width: 80px; + text-align: left; +} + +.colAccess { + width: 40px; + text-align: center; +} + +.colTagId { + width: 85px; + text-align: center; +} + +.colTime { + flex: 1; + text-align: right; + letter-spacing: -0.28px; + white-space: nowrap; +} + +.tableHeader .colTime { + text-align: center; +} diff --git a/src/pages/AllLogPage.tsx b/src/pages/AllLogPage.tsx new file mode 100644 index 0000000..7c44d38 --- /dev/null +++ b/src/pages/AllLogPage.tsx @@ -0,0 +1,77 @@ +import { useNavigate } from 'react-router-dom' +import styles from './AllLogPage.module.css' + +interface LogEntry { + id: number + door: string + type: 'IN' | 'OUT' + tagId: string + time: string +} + +// Mock data - 추후 API 연동 +const mockLogs: LogEntry[] = [ + { id: 1, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 2, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 3, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 4, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 5, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 6, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 7, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 8, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 9, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 10, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 11, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 12, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 13, door: '옥상출입구', type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 14, door: '옥상출입구', type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 15, door: '서쪽출입문', type: 'OUT', tagId: 'MK0105', time: '2025-12-01 17:00:55' }, + { id: 16, door: '동쪽출입문', type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, + { id: 17, door: '동쪽출입문', type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, +] + +export default function AllLogPage() { + const navigate = useNavigate() + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* Close Button */} + + + {/* Content */} +
+

전체 출입로그

+ +
+ {/* Table Header */} +
+ 출입문 + 출입 + TagID + 시간 +
+ + {/* Table Body */} +
+ {mockLogs.map((log) => ( +
+ {log.door} + {log.type} + {log.tagId} + {log.time} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/DashboardPage.module.css b/src/pages/DashboardPage.module.css new file mode 100644 index 0000000..df082f1 --- /dev/null +++ b/src/pages/DashboardPage.module.css @@ -0,0 +1,129 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(157deg, #010306 0.81%, #091A3F 100%); + overflow-x: hidden; + padding-bottom: 40px; +} + +/* Background blur effect */ +.bgBlur { + position: absolute; + width: 426px; + height: 409px; + right: -234px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 82.5%); + filter: blur(100px); + pointer-events: none; +} + +/* 전체 현황 섹션 */ +.statusSection { + position: relative; + display: flex; + flex-direction: column; + gap: 24px; + padding: 120px 20px 0; + width: 100%; +} + +.sectionTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: rgba(255, 255, 255, 0.8); + margin: 0; +} + +.statusMetrics { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; +} + +.metric { + display: flex; + flex-direction: column; + gap: 8px; +} + +.metricLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 12px; + color: #748CFF; +} + +.metricValue { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + color: #FFFFFF; + text-shadow: 0px 0px 6.557px rgba(110, 120, 191, 0.5); +} + +/* 건물 선택 섹션 */ +.buildingSection { + position: relative; + margin: 20px 10px 0; + padding: 24px 10px; + background: linear-gradient(100.52deg, #171C30 1.96%, #172241 94.63%); + border-radius: 20px; + backdrop-filter: blur(25px); +} + +.buildingSectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 4px 0 4px; + margin-bottom: 16px; +} + +.buildingSectionTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: #FFFFFF; +} + +.buildingGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +/* 배터리 헬스 체크 섹션 */ +.batterySection { + position: relative; + margin: 20px 10px 0; + padding: 24px 10px; + background: linear-gradient(120.27deg, #222843 1.96%, #172241 94.63%); + border-radius: 20px; + backdrop-filter: blur(25px); +} + +.batterySectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 4px 0 4px; + margin-bottom: 16px; +} + +.batterySectionTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: #FFFFFF; +} + +.batteryGrid { + display: flex; + gap: 8px; +} diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..29d5974 --- /dev/null +++ b/src/pages/DashboardPage.tsx @@ -0,0 +1,84 @@ +import styles from './DashboardPage.module.css' +import Header from '../components/layout/Header' +import BuildingCard from '../components/dashboard/BuildingCard' +import BatteryHealthCard from '../components/dashboard/BatteryHealthCard' + +// 건물 데이터 (추후 API 연동) +const buildings = [ + { id: 1, name: '물류동', workers: 1107, anchorNormal: 6, anchorBroken: 0 }, + { id: 2, name: '생산동', workers: 1107, anchorNormal: 12, anchorBroken: 3 }, + { id: 3, name: '관리동', workers: 1107, anchorNormal: 2, anchorBroken: 3 }, + { id: 4, name: '위험물', workers: 117, anchorNormal: 12, anchorBroken: 0 }, + { id: 5, name: '휴게동', workers: 1107, anchorNormal: 120, anchorBroken: 1 }, + { id: 6, name: '식당', workers: 25, anchorNormal: 6, anchorBroken: 0 }, +] + +export default function DashboardPage() { + return ( +
+ {/* Background blur effect */} +
+ + {/* Header */} +
+ + {/* 전체 현황 섹션 */} +
+

전체 현황

+
+
+ 등록된 작업자 + 4,597 +
+
+ 단지내 재실자 + 4,487 +
+
+ 미입실 + 110 +
+
+
+ + {/* 건물 선택 섹션 */} +
+
+ 건물을 선택해주세요. + +
+
+ {buildings.map((building) => ( + + ))} +
+
+ + {/* 배터리 헬스 체크 섹션 */} +
+
+ 배터리 헬스 체크 + +
+
+ + +
+
+
+ ) +} + +function ChevronIcon() { + return ( + + + + ) +} diff --git a/src/pages/EmergencyAlertPage.module.css b/src/pages/EmergencyAlertPage.module.css new file mode 100644 index 0000000..2865d0f --- /dev/null +++ b/src/pages/EmergencyAlertPage.module.css @@ -0,0 +1,209 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(146deg, #010306 0.81%, #091A3F 100%); + overflow-x: hidden; + padding-bottom: 40px; +} + +/* Background blur effect - Figma Ellipse style */ +.bgBlur { + position: absolute; + width: 426px; + height: 409px; + left: 198px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +/* Alert Popup */ +.popup { + position: absolute; + left: 50%; + top: calc(50% + 22px); + transform: translate(-50%, -50%); + width: 340px; + height: 490px; + background: linear-gradient(193deg, rgb(195, 0, 0) 4.28%, rgb(123, 14, 11) 93.12%); + border-radius: 20px; + overflow: hidden; +} + +.popupBgImage { + position: absolute; + left: 160px; + top: 80px; + width: 303px; + height: 303px; + object-fit: cover; + filter: blur(2px); + opacity: 0.3; + transform: rotate(20.5deg); + pointer-events: none; +} + +.popupContent { + position: absolute; + top: 24px; + left: 10px; + width: 320px; + display: flex; + flex-direction: column; + gap: 36px; + align-items: center; +} + +.topRow { + display: flex; + justify-content: space-between; + align-items: flex-start; + width: 300px; +} + +.timestamp { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: rgba(255, 255, 255, 0.8); + text-align: center; +} + +.alertType { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; +} + +.mainInfo { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + color: #FFFFFF; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 1.4; + margin: 0; +} + +.description { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 20px; + opacity: 0.8; + margin: 0; +} + +/* Shelter info card */ +.shelterCard { + position: absolute; + top: 184px; + left: 50%; + transform: translateX(-50%); + width: 300px; + height: 180px; + background: rgba(255, 255, 255, 0.16); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + overflow: hidden; +} + +.shelterTag { + position: absolute; + top: 19px; + left: 50%; + transform: translateX(-50%); + padding: 10px 18px; + background: #6d0100; + border-radius: 100px; + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + line-height: 1; + color: #FFFFFF; + letter-spacing: -0.32px; + white-space: nowrap; +} + +.shelterLocation { + position: absolute; + top: 85px; + left: calc(50% + 100px); + transform: translate(-100%, -50%); + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 32px; + color: #FFFFFF; + text-align: right; + letter-spacing: -0.64px; + white-space: nowrap; +} + +.evacuationStats { + position: absolute; + top: 131px; + left: 82px; + display: flex; + align-items: flex-end; + gap: 6px; + opacity: 0.8; +} + +.evacuatedCount { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 24px; + color: #FFFFFF; + letter-spacing: -0.48px; +} + +.statsDivider { + font-family: 'Pretendard', sans-serif; + font-weight: 400; + font-size: 20px; + color: #ff7c7c; + letter-spacing: -0.4px; +} + +.totalCount { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 20px; + color: #ff7c7c; + letter-spacing: -0.4px; +} + +/* Confirm button */ +.confirmButton { + position: absolute; + bottom: 20px; + left: 20px; + width: 300px; + height: 60px; + background: #000000; + border-radius: 8px; + border: none; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 18px; + color: #FFFFFF; + cursor: pointer; +} + +.confirmButton:hover { + background: #1a1a1a; +} diff --git a/src/pages/EmergencyAlertPage.tsx b/src/pages/EmergencyAlertPage.tsx new file mode 100644 index 0000000..3f37cce --- /dev/null +++ b/src/pages/EmergencyAlertPage.tsx @@ -0,0 +1,70 @@ +import { useNavigate } from 'react-router-dom' +import Header from '../components/layout/Header' +import styles from './EmergencyAlertPage.module.css' +import emergencyBellImg from '../assets/emergency-bell.png' + +// Mock data - 추후 API 연동 +const alertData = { + timestamp: '2025-11-18 13:01:24', + type: '비상 : 화재', + title: '비상알림', + description: '생산동 2층 유류화재 발생', + shelter: '물류동 4층 옥상', + evacuated: 2345, + total: 4597 +} + +export default function EmergencyAlertPage() { + const navigate = useNavigate() + + const handleConfirm = () => { + navigate(-1) + } + + return ( +
+ {/* Background blur effect */} +
+ + {/* Header */} +
+ + {/* Alert Popup */} +
+ {/* Background image */} + + + {/* Content */} +
+ {/* Top row: timestamp and type */} +
+ {alertData.timestamp} + {alertData.type} +
+ + {/* Main alert info */} +
+

{alertData.title}

+

{alertData.description}

+
+
+ + {/* Shelter info card */} +
+
담당 대피소
+
{alertData.shelter}
+
+ {alertData.evacuated.toLocaleString()} + / + {alertData.total.toLocaleString()} +
+
+ + {/* Confirm button */} + +
+
+ ) +} diff --git a/src/pages/EmergencyFirePage.module.css b/src/pages/EmergencyFirePage.module.css new file mode 100644 index 0000000..7637807 --- /dev/null +++ b/src/pages/EmergencyFirePage.module.css @@ -0,0 +1,244 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(158deg, #010306 0.81%, #000000 100%); + overflow-x: hidden; + padding-bottom: 40px; +} + +/* Background red ellipse blur effect */ +.bgRedEllipse { + position: absolute; + width: 415px; + height: 397px; + left: 107px; + top: -34px; + pointer-events: none; + z-index: 0; +} + +.bgRedEllipse img { + position: absolute; + width: 200%; + height: 200%; + top: -50%; + left: -50%; +} + +/* Background emergency bell */ +.bgBellWrapper { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + width: 348px; + height: 348px; + left: 151px; + top: 4px; + pointer-events: none; + z-index: 0; +} + +.bgBellImage { + width: 270px; + height: 270px; + object-fit: cover; + transform: rotate(20.5deg); + filter: blur(2px); + opacity: 0.3; +} + +/* 담당 대피소 카드 */ +.shelterAssignCard { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 16px; + margin: 120px 20px 0; + padding: 24px 20px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; +} + +.shelterLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 16px; + color: rgba(255, 255, 255, 0.6); +} + +.shelterName { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + letter-spacing: -0.48px; +} + +.taggingButton { + width: 100%; + height: 60px; + background: #000000; + border-radius: 8px; + border: none; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 18px; + color: #FFFFFF; + cursor: pointer; +} + +.taggingButton:hover { + background: #1a1a1a; +} + +/* 통계 정보 */ +.statsContainer { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + margin: 24px 20px 0; +} + +.statItem { + display: flex; + flex-direction: column; + gap: 6px; +} + +.statLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 12px; + color: rgba(255, 255, 255, 0.7); +} + +.statValue { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + color: #FFFCFC; + letter-spacing: -0.56px; +} + +/* 카드 목록 */ +.cardList { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 16px; + margin: 24px 20px 0; +} + +/* 대피소 카드 */ +.shelterCard { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + cursor: pointer; + text-align: left; +} + +.shelterCard:hover { + background: rgba(255, 255, 255, 0.15); +} + +.shelterCardHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.shelterCardName { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 20px; + color: #FFFFFF; +} + +.shelterCardCount { + display: flex; + align-items: center; + gap: 10px; + justify-content: flex-end; +} + +.shelterCardValue { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + color: #FFFFFF; + letter-spacing: -0.56px; +} + +/* 미대피자 목록 카드 */ +.notEvacuatedCard { + display: flex; + flex-direction: column; + gap: 32px; + padding: 20px; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.4) 50%, rgba(232, 4, 0, 0.4) 114.14%); + backdrop-filter: blur(25px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + cursor: pointer; + text-align: left; +} + +.notEvacuatedCard:hover { + background: linear-gradient(180deg, rgba(0, 0, 0, 0.5) 50%, rgba(232, 4, 0, 0.5) 114.14%); +} + +.notEvacuatedHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.notEvacuatedTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 20px; + color: #FFFFFF; +} + +.notEvacuatedGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px 10px; +} + +.notEvacuatedItem { + display: flex; + flex-direction: column; + gap: 6px; +} + +.notEvacuatedBuilding { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 14px; + color: #777777; + letter-spacing: -0.28px; +} + +.notEvacuatedCount { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + letter-spacing: -0.48px; +} diff --git a/src/pages/EmergencyFirePage.tsx b/src/pages/EmergencyFirePage.tsx new file mode 100644 index 0000000..d4ac44c --- /dev/null +++ b/src/pages/EmergencyFirePage.tsx @@ -0,0 +1,149 @@ +import { useNavigate } from 'react-router-dom' +import Header from '../components/layout/Header' +import styles from './EmergencyFirePage.module.css' +import emergencyBellImg from '../assets/emergency-bell.png' +import redEllipseBg from '../assets/red-ellipse-bg.svg' +import arrowIcon from '../assets/arrow-up-right.svg' + +// Mock data - 추후 API 연동 +const emergencyData = { + type: '비상 : 화재', + shelter: { + name: '물류동 4층 옥상' + }, + stats: { + total: 4597, + remaining: 2087, + evacuated: 2345 + }, + shelters: [ + { id: 1, name: '휴게동 테니스장', count: 1569 }, + { id: 2, name: '구내식당', count: 352 } + ], + notEvacuated: [ + { building: 'A동', count: 1435 }, + { building: 'B동', count: 356 }, + { building: 'C동', count: 7 }, + { building: 'D동', count: 156 }, + { building: 'E동', count: 0 } + ] +} + +export default function EmergencyFirePage() { + const navigate = useNavigate() + + const handleTagging = () => { + // 인원태깅 기능 구현 + console.log('인원태깅 클릭') + } + + const handleShelterClick = () => { + // 대피자 목록 페이지로 이동 + navigate('/emergency-fire/evacuee-list') + } + + const handleNotEvacuatedClick = () => { + // 미대피자 목록 페이지로 이동 + navigate('/emergency-fire/non-evacuee-list') + } + + return ( +
+ {/* Background red ellipse blur effect */} +
+ +
+ + {/* Background emergency bell image */} +
+ +
+ + {/* Header */} +
+ + {/* 담당 대피소 카드 */} +
+ 담당 대피소 + {emergencyData.shelter.name} + +
+ + {/* 통계 정보 */} +
+
+ 총원 + {emergencyData.stats.total.toLocaleString()} +
+
+ 건물내 잔류 + {emergencyData.stats.remaining.toLocaleString()} +
+
+ 퇴실(대피소) + {emergencyData.stats.evacuated.toLocaleString()} +
+
+ + {/* 대피소 카드 목록 */} +
+ {emergencyData.shelters.map((shelter) => ( + + ))} + + {/* 미대피자 목록 카드 */} + +
+
+ ) +} + +function PersonIcon() { + return ( + + + + + ) +} + +function ArrowIcon() { + return ( + + ) +} + diff --git a/src/pages/EvacueeListPage.module.css b/src/pages/EvacueeListPage.module.css new file mode 100644 index 0000000..32be7c8 --- /dev/null +++ b/src/pages/EvacueeListPage.module.css @@ -0,0 +1,145 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + padding-bottom: 40px; +} + +/* 닫기 버튼 */ +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.closeButton img { + width: 40px; + height: 40px; +} + +/* 제목 섹션 */ +.titleSection { + padding: 50px 24px 0; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + color: #000000; + letter-spacing: -0.56px; + margin: 0; +} + +/* 로그 컨텐츠 */ +.logContent { + display: flex; + flex-direction: column; + gap: 32px; + padding: 28px 24px 0; +} + +/* 카테고리 탭 */ +.categoryTabs { + display: flex; + gap: 4px; + overflow-x: auto; + -ms-overflow-style: none; + scrollbar-width: none; + margin-left: -24px; + margin-right: -24px; + padding-left: 24px; + padding-right: 24px; +} + +.categoryTabs::-webkit-scrollbar { + display: none; +} + +.categoryTab { + padding: 12px 20px; + border-radius: 100px; + border: 1px solid #DDDDDD; + background: transparent; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #000000; + cursor: pointer; + white-space: nowrap; +} + +.categoryTab.active { + background: #000000; + border-color: #000000; + color: #FFFFFF; +} + +/* 대피소 정보 */ +.shelterInfo { + display: flex; + flex-direction: column; + gap: 20px; +} + +.shelterTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 20px; + color: #000000; +} + +/* 검색결과 테이블 */ +.resultTable { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tableHeader { + display: flex; + justify-content: space-between; +} + +.headerCell { + flex: 1; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #000000; + text-align: center; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 12px; +} + +.tableRow { + display: flex; + justify-content: space-between; +} + +.tableCell { + flex: 1; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: rgba(0, 0, 0, 0.6); + text-align: center; +} diff --git a/src/pages/EvacueeListPage.tsx b/src/pages/EvacueeListPage.tsx new file mode 100644 index 0000000..bdbac0d --- /dev/null +++ b/src/pages/EvacueeListPage.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import styles from './EvacueeListPage.module.css' +import closeIcon from '../assets/close-icon.svg' + +// Mock data - 추후 API 연동 +const categories = ['구내식당', '휴게동', '주차장', '옥상'] + +const evacueeData: Record = { + '구내식당': { + count: 352, + list: Array(16).fill(null).map((_, i) => ({ + tagId: `MK${String(100 + i).padStart(4, '0')}`, + datetime: '2025-12-23 15:00:23' + })) + }, + '휴게동': { + count: 128, + list: Array(10).fill(null).map((_, i) => ({ + tagId: `MK${String(200 + i).padStart(4, '0')}`, + datetime: '2025-12-23 14:30:15' + })) + }, + '주차장': { + count: 45, + list: Array(5).fill(null).map((_, i) => ({ + tagId: `MK${String(300 + i).padStart(4, '0')}`, + datetime: '2025-12-23 14:15:00' + })) + }, + '옥상': { + count: 67, + list: Array(8).fill(null).map((_, i) => ({ + tagId: `MK${String(400 + i).padStart(4, '0')}`, + datetime: '2025-12-23 13:45:30' + })) + } +} + +export default function EvacueeListPage() { + const navigate = useNavigate() + const [selectedCategory, setSelectedCategory] = useState(categories[0]) + + const currentData = evacueeData[selectedCategory] + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* 닫기 버튼 */} + + + {/* 제목 */} +
+

대피자 목록

+
+ + {/* 로그 컨텐츠 */} +
+ {/* 카테고리 탭 */} +
+ {categories.map((category) => ( + + ))} +
+ + {/* 대피소 정보 */} +
+ + 대피소: {selectedCategory} ({currentData.count}) + +
+ + {/* 검색결과 테이블 */} +
+ {/* 테이블 헤더 */} +
+ TagID + 확인 일시 +
+ + {/* 테이블 바디 */} +
+ {currentData.list.map((item, index) => ( +
+ {item.tagId} + {item.datetime} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/FacilityDetailPage.module.css b/src/pages/FacilityDetailPage.module.css new file mode 100644 index 0000000..28312e1 --- /dev/null +++ b/src/pages/FacilityDetailPage.module.css @@ -0,0 +1,71 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(146deg, #010306 0.81%, #091A3F 100%); + overflow-x: hidden; + padding-bottom: 40px; +} + +/* Background blur effect */ +.bgBlur { + position: absolute; + width: 426px; + height: 409px; + right: -234px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 82.5%); + filter: blur(100px); + pointer-events: none; +} + +/* Content area */ +.content { + position: relative; + padding-top: 92px; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Category tabs */ +.categoryArea { + padding-top: 40px; +} + +/* All log link */ +.allLogLink { + display: flex; + justify-content: space-between; + align-items: center; + width: 340px; + margin: 10px auto; + padding: 0 10px 0 4px; + cursor: pointer; +} + +.allLogLink:hover { + opacity: 0.8; +} + +.allLogText { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: #FFFFFF; +} + +.chevronIcon { + width: 28px; + height: 28px; +} + +/* Floor sections */ +.floorSections { + display: flex; + flex-direction: column; + gap: 22px; + padding: 0 10px; +} diff --git a/src/pages/FacilityDetailPage.tsx b/src/pages/FacilityDetailPage.tsx new file mode 100644 index 0000000..f9c5e78 --- /dev/null +++ b/src/pages/FacilityDetailPage.tsx @@ -0,0 +1,140 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import Header from '../components/layout/Header' +import CategoryTab from '../components/facility/CategoryTab' +import FloorSection from '../components/facility/FloorSection' +import EntryCard from '../components/facility/EntryCard' +import styles from './FacilityDetailPage.module.css' + +const categories = ['물류동', '생산동', '관리동', '위험물', '휴게동', '식당'] + +// Mock data - 추후 API 연동 +const floorData = [ + { + id: 1, + name: '2F(RF)', + workerCount: 7, + entries: [ + { + id: 1, + floor: '2F(RF)', + location: '옥상 출입구', + inCount: 56, + outCount: 25, + anchorStatus: 'normal' as const + } + ] + }, + { + id: 2, + name: '2F', + workerCount: 86, + entries: [ + { + id: 1, + floor: '2F', + location: '서쪽출입문', + inCount: 56, + outCount: 25, + anchorStatus: 'broken' as const + }, + { + id: 2, + floor: '2F', + location: '동쪽출입문', + inCount: 56, + outCount: 25, + anchorStatus: 'normal' as const + }, + { + id: 3, + floor: '2F', + location: '남쪽출입문', + inCount: 56, + outCount: 25, + anchorStatus: 'normal' as const + } + ] + } +] + +export default function FacilityDetailPage() { + const navigate = useNavigate() + const { facilityName } = useParams<{ facilityName: string }>() + const [activeCategory, setActiveCategory] = useState(facilityName || '물류동') + + const handleAllLogClick = () => { + navigate(`/facility-status/${activeCategory}/all-log`) + } + + const handleFloorLogClick = (floorName: string) => { + navigate(`/facility-status/${activeCategory}/floor-log/${encodeURIComponent(floorName)}`) + } + + const handleEntryLogClick = (location: string) => { + navigate(`/facility-status/${activeCategory}/log/${encodeURIComponent(location)}`) + } + + return ( +
+ {/* Background blur effect */} +
+ + {/* Header with back button and menu */} +
+ + {/* Content */} +
+ {/* Category Tabs */} +
+ +
+ + {/* All Log Link */} +
+ 전체 출입로그 보기 + + + +
+ + {/* Floor Sections */} +
+ {floorData.map((floor) => ( + handleFloorLogClick(floor.name)} + > + {floor.entries.map((entry) => ( + handleEntryLogClick(entry.location)} + /> + ))} + + ))} +
+
+
+ ) +} diff --git a/src/pages/FacilityStatusPage.module.css b/src/pages/FacilityStatusPage.module.css new file mode 100644 index 0000000..f0c9c88 --- /dev/null +++ b/src/pages/FacilityStatusPage.module.css @@ -0,0 +1,56 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(163deg, #010306 0.81%, #091A3F 100%); + overflow-x: hidden; + padding-bottom: 40px; +} + +/* Background blur effect */ +.bgBlur { + position: absolute; + width: 426px; + height: 409px; + right: -234px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 82.5%); + filter: blur(100px); + pointer-events: none; +} + +/* Content area */ +.content { + position: relative; + padding: 120px 20px 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Page title */ +.pageTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + margin: 0 0 36px 0; +} + +/* Section title */ +.sectionTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + margin: 0; +} + +/* Facility grid */ +.facilityGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} diff --git a/src/pages/FacilityStatusPage.tsx b/src/pages/FacilityStatusPage.tsx new file mode 100644 index 0000000..0a406c2 --- /dev/null +++ b/src/pages/FacilityStatusPage.tsx @@ -0,0 +1,55 @@ +import { useNavigate } from 'react-router-dom' +import Header from '../components/layout/Header' +import FacilityCard from '../components/facility/FacilityCard' +import styles from './FacilityStatusPage.module.css' + +interface Facility { + id: number + name: string + workers: number +} + +export default function FacilityStatusPage() { + const navigate = useNavigate() + + const facilities: Facility[] = [ + { id: 1, name: '물류동', workers: 1256 }, + { id: 2, name: '생산동', workers: 564 }, + { id: 3, name: '관리동', workers: 2 }, + { id: 4, name: '위험물', workers: 36 }, + { id: 5, name: '휴게동', workers: 56 }, + { id: 6, name: '생산동', workers: 64 }, + ] + + const handleFacilityClick = (facility: Facility) => { + navigate(`/facility-status/${facility.name}`) + } + + return ( +
+ {/* Background blur effect */} +
+ + {/* Header with back button and menu */} +
+ + {/* Content */} +
+

시설별 현황

+ +

전체 건물

+ +
+ {facilities.map((facility) => ( + handleFacilityClick(facility)} + /> + ))} +
+
+
+ ) +} diff --git a/src/pages/FloorLogPage.module.css b/src/pages/FloorLogPage.module.css new file mode 100644 index 0000000..f52c047 --- /dev/null +++ b/src/pages/FloorLogPage.module.css @@ -0,0 +1,106 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + overflow-x: hidden; + padding-bottom: 40px; +} + +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.closeButton:hover { + opacity: 0.7; +} + +.content { + display: flex; + flex-direction: column; + gap: 40px; + padding-top: 50px; + width: 320px; + margin: 0 auto; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + letter-spacing: -0.56px; + color: #000000; + margin: 0; +} + +.logTable { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.tableHeader { + display: flex; + align-items: flex-start; + height: 20px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #111111; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 16px; +} + +.tableRow { + display: flex; + align-items: flex-start; + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 14px; + color: #444444; +} + +.colDoor { + width: 80px; + text-align: left; +} + +.colAccess { + width: 40px; + text-align: center; +} + +.colTagId { + width: 85px; + text-align: center; +} + +.colTime { + flex: 1; + text-align: right; + letter-spacing: -0.28px; + white-space: nowrap; +} + +.tableHeader .colTime { + text-align: center; +} diff --git a/src/pages/FloorLogPage.tsx b/src/pages/FloorLogPage.tsx new file mode 100644 index 0000000..25d26b7 --- /dev/null +++ b/src/pages/FloorLogPage.tsx @@ -0,0 +1,78 @@ +import { useNavigate, useParams } from 'react-router-dom' +import styles from './FloorLogPage.module.css' + +interface LogEntry { + id: number + door: string + type: 'IN' | 'OUT' + tagId: string + time: string +} + +// Mock data - 추후 API 연동 +const mockLogs: LogEntry[] = [ + { id: 1, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 2, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 3, door: '옥상출입구', type: 'IN', tagId: 'MK0100', time: '2025-12-01 14:36:12' }, + { id: 4, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 5, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 6, door: '서쪽출입문', type: 'OUT', tagId: 'MK0101', time: '2025-12-01 15:00:00' }, + { id: 7, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 8, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 9, door: '동쪽출입문', type: 'IN', tagId: 'MK0102', time: '2025-12-01 15:30:45' }, + { id: 10, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 11, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 12, door: '남쪽출입문', type: 'OUT', tagId: 'MK0103', time: '2025-12-01 16:15:20' }, + { id: 13, door: '옥상출입구', type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 14, door: '옥상출입구', type: 'IN', tagId: 'MK0104', time: '2025-12-01 16:45:10' }, + { id: 15, door: '서쪽출입문', type: 'OUT', tagId: 'MK0105', time: '2025-12-01 17:00:55' }, + { id: 16, door: '동쪽출입문', type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, + { id: 17, door: '동쪽출입문', type: 'IN', tagId: 'MK0106', time: '2025-12-01 17:30:30' }, +] + +export default function FloorLogPage() { + const navigate = useNavigate() + const { floorName } = useParams<{ floorName: string }>() + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* Close Button */} + + + {/* Content */} +
+

{floorName || '2F'} 출입로그

+ +
+ {/* Table Header */} +
+ 출입문 + 출입 + TagID + 시간 +
+ + {/* Table Body */} +
+ {mockLogs.map((log) => ( +
+ {log.door} + {log.type} + {log.tagId} + {log.time} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/LoginPage.module.css b/src/pages/LoginPage.module.css new file mode 100644 index 0000000..6078eea --- /dev/null +++ b/src/pages/LoginPage.module.css @@ -0,0 +1,82 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(163deg, #010306 0.81%, #091A3F 100%); + overflow: hidden; +} + +/* Background blur effect - Ellipse 2327 */ +.bgBlur { + position: absolute; + width: 426px; + height: 409px; + right: -234px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 82.5%); + filter: blur(100px); + pointer-events: none; +} + +/* GNB Header */ +.gnb { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px 16px 0px 24px; + position: absolute; + width: 100%; + height: 38px; + left: 0; + top: 40px; +} + +/* Title Area */ +.titleArea { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + padding: 0px; + gap: 4px; + position: absolute; + width: 320px; + left: 20px; + top: 128px; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-style: normal; + font-weight: 700; + font-size: 28px; + line-height: 140%; + display: flex; + align-items: center; + color: #FFFFFF; +} + +/* Form Area */ +.formArea { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 91px; + position: absolute; + width: 320px; + left: calc(50% - 320px/2); + top: 244px; +} + +.inputGroup { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 24px; + width: 320px; +} diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx new file mode 100644 index 0000000..3db47f5 --- /dev/null +++ b/src/pages/LoginPage.tsx @@ -0,0 +1,89 @@ +import { useState, FormEvent } from 'react' +import Logo from '../components/common/Logo' +import Input from '../components/common/Input' +import Button from '../components/common/Button' +import styles from './LoginPage.module.css' + +interface LoginErrors { + userId?: string + password?: string + general?: string +} + +export default function LoginPage() { + const [userId, setUserId] = useState('') + const [password, setPassword] = useState('') + const [errors, setErrors] = useState({}) + + const validateForm = (): boolean => { + const newErrors: LoginErrors = {} + + if (!userId.trim()) { + newErrors.userId = '아이디를 바르게 입력해주세요' + } + + if (!password.trim()) { + newErrors.password = '비밀번호를 입력해주세요' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + + if (!validateForm()) { + return + } + + // TODO: API 로그인 로직 구현 + setErrors({ + general: '존재하지 않는 아이디이거나\n비밀번호가 잘못 입력되었습니다' + }) + } + + return ( +
+ {/* Background blur effect */} +
+ + {/* Header / GNB */} +
+ +
+ + {/* Title */} +
+

머크코리아

+

작업자 위치관제 시스템

+
+ + {/* Form Area */} +
+
+ setUserId(e.target.value)} + error={errors.userId} + /> + + setPassword(e.target.value)} + showPasswordToggle + error={errors.password || errors.general} + /> +
+ + +
+
+ ) +} diff --git a/src/pages/MyInfoPage.module.css b/src/pages/MyInfoPage.module.css new file mode 100644 index 0000000..9079779 --- /dev/null +++ b/src/pages/MyInfoPage.module.css @@ -0,0 +1,173 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(159.19deg, rgb(1, 3, 6) 0.81%, rgb(9, 26, 63) 100%); + overflow-x: hidden; + overflow-y: auto; +} + +/* Background blur effect */ +.bgWrapper { + position: absolute; + width: 426px; + height: 409px; + left: 198px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +/* Title Section */ +.titleSection { + position: relative; + padding: 120px 20px 0; + display: flex; + flex-direction: column; + gap: 20px; +} + +.pageTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + color: #FFFFFF; + margin: 0; + line-height: 22px; +} + +.userId { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 20px; + color: rgba(255, 255, 255, 0.6); + margin: 0; + line-height: 1; +} + +/* Form Area */ +.formArea { + position: relative; + display: flex; + flex-direction: column; + gap: 40px; + padding: 24px 24px 0; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 16px; +} + +.inputLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + line-height: 1; +} + +.inputWrapper { + display: flex; + align-items: center; + gap: 10px; + background: linear-gradient(90deg, rgb(28, 37, 66) 0%, rgb(28, 37, 66) 100%); + border-radius: 8px; + padding: 22px 16px; + overflow: hidden; +} + +.input { + flex: 1; + background: transparent; + border: none; + outline: none; + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: #FFFFFF; + letter-spacing: -0.32px; + line-height: 27px; +} + +.input::placeholder { + color: #D1D9FF; +} + +.eyeButton { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + background: transparent; + border: none; + padding: 0; + cursor: pointer; +} + +.errorMessages { + display: flex; + flex-direction: column; + gap: 0; +} + +.errorMessage { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: #BF151B; + line-height: 27px; + letter-spacing: -0.32px; + margin: 0; +} + +/* Password Requirements */ +.requirements { + position: relative; + padding: 40px 23px 0; +} + +.requirements p { + font-family: 'Pretendard', sans-serif; + font-weight: 400; + font-size: 14px; + color: rgba(171, 177, 230, 0.8); + line-height: 1.6; + margin: 0; +} + +/* Submit Button */ +.buttonArea { + position: relative; + display: flex; + justify-content: center; + padding: 40px 25px 32px; +} + +.submitButton { + width: 310px; + height: 60px; + background: #000000; + border: none; + border-radius: 8px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + cursor: pointer; +} + +.submitButton:hover { + background: #1a1a1a; +} + +.submitButton:active { + background: #333333; +} diff --git a/src/pages/MyInfoPage.tsx b/src/pages/MyInfoPage.tsx new file mode 100644 index 0000000..317218a --- /dev/null +++ b/src/pages/MyInfoPage.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import Header from '../components/layout/Header' +import styles from './MyInfoPage.module.css' + +export default function MyInfoPage() { + const navigate = useNavigate() + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showCurrentPassword, setShowCurrentPassword] = useState(false) + const [showNewPassword, setShowNewPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const [errors, setErrors] = useState<{ + currentPassword?: string + newPassword?: string + confirmPassword?: string + }>({}) + + const handleSubmit = () => { + const newErrors: typeof errors = {} + + // Validate current password + if (!currentPassword) { + newErrors.currentPassword = '암호가 일치하지 않습니다.' + } + + // Validate new password length + if (newPassword.length < 9) { + newErrors.newPassword = '최소 아홉글자 이상을 입력해야 합니다.' + } + + // Validate password match + if (newPassword !== confirmPassword) { + newErrors.confirmPassword = '암호가 일치하지 않습니다.' + } + + setErrors(newErrors) + + if (Object.keys(newErrors).length === 0) { + // Success - navigate back or show success message + alert('비밀번호가 변경되었습니다.') + navigate(-1) + } + } + + return ( +
+ {/* Background blur effect */} +
+ + {/* Header */} +
+ + {/* Title Section */} +
+

내 정보 설정

+

ID: admin0705

+
+ + {/* Form Area */} +
+ {/* Current Password */} +
+ +
+ setCurrentPassword(e.target.value)} + /> + +
+ {errors.currentPassword && ( +

{errors.currentPassword}

+ )} +
+ + {/* New Password */} +
+ +
+ setNewPassword(e.target.value)} + /> + +
+
+ setConfirmPassword(e.target.value)} + /> + +
+ {(errors.newPassword || errors.confirmPassword) && ( +
+ {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} + {errors.newPassword && ( +

{errors.newPassword}

+ )} +
+ )} +
+
+ + {/* Password Requirements */} +
+

영대문자(A~Z, 26개), 영소문자(a~z, 26개),

+

숫자(0~9, 10개) 및 특수문자(32개) 중 3종류

+

이상으로 구성하여 최소 9자 이상으로 입력합니다.

+
+ + {/* Submit Button */} +
+ +
+
+ ) +} + +function EyeIcon() { + return ( + + + + + ) +} diff --git a/src/pages/NonEvacueeListPage.module.css b/src/pages/NonEvacueeListPage.module.css new file mode 100644 index 0000000..680ddec --- /dev/null +++ b/src/pages/NonEvacueeListPage.module.css @@ -0,0 +1,95 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + padding-bottom: 40px; +} + +/* 닫기 버튼 */ +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.closeButton img { + width: 40px; + height: 40px; +} + +/* 제목 섹션 */ +.titleSection { + padding: 50px 24px 0; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + color: #000000; + letter-spacing: -0.56px; + margin: 0; +} + +/* 테이블 컨텐츠 */ +.tableContent { + display: flex; + flex-direction: column; + gap: 32px; + padding: 28px 24px 0; +} + +/* 검색결과 테이블 */ +.resultTable { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tableHeader { + display: flex; + justify-content: space-between; +} + +.headerCell { + flex: 1; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #000000; + text-align: center; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 12px; +} + +.tableRow { + display: flex; + justify-content: space-between; +} + +.tableCell { + flex: 1; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: rgba(0, 0, 0, 0.6); + text-align: center; +} diff --git a/src/pages/NonEvacueeListPage.tsx b/src/pages/NonEvacueeListPage.tsx new file mode 100644 index 0000000..6b6ec3c --- /dev/null +++ b/src/pages/NonEvacueeListPage.tsx @@ -0,0 +1,70 @@ +import { useNavigate } from 'react-router-dom' +import styles from './NonEvacueeListPage.module.css' +import closeIcon from '../assets/close-icon.svg' + +// Mock data - 추후 API 연동 +const nonEvacueeData = { + count: 356, + list: [ + { tagId: 'MK0100', location: '연구A동' }, + { tagId: 'MK0101', location: '연구A동' }, + { tagId: 'MK0102', location: '본관' }, + { tagId: 'MK0103', location: '휴게동' }, + { tagId: 'MK0104', location: '연구B동' }, + { tagId: 'MK0105', location: '주차장' }, + { tagId: 'MK0106', location: '연구A동' }, + { tagId: 'MK0107', location: '본관' }, + { tagId: 'MK0108', location: '휴게동' }, + { tagId: 'MK0109', location: '연구A동' }, + { tagId: 'MK0110', location: '연구B동' }, + { tagId: 'MK0111', location: '본관' }, + { tagId: 'MK0112', location: '주차장' }, + { tagId: 'MK0113', location: '연구A동' }, + { tagId: 'MK0114', location: '휴게동' }, + { tagId: 'MK0115', location: '연구B동' }, + ] +} + +export default function NonEvacueeListPage() { + const navigate = useNavigate() + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* 닫기 버튼 */} + + + {/* 제목 */} +
+

미대피자 목록 ({nonEvacueeData.count})

+
+ + {/* 테이블 컨텐츠 */} +
+ {/* 검색결과 테이블 */} +
+ {/* 테이블 헤더 */} +
+ TagID + 최종 위치 +
+ + {/* 테이블 바디 */} +
+ {nonEvacueeData.list.map((item, index) => ( +
+ {item.tagId} + {item.location} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/QRScanCompletePage.module.css b/src/pages/QRScanCompletePage.module.css new file mode 100644 index 0000000..c854da6 --- /dev/null +++ b/src/pages/QRScanCompletePage.module.css @@ -0,0 +1,215 @@ +.container { + position: relative; + width: 100%; + height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(163deg, #010306 0.81%, #091530 100%); + overflow: hidden; +} + +/* Background blur effects - Figma Ellipse style */ +.bgBlurTop { + position: absolute; + width: 426px; + height: 409px; + left: 198px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +.bgBlurLeft { + position: absolute; + width: 426px; + height: 409px; + left: -264px; + top: -327px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +/* Close button */ +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.closeButton:hover { + opacity: 0.7; +} + +/* Title */ +.titleContainer { + position: absolute; + top: 92px; + left: 24px; + right: 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + z-index: 1; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 1.4; + text-align: center; + color: #FFFFFF; + margin: 0; + letter-spacing: -0.56px; +} + +.tagId { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 20px; + line-height: 1.4; + text-align: center; + color: #477BFF; + margin: 0; + letter-spacing: -0.4px; +} + +/* Success Circle */ +.successCircleContainer { + position: absolute; + top: 208px; + left: 50%; + transform: translateX(-50%); + width: 315px; + height: 315px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.pulseRing { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 185px; + height: 185px; + border-radius: 50%; + background: linear-gradient(139deg, #77CEA3 0%, #39CD31 100%); + animation: successPulse 3.5s ease-out infinite; + z-index: 0; +} + +@keyframes successPulse { + 0% { + width: 185px; + height: 185px; + opacity: 0.5; + } + 100% { + width: 350px; + height: 350px; + opacity: 0; + } +} + +/* Center Circle */ +.centerCircle { + position: absolute; + width: 185px; + height: 185px; + border-radius: 50%; + background: linear-gradient(41deg, #23B91B 0%, #77C4CE 100%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + z-index: 1; +} + +.checkIcon { + width: 38px; + height: 28px; +} + +.completeText { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + letter-spacing: -0.32px; + white-space: nowrap; +} + +/* Bottom buttons */ +.buttonContainer { + position: absolute; + top: 588px; + left: 25px; + right: 25px; + display: flex; + gap: 10px; + z-index: 1; +} + +.finishButton { + flex: 1; + height: 56px; + background: #FFFFFF; + border: 1px solid #000000; + border-radius: 8px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #000000; + cursor: pointer; + transition: background 0.2s; +} + +.finishButton:hover { + background: #f0f0f0; +} + +.finishButton:active { + background: #e0e0e0; +} + +.continueButton { + flex: 1; + height: 56px; + background: #000000; + border: none; + border-radius: 8px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + cursor: pointer; + transition: background 0.2s; +} + +.continueButton:hover { + background: #1a1a1a; +} + +.continueButton:active { + background: #333333; +} diff --git a/src/pages/QRScanCompletePage.tsx b/src/pages/QRScanCompletePage.tsx new file mode 100644 index 0000000..aed7e28 --- /dev/null +++ b/src/pages/QRScanCompletePage.tsx @@ -0,0 +1,72 @@ +import { useNavigate, useLocation } from 'react-router-dom' +import styles from './QRScanCompletePage.module.css' +import checkIcon from '../assets/check-icon.svg' + +export default function QRScanCompletePage() { + const navigate = useNavigate() + const location = useLocation() + const tagId = location.state?.tagId || 'TF-003' + + const handleFinish = () => { + navigate('/dashboard') + } + + const handleContinue = () => { + navigate('/qr-scan') + } + + const handleClose = () => { + navigate('/dashboard') + } + + return ( +
+ {/* Background blur effects */} +
+
+ + {/* Close button */} + + + {/* Title */} +
+

태그 인식 완료

+

TagID : {tagId}

+
+ + {/* Success Circle with Pulse Animation */} +
+ {/* Pulse Animation */} +
+
+
+ + {/* Center Circle */} +
+ 완료 + 완료 되었습니다. +
+
+ + {/* Bottom buttons */} +
+ + +
+
+ ) +} + +function CloseIcon() { + return ( + + + + ) +} diff --git a/src/pages/QRScanPage.module.css b/src/pages/QRScanPage.module.css new file mode 100644 index 0000000..cd61752 --- /dev/null +++ b/src/pages/QRScanPage.module.css @@ -0,0 +1,196 @@ +.container { + position: relative; + width: 100%; + height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(163deg, #010306 0.81%, #091530 100%); + overflow: hidden; +} + +/* Popup overlay - 배경 제거, container에서 처리 */ +.popup { + display: none; +} + +/* Background blur effects - Figma Ellipse style */ +.bgBlurTop { + position: absolute; + width: 426px; + height: 409px; + left: 198px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +.bgBlurLeft { + position: absolute; + width: 426px; + height: 409px; + left: -264px; + top: -327px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +/* Close button */ +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.closeButton:hover { + opacity: 0.7; +} + +/* Title */ +.titleContainer { + position: absolute; + top: 92px; + left: 24px; + right: 24px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 5; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 1.4; + text-align: center; + color: #FFFFFF; + margin: 0; + letter-spacing: -0.56px; +} + +/* Illustration */ +.illustrationContainer { + position: absolute; + top: 180px; + left: 50%; + transform: translateX(-50%); + width: 320px; + height: 360px; + display: flex; + align-items: center; + justify-content: center; + z-index: 5; +} + +.illustration { + width: 100%; + height: auto; + object-fit: contain; +} + +/* Bottom buttons */ +.buttonContainer { + position: absolute; + top: 588px; + left: 25px; + right: 25px; + display: flex; + gap: 10px; + z-index: 5; +} + +.cancelButton { + flex: 1; + height: 56px; + background: #FFFFFF; + border: 1px solid #000000; + border-radius: 8px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #000000; + cursor: pointer; + transition: background 0.2s; +} + +.cancelButton:hover { + background: #f0f0f0; +} + +.cancelButton:active { + background: #e0e0e0; +} + +.confirmButton { + flex: 1; + height: 56px; + background: #000000; + border: none; + border-radius: 8px; + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + cursor: pointer; + transition: background 0.2s; +} + +.confirmButton:hover { + background: #1a1a1a; +} + +.confirmButton:active { + background: #333333; +} + +/* Pulse animation */ +.pulseContainer { + position: absolute; + top: 244px; + left: 50%; + transform: translateX(-50%); + width: 300px; + height: 300px; + pointer-events: none; + z-index: 3; +} + +.pulseRing { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100px; + height: 100px; + border-radius: 50%; + background: linear-gradient(139deg, #7798CE 0%, #3C5BCD 100%); + animation: pulse 3.5s ease-out infinite; +} + +@keyframes pulse { + 0% { + width: 100px; + height: 100px; + opacity: 0.5; + } + 100% { + width: 350px; + height: 350px; + opacity: 0; + } +} diff --git a/src/pages/QRScanPage.tsx b/src/pages/QRScanPage.tsx new file mode 100644 index 0000000..2596d75 --- /dev/null +++ b/src/pages/QRScanPage.tsx @@ -0,0 +1,78 @@ +import { useNavigate } from 'react-router-dom' +import styles from './QRScanPage.module.css' +import qrIllustrationImg from '../assets/qr-illustration.png' + +export default function QRScanPage() { + const navigate = useNavigate() + + const handleCancel = () => { + navigate(-1) + } + + const handleConfirm = () => { + // QR 스캔 완료 페이지로 이동 + navigate('/qr-scan/complete', { state: { tagId: 'TF-003' } }) + } + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* Background blur effects */} +
+
+ + {/* Popup container */} +
+ + {/* Close button */} + + + {/* Title */} +
+

+ QR코드를
+ 인식시켜 주세요 +

+
+ + {/* Pulse Animation */} +
+
+
+
+
+ + {/* QR Phone Illustration */} +
+ QR 코드 스캔 +
+ + {/* Bottom buttons */} +
+ + +
+
+ ) +} + +function CloseIcon() { + return ( + + + + ) +} diff --git a/src/pages/TagBatteryListPage.module.css b/src/pages/TagBatteryListPage.module.css new file mode 100644 index 0000000..d5f82a0 --- /dev/null +++ b/src/pages/TagBatteryListPage.module.css @@ -0,0 +1,153 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: #FFFFFF; + overflow-x: hidden; + padding-bottom: 40px; +} + +.closeButton { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.closeButton:hover { + opacity: 0.7; +} + +.content { + display: flex; + flex-direction: column; + gap: 40px; + padding-top: 50px; + padding-left: 24px; + padding-right: 24px; +} + +/* Title Section */ +.titleSection { + display: flex; + flex-direction: column; + gap: 20px; +} + +.title { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 28px; + line-height: 32px; + letter-spacing: -0.56px; + color: #000000; + margin: 0; +} + +.description { + font-family: 'Pretendard', sans-serif; + font-weight: 400; + font-size: 16px; + line-height: 1.6; + color: #111111; + margin: 0; + white-space: pre-line; +} + +/* List Section */ +.listSection { + display: flex; + flex-direction: column; + gap: 12px; +} + +.countRow { + display: flex; + justify-content: space-between; + align-items: center; + height: 20px; +} + +.totalCount { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #477BFF; + line-height: 1; +} + +/* Table */ +.table { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tableHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.headerTagId { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #000000; + line-height: 1; +} + +.headerDate { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #000000; + text-align: center; + width: 135px; + line-height: 1; +} + +.tableBody { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 12px; + padding-right: 12px; + max-height: 448px; + overflow-y: auto; +} + +.tableRow { + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 4px; +} + +.cellTagId { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #444444; + width: 60px; + line-height: 1; +} + +.cellDate { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 14px; + color: #444444; + text-align: center; + line-height: 1; +} diff --git a/src/pages/TagBatteryListPage.tsx b/src/pages/TagBatteryListPage.tsx new file mode 100644 index 0000000..b0229e4 --- /dev/null +++ b/src/pages/TagBatteryListPage.tsx @@ -0,0 +1,116 @@ +import { useNavigate, useParams } from 'react-router-dom' +import styles from './TagBatteryListPage.module.css' + +interface TagEntry { + id: number + tagId: string + registeredAt: string +} + +// Mock data - 추후 API 연동 +const mockLowBatteryTags: TagEntry[] = [ + { id: 1, tagId: 'P001', registeredAt: '2025-11-24 15:26:35' }, + { id: 2, tagId: 'P005', registeredAt: '2025-11-24 15:26:35' }, + { id: 3, tagId: 'P006', registeredAt: '2025-11-24 15:30:00' }, + { id: 4, tagId: 'P006', registeredAt: '2025-11-24 15:30:00' }, + { id: 5, tagId: 'P008', registeredAt: '2025-11-24 15:40:25' }, + { id: 6, tagId: 'P009', registeredAt: '2025-11-25 10:30:00' }, + { id: 7, tagId: 'P010', registeredAt: '2025-11-26 09:15:45' }, + { id: 8, tagId: 'P011', registeredAt: '2025-11-24 15:55:30' }, + { id: 9, tagId: 'P012', registeredAt: '2025-11-24 16:00:45' }, + { id: 10, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 11, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 12, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 13, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 14, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 15, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, + { id: 16, tagId: 'P013', registeredAt: '2025-11-24 16:05:00' }, +] + +const mockHighBatteryTags: TagEntry[] = [ + { id: 1, tagId: 'H001', registeredAt: '2025-11-24 10:00:00' }, + { id: 2, tagId: 'H002', registeredAt: '2025-11-24 10:15:00' }, + { id: 3, tagId: 'H003', registeredAt: '2025-11-24 10:30:00' }, + { id: 4, tagId: 'H004', registeredAt: '2025-11-24 11:00:00' }, + { id: 5, tagId: 'H005', registeredAt: '2025-11-24 11:30:00' }, + { id: 6, tagId: 'H006', registeredAt: '2025-11-24 12:00:00' }, + { id: 7, tagId: 'H007', registeredAt: '2025-11-24 12:30:00' }, + { id: 8, tagId: 'H008', registeredAt: '2025-11-24 13:00:00' }, + { id: 9, tagId: 'H009', registeredAt: '2025-11-24 13:30:00' }, + { id: 10, tagId: 'H010', registeredAt: '2025-11-24 14:00:00' }, + { id: 11, tagId: 'H011', registeredAt: '2025-11-24 14:30:00' }, + { id: 12, tagId: 'H012', registeredAt: '2025-11-24 15:00:00' }, +] + +const pageConfig = { + low: { + title: 'Low Battery', + description: '남은 전력이 25% 이하로\n배터리 교체 대상인 태그의 목록입니다', + data: mockLowBatteryTags, + totalCount: 1376, + }, + high: { + title: 'High Battery', + description: '배터리의 남은 전력이 25% 를 초과하는\n태그의 목록입니다', + data: mockHighBatteryTags, + totalCount: 1376, + }, +} + +export default function TagBatteryListPage() { + const navigate = useNavigate() + const { type } = useParams<{ type: 'low' | 'high' }>() + + const config = pageConfig[type as 'low' | 'high'] || pageConfig.low + + const handleClose = () => { + navigate(-1) + } + + return ( +
+ {/* Close Button */} + + + {/* Content */} +
+ {/* Title Section */} +
+

{config.title}

+

{config.description}

+
+ + {/* List Section */} +
+ {/* Total Count */} +
+ 총 {config.totalCount.toLocaleString()}개 +
+ + {/* Table */} +
+ {/* Header */} +
+ TagID + 등록일 +
+ + {/* Body */} +
+ {config.data.map((entry) => ( +
+ {entry.tagId} + {entry.registeredAt} +
+ ))} +
+
+
+
+
+ ) +} diff --git a/src/pages/TagBatteryPage.module.css b/src/pages/TagBatteryPage.module.css new file mode 100644 index 0000000..45a0935 --- /dev/null +++ b/src/pages/TagBatteryPage.module.css @@ -0,0 +1,132 @@ +.container { + position: relative; + width: 100%; + min-height: 100vh; + max-width: 360px; + margin: 0 auto; + background: linear-gradient(163.03deg, rgb(1, 3, 6) 0.81%, rgb(9, 26, 63) 100%); + overflow: hidden; + padding-bottom: 40px; +} + +/* Background blur effect - Figma Ellipse 2327 */ +.bgWrapper { + position: absolute; + width: 426px; + height: 409px; + left: 198px; + top: -275px; + background: linear-gradient(228.57deg, rgba(119, 152, 206, 0.8) 20.5%, rgba(79, 72, 176, 0.8) 63.68%); + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; +} + +/* Title Section */ +.titleSection { + position: relative; + padding: 120px 20px 0; +} + +.pageTitle { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + margin: 0; + line-height: 1; +} + +/* Cards Section */ +.cardsSection { + position: relative; + display: flex; + flex-direction: column; + gap: 18px; + padding: 24px 20px 0; +} + +/* Battery Card */ +.batteryCard { + display: flex; + flex-direction: column; + gap: 20px; + padding: 24px 20px; + background: linear-gradient(120deg, rgba(58, 69, 123, 0.64) 2%, rgba(36, 44, 85, 0.32) 95%); + backdrop-filter: blur(20px); + border-radius: 16px; + overflow: hidden; +} + +.cardContent { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 0 4px; +} + +.cardLeft { + display: flex; + align-items: center; + gap: 20px; +} + +.cardInfo { + display: flex; + flex-direction: column; + gap: 8px; +} + +.batteryLabel { + font-family: 'Pretendard', sans-serif; + font-weight: 500; + font-size: 16px; + color: rgba(255, 255, 255, 0.6); + opacity: 0.8; + line-height: 1; +} + +.batteryCount { + font-family: 'Pretendard', sans-serif; + font-weight: 700; + font-size: 24px; + color: #FFFFFF; + line-height: 1; +} + +.batteryPercentage { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 28px; + color: #FFFFFF; + letter-spacing: -0.56px; + text-align: right; + line-height: 1; +} + +.cardDivider { + width: 100%; + height: 1px; + background: rgba(255, 255, 255, 0.1); +} + +.listButton { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0; + background: transparent; + border: none; + cursor: pointer; +} + +.listButtonText { + font-family: 'Pretendard', sans-serif; + font-weight: 600; + font-size: 16px; + color: #FFFFFF; + text-transform: uppercase; + line-height: 1; +} diff --git a/src/pages/TagBatteryPage.tsx b/src/pages/TagBatteryPage.tsx new file mode 100644 index 0000000..a153db1 --- /dev/null +++ b/src/pages/TagBatteryPage.tsx @@ -0,0 +1,89 @@ +import { useNavigate } from 'react-router-dom' +import Header from '../components/layout/Header' +import styles from './TagBatteryPage.module.css' +import highBatteryIcon from '../assets/battery_high.svg' +import lowBatteryIcon from '../assets/battery_low.svg' + +// 배터리 데이터 (추후 API 연동) +const batteryData = { + high: { count: 1686, percentage: 61 }, + low: { count: 1316, percentage: 39 } +} + +export default function TagBatteryPage() { + const navigate = useNavigate() + + return ( +
+ {/* Background blur effect - Figma Ellipse 2327 */} +
+ + {/* Header */} +
+ + {/* Title */} +
+

태그 배터리

+
+ + {/* Battery Cards */} +
+ {/* High Battery Card */} +
+
+
+ +
+ High Battery + {batteryData.high.count.toLocaleString()}개 +
+
+ {batteryData.high.percentage}% +
+
+ +
+ + {/* Low Battery Card */} +
+
+
+ +
+ Low Battery + {batteryData.low.count.toLocaleString()}개 +
+
+ {batteryData.low.percentage}% +
+
+ +
+
+
+ ) +} + +function BatteryIcon({ level }: { level: 'high' | 'low' }) { + return ( + {`${level} + ) +} + +function ChevronIcon() { + return ( + + + + ) +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..74c956e --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,71 @@ +@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); + +:root { + /* Background */ + --bg-gradient: linear-gradient(163deg, #010306 0.81%, #091A3F 100%); + --bg-input: #1C2542; + + /* Colors */ + --color-primary: #FFFFFF; + --color-error: #BF151B; + --color-error-text: #FF0000; + --color-success: #17A200; + + /* Text */ + --text-primary: #FFFFFF; + --text-placeholder: #D1D9FF; + --text-secondary: rgba(255, 255, 255, 0.8); + --text-gray: #686868; + + /* Button */ + --btn-primary: #000000; + + /* Menu */ + --bg-menu: #FFFFFF; + --text-menu-primary: #000000; + --text-menu-secondary: #686868; + --border-menu: #E5E5E5; + + /* Border radius */ + --radius: 8px; + + /* Font */ + --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + width: 100%; + height: 100%; + font-family: var(--font-family); + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + width: 100%; + height: 100%; +} + +input, button { + font-family: inherit; + font-size: inherit; +} + +button { + cursor: pointer; + border: none; + outline: none; +} + +a { + text-decoration: none; + color: inherit; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..488cd9d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 0000000..b17fa35 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/common/button.tsx","./src/components/common/input.tsx","./src/components/common/logo.tsx","./src/components/dashboard/batteryhealthcard.tsx","./src/components/dashboard/buildingcard.tsx","./src/components/facility/categorytab.tsx","./src/components/facility/entrycard.tsx","./src/components/facility/facilitycard.tsx","./src/components/facility/floorsection.tsx","./src/components/layout/header.tsx","./src/components/layout/sidemenu.tsx","./src/pages/accesslogpage.tsx","./src/pages/alllogpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/emergencyalertpage.tsx","./src/pages/emergencyfirepage.tsx","./src/pages/evacueelistpage.tsx","./src/pages/facilitydetailpage.tsx","./src/pages/facilitystatuspage.tsx","./src/pages/floorlogpage.tsx","./src/pages/loginpage.tsx","./src/pages/myinfopage.tsx","./src/pages/nonevacueelistpage.tsx","./src/pages/qrscancompletepage.tsx","./src/pages/qrscanpage.tsx","./src/pages/tagbatterylistpage.tsx","./src/pages/tagbatterypage.tsx"],"version":"5.6.3"} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0d51f19 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})