Spaces:
Paused
Paused
Commit ·
b289b19
0
Parent(s):
feat: initial release without tracked binary images
Browse files- .github/workflows/deploy.yml +31 -0
- Dockerfile +12 -0
- README.md +39 -0
- app.js +1546 -0
- index.html +550 -0
- styles.css +2139 -0
.github/workflows/deploy.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
# GitHub Actions 탭에서 수동 실행 허용
|
| 8 |
+
workflow_dispatch:
|
| 9 |
+
|
| 10 |
+
jobs:
|
| 11 |
+
deploy:
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
steps:
|
| 14 |
+
- name: Checkout Code
|
| 15 |
+
uses: actions/checkout@v3
|
| 16 |
+
with:
|
| 17 |
+
fetch-depth: 0
|
| 18 |
+
|
| 19 |
+
- name: Setup Git Credentials & Push to HF
|
| 20 |
+
env:
|
| 21 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 22 |
+
# 본인의 Hugging Face Username 및 Space Name에 맞춰 변경하십시오.
|
| 23 |
+
HF_USERNAME: "HwangJinwook"
|
| 24 |
+
HF_SPACE_NAME: "caretable"
|
| 25 |
+
run: |
|
| 26 |
+
git config --global user.email "actions@github.com"
|
| 27 |
+
git config --global user.name "GitHub Actions"
|
| 28 |
+
|
| 29 |
+
# Hugging Face Spaces Git Remote 추가 및 강제 푸시 (Docker 배포 진행)
|
| 30 |
+
git remote add hf https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/$HF_USERNAME/$HF_SPACE_NAME
|
| 31 |
+
git push --force hf main
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM nginx:alpine
|
| 2 |
+
|
| 3 |
+
# 프로젝트 전체 정적 파일을 Nginx 기본 서빙 경로로 복사
|
| 4 |
+
COPY . /usr/share/nginx/html
|
| 5 |
+
|
| 6 |
+
# Hugging Face Spaces는 기본 포트로 7860을 사용하므로 default.conf의 80 포트를 7860으로 리라이팅
|
| 7 |
+
RUN sed -i 's/listen[[:space:]]*80;/listen 7860;/g' /etc/nginx/conf.d/default.conf
|
| 8 |
+
|
| 9 |
+
# 7860 포트 노출
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["nginx", "-g", "daemon off;"]
|
README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CareTable
|
| 3 |
+
emoji: 🍱
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# CareTable - 취약계층 및 가정 맞춤형 급식 안전 스케줄러 & 증빙 자동화 플랫폼
|
| 11 |
+
|
| 12 |
+
2026 사회보장정보와 공공·민간 빅데이터를 활용한 국민행복 서비스 발굴‧창업경진대회 출품작입니다.
|
| 13 |
+
|
| 14 |
+
본 저장소는 **Hugging Face Spaces**의 Docker SDK 환경을 통해 호스팅되는 CareTable 데모 웹 사이트입니다.
|
| 15 |
+
|
| 16 |
+
## 🚀 주요 기능
|
| 17 |
+
1. **일반 가정 / 사회복지시설 범용 토글**: 시설용 모드와 일반 소비자(가정) 모드를 스위칭하여 맞춤형 데이터베이스 및 ESG 일지를 관리합니다.
|
| 18 |
+
2. **OpenRouter 무료 AI 모델 실시간 연동**: Llama-3.1 Nemotron, Llama-3, Gemma-2 등 오픈라우터 무료 LLM 모델 폴백(Fallback) 체인 및 Llama-3.2 Vision VLM 무료 모델 실시간 호출을 지원합니다. (API Key가 없어도 시뮬레이션 모드로 정상 작동합니다.)
|
| 19 |
+
3. **주간/월간 대량 식단 스케줄 분석**: 월/주 단위 식단을 통째로 입력하면 일목요연한 캘린더 그리드 및 아코디언 카드 뷰로 시각화합니다.
|
| 20 |
+
4. **VLM(Vision-Language Model) 오배식 방지 검증**: 식판 이미지 촬영 및 업로드 시 비전 AI가 유해 식자재 혼입 여부를 2차 자동 검증합니다.
|
| 21 |
+
5. **디지털 자동 일지 및 ESG 성과 인쇄**: 시설 평가 증빙 및 ESG 가치 수치 통계를 대시보드로 요약하고 공식 PDF로 깔끔하게 인쇄합니다.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 🛠️ GitHub Actions를 활용한 Hugging Face Spaces CI/CD 배포 구성법
|
| 26 |
+
|
| 27 |
+
본 프로젝트의 코드를 GitHub에 올린 뒤, 코드 변경 시마다 허깅페이스 스페이스로 배포 자동화를 하려면 아래 단계를 완료하십시오.
|
| 28 |
+
|
| 29 |
+
### 1단계: Hugging Face Access Token 발급
|
| 30 |
+
1. [Hugging Face Settings](https://huggingface.co/settings/tokens)로 이동합니다.
|
| 31 |
+
2. **New token** 버튼을 클릭하고 Role을 **Write**로 지정하여 토큰을 생성 및 복사합니다.
|
| 32 |
+
|
| 33 |
+
### 2단계: GitHub Secrets에 토큰 추가
|
| 34 |
+
1. 배포할 GitHub Repository의 **Settings ➔ Secrets and variables ➔ Actions**로 이동합니다.
|
| 35 |
+
2. **New repository secret** 버튼을 클릭합니다.
|
| 36 |
+
3. Name에 `HF_TOKEN`을 입력하고, Value에 복사한 Hugging Face Access Token을 붙여넣습니다.
|
| 37 |
+
|
| 38 |
+
### 3단계: GitHub push 진행
|
| 39 |
+
- 코드가 GitHub 저장소의 `main` 브랜치에 push될 때마다 GitHub Actions 워크플로우가 자동으로 트리거되어 Hugging Face Spaces로 코드를 빌드/복사합니다.
|
app.js
ADDED
|
@@ -0,0 +1,1546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// CareTable - Premium Demo Logic Engine (Version 2.2)
|
| 2 |
+
|
| 3 |
+
// Global State Default Data Sets
|
| 4 |
+
const defaultMembers = {
|
| 5 |
+
child: [
|
| 6 |
+
{ id: 1, name: '김민수', age: '5세 / 남', type: '알레르기', detail: '달걀, 우유', instruction: '달걀/우유 성분 완전 차단 및 대체 간식 제공', status: 'care' },
|
| 7 |
+
{ id: 2, name: '박예린', age: '6세 / 여', type: '알레르기', detail: '땅콩, 견과류', instruction: '견과류 함유 소스/조미료 전면 금지 및 대체 견과 무첨가 제품 사용', status: 'safe' },
|
| 8 |
+
{ id: 3, name: '최지환', age: '4세 / 남', type: '알레르기', detail: '대두, 콩류', instruction: '간장, 된장 대체 소스 사용 및 콩 단백질 대체 반찬 제공', status: 'safe' }
|
| 9 |
+
],
|
| 10 |
+
senior: [
|
| 11 |
+
{ id: 1, name: '최옥분', age: '82세 / 여', type: '질환식', detail: '당뇨, 연하곤란 2단계', instruction: '저염/저당질 식사, 1.5cm 이하 다진식(Soft Food) 및 연하보조제 믹스 제공', status: 'care' },
|
| 12 |
+
{ id: 2, name: '정순길', age: '78세 / 남', type: '질환식', detail: '신부전증 (칼륨 제한)', instruction: '나트륨 최소화, 생채소 섭취 금지(데친 채소 제공), 잡곡밥 대신 흰쌀밥 제공', status: 'care' },
|
| 13 |
+
{ id: 3, name: '이명자', age: '85세 / 여', type: '질환식', detail: '유당불내증', instruction: '일반 우유/요구르트 급식 금지, 락토프리 우유 또는 대체 두유 제공', status: 'care' }
|
| 14 |
+
],
|
| 15 |
+
family: [
|
| 16 |
+
{ id: 1, name: '우리남편', age: '42세 / 남', type: '질환식', detail: '고혈압, 통풍', instruction: '나트륨 제한식, 퓨린이 많은 맥주/조개류 반찬 금지, 식이섬유 증량', status: 'care' },
|
| 17 |
+
{ id: 2, name: '첫째딸', age: '8세 / 여', type: '알레르기', detail: '복숭아, 키위', instruction: '해당 과일 디저트 일절 배제 및 과일 칼 분리 사용 철저', status: 'safe' },
|
| 18 |
+
{ id: 3, name: '막내아들', age: '5세 / 남', type: '알레르기', detail: '우유, 밀가루', instruction: '락토프리 유제품 급지 및 쌀식빵/글루텐프리 대체 베이킹 제공', status: 'care' }
|
| 19 |
+
]
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const state = {
|
| 23 |
+
currentTab: 'dashboard',
|
| 24 |
+
currentMode: 'agency', // 'agency' (기관) or 'family' (가정)
|
| 25 |
+
currentFacility: 'child', // 'child' (보육) or 'senior' (노인)
|
| 26 |
+
members: JSON.parse(JSON.stringify(defaultMembers)), // Deep Copy
|
| 27 |
+
|
| 28 |
+
// Simulation / Analyzer variables
|
| 29 |
+
activeDietInput: '',
|
| 30 |
+
analysisResult: null, // Schedule cards list
|
| 31 |
+
selectedVlmImage: null, // 'normal' or 'alternative' or 'custom'
|
| 32 |
+
customVlmImageSrc: null,
|
| 33 |
+
vlmApproved: false,
|
| 34 |
+
|
| 35 |
+
// Database of logs to generate reports
|
| 36 |
+
logs: [
|
| 37 |
+
{
|
| 38 |
+
date: '2026-05-30',
|
| 39 |
+
menu: '보리밥, 계란국, 제육볶음, 두부조림, 오렌지주스',
|
| 40 |
+
facility: 'child',
|
| 41 |
+
target: '김민수 아동 (달걀/우유)',
|
| 42 |
+
alternative: '계란국 ➔ 맑은 무국 대체 제공',
|
| 43 |
+
guideline: '계란 조리 시 교차오염 방지용 독립 조리기구 사용',
|
| 44 |
+
vlmStatus: 'PASS',
|
| 45 |
+
timestamp: '2026-05-30 11:45'
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
date: '2026-05-30',
|
| 49 |
+
menu: '잡곡밥, 버섯국, 불고기, 고구마순나물, 요플레',
|
| 50 |
+
facility: 'senior',
|
| 51 |
+
target: '이명자 어르신 (유당불내증)',
|
| 52 |
+
alternative: '요플레 ➔ 락토프리 요거트 대체 제공',
|
| 53 |
+
guideline: '유제품 보관 구역 구별 및 라벨링 철저',
|
| 54 |
+
vlmStatus: 'PASS',
|
| 55 |
+
timestamp: '2026-05-30 11:52'
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
charts: {
|
| 59 |
+
dashboard: null,
|
| 60 |
+
esg: null
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// API settings configuration
|
| 65 |
+
const apiConfig = {
|
| 66 |
+
key: localStorage.getItem('caremeal_api_key') || '',
|
| 67 |
+
model: localStorage.getItem('caremeal_api_model') || 'nvidia/llama-3.1-nemotron-70b-instruct:free'
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
// OpenRouter Free Models for Fallback Strategy
|
| 71 |
+
const freeLLMModels = [
|
| 72 |
+
"nvidia/llama-3.1-nemotron-70b-instruct:free",
|
| 73 |
+
"meta-llama/llama-3-8b-instruct:free",
|
| 74 |
+
"google/gemma-2-9b-it:free",
|
| 75 |
+
"mistralai/mistral-7b-instruct:free"
|
| 76 |
+
];
|
| 77 |
+
|
| 78 |
+
const freeVLMModels = [
|
| 79 |
+
"meta-llama/llama-3.2-11b-vision-instruct:free",
|
| 80 |
+
"qwen/qwen-2-vl-7b-instruct:free"
|
| 81 |
+
];
|
| 82 |
+
|
| 83 |
+
// DOM Elements
|
| 84 |
+
const elements = {
|
| 85 |
+
navButtons: document.querySelectorAll('.nav-btn'),
|
| 86 |
+
tabContents: document.querySelectorAll('.tab-content'),
|
| 87 |
+
facilitySelect: document.getElementById('facility-select'),
|
| 88 |
+
facilitySelectorWrapper: document.getElementById('facility-selector-wrapper'),
|
| 89 |
+
pageMainTitle: document.getElementById('page-main-title'),
|
| 90 |
+
pageSubTitle: document.getElementById('page-sub-title'),
|
| 91 |
+
mainBadge: document.getElementById('main-badge'),
|
| 92 |
+
|
| 93 |
+
// Theme Toggle
|
| 94 |
+
themeToggleBtn: document.getElementById('theme-toggle-btn'),
|
| 95 |
+
|
| 96 |
+
// Mode Switch Toggle
|
| 97 |
+
modeToggleCheckbox: document.getElementById('mode-toggle-checkbox'),
|
| 98 |
+
modeTextLeft: document.getElementById('mode-text-left'),
|
| 99 |
+
modeTextRight: document.getElementById('mode-text-right'),
|
| 100 |
+
userAvatarInitial: document.getElementById('user-avatar-initial'),
|
| 101 |
+
userDisplayName: document.getElementById('user-display-name'),
|
| 102 |
+
userDisplayRole: document.getElementById('user-display-role'),
|
| 103 |
+
|
| 104 |
+
// Stats
|
| 105 |
+
statTotalMembers: document.getElementById('stat-total-members'),
|
| 106 |
+
statRiskMembers: document.getElementById('stat-risk-members'),
|
| 107 |
+
statSavedTime: document.getElementById('stat-saved-time'),
|
| 108 |
+
statSafetyScore: document.getElementById('stat-safety-score'),
|
| 109 |
+
|
| 110 |
+
lblTotalMembers: document.getElementById('lbl-total-members'),
|
| 111 |
+
lblTotalDesc: document.getElementById('lbl-total-desc'),
|
| 112 |
+
lblSavedTime: document.getElementById('lbl-saved-time'),
|
| 113 |
+
lblSavedDesc: document.getElementById('lbl-saved-desc'),
|
| 114 |
+
esgBottomDesc: document.getElementById('esg-bottom-desc'),
|
| 115 |
+
|
| 116 |
+
// GNB & API Connection Status
|
| 117 |
+
openApiConfigBtn: document.getElementById('open-api-config-btn'),
|
| 118 |
+
apiStatusBadge: document.getElementById('api-status-badge'),
|
| 119 |
+
apiStatusText: document.getElementById('api-status-text'),
|
| 120 |
+
|
| 121 |
+
// Tables
|
| 122 |
+
memberTableBody: document.getElementById('member-table-body'),
|
| 123 |
+
panelMemberTitle: document.getElementById('panel-member-title'),
|
| 124 |
+
|
| 125 |
+
// Bulk Diet tab
|
| 126 |
+
dietBulkInput: document.getElementById('diet-bulk-input'),
|
| 127 |
+
analyzeDietBtn: document.getElementById('analyze-diet-btn'),
|
| 128 |
+
analyzerLoading: document.getElementById('analyzer-loading'),
|
| 129 |
+
analyzerModelIndicator: document.getElementById('analyzer-model-indicator'),
|
| 130 |
+
loadingText: document.getElementById('loading-text'),
|
| 131 |
+
scheduleResultView: document.getElementById('schedule-result-view'),
|
| 132 |
+
scheduleCardsContainer: document.getElementById('schedule-cards-container'),
|
| 133 |
+
scheduleStatusBanner: document.getElementById('schedule-status-banner'),
|
| 134 |
+
scheduleSummaryTitle: document.getElementById('schedule-summary-title'),
|
| 135 |
+
scheduleSummaryDesc: document.getElementById('schedule-summary-desc'),
|
| 136 |
+
scheduleEmptyView: document.getElementById('schedule-empty-view'),
|
| 137 |
+
confirmAllDietBtn: document.getElementById('confirm-all-diet-btn'),
|
| 138 |
+
exampleTags: document.querySelectorAll('.example-tag'),
|
| 139 |
+
|
| 140 |
+
// VLM tab
|
| 141 |
+
vlmOptNormal: document.getElementById('sim-opt-normal'),
|
| 142 |
+
vlmOptAlternative: document.getElementById('sim-opt-alternative'),
|
| 143 |
+
vlmTargetImage: document.getElementById('vlm-target-image'),
|
| 144 |
+
vlmViewportPlaceholder: document.querySelector('.viewport-placeholder'),
|
| 145 |
+
runVlmBtn: document.getElementById('run-vlm-btn'),
|
| 146 |
+
scannerLaser: document.getElementById('scanner-laser'),
|
| 147 |
+
vlmEmptyResult: document.getElementById('vlm-empty-result'),
|
| 148 |
+
vlmResultPanel: document.getElementById('vlm-result-panel'),
|
| 149 |
+
vlmVerdictBox: document.getElementById('vlm-verdict-box'),
|
| 150 |
+
verdictIconContainer: document.getElementById('verdict-icon-container'),
|
| 151 |
+
verdictTitle: document.getElementById('verdict-title'),
|
| 152 |
+
vlmMatchRate: document.getElementById('vlm-match-rate'),
|
| 153 |
+
vlmMatchValue: document.getElementById('vlm-match-value'),
|
| 154 |
+
vlmAllergenStatus: document.getElementById('vlm-allergen-status'),
|
| 155 |
+
vlmPortionStatus: document.getElementById('vlm-portion-status'),
|
| 156 |
+
vlmAnalysisReason: document.getElementById('vlm-analysis-reason'),
|
| 157 |
+
vlmActionFooter: document.getElementById('vlm-action-footer'),
|
| 158 |
+
triggerFileBtn: document.getElementById('trigger-file-btn'),
|
| 159 |
+
vlmFileInput: document.getElementById('vlm-file-input'),
|
| 160 |
+
|
| 161 |
+
// Report tab
|
| 162 |
+
reportSubTabBtns: document.querySelectorAll('.sub-tab-btn'),
|
| 163 |
+
reportDocTitle: document.getElementById('report-doc-title'),
|
| 164 |
+
rFacilityName: document.getElementById('r-facility-name'),
|
| 165 |
+
rDate: document.getElementById('r-date'),
|
| 166 |
+
rWriter: document.getElementById('r-writer'),
|
| 167 |
+
reportDocContent: document.getElementById('report-doc-content'),
|
| 168 |
+
printReportBtn: document.getElementById('print-report-btn'),
|
| 169 |
+
reportConfirmStatement: document.getElementById('report-confirm-statement'),
|
| 170 |
+
reportSignerName: document.getElementById('report-signer-name'),
|
| 171 |
+
reportFooterDesc: document.getElementById('report-footer-desc'),
|
| 172 |
+
navReportBtn: document.getElementById('nav-report-btn'),
|
| 173 |
+
esgCo2: document.getElementById('esg-co2'),
|
| 174 |
+
esgTime: document.getElementById('esg-time'),
|
| 175 |
+
esgLocal: document.getElementById('esg-local'),
|
| 176 |
+
|
| 177 |
+
// Modals & Forms
|
| 178 |
+
addMemberBtn: document.getElementById('add-member-btn'),
|
| 179 |
+
addMemberModal: document.getElementById('add-member-modal'),
|
| 180 |
+
closeMemberModalBtn: document.getElementById('close-member-modal-btn'),
|
| 181 |
+
cancelMemberBtn: document.getElementById('cancel-member-btn'),
|
| 182 |
+
memberForm: document.getElementById('member-form'),
|
| 183 |
+
modalMemberTitle: document.getElementById('modal-member-title'),
|
| 184 |
+
|
| 185 |
+
apiConfigModal: document.getElementById('api-config-modal'),
|
| 186 |
+
closeApiModalBtn: document.getElementById('close-api-modal-btn'),
|
| 187 |
+
apiConfigForm: document.getElementById('api-config-form'),
|
| 188 |
+
apiKeyInput: document.getElementById('api-key-input'),
|
| 189 |
+
apiModelSelect: document.getElementById('api-model-select'),
|
| 190 |
+
toggleKeyVisibilityBtn: document.getElementById('toggle-key-visibility-btn'),
|
| 191 |
+
resetApiKeyBtn: document.getElementById('reset-api-key-btn')
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
// Templates
|
| 195 |
+
const templates = {
|
| 196 |
+
weekly: `[월요일] 쌀밥, 계란말이, 어묵볶음, 요구르트\n[화요일] 잡곡밥, 소불고기, 고구마순나물, 두부국, 우유\n[수요일] 현미밥, 맑은 무국, 생선구이, 콩자반, 식혜\n[목요일] 쌀밥, 닭볶음탕, 오징어채무침, 땅콩조림, 사과주스\n[금요일] 잡곡밥, 버섯찌개, 계란찜, 시금치나물, 핫초코`,
|
| 197 |
+
monthly: `[1주 월요일] 쌀밥, 계란말이, 어묵볶음, 요구르트\n[1주 화요일] 잡곡밥, 소불고기, 시금치, 두부국, 우유\n[1주 수요일] 현미밥, 맑은 무국, 생선구이, 콩자반, 요구르트\n[1주 목요일] 쌀밥, 닭볶음탕, 오징어채무침, 땅콩조림, 주스\n[1주 금요일] 잡곡밥, 버섯찌개, 만두국, 고구마순나물, 요구르트\n[2주 월요일] 귀리밥, 돈까스, 샐러드, 계란국, 우유\n[2주 화요일] 쌀밥, 된장찌개, 갈치구이, 멸치볶음, 식혜`
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
// 1. Initial System Setup
|
| 201 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 202 |
+
loadDatabase();
|
| 203 |
+
initThemeManager();
|
| 204 |
+
initModeToggler();
|
| 205 |
+
initTabs();
|
| 206 |
+
initFacilitySelector();
|
| 207 |
+
initCharts();
|
| 208 |
+
initDietAnalyzer();
|
| 209 |
+
initVlmScanner();
|
| 210 |
+
initReportCenter();
|
| 211 |
+
initModal();
|
| 212 |
+
initApiConfig();
|
| 213 |
+
|
| 214 |
+
// Draw initial dashboard & status
|
| 215 |
+
switchTab('dashboard');
|
| 216 |
+
updateApiStatusUI();
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
// Load DB from localStorage or restore defaults if empty/not-present
|
| 220 |
+
function loadDatabase() {
|
| 221 |
+
['child', 'senior', 'family'].forEach(key => {
|
| 222 |
+
const stored = localStorage.getItem(`caremeal_members_${key}`);
|
| 223 |
+
if (stored && JSON.parse(stored).length > 0) {
|
| 224 |
+
state.members[key] = JSON.parse(stored);
|
| 225 |
+
} else {
|
| 226 |
+
state.members[key] = JSON.parse(JSON.stringify(defaultMembers[key]));
|
| 227 |
+
saveDatabase(key);
|
| 228 |
+
}
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
const storedLogs = localStorage.getItem('caremeal_logs');
|
| 232 |
+
if (storedLogs) {
|
| 233 |
+
state.logs = JSON.parse(storedLogs);
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function saveDatabase(key) {
|
| 238 |
+
localStorage.setItem(`caremeal_members_${key}`, JSON.stringify(state.members[key]));
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
function saveLogs() {
|
| 242 |
+
localStorage.setItem('caremeal_logs', JSON.stringify(state.logs));
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// 2. Light / Dark Theme Management
|
| 246 |
+
function initThemeManager() {
|
| 247 |
+
const storedTheme = localStorage.getItem('caremeal_theme') || 'light';
|
| 248 |
+
applyTheme(storedTheme);
|
| 249 |
+
|
| 250 |
+
elements.themeToggleBtn.addEventListener('click', () => {
|
| 251 |
+
const isDark = document.body.classList.contains('dark-mode');
|
| 252 |
+
const nextTheme = isDark ? 'light' : 'dark';
|
| 253 |
+
applyTheme(nextTheme);
|
| 254 |
+
localStorage.setItem('caremeal_theme', nextTheme);
|
| 255 |
+
});
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
function applyTheme(theme) {
|
| 259 |
+
const icon = elements.themeToggleBtn.querySelector('i');
|
| 260 |
+
const text = document.getElementById('theme-toggle-text');
|
| 261 |
+
|
| 262 |
+
if (theme === 'dark') {
|
| 263 |
+
document.body.classList.add('dark-mode');
|
| 264 |
+
icon.className = 'fa-solid fa-sun';
|
| 265 |
+
text.textContent = '라이트 모드';
|
| 266 |
+
} else {
|
| 267 |
+
document.body.classList.remove('dark-mode');
|
| 268 |
+
icon.className = 'fa-solid fa-moon';
|
| 269 |
+
text.textContent = '다크 모드';
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// 3. Mode Switching (Agency vs Family)
|
| 274 |
+
function initModeToggler() {
|
| 275 |
+
const storedMode = localStorage.getItem('caremeal_mode');
|
| 276 |
+
if (storedMode) {
|
| 277 |
+
state.currentMode = storedMode;
|
| 278 |
+
elements.modeToggleCheckbox.checked = (storedMode === 'family');
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
applyModeTheme();
|
| 282 |
+
|
| 283 |
+
elements.modeToggleCheckbox.addEventListener('change', (e) => {
|
| 284 |
+
state.currentMode = e.target.checked ? 'family' : 'agency';
|
| 285 |
+
localStorage.setItem('caremeal_mode', state.currentMode);
|
| 286 |
+
|
| 287 |
+
applyModeTheme();
|
| 288 |
+
|
| 289 |
+
resetDietUI();
|
| 290 |
+
resetVlmUI();
|
| 291 |
+
|
| 292 |
+
switchTab(state.currentTab);
|
| 293 |
+
showNotification(`${state.currentMode === 'family' ? '가정(가족) 건강 케어' : '보건복지시설 관리'} 모드로 전환되었습니다.`);
|
| 294 |
+
});
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function applyModeTheme() {
|
| 298 |
+
if (state.currentMode === 'family') {
|
| 299 |
+
elements.modeTextLeft.classList.remove('active-mode');
|
| 300 |
+
elements.modeTextRight.classList.add('active-mode');
|
| 301 |
+
elements.facilitySelectorWrapper.classList.add('hidden');
|
| 302 |
+
elements.mainBadge.textContent = '가족 건강 관리 모드';
|
| 303 |
+
elements.mainBadge.style.background = 'rgba(124, 58, 237, 0.08)';
|
| 304 |
+
elements.mainBadge.style.borderColor = 'rgba(124, 58, 237, 0.2)';
|
| 305 |
+
elements.mainBadge.style.color = '#7c3aed';
|
| 306 |
+
|
| 307 |
+
elements.userAvatarInitial.textContent = '가';
|
| 308 |
+
elements.userDisplayName.textContent = '엄마/아빠';
|
| 309 |
+
elements.userDisplayRole.textContent = '우리 집 영양 안심 케어';
|
| 310 |
+
|
| 311 |
+
elements.navReportBtn.innerHTML = '<i class="fa-solid fa-file-invoice"></i> <span>안전 증빙 일지</span>';
|
| 312 |
+
} else {
|
| 313 |
+
elements.modeTextLeft.classList.add('active-mode');
|
| 314 |
+
elements.modeTextRight.classList.remove('active-mode');
|
| 315 |
+
elements.facilitySelectorWrapper.classList.remove('hidden');
|
| 316 |
+
elements.mainBadge.textContent = '사보원 공공데이터 연계';
|
| 317 |
+
elements.mainBadge.style.background = 'rgba(37, 99, 235, 0.08)';
|
| 318 |
+
elements.mainBadge.style.borderColor = 'rgba(37, 99, 235, 0.2)';
|
| 319 |
+
elements.mainBadge.style.color = 'var(--color-blue)';
|
| 320 |
+
|
| 321 |
+
elements.userAvatarInitial.textContent = '복';
|
| 322 |
+
elements.userDisplayName.textContent = '박아름 사회복지사';
|
| 323 |
+
elements.userDisplayRole.textContent = '마포구 시설 운영대표';
|
| 324 |
+
elements.navReportBtn.innerHTML = '<i class="fa-solid fa-file-shield"></i> <span>평가 증빙 & ESG</span>';
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// 4. Navigation System
|
| 329 |
+
function initTabs() {
|
| 330 |
+
elements.navButtons.forEach(btn => {
|
| 331 |
+
btn.addEventListener('click', () => {
|
| 332 |
+
const tabId = btn.getAttribute('data-tab');
|
| 333 |
+
switchTab(tabId);
|
| 334 |
+
});
|
| 335 |
+
});
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function switchTab(tabId) {
|
| 339 |
+
state.currentTab = tabId;
|
| 340 |
+
|
| 341 |
+
elements.navButtons.forEach(btn => {
|
| 342 |
+
if (btn.getAttribute('data-tab') === tabId) {
|
| 343 |
+
btn.classList.add('active');
|
| 344 |
+
} else {
|
| 345 |
+
btn.classList.remove('active');
|
| 346 |
+
}
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
elements.tabContents.forEach(content => {
|
| 350 |
+
if (content.id === tabId) {
|
| 351 |
+
content.classList.add('active-tab');
|
| 352 |
+
} else {
|
| 353 |
+
content.classList.remove('active-tab');
|
| 354 |
+
}
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
updateHeaderTitles(tabId);
|
| 358 |
+
|
| 359 |
+
if (tabId === 'dashboard') {
|
| 360 |
+
renderDashboard();
|
| 361 |
+
setTimeout(() => updateCharts(), 100);
|
| 362 |
+
} else if (tabId === 'report-generator') {
|
| 363 |
+
renderReport();
|
| 364 |
+
setTimeout(() => updateCharts(), 100);
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function updateHeaderTitles(tabId) {
|
| 369 |
+
let modeTitle = '';
|
| 370 |
+
if (state.currentMode === 'family') {
|
| 371 |
+
modeTitle = '가족 건강 스케줄러';
|
| 372 |
+
} else {
|
| 373 |
+
modeTitle = state.currentFacility === 'child' ? '튼튼어린이집' : '행복실버요양원';
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
switch (tabId) {
|
| 377 |
+
case 'dashboard':
|
| 378 |
+
elements.pageMainTitle.textContent = `${modeTitle} 종합 대시보드`;
|
| 379 |
+
elements.pageSubTitle.textContent = state.currentMode === 'family'
|
| 380 |
+
? '가정 내 가족별 알레르기 및 유해인자 스크리닝 요약'
|
| 381 |
+
: '시설 맞춤형 프로파일 및 취약계층 급식 위험 관리 요약';
|
| 382 |
+
break;
|
| 383 |
+
case 'diet-analyzer':
|
| 384 |
+
elements.pageMainTitle.textContent = '주간/월간 식단 AI 대량 분석';
|
| 385 |
+
elements.pageSubTitle.textContent = '식단표를 붙여넣으면 OpenRouter AI 모델이 캘린더 대체 식단과 레시피를 일목요연하게 자동 분석합니다.';
|
| 386 |
+
break;
|
| 387 |
+
case 'vlm-scanner':
|
| 388 |
+
elements.pageMainTitle.textContent = 'VLM 배식 오배 예방 모니터';
|
| 389 |
+
elements.pageSubTitle.textContent = '배식 직전 촬영된 식판 이미지를 Vision AI가 분석하여 제한 식재료 교차 감지를 실시간 2차 예방합니다.';
|
| 390 |
+
break;
|
| 391 |
+
case 'report-generator':
|
| 392 |
+
elements.pageMainTitle.textContent = state.currentMode === 'family' ? '가정용 식단 위생 안전 일지' : '사회보장/가족 건강 데이터 증빙 센터';
|
| 393 |
+
elements.pageSubTitle.textContent = state.currentMode === 'family'
|
| 394 |
+
? '가정 내 배식 일지 보관 및 페이퍼리스 디지털 케어 실천 점수'
|
| 395 |
+
: '한국사회보장정보원 시스템 제출용 증빙 서류 자동 생성 및 ESG 혁신 통계';
|
| 396 |
+
break;
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Render Dashboard Panel
|
| 401 |
+
function renderDashboard() {
|
| 402 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 403 |
+
const list = state.members[listKey];
|
| 404 |
+
const totalMembers = list.length;
|
| 405 |
+
const riskMembers = list.filter(m => m.type !== '일반').length;
|
| 406 |
+
const totalSavedTime = (state.logs.filter(l => state.currentMode === 'family' ? l.facility === 'family' : l.facility === state.currentFacility).length * 4.2).toFixed(1);
|
| 407 |
+
|
| 408 |
+
if (state.currentMode === 'family') {
|
| 409 |
+
elements.lblTotalMembers.textContent = '가족 등록 인원';
|
| 410 |
+
elements.lblTotalDesc.innerHTML = '<i class="fa-solid fa-house"></i> 가정용 프로필 기준';
|
| 411 |
+
elements.lblSavedTime.textContent = '가사 노동 및 행정 절감 시간';
|
| 412 |
+
elements.lblSavedDesc.innerHTML = '<i class="fa-solid fa-clock"></i> 식단 작성 및 증빙 자동화 효과';
|
| 413 |
+
elements.panelMemberTitle.innerHTML = '<i class="fa-solid fa-people-roof"></i> 가족 구성원 건강/알레르기 DB';
|
| 414 |
+
elements.esgBottomDesc.textContent = '친환경 로컬 식단 구성 및 식료품 소비 데이터 연동을 통해 에코 탄소 마일리지 120kg CO2e 감축 기여';
|
| 415 |
+
elements.modalMemberTitle.innerHTML = '<i class="fa-solid fa-house-chimney-medical"></i> 가족 건강 프로필 신규 등록';
|
| 416 |
+
} else {
|
| 417 |
+
elements.lblTotalMembers.textContent = '관리 대상자 수';
|
| 418 |
+
elements.lblTotalDesc.innerHTML = '<i class="fa-solid fa-circle-info"></i> 사보원 데이터 기준';
|
| 419 |
+
elements.lblSavedTime.textContent = '누적 행정 절감 시간';
|
| 420 |
+
elements.lblSavedDesc.innerHTML = '<i class="fa-solid fa-arrow-trend-up"></i> 돌봄 시간으로 환원됨';
|
| 421 |
+
elements.panelMemberTitle.innerHTML = '<i class="fa-solid fa-id-card"></i> 대상자 집중 관리 명단';
|
| 422 |
+
elements.esgBottomDesc.textContent = '친환경 저탄소 식단 추천 및 페이퍼리스 행정 자동화를 통한 탄소 발자국 240kg CO2e 감축 달성';
|
| 423 |
+
elements.modalMemberTitle.innerHTML = '<i class="fa-solid fa-user-plus"></i> 신규 관리 대상자 등록';
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
elements.statTotalMembers.textContent = `${totalMembers}명`;
|
| 427 |
+
elements.statRiskMembers.textContent = `${riskMembers}명`;
|
| 428 |
+
elements.statSavedTime.textContent = `${totalSavedTime}시간`;
|
| 429 |
+
elements.statSafetyScore.textContent = '100%';
|
| 430 |
+
|
| 431 |
+
elements.memberTableBody.innerHTML = '';
|
| 432 |
+
|
| 433 |
+
list.forEach(member => {
|
| 434 |
+
const tr = document.createElement('tr');
|
| 435 |
+
|
| 436 |
+
const riskTag = member.type === '일반'
|
| 437 |
+
? `<span style="color:var(--text-muted)">일반 급식군</span>`
|
| 438 |
+
: `<span class="risk-tag ${member.type === '알레르기' ? 'allergy' : 'disease'}">${member.type}: ${member.detail}</span>`;
|
| 439 |
+
|
| 440 |
+
tr.innerHTML = `
|
| 441 |
+
<td><strong>${member.name}</strong></td>
|
| 442 |
+
<td>${member.age}</td>
|
| 443 |
+
<td>${riskTag}</td>
|
| 444 |
+
<td style="color: var(--text-secondary); max-width: 250px; line-height: 1.4;">${member.instruction}</td>
|
| 445 |
+
<td>
|
| 446 |
+
<button class="delete-member-btn" onclick="deleteMember(${member.id})">
|
| 447 |
+
<i class="fa-solid fa-trash-can"></i>
|
| 448 |
+
</button>
|
| 449 |
+
</td>
|
| 450 |
+
`;
|
| 451 |
+
elements.memberTableBody.appendChild(tr);
|
| 452 |
+
});
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
// Facility switch logic for agency mode
|
| 456 |
+
function initFacilitySelector() {
|
| 457 |
+
elements.facilitySelect.addEventListener('change', (e) => {
|
| 458 |
+
state.currentFacility = e.target.value;
|
| 459 |
+
|
| 460 |
+
resetDietUI();
|
| 461 |
+
resetVlmUI();
|
| 462 |
+
|
| 463 |
+
switchTab(state.currentTab);
|
| 464 |
+
showNotification(`${state.currentFacility === 'child' ? '보육시설 (어린이집)' : '노인요양시설 (실버케어)'} 데이터로 갱신되었습니다.`);
|
| 465 |
+
});
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
// 5. Diet Bulk Scheduler and OpenRouter API Engine
|
| 469 |
+
function initDietAnalyzer() {
|
| 470 |
+
elements.dietBulkInput.value = state.currentMode === 'family' ? `[월요일] 쌀밥, 계란말이, 요구르트\n[화요일] 쌀식빵, 사과잼, 우유\n[수요일] 잡곡밥, 돈까스, 샐러드` : templates.weekly;
|
| 471 |
+
|
| 472 |
+
elements.exampleTags.forEach(tag => {
|
| 473 |
+
tag.addEventListener('click', () => {
|
| 474 |
+
elements.exampleTags.forEach(t => t.classList.remove('active'));
|
| 475 |
+
tag.classList.add('active');
|
| 476 |
+
const period = tag.getAttribute('data-period');
|
| 477 |
+
elements.dietBulkInput.value = templates[period];
|
| 478 |
+
});
|
| 479 |
+
});
|
| 480 |
+
|
| 481 |
+
elements.analyzeDietBtn.addEventListener('click', async () => {
|
| 482 |
+
const textInput = elements.dietBulkInput.value.trim();
|
| 483 |
+
if (!textInput) return;
|
| 484 |
+
|
| 485 |
+
state.activeDietInput = textInput;
|
| 486 |
+
|
| 487 |
+
elements.scheduleEmptyView.classList.add('hidden');
|
| 488 |
+
elements.scheduleResultView.classList.add('hidden');
|
| 489 |
+
elements.analyzerLoading.classList.remove('hidden');
|
| 490 |
+
elements.analyzeDietBtn.disabled = true;
|
| 491 |
+
|
| 492 |
+
elements.loadingText.textContent = apiConfig.key
|
| 493 |
+
? "오픈라우터 무료 AI 모델과 실시간 통신 중입니다 (API 폴백 활성화)..."
|
| 494 |
+
: "인터넷 지연 및 API 키 미검출에 따라 로컬 고성능 시뮬레이션 알고리즘으로 폴백하여 식단을 분석하는 중...";
|
| 495 |
+
|
| 496 |
+
try {
|
| 497 |
+
if (apiConfig.key) {
|
| 498 |
+
// Call real OpenRouter API with Fallback Chain
|
| 499 |
+
state.analysisResult = await callOpenRouterLLMFallback(textInput);
|
| 500 |
+
} else {
|
| 501 |
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
| 502 |
+
state.analysisResult = runLocalSimParser(textInput);
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
renderScheduleCards();
|
| 506 |
+
} catch (error) {
|
| 507 |
+
console.error(error);
|
| 508 |
+
showNotification("AI 모델 호출 실패로 안전 시뮬레이션 데이터로 대체 처리합니다.");
|
| 509 |
+
state.analysisResult = runLocalSimParser(textInput);
|
| 510 |
+
renderScheduleCards();
|
| 511 |
+
} finally {
|
| 512 |
+
elements.analyzerLoading.classList.add('hidden');
|
| 513 |
+
elements.analyzeDietBtn.disabled = false;
|
| 514 |
+
}
|
| 515 |
+
});
|
| 516 |
+
|
| 517 |
+
elements.confirmAllDietBtn.addEventListener('click', () => {
|
| 518 |
+
showNotification("대체 가이드라인을 반영하여 전체 주간/월간 식단 검토가 확정되었습니다. 배식 검증 단계를 진행하십시오.");
|
| 519 |
+
switchTab('vlm-scanner');
|
| 520 |
+
|
| 521 |
+
elements.runVlmBtn.disabled = false;
|
| 522 |
+
elements.vlmOptNormal.click();
|
| 523 |
+
});
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function resetDietUI() {
|
| 527 |
+
elements.dietBulkInput.value = state.currentMode === 'family' ? `[월요일] 쌀밥, 계란말이, 요구르트\n[화요일] 쌀식빵, 사과잼, 우유\n[수요일] 잡곡밥, 돈까스, 샐러드` : templates.weekly;
|
| 528 |
+
elements.scheduleEmptyView.classList.remove('hidden');
|
| 529 |
+
elements.scheduleResultView.classList.add('hidden');
|
| 530 |
+
elements.analyzerLoading.classList.add('hidden');
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// LLM Fallback Sequential API Calling
|
| 534 |
+
async function callOpenRouterLLMFallback(dietText) {
|
| 535 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 536 |
+
const list = state.members[listKey];
|
| 537 |
+
const memberProfileStr = list.map(m => `- 이름: ${m.name}, 유형: ${m.type}, 원인물질/질환: ${m.detail}, 처방: ${m.instruction}`).join('\n');
|
| 538 |
+
|
| 539 |
+
const prompt = `
|
| 540 |
+
당신은 취약계층 급식 또는 가정을 관리하는 영양 안전 케어 AI 시스템인 CareTable입니다.
|
| 541 |
+
다음 입력된 식단표를 바탕으로 현재 관리 대상자들의 알레르기 및 만성질환 유해 성분 정보와 비교하여 위험 요소를 분석하고 대체 식단과 위생/조리지침 레시피 가이드를 수립하십시오.
|
| 542 |
+
|
| 543 |
+
[수혜 대상자 알레르기/질환 명단]
|
| 544 |
+
${memberProfileStr}
|
| 545 |
+
|
| 546 |
+
[식단표]
|
| 547 |
+
${dietText}
|
| 548 |
+
|
| 549 |
+
[출력 요구사항]
|
| 550 |
+
반드시 다른 잡다한 말(인사말 등)이나 마크업(```json 등)을 작성하지 말고 오직 아래 양식에 맞춘 유효한 JSON 배열만 출력하십시오.
|
| 551 |
+
|
| 552 |
+
JSON 양식:
|
| 553 |
+
[
|
| 554 |
+
{
|
| 555 |
+
"day": "요일/일자 구분 (예: 월요일 또는 1주 화요일)",
|
| 556 |
+
"date": "임의의 날짜 (예: 06/01)",
|
| 557 |
+
"hasRisk": true/false (유해인자가 식단에 감지되면 true, 없으면 false),
|
| 558 |
+
"risks": ["감지된 위험 요소 상세 요약 (예: 김민수(달걀 알레르기) - 계란말이 유래 성분 충돌)"],
|
| 559 |
+
"menuOrig": "입력된 원래 식단",
|
| 560 |
+
"menuAlt": "대체 처방이 적용된 신규 식단 (원래 식단에서 위험 성분 제거/대체식 교체)",
|
| 561 |
+
"recipe": "조리 지침 및 주의 가이드 (교차오염 방지 및 대체 조리법 서술)"
|
| 562 |
+
}
|
| 563 |
+
]
|
| 564 |
+
`;
|
| 565 |
+
|
| 566 |
+
// Try selected model first
|
| 567 |
+
const modelsToTry = [apiConfig.model, ...freeLLMModels.filter(m => m !== apiConfig.model)];
|
| 568 |
+
|
| 569 |
+
for (let model of modelsToTry) {
|
| 570 |
+
try {
|
| 571 |
+
console.log(`Trying OpenRouter LLM model: ${model}`);
|
| 572 |
+
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
| 573 |
+
method: "POST",
|
| 574 |
+
headers: {
|
| 575 |
+
"Authorization": `Bearer ${apiConfig.key}`,
|
| 576 |
+
"Content-Type": "application/json",
|
| 577 |
+
"HTTP-Referer": "https://huggingface.co/spaces/HwangJinwook/caretable",
|
| 578 |
+
"X-Title": "CareTable Client"
|
| 579 |
+
},
|
| 580 |
+
body: JSON.stringify({
|
| 581 |
+
model: model,
|
| 582 |
+
messages: [
|
| 583 |
+
{ role: "system", content: "You are an expert clinical dietitian AI. Output only JSON array. Do not wrap in markdown codeblocks." },
|
| 584 |
+
{ role: "user", content: prompt }
|
| 585 |
+
]
|
| 586 |
+
})
|
| 587 |
+
});
|
| 588 |
+
|
| 589 |
+
if (!res.ok) continue; // Try next model on failure
|
| 590 |
+
|
| 591 |
+
const data = await res.json();
|
| 592 |
+
let text = data.choices[0].message.content.trim();
|
| 593 |
+
|
| 594 |
+
if (text.includes("```json")) {
|
| 595 |
+
text = text.split("```json")[1].split("```")[0].trim();
|
| 596 |
+
} else if (text.includes("```")) {
|
| 597 |
+
text = text.split("```")[1].split("```")[0].trim();
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
return JSON.parse(text);
|
| 601 |
+
} catch (e) {
|
| 602 |
+
console.warn(`Model ${model} failed: `, e);
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
throw new Error("All free OpenRouter LLM models failed.");
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Local simulation fallback parser
|
| 610 |
+
function runLocalSimParser(dietText) {
|
| 611 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 612 |
+
const list = state.members[listKey];
|
| 613 |
+
const lines = dietText.split('\n').filter(l => l.trim().length > 0);
|
| 614 |
+
|
| 615 |
+
const results = [];
|
| 616 |
+
|
| 617 |
+
lines.forEach((line, index) => {
|
| 618 |
+
const dayMatch = line.match(/^\[(.*?)\]/);
|
| 619 |
+
const dayName = dayMatch ? dayMatch[1] : `일차 ${index+1}`;
|
| 620 |
+
const menuString = line.replace(/^\[.*?\]/, '').trim();
|
| 621 |
+
const foods = menuString.split(',').map(f => f.trim());
|
| 622 |
+
|
| 623 |
+
const risks = [];
|
| 624 |
+
const alternatives = [];
|
| 625 |
+
const guidelines = [];
|
| 626 |
+
|
| 627 |
+
foods.forEach(food => {
|
| 628 |
+
list.forEach(m => {
|
| 629 |
+
if (m.type === '알레르기') {
|
| 630 |
+
const allergens = m.detail.split(',').map(a => a.trim());
|
| 631 |
+
allergens.forEach(allergen => {
|
| 632 |
+
if (isAllergenContained(food, allergen)) {
|
| 633 |
+
risks.push(`${m.name} (${allergen} 알레르기) - ${food} 성분 유의`);
|
| 634 |
+
const repl = getAlternativeFood(food, allergen);
|
| 635 |
+
alternatives.push({ from: food, to: repl.food });
|
| 636 |
+
guidelines.push(`${m.name}의 알레르기 교차오염을 차단하기 위해 ${food} 대체식 조리 시 도구 분리 사용.`);
|
| 637 |
+
}
|
| 638 |
+
});
|
| 639 |
+
} else if (m.type === '질환식') {
|
| 640 |
+
if (m.detail.includes('당뇨') && (food.includes('밥') || food.includes('요플레') || food.includes('요구르트') || food.includes('설탕'))) {
|
| 641 |
+
if (!food.includes('잡곡') && !food.includes('현미')) {
|
| 642 |
+
risks.push(`${m.name} (당뇨 케어) - ${food} 단순당/혈당상승 위험`);
|
| 643 |
+
const repl = getAlternativeFood(food, '당뇨');
|
| 644 |
+
alternatives.push({ from: food, to: repl.food });
|
| 645 |
+
guidelines.push(`${m.name} 어르신의 급격한 인슐린 반응 억제를 위한 저당 잡곡밥 믹싱 및 천연 에리스리톨 가미.`);
|
| 646 |
+
}
|
| 647 |
+
}
|
| 648 |
+
if (m.detail.includes('유당불내증') && (food.includes('요구르트') || food.includes('우유') || food.includes('요플레'))) {
|
| 649 |
+
risks.push(`${m.name} (유당불내) - ${food} 유당 함유 위험`);
|
| 650 |
+
const repl = getAlternativeFood(food, '유당불내증');
|
| 651 |
+
alternatives.push({ from: food, to: repl.food });
|
| 652 |
+
guidelines.push(`${m.name}님에게 일반 요구르트/유제품 대신 대체 두유 팩 제공.`);
|
| 653 |
+
}
|
| 654 |
+
if (m.detail.includes('통풍') && (food.includes('조개') || food.includes('새우') || food.includes('맥주'))) {
|
| 655 |
+
risks.push(`${m.name} (통풍 케어) - ${food} 고퓨린 성분 위험`);
|
| 656 |
+
alternatives.push({ from: food, to: '버섯구이 또는 야채찜' });
|
| 657 |
+
guidelines.push(`통풍 발작 예방을 위해 퓨린 함량이 높은 해산물 요리 대신 식이섬유가 많은 뿌리채소 구이로 교체.`);
|
| 658 |
+
}
|
| 659 |
+
}
|
| 660 |
+
});
|
| 661 |
+
});
|
| 662 |
+
|
| 663 |
+
const hasRisk = risks.length > 0;
|
| 664 |
+
|
| 665 |
+
let menuAlt = menuString;
|
| 666 |
+
alternatives.forEach(alt => {
|
| 667 |
+
menuAlt = menuAlt.replace(alt.from, alt.to);
|
| 668 |
+
});
|
| 669 |
+
|
| 670 |
+
let recipe = '';
|
| 671 |
+
if (hasRisk) {
|
| 672 |
+
recipe = `[대체 조리 수칙]\n` + guidelines.map((g, i) => `${i+1}. ${g}`).join('\n');
|
| 673 |
+
} else {
|
| 674 |
+
recipe = "일반 조리 시설 보건 위생 규정 및 교차 오염 기본 예방 수칙을 준수하여 정성껏 위생 조리하십시오.";
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
const dateObj = new Date();
|
| 678 |
+
dateObj.setDate(dateObj.getDate() + index);
|
| 679 |
+
const dateStr = `${String(dateObj.getMonth() + 1).padStart(2, '0')}/${String(dateObj.getDate()).padStart(2, '0')}`;
|
| 680 |
+
|
| 681 |
+
results.push({
|
| 682 |
+
day: dayName,
|
| 683 |
+
date: dateStr,
|
| 684 |
+
hasRisk: hasRisk,
|
| 685 |
+
risks: risks,
|
| 686 |
+
menuOrig: menuString,
|
| 687 |
+
menuAlt: menuAlt,
|
| 688 |
+
recipe: recipe
|
| 689 |
+
});
|
| 690 |
+
});
|
| 691 |
+
|
| 692 |
+
return results;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
function isAllergenContained(food, allergen) {
|
| 696 |
+
if (allergen === '달걀' && (food.includes('계란') || food.includes('달걀') || food.includes('난황') || food.includes('알'))) return true;
|
| 697 |
+
if (allergen === '우유' && (food.includes('우유') || food.includes('밀크') || food.includes('요구르트') || food.includes('야쿠르트') || food.includes('요플레') || food.includes('치즈'))) return true;
|
| 698 |
+
if (allergen === '땅콩' && (food.includes('땅콩') || food.includes('피넛') || food.includes('견과'))) return true;
|
| 699 |
+
if (allergen === '대두' && (food.includes('대두') || food.includes('콩') || food.includes('간장') || food.includes('된장') || food.includes('두부'))) return true;
|
| 700 |
+
return food.includes(allergen);
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
function getAlternativeFood(food, allergen) {
|
| 704 |
+
if (allergen === '달걀') {
|
| 705 |
+
return { food: '두부구이 (연화처리)', reason: '대체 단백질 및 가루 응집 식감 구현 가능, 알레르기 제로' };
|
| 706 |
+
}
|
| 707 |
+
if (allergen === '우유') {
|
| 708 |
+
return { food: '오렌지주스 (무칼슘/천연)', reason: '우유 지질 성분 제로 및 비타민 보존 대체' };
|
| 709 |
+
}
|
| 710 |
+
if (allergen === '당뇨') {
|
| 711 |
+
if (food.includes('밥')) return { food: '귀리잡곡밥', reason: '식이섬유가 풍부하여 식후 혈당 급상승을 예방하는 복합탄수화물 대체' };
|
| 712 |
+
if (food.includes('요플레')) return { food: '무설탕 플레인 요거트', reason: '과당/정제 설탕 배제로 인슐린 급증 억제' };
|
| 713 |
+
return { food: '저당 식이 반찬', reason: '천연 대체 감미료 조리법 적용' };
|
| 714 |
+
}
|
| 715 |
+
if (allergen === '유당불내증') {
|
| 716 |
+
return { food: '약콩 두유 (락토프리)', reason: '유당 불포함 천연 유기농 식물성 단유 대체' };
|
| 717 |
+
}
|
| 718 |
+
return { food: '대체 자연식품', reason: '식약처 식자재 DB 대체 지표에 따른 추천' };
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// Render schedule cards in DOM
|
| 722 |
+
function renderScheduleCards() {
|
| 723 |
+
elements.scheduleCardsContainer.innerHTML = '';
|
| 724 |
+
|
| 725 |
+
const results = state.analysisResult;
|
| 726 |
+
const hasAnyRisk = results.some(r => r.hasRisk);
|
| 727 |
+
const riskCount = results.filter(r => r.hasRisk).length;
|
| 728 |
+
|
| 729 |
+
if (hasAnyRisk) {
|
| 730 |
+
elements.scheduleStatusBanner.className = 'summary-status-badge alert-red';
|
| 731 |
+
elements.scheduleSummaryTitle.textContent = '대체식 처방 식단 스케줄 생성 완료';
|
| 732 |
+
elements.scheduleSummaryDesc.textContent = `전체 분석 식단 중 ${riskCount}일의 식단에서 유해 요인이 확인되어 AI 대체 처방 및 안전 조리법을 발행했습니다.`;
|
| 733 |
+
elements.scheduleStatusBanner.querySelector('i').className = 'fa-solid fa-circle-exclamation text-red animate-bounce';
|
| 734 |
+
} else {
|
| 735 |
+
elements.scheduleStatusBanner.className = 'summary-status-badge safe-green';
|
| 736 |
+
elements.scheduleSummaryTitle.textContent = '식단 스크리닝 통과 (전원 안전)';
|
| 737 |
+
elements.scheduleSummaryDesc.textContent = '입력된 식단 전체에서 현재 등록된 대상자들의 알레르기 및 만성질환 위험군 제한 물질이 검출되지 않았습니다.';
|
| 738 |
+
elements.scheduleStatusBanner.querySelector('i').className = 'fa-solid fa-circle-check text-green';
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
results.forEach((item, idx) => {
|
| 742 |
+
const card = document.createElement('div');
|
| 743 |
+
card.className = `schedule-card ${item.hasRisk ? 'has-risk' : 'is-safe'}`;
|
| 744 |
+
|
| 745 |
+
const statusPill = item.hasRisk
|
| 746 |
+
? `<span class="status-pill danger">위험 처방 완료</span>`
|
| 747 |
+
: `<span class="status-pill success">검증 통과</span>`;
|
| 748 |
+
|
| 749 |
+
card.innerHTML = `
|
| 750 |
+
<div class="card-header-toggle" onclick="toggleCardAccordion(${idx})">
|
| 751 |
+
<div class="header-day-info">
|
| 752 |
+
<span class="day-title">[${item.day}]</span>
|
| 753 |
+
<span class="day-date">예정일자: ${item.date}</span>
|
| 754 |
+
</div>
|
| 755 |
+
<div class="status-indicator-box">
|
| 756 |
+
${statusPill}
|
| 757 |
+
<i class="fa-solid fa-chevron-down toggle-arrow-icon" id="arrow-${idx}"></i>
|
| 758 |
+
</div>
|
| 759 |
+
</div>
|
| 760 |
+
|
| 761 |
+
<div class="accordion-content" id="accordion-${idx}">
|
| 762 |
+
<div class="card-body-details">
|
| 763 |
+
<div class="menu-orig-box">
|
| 764 |
+
<h5>기존 원본 식단</h5>
|
| 765 |
+
<p class="menu-content-text" style="${item.hasRisk ? 'color: var(--text-secondary); text-decoration: line-through;' : ''}">
|
| 766 |
+
${item.menuOrig}
|
| 767 |
+
</p>
|
| 768 |
+
</div>
|
| 769 |
+
|
| 770 |
+
${item.hasRisk ? `
|
| 771 |
+
<div class="menu-alt-box">
|
| 772 |
+
<h5>AI 처방 대체식 식단</h5>
|
| 773 |
+
<p class="menu-content-text text-green font-bold">
|
| 774 |
+
${item.menuAlt}
|
| 775 |
+
</p>
|
| 776 |
+
</div>
|
| 777 |
+
|
| 778 |
+
<div class="detected-risk-sublist" style="background: rgba(220,38,38,0.03); border-left: 2px solid var(--color-red); padding: 8px 12px; border-radius: 4px; font-size:11px;">
|
| 779 |
+
<strong class="text-red">충돌 정보:</strong><br>
|
| 780 |
+
${item.risks.map(r => `• ${r}`).join('<br>')}
|
| 781 |
+
</div>
|
| 782 |
+
` : ''}
|
| 783 |
+
|
| 784 |
+
<div class="recipe-guide-box">
|
| 785 |
+
<h5>${item.hasRisk ? 'AI 맞춤 조리 가이드라인' : '표준 조리 지침'}</h5>
|
| 786 |
+
<p class="recipe-content-text">
|
| 787 |
+
${item.recipe.replace(/\n/g, '<br>')}
|
| 788 |
+
</p>
|
| 789 |
+
</div>
|
| 790 |
+
</div>
|
| 791 |
+
</div>
|
| 792 |
+
`;
|
| 793 |
+
|
| 794 |
+
elements.scheduleCardsContainer.appendChild(card);
|
| 795 |
+
});
|
| 796 |
+
|
| 797 |
+
elements.scheduleResultView.classList.remove('hidden');
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
window.toggleCardAccordion = function(index) {
|
| 801 |
+
const content = document.getElementById(`accordion-${index}`);
|
| 802 |
+
const arrow = document.getElementById(`arrow-${index}`);
|
| 803 |
+
|
| 804 |
+
if (content.classList.contains('expanded')) {
|
| 805 |
+
content.classList.remove('expanded');
|
| 806 |
+
arrow.classList.remove('rotate-icon');
|
| 807 |
+
} else {
|
| 808 |
+
content.classList.add('expanded');
|
| 809 |
+
arrow.classList.add('rotate-icon');
|
| 810 |
+
}
|
| 811 |
+
};
|
| 812 |
+
|
| 813 |
+
// 6. VLM Scanner Integration
|
| 814 |
+
function initVlmScanner() {
|
| 815 |
+
elements.vlmOptNormal.addEventListener('click', () => {
|
| 816 |
+
selectVlmImage('normal');
|
| 817 |
+
});
|
| 818 |
+
|
| 819 |
+
elements.vlmOptAlternative.addEventListener('click', () => {
|
| 820 |
+
selectVlmImage('alternative');
|
| 821 |
+
});
|
| 822 |
+
|
| 823 |
+
elements.triggerFileBtn.addEventListener('click', () => {
|
| 824 |
+
elements.vlmFileInput.click();
|
| 825 |
+
});
|
| 826 |
+
|
| 827 |
+
elements.vlmFileInput.addEventListener('change', (e) => {
|
| 828 |
+
const file = e.target.files[0];
|
| 829 |
+
if (!file) return;
|
| 830 |
+
|
| 831 |
+
const reader = new FileReader();
|
| 832 |
+
reader.onload = (event) => {
|
| 833 |
+
state.customVlmImageSrc = event.target.result;
|
| 834 |
+
selectVlmImage('custom');
|
| 835 |
+
};
|
| 836 |
+
reader.readAsDataURL(file);
|
| 837 |
+
});
|
| 838 |
+
|
| 839 |
+
elements.runVlmBtn.addEventListener('click', async () => {
|
| 840 |
+
if (!state.selectedVlmImage) return;
|
| 841 |
+
|
| 842 |
+
elements.vlmEmptyResult.classList.add('hidden');
|
| 843 |
+
elements.vlmResultPanel.classList.add('hidden');
|
| 844 |
+
elements.scannerLaser.style.display = 'block';
|
| 845 |
+
elements.scannerLaser.style.animation = 'scan 2.5s infinite linear';
|
| 846 |
+
elements.runVlmBtn.disabled = true;
|
| 847 |
+
|
| 848 |
+
try {
|
| 849 |
+
if (apiConfig.key && (state.selectedVlmImage === 'custom' || state.customVlmImageSrc)) {
|
| 850 |
+
// Call Real OpenRouter VLM (Vision) models
|
| 851 |
+
const visionResult = await callOpenRouterVLMFallback();
|
| 852 |
+
// Fill result with vision result
|
| 853 |
+
setTimeout(() => {
|
| 854 |
+
elements.scannerLaser.style.display = 'none';
|
| 855 |
+
elements.scannerLaser.style.animation = 'none';
|
| 856 |
+
elements.runVlmBtn.disabled = false;
|
| 857 |
+
renderVlmResult(visionResult);
|
| 858 |
+
}, 2500);
|
| 859 |
+
} else {
|
| 860 |
+
// Default Sim Fallback
|
| 861 |
+
setTimeout(() => {
|
| 862 |
+
elements.scannerLaser.style.display = 'none';
|
| 863 |
+
elements.scannerLaser.style.animation = 'none';
|
| 864 |
+
elements.runVlmBtn.disabled = false;
|
| 865 |
+
showVlmScanResult();
|
| 866 |
+
}, 2500);
|
| 867 |
+
}
|
| 868 |
+
} catch (error) {
|
| 869 |
+
console.error(error);
|
| 870 |
+
showNotification("VLM 모델 인식 지연으로 시뮬레이션 결과로 연동 대체합니다.");
|
| 871 |
+
setTimeout(() => {
|
| 872 |
+
elements.scannerLaser.style.display = 'none';
|
| 873 |
+
elements.scannerLaser.style.animation = 'none';
|
| 874 |
+
elements.runVlmBtn.disabled = false;
|
| 875 |
+
showVlmScanResult();
|
| 876 |
+
}, 1000);
|
| 877 |
+
}
|
| 878 |
+
});
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
// OpenRouter VLM Vision Fallback Call
|
| 882 |
+
async function callOpenRouterVLMFallback() {
|
| 883 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 884 |
+
const list = state.members[listKey];
|
| 885 |
+
const memberProfileStr = list.map(m => `- 이름: ${m.name}, 유형: ${m.type}, 원인물질/질환: ${m.detail}`).join('\n');
|
| 886 |
+
|
| 887 |
+
const prompt = `
|
| 888 |
+
이 이미지는 배식 조리 완료된 식판 사진입니다.
|
| 889 |
+
아래에 제공된 관리 대상자들의 알레르기/질환 과민 반응 제한 성분 목록을 분석하여, 이 식판에 해당 대상자에게 유해한 식품이나 알레르기 유발 요소가 오배식 되었는지 검증하십시오.
|
| 890 |
+
|
| 891 |
+
[제한 대상 명단]
|
| 892 |
+
${memberProfileStr}
|
| 893 |
+
|
| 894 |
+
반드시 다른 텍스트 설명 없이 오직 아래 형태의 JSON 객체 하나만 반환하십시오:
|
| 895 |
+
{
|
| 896 |
+
"verdict": "PASS" 또는 "REJECT" (위해물질이 없으면 PASS, 있으면 REJECT),
|
| 897 |
+
"allergen": "검출된 유해 물질 요약 (예: 달걀 성분 또는 없음)",
|
| 898 |
+
"portion": "정량 및 형태 분석 (예: 적정 배식 98% 또는 적정)",
|
| 899 |
+
"reason": "AI Vision 분석 상세 근거 (식판 내 물체의 위치 및 매칭 사유)"
|
| 900 |
+
}
|
| 901 |
+
`;
|
| 902 |
+
|
| 903 |
+
const base64Image = state.customVlmImageSrc.split(',')[1];
|
| 904 |
+
|
| 905 |
+
for (let model of freeVLMModels) {
|
| 906 |
+
try {
|
| 907 |
+
console.log(`Trying OpenRouter VLM model: ${model}`);
|
| 908 |
+
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
| 909 |
+
method: "POST",
|
| 910 |
+
headers: {
|
| 911 |
+
"Authorization": `Bearer ${apiConfig.key}`,
|
| 912 |
+
"Content-Type": "application/json",
|
| 913 |
+
"HTTP-Referer": "https://huggingface.co/spaces/HwangJinwook/caretable",
|
| 914 |
+
"X-Title": "CareTable Client"
|
| 915 |
+
},
|
| 916 |
+
body: JSON.stringify({
|
| 917 |
+
model: model,
|
| 918 |
+
messages: [
|
| 919 |
+
{
|
| 920 |
+
role: "user",
|
| 921 |
+
content: [
|
| 922 |
+
{ type: "text", text: prompt },
|
| 923 |
+
{
|
| 924 |
+
type: "image_url",
|
| 925 |
+
image_url: {
|
| 926 |
+
url: `data:image/jpeg;base64,${base64Image}`
|
| 927 |
+
}
|
| 928 |
+
}
|
| 929 |
+
]
|
| 930 |
+
}
|
| 931 |
+
]
|
| 932 |
+
})
|
| 933 |
+
});
|
| 934 |
+
|
| 935 |
+
if (!res.ok) continue;
|
| 936 |
+
|
| 937 |
+
const data = await res.json();
|
| 938 |
+
let text = data.choices[0].message.content.trim();
|
| 939 |
+
|
| 940 |
+
if (text.includes("```json")) {
|
| 941 |
+
text = text.split("```json")[1].split("```")[0].trim();
|
| 942 |
+
} else if (text.includes("```")) {
|
| 943 |
+
text = text.split("```")[1].split("```")[0].trim();
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
return JSON.parse(text);
|
| 947 |
+
} catch (e) {
|
| 948 |
+
console.warn(`VLM Model ${model} failed: `, e);
|
| 949 |
+
}
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
throw new Error("All free OpenRouter VLM models failed.");
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
function renderVlmResult(apiResult) {
|
| 956 |
+
elements.vlmResultPanel.classList.remove('hidden');
|
| 957 |
+
|
| 958 |
+
if (apiResult.verdict === 'REJECT') {
|
| 959 |
+
elements.vlmVerdictBox.className = 'vlm-verdict-box rejected';
|
| 960 |
+
elements.verdictIconContainer.innerHTML = '<i class="fa-solid fa-circle-xmark"></i>';
|
| 961 |
+
elements.verdictTitle.textContent = '배식 보류 (REJECTED)';
|
| 962 |
+
|
| 963 |
+
elements.vlmMatchRate.style.width = '72%';
|
| 964 |
+
elements.vlmMatchRate.style.backgroundColor = 'var(--color-red)';
|
| 965 |
+
elements.vlmMatchValue.textContent = '72%';
|
| 966 |
+
|
| 967 |
+
elements.vlmAllergenStatus.textContent = apiResult.allergen;
|
| 968 |
+
elements.vlmAllergenStatus.className = 'detail-value text-red';
|
| 969 |
+
elements.vlmPortionStatus.textContent = apiResult.portion;
|
| 970 |
+
elements.vlmAnalysisReason.textContent = apiResult.reason;
|
| 971 |
+
|
| 972 |
+
elements.vlmActionFooter.innerHTML = `
|
| 973 |
+
<button class="secondary-btn" onclick="switchTab('diet-analyzer')"><i class="fa-solid fa-calendar-days"></i> 식단 분석 재진행</button>
|
| 974 |
+
<button class="primary-btn" style="background:var(--gradient-danger);" disabled><i class="fa-solid fa-ban"></i> 배식 불가 상태</button>
|
| 975 |
+
`;
|
| 976 |
+
} else {
|
| 977 |
+
elements.vlmVerdictBox.className = 'vlm-verdict-box passed';
|
| 978 |
+
elements.verdictIconContainer.innerHTML = '<i class="fa-solid fa-circle-check"></i>';
|
| 979 |
+
elements.verdictTitle.textContent = '배식 승인 (PASSED)';
|
| 980 |
+
|
| 981 |
+
elements.vlmMatchRate.style.width = '99%';
|
| 982 |
+
elements.vlmMatchRate.style.backgroundColor = 'var(--color-green)';
|
| 983 |
+
elements.vlmMatchValue.textContent = '99%';
|
| 984 |
+
|
| 985 |
+
elements.vlmAllergenStatus.textContent = '유해 성분 없음 (검증 완료)';
|
| 986 |
+
elements.vlmAllergenStatus.className = 'detail-value text-green';
|
| 987 |
+
elements.vlmPortionStatus.textContent = apiResult.portion;
|
| 988 |
+
elements.vlmAnalysisReason.textContent = apiResult.reason;
|
| 989 |
+
|
| 990 |
+
elements.vlmActionFooter.innerHTML = `
|
| 991 |
+
<button id="approve-final-btn" class="primary-btn" style="background:var(--gradient-success); width:100%;"><i class="fa-solid fa-circle-check"></i> 최종 배식 확정 및 디지털 이력 저장</button>
|
| 992 |
+
`;
|
| 993 |
+
|
| 994 |
+
document.getElementById('approve-final-btn').addEventListener('click', saveFinalVerificationLog);
|
| 995 |
+
}
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
function selectVlmImage(type) {
|
| 999 |
+
state.selectedVlmImage = type;
|
| 1000 |
+
elements.vlmViewportPlaceholder.classList.add('hidden');
|
| 1001 |
+
elements.vlmTargetImage.classList.remove('hidden');
|
| 1002 |
+
|
| 1003 |
+
elements.vlmOptNormal.classList.remove('selected');
|
| 1004 |
+
elements.vlmOptAlternative.classList.remove('selected');
|
| 1005 |
+
|
| 1006 |
+
if (type === 'normal') {
|
| 1007 |
+
elements.vlmTargetImage.src = 'assets/meal_normal.png';
|
| 1008 |
+
elements.vlmOptNormal.classList.add('selected');
|
| 1009 |
+
} else if (type === 'alternative') {
|
| 1010 |
+
elements.vlmTargetImage.src = 'assets/meal_alternative.png';
|
| 1011 |
+
elements.vlmOptAlternative.classList.add('selected');
|
| 1012 |
+
} else if (type === 'custom' && state.customVlmImageSrc) {
|
| 1013 |
+
elements.vlmTargetImage.src = state.customVlmImageSrc;
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
elements.runVlmBtn.disabled = false;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
function resetVlmUI() {
|
| 1020 |
+
state.selectedVlmImage = null;
|
| 1021 |
+
state.customVlmImageSrc = null;
|
| 1022 |
+
state.vlmApproved = false;
|
| 1023 |
+
elements.vlmViewportPlaceholder.classList.remove('hidden');
|
| 1024 |
+
elements.vlmTargetImage.classList.add('hidden');
|
| 1025 |
+
elements.vlmTargetImage.src = '';
|
| 1026 |
+
elements.vlmOptNormal.classList.remove('selected');
|
| 1027 |
+
elements.vlmOptAlternative.classList.remove('selected');
|
| 1028 |
+
elements.vlmEmptyResult.classList.remove('hidden');
|
| 1029 |
+
elements.vlmResultPanel.classList.add('hidden');
|
| 1030 |
+
elements.runVlmBtn.disabled = true;
|
| 1031 |
+
elements.vlmFileInput.value = '';
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
function showVlmScanResult() {
|
| 1035 |
+
elements.vlmResultPanel.classList.remove('hidden');
|
| 1036 |
+
|
| 1037 |
+
if (state.selectedVlmImage === 'normal') {
|
| 1038 |
+
elements.vlmVerdictBox.className = 'vlm-verdict-box rejected';
|
| 1039 |
+
elements.verdictIconContainer.innerHTML = '<i class="fa-solid fa-circle-xmark"></i>';
|
| 1040 |
+
elements.verdictTitle.textContent = '배식 보류 (REJECTED)';
|
| 1041 |
+
|
| 1042 |
+
elements.vlmMatchRate.style.width = '70%';
|
| 1043 |
+
elements.vlmMatchRate.style.backgroundColor = 'var(--color-red)';
|
| 1044 |
+
elements.vlmMatchValue.textContent = '70%';
|
| 1045 |
+
|
| 1046 |
+
if (state.currentMode === 'family') {
|
| 1047 |
+
elements.vlmAllergenStatus.textContent = '계란 및 요구르트 포함 (막내아들 알러지 충돌)';
|
| 1048 |
+
elements.vlmAllergenStatus.className = 'detail-value text-red';
|
| 1049 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 준비된 식판에 계란말이와 일반 우유 성분 요구르트가 감지되었습니다. 막내아들(우유/밀가루 알레르기)에게는 유해하므로 대체 배식으로 교체하십시오.';
|
| 1050 |
+
} else if (state.currentFacility === 'child') {
|
| 1051 |
+
elements.vlmAllergenStatus.textContent = '달걀, 우유 성분 검출 (김민수 아동 위험)';
|
| 1052 |
+
elements.vlmAllergenStatus.className = 'detail-value text-red';
|
| 1053 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 튼튼어린이집 김민수 아동의 유해 인자인 달걀 성분의 계란말이 및 우유 성분의 요구르트가 그대로 배식되어 오배식 경고를 발동합니다.';
|
| 1054 |
+
} else {
|
| 1055 |
+
elements.vlmAllergenStatus.textContent = '백미 쌀밥 및 가당 유제품 검출 (노인 질환 위험)';
|
| 1056 |
+
elements.vlmAllergenStatus.className = 'detail-value text-red';
|
| 1057 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 요양시설 당뇨 최옥분 어르신과 유당불내 이명자 어르신에게 유해한 백미 쌀밥 및 요플레가 확인되었습니다. 배식을 차단하고 저당 잡곡밥 및 락토프리 두유로 재배식하십시오.';
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
elements.vlmPortionStatus.textContent = '배식량 적정 (94%)';
|
| 1061 |
+
|
| 1062 |
+
elements.vlmActionFooter.innerHTML = `
|
| 1063 |
+
<button class="secondary-btn" onclick="switchTab('diet-analyzer')"><i class="fa-solid fa-calendar-days"></i> 식단 분석 재진행</button>
|
| 1064 |
+
<button class="primary-btn" style="background:var(--gradient-danger);" disabled><i class="fa-solid fa-ban"></i> 배식 불가 상태</button>
|
| 1065 |
+
`;
|
| 1066 |
+
} else {
|
| 1067 |
+
elements.vlmVerdictBox.className = 'vlm-verdict-box passed';
|
| 1068 |
+
elements.verdictIconContainer.innerHTML = '<i class="fa-solid fa-circle-check"></i>';
|
| 1069 |
+
elements.verdictTitle.textContent = '배식 승인 (PASSED)';
|
| 1070 |
+
|
| 1071 |
+
elements.vlmMatchRate.style.width = '99%';
|
| 1072 |
+
elements.vlmMatchRate.style.backgroundColor = 'var(--color-green)';
|
| 1073 |
+
elements.vlmMatchValue.textContent = '99%';
|
| 1074 |
+
elements.vlmAllergenStatus.textContent = '위험 성분 없음 (대체식 안전 적용 완료)';
|
| 1075 |
+
elements.vlmAllergenStatus.className = 'detail-value text-green';
|
| 1076 |
+
|
| 1077 |
+
if (state.selectedVlmImage === 'custom') {
|
| 1078 |
+
elements.vlmAnalysisReason.textContent = '직접 업로드한 사진 분석 결과, 조리지침에 따른 대체 구성 성분 매칭이 완벽하여 위험 물질이 전혀 감지되지 않았습니다. 안전 배식이 허가되었습니다.';
|
| 1079 |
+
} else if (state.currentMode === 'family') {
|
| 1080 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 계란말이 대신 두부구이와 일반 요구르트 대신 유당 성분이 없는 오렌지주스가 완벽히 배식되어 막내아들 및 가족 알레르기 수칙 통과를 보증합니다.';
|
| 1081 |
+
} else if (state.currentFacility === 'child') {
|
| 1082 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 김민수 아동의 대체 처방 식자재인 두부구이와 오렌지주스가 오배식 없이 완벽 매핑되었습니다.';
|
| 1083 |
+
} else {
|
| 1084 |
+
elements.vlmAnalysisReason.textContent = 'Vision 분석 결과, 당뇨 및 유당불내 어르신들에게 적합한 잡곡밥 및 락토프리 두유 대체 급식이 검증 완료되었습니다.';
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
elements.vlmPortionStatus.textContent = '정량 배식 검증 (98%)';
|
| 1088 |
+
|
| 1089 |
+
elements.vlmActionFooter.innerHTML = `
|
| 1090 |
+
<button id="approve-final-btn" class="primary-btn" style="background:var(--gradient-success); width:100%;"><i class="fa-solid fa-circle-check"></i> 최종 배식 확정 및 디지털 이력 저장</button>
|
| 1091 |
+
`;
|
| 1092 |
+
|
| 1093 |
+
document.getElementById('approve-final-btn').addEventListener('click', saveFinalVerificationLog);
|
| 1094 |
+
}
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
function saveFinalVerificationLog() {
|
| 1098 |
+
const today = new Date();
|
| 1099 |
+
const dateStr = today.toISOString().split('T')[0];
|
| 1100 |
+
const timeStr = today.toTimeString().split(' ')[0].substring(0, 5);
|
| 1101 |
+
|
| 1102 |
+
let logItem = {};
|
| 1103 |
+
if (state.currentMode === 'family') {
|
| 1104 |
+
logItem = {
|
| 1105 |
+
date: dateStr,
|
| 1106 |
+
menu: '쌀밥, 두부구이, 어묵볶음, 오렌지주스',
|
| 1107 |
+
facility: 'family',
|
| 1108 |
+
target: '막내아들 (우유, 밀가루)',
|
| 1109 |
+
alternative: '계란말이 ➔ 두부구이, 요구르트 ➔ 오렌지주스 대체',
|
| 1110 |
+
guideline: '가정 내 유제품 독립 보관 및 배식 도구 철저 분리',
|
| 1111 |
+
vlmStatus: 'PASS',
|
| 1112 |
+
timestamp: `${dateStr} ${timeStr}`
|
| 1113 |
+
};
|
| 1114 |
+
} else if (state.currentFacility === 'child') {
|
| 1115 |
+
logItem = {
|
| 1116 |
+
date: dateStr,
|
| 1117 |
+
menu: '쌀밥, 두부구이, 어묵볶음, 맑은 무국, 오렌지주스',
|
| 1118 |
+
facility: 'child',
|
| 1119 |
+
target: '김민수 아동 (달걀/우유)',
|
| 1120 |
+
alternative: '달걀말이 ➔ 두부구이, 요구르트 ➔ 오렌지주스 대체',
|
| 1121 |
+
guideline: '계란 조리기구 분리 사용 및 대체 급식 라벨 체크',
|
| 1122 |
+
vlmStatus: 'PASS',
|
| 1123 |
+
timestamp: `${dateStr} ${timeStr}`
|
| 1124 |
+
};
|
| 1125 |
+
} else {
|
| 1126 |
+
logItem = {
|
| 1127 |
+
date: dateStr,
|
| 1128 |
+
menu: '귀리잡곡밥, 버섯국, 불고기, 고구마순나물, 약콩 두유',
|
| 1129 |
+
facility: 'senior',
|
| 1130 |
+
target: '최옥분(당뇨), 이명자(유당불내)',
|
| 1131 |
+
alternative: '쌀밥 ➔ 귀리잡곡밥, 요플레 ➔ 약콩 두유 대체',
|
| 1132 |
+
guideline: '당뇨 혈당 모니터링 식단 및 비유제품 간식 교체 검증',
|
| 1133 |
+
vlmStatus: 'PASS',
|
| 1134 |
+
timestamp: `${dateStr} ${timeStr}`
|
| 1135 |
+
};
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
state.logs.push(logItem);
|
| 1139 |
+
saveLogs();
|
| 1140 |
+
state.vlmApproved = true;
|
| 1141 |
+
|
| 1142 |
+
showNotification("배식 이력이 안전하게 저장되었습니다. 자동 생성된 디지털 일지를 보관합니다.");
|
| 1143 |
+
switchTab('report-generator');
|
| 1144 |
+
}
|
| 1145 |
+
|
| 1146 |
+
// 7. Report Center and ESG Integration
|
| 1147 |
+
function initReportCenter() {
|
| 1148 |
+
elements.reportSubTabBtns.forEach(btn => {
|
| 1149 |
+
btn.addEventListener('click', () => {
|
| 1150 |
+
elements.reportSubTabBtns.forEach(b => b.classList.remove('active'));
|
| 1151 |
+
btn.classList.add('active');
|
| 1152 |
+
renderReport();
|
| 1153 |
+
});
|
| 1154 |
+
});
|
| 1155 |
+
|
| 1156 |
+
elements.printReportBtn.addEventListener('click', () => {
|
| 1157 |
+
window.print();
|
| 1158 |
+
});
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
function renderReport() {
|
| 1162 |
+
const activeSubTab = document.querySelector('.sub-tab-btn.active').getAttribute('data-report');
|
| 1163 |
+
|
| 1164 |
+
let facilityName = '';
|
| 1165 |
+
let reportWriter = '';
|
| 1166 |
+
let reportSigner = '';
|
| 1167 |
+
|
| 1168 |
+
if (state.currentMode === 'family') {
|
| 1169 |
+
facilityName = '마포구 서교동 삼총사 가정';
|
| 1170 |
+
reportWriter = '엄마/아빠 (가정 안심 영양사)';
|
| 1171 |
+
reportSigner = '가정대표 보호자 (서명)';
|
| 1172 |
+
elements.reportConfirmStatement.textContent = '위와 같이 가족 구성원의 특이 체질 및 지침에 부합하는 안전 식단과 대체 배식이 올바르게 실행되었음을 확인하며 VLM 스캔 검증 이력을 디지털 기록합니다.';
|
| 1173 |
+
elements.reportSignerName.textContent = '가족 건강 확인자: 엄마/아빠 (인)';
|
| 1174 |
+
elements.reportFooterDesc.textContent = 'CareTable 가정용 식단 위생 자율 점검 대장 (F-2026)';
|
| 1175 |
+
} else {
|
| 1176 |
+
facilityName = state.currentFacility === 'child' ? '튼튼어린이집' : '행복실버요양원';
|
| 1177 |
+
reportWriter = '박아름 사회복지사';
|
| 1178 |
+
reportSigner = '시설대표 박아름 (서명)';
|
| 1179 |
+
elements.reportConfirmStatement.textContent = '위와 같이 취약계층 급식 관리 및 대체 배식이 올바르게 수행되었으며, 배식 전 VLM(Vision-Language Model) 검증 절차를 완료하였음을 확인합니다.';
|
| 1180 |
+
elements.reportSignerName.textContent = '확인자: 시설대표 박아름 (서명/인)';
|
| 1181 |
+
elements.reportFooterDesc.textContent = '한국사회보장정보원 사회복지시설평가 증빙 표준 서식 (SSIS 14-2)';
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
elements.rFacilityName.textContent = facilityName;
|
| 1185 |
+
elements.rWriter.textContent = reportWriter;
|
| 1186 |
+
|
| 1187 |
+
const today = new Date();
|
| 1188 |
+
elements.rDate.textContent = `${today.getFullYear()}년 ${String(today.getMonth() + 1).padStart(2, '0')}월 ${String(today.getDate()).padStart(2, '0')}일`;
|
| 1189 |
+
|
| 1190 |
+
const currentFilterKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 1191 |
+
const filteredLogs = state.logs.filter(l => l.facility === currentFilterKey);
|
| 1192 |
+
|
| 1193 |
+
let contentHtml = '';
|
| 1194 |
+
|
| 1195 |
+
if (activeSubTab === '급식일지') {
|
| 1196 |
+
elements.reportDocTitle.textContent = state.currentMode === 'family' ? '가 정 식 단 위 생 일 지' : '영 양 급 식 일 지';
|
| 1197 |
+
|
| 1198 |
+
contentHtml = `
|
| 1199 |
+
<div class="report-paper-body">
|
| 1200 |
+
<p>${state.currentMode === 'family' ? '가정 내 자율 보건 위생 관리와 오배식 방지 수칙에 따른 안심 식단 급지 기록입니다.' : '보건복지부 급식안전 기준 및 한국사회보장정보원 시설 평가 가이드라인에 근거한 당일 배식 현황 보고서입니다.'}</p>
|
| 1201 |
+
<table class="report-table">
|
| 1202 |
+
<thead>
|
| 1203 |
+
<tr>
|
| 1204 |
+
<th>작성일자</th>
|
| 1205 |
+
<th>배식 식단 내용 (대체식 포함)</th>
|
| 1206 |
+
<th>집중 케어 대상자</th>
|
| 1207 |
+
<th>VLM AI 분석 적합성</th>
|
| 1208 |
+
<th>인증 확인</th>
|
| 1209 |
+
</tr>
|
| 1210 |
+
</thead>
|
| 1211 |
+
<tbody>
|
| 1212 |
+
${filteredLogs.length > 0 ? filteredLogs.map(log => `
|
| 1213 |
+
<tr>
|
| 1214 |
+
<td>${log.date}</td>
|
| 1215 |
+
<td class="align-left">${log.menu}</td>
|
| 1216 |
+
<td>${log.target}</td>
|
| 1217 |
+
<td><span style="color:var(--color-green); font-weight:bold;">${log.vlmStatus} (99%)</span></td>
|
| 1218 |
+
<td>확인완료</td>
|
| 1219 |
+
</tr>
|
| 1220 |
+
`).join('') : `
|
| 1221 |
+
<tr>
|
| 1222 |
+
<td colspan="5" style="color: var(--text-muted); padding:20px 0;">배식 이력이 존재하지 않습니다. VLM 검증 탭에서 배식을 최종 확정해 주십시오.</td>
|
| 1223 |
+
</tr>
|
| 1224 |
+
`}
|
| 1225 |
+
</tbody>
|
| 1226 |
+
</table>
|
| 1227 |
+
<p><strong>주요 안전 점검 준수 사항:</strong></p>
|
| 1228 |
+
<ul>
|
| 1229 |
+
<li>식자재 보관 냉장/냉동고 온도 적격 확인</li>
|
| 1230 |
+
<li>VLM 비전 분석 스캔을 통한 알레르기 유발 유래 성분 최종 격리 점검 양호</li>
|
| 1231 |
+
<li>지정 조리사의 교차 오염 방지용 조리기구 분리 사용 실행 완료</li>
|
| 1232 |
+
</ul>
|
| 1233 |
+
</div>
|
| 1234 |
+
`;
|
| 1235 |
+
} else if (activeSubTab === '알레르기대장') {
|
| 1236 |
+
elements.reportDocTitle.textContent = state.currentMode === 'family' ? '가족 알레르기 및 만성질환 DB' : '집중 케어 대상자 대장';
|
| 1237 |
+
|
| 1238 |
+
contentHtml = `
|
| 1239 |
+
<div class="report-paper-body">
|
| 1240 |
+
<p>${state.currentMode === 'family' ? '가족들의 건강 및 식품 과민반응 정보를 지속 보관하여 오인 사고를 예방하는 집중 예방 DB입니다.' : '사회복지시설 평가지표 24번 취약계층 식품 유해인자 맞춤 안전 관리대장 양식입니다.'}</p>
|
| 1241 |
+
<table class="report-table">
|
| 1242 |
+
<thead>
|
| 1243 |
+
<tr>
|
| 1244 |
+
<th>순번</th>
|
| 1245 |
+
<th>수혜자명</th>
|
| 1246 |
+
<th>제한 식품 및 요인</th>
|
| 1247 |
+
<th>추천 대체 식자재</th>
|
| 1248 |
+
<th>핵심 대체 조리 지침</th>
|
| 1249 |
+
<th>검증 방식</th>
|
| 1250 |
+
</tr>
|
| 1251 |
+
</thead>
|
| 1252 |
+
<tbody>
|
| 1253 |
+
${state.members[currentFilterKey].filter(m => m.type !== '일반').map((m, idx) => {
|
| 1254 |
+
const pair = getAlternativeFood(m.detail.split(',')[0].trim(), m.detail.includes('당뇨') ? '당뇨' : m.detail.includes('고혈압') ? '당뇨' : m.detail.split(',')[0].trim());
|
| 1255 |
+
return `
|
| 1256 |
+
<tr>
|
| 1257 |
+
<td>${idx + 1}</td>
|
| 1258 |
+
<td><strong>${m.name}</strong></td>
|
| 1259 |
+
<td><span style="color:#e11d48; font-weight:600;">${m.detail}</span></td>
|
| 1260 |
+
<td><span style="color:#0d9488; font-weight:600;">${pair.food}</span></td>
|
| 1261 |
+
<td class="align-left">${m.instruction}</td>
|
| 1262 |
+
<td>VLM 비전 매칭</td>
|
| 1263 |
+
</tr>
|
| 1264 |
+
`;
|
| 1265 |
+
}).join('')}
|
| 1266 |
+
</tbody>
|
| 1267 |
+
</table>
|
| 1268 |
+
</div>
|
| 1269 |
+
`;
|
| 1270 |
+
} else if (activeSubTab === '대체식대장') {
|
| 1271 |
+
elements.reportDocTitle.textContent = '대 체 급 식 제 공 대 장';
|
| 1272 |
+
|
| 1273 |
+
contentHtml = `
|
| 1274 |
+
<div class="report-paper-body">
|
| 1275 |
+
<p>특이 체질이나 의학적 위험 인자로 인해 일반 식사를 섭취하지 못하는 대상자에게 안전 대체식을 처방하고 지급을 검증한 이력서입니다.</p>
|
| 1276 |
+
<table class="report-table">
|
| 1277 |
+
<thead>
|
| 1278 |
+
<tr>
|
| 1279 |
+
<th>제공일시</th>
|
| 1280 |
+
<th>대상자</th>
|
| 1281 |
+
<th>제한 원본 메뉴</th>
|
| 1282 |
+
<th>실제 대체 급식 내용</th>
|
| 1283 |
+
<th>대체 사유</th>
|
| 1284 |
+
<th>VLM 스캔 완료시각</th>
|
| 1285 |
+
</tr>
|
| 1286 |
+
</thead>
|
| 1287 |
+
<tbody>
|
| 1288 |
+
${filteredLogs.length > 0 ? filteredLogs.map(log => `
|
| 1289 |
+
<tr>
|
| 1290 |
+
<td>${log.date}</td>
|
| 1291 |
+
<td><strong>${log.target.split(' ')[0]}</strong></td>
|
| 1292 |
+
<td><span style="color:#e11d48; text-decoration:line-through;">${log.target.includes('아동') || log.target.includes('아들') ? '계란말이, 요구르트' : '쌀밥, 요플레'}</span></td>
|
| 1293 |
+
<td><span style="color:#0d9488; font-weight:bold;">${log.alternative.split('➔')[1] || log.alternative}</span></td>
|
| 1294 |
+
<td>${log.target.includes('아동') || log.target.includes('아들') ? '식품 알레르기 격리' : '혈당/유당 만성질환 케어'}</td>
|
| 1295 |
+
<td>${log.timestamp}</td>
|
| 1296 |
+
</tr>
|
| 1297 |
+
`).join('') : `
|
| 1298 |
+
<tr>
|
| 1299 |
+
<td colspan="6" style="color: var(--text-muted); padding:20px 0;">배식 이력이 존재하지 않습니다. VLM 검증 탭에서 배식을 최종 확정해 주십시오.</td>
|
| 1300 |
+
</tr>
|
| 1301 |
+
`}
|
| 1302 |
+
</tbody>
|
| 1303 |
+
</table>
|
| 1304 |
+
</div>
|
| 1305 |
+
`;
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
elements.reportDocContent.innerHTML = contentHtml;
|
| 1309 |
+
|
| 1310 |
+
const savedHours = (filteredLogs.length * 4.2).toFixed(1);
|
| 1311 |
+
const co2Saved = (filteredLogs.length * 12.0).toFixed(1);
|
| 1312 |
+
|
| 1313 |
+
elements.esgCo2.textContent = `${co2Saved}kg`;
|
| 1314 |
+
elements.esgTime.textContent = `${savedHours}시간`;
|
| 1315 |
+
elements.esgLocal.textContent = state.currentMode === 'family' ? '75%' : (state.currentFacility === 'child' ? '45%' : '60%');
|
| 1316 |
+
|
| 1317 |
+
if (state.currentMode === 'family') {
|
| 1318 |
+
elements.esgTime.parentElement.querySelector('.esg-lbl').textContent = '가사 노동 및 행정 절감 시간';
|
| 1319 |
+
elements.esgTime.parentElement.querySelector('.esg-sub').textContent = '레시피 고민 및 자율 기록 자동화 효과';
|
| 1320 |
+
} else {
|
| 1321 |
+
elements.esgTime.parentElement.querySelector('.esg-lbl').textContent = '행정 시간 ➔ 직접 돌봄 시간 환원';
|
| 1322 |
+
elements.esgTime.parentElement.querySelector('.esg-sub').textContent = '시설 평가 문서 자동화에 따른 절감';
|
| 1323 |
+
}
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
// 8. API configuration modal functions
|
| 1327 |
+
function initApiConfig() {
|
| 1328 |
+
elements.openApiConfigBtn.addEventListener('click', () => {
|
| 1329 |
+
elements.apiKeyInput.value = apiConfig.key;
|
| 1330 |
+
elements.apiModelSelect.value = apiConfig.model;
|
| 1331 |
+
elements.apiConfigModal.classList.remove('hidden');
|
| 1332 |
+
});
|
| 1333 |
+
|
| 1334 |
+
elements.closeApiModalBtn.addEventListener('click', closeApiModal);
|
| 1335 |
+
|
| 1336 |
+
elements.apiConfigModal.addEventListener('click', (e) => {
|
| 1337 |
+
if (e.target === elements.apiConfigModal) closeApiModal();
|
| 1338 |
+
});
|
| 1339 |
+
|
| 1340 |
+
elements.toggleKeyVisibilityBtn.addEventListener('click', () => {
|
| 1341 |
+
const type = elements.apiKeyInput.type === 'password' ? 'text' : 'password';
|
| 1342 |
+
elements.apiKeyInput.type = type;
|
| 1343 |
+
elements.toggleKeyVisibilityBtn.innerHTML = type === 'password' ? '<i class="fa-solid fa-eye"></i>' : '<i class="fa-solid fa-eye-slash"></i>';
|
| 1344 |
+
});
|
| 1345 |
+
|
| 1346 |
+
elements.resetApiKeyBtn.addEventListener('click', () => {
|
| 1347 |
+
elements.apiKeyInput.value = '';
|
| 1348 |
+
apiConfig.key = '';
|
| 1349 |
+
localStorage.removeItem('caremeal_api_key');
|
| 1350 |
+
updateApiStatusUI();
|
| 1351 |
+
closeApiModal();
|
| 1352 |
+
showNotification("오픈라우터 API Key가 삭제되었습니다. 데모 시뮬레이션 모드로 작동합니다.");
|
| 1353 |
+
});
|
| 1354 |
+
|
| 1355 |
+
elements.apiConfigForm.addEventListener('submit', (e) => {
|
| 1356 |
+
e.preventDefault();
|
| 1357 |
+
|
| 1358 |
+
const key = elements.apiKeyInput.value.trim();
|
| 1359 |
+
const model = elements.apiModelSelect.value;
|
| 1360 |
+
|
| 1361 |
+
apiConfig.key = key;
|
| 1362 |
+
apiConfig.model = model;
|
| 1363 |
+
|
| 1364 |
+
if (key) {
|
| 1365 |
+
localStorage.setItem('caremeal_api_key', key);
|
| 1366 |
+
localStorage.setItem('caremeal_api_model', model);
|
| 1367 |
+
showNotification("오픈라우터 무료 AI 연동 및 순차 폴백 체인 활성화!");
|
| 1368 |
+
} else {
|
| 1369 |
+
localStorage.removeItem('caremeal_api_key');
|
| 1370 |
+
showNotification("API Key가 비어있어 로컬 시뮬레이션 모드로 전환되었습니다.");
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
updateApiStatusUI();
|
| 1374 |
+
closeApiModal();
|
| 1375 |
+
});
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
function closeApiModal() {
|
| 1379 |
+
elements.apiConfigModal.classList.add('hidden');
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
function updateApiStatusUI() {
|
| 1383 |
+
if (apiConfig.key) {
|
| 1384 |
+
elements.apiStatusBadge.className = 'api-status live-connected';
|
| 1385 |
+
elements.apiStatusText.textContent = `AI 연동: 폴백 체인`;
|
| 1386 |
+
elements.analyzerModelIndicator.textContent = `${apiConfig.model.split('/')[1] || apiConfig.model} (실시간 무료 AI 폴백 활성화)`;
|
| 1387 |
+
} else {
|
| 1388 |
+
elements.apiStatusBadge.className = 'api-status online';
|
| 1389 |
+
elements.apiStatusText.textContent = 'AI: 시뮬레이션 모드';
|
| 1390 |
+
elements.analyzerModelIndicator.textContent = 'Nemotron-70B / Llama-3 (시뮬레이션 폴백 모드)';
|
| 1391 |
+
}
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
// 9. Interactive Person management Modal
|
| 1395 |
+
function initModal() {
|
| 1396 |
+
elements.addMemberBtn.addEventListener('click', () => {
|
| 1397 |
+
elements.addMemberModal.classList.remove('hidden');
|
| 1398 |
+
});
|
| 1399 |
+
|
| 1400 |
+
elements.closeMemberModalBtn.addEventListener('click', closeMemberModal);
|
| 1401 |
+
elements.cancelMemberBtn.addEventListener('click', closeMemberModal);
|
| 1402 |
+
|
| 1403 |
+
elements.addMemberModal.addEventListener('click', (e) => {
|
| 1404 |
+
if (e.target === elements.addMemberModal) closeMemberModal();
|
| 1405 |
+
});
|
| 1406 |
+
|
| 1407 |
+
elements.memberForm.addEventListener('submit', (e) => {
|
| 1408 |
+
e.preventDefault();
|
| 1409 |
+
|
| 1410 |
+
const name = document.getElementById('m-name').value.trim();
|
| 1411 |
+
const ageVal = document.getElementById('m-age').value.trim();
|
| 1412 |
+
const gender = document.getElementById('m-gender').value;
|
| 1413 |
+
const type = document.getElementById('m-risk-type').value;
|
| 1414 |
+
const detail = document.getElementById('m-detail').value.trim();
|
| 1415 |
+
const instruction = document.getElementById('m-instruction').value.trim();
|
| 1416 |
+
|
| 1417 |
+
if (!name || !ageVal || !detail || !instruction) return;
|
| 1418 |
+
|
| 1419 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 1420 |
+
const newId = state.members[listKey].length > 0 ? Math.max(...state.members[listKey].map(m => m.id)) + 1 : 1;
|
| 1421 |
+
|
| 1422 |
+
const memberObj = {
|
| 1423 |
+
id: newId,
|
| 1424 |
+
name: name,
|
| 1425 |
+
age: `${ageVal}세 / ${gender}`,
|
| 1426 |
+
type: type,
|
| 1427 |
+
detail: detail,
|
| 1428 |
+
instruction: instruction,
|
| 1429 |
+
status: type === '일반' ? 'safe' : 'care'
|
| 1430 |
+
};
|
| 1431 |
+
|
| 1432 |
+
state.members[listKey].push(memberObj);
|
| 1433 |
+
saveDatabase(listKey);
|
| 1434 |
+
|
| 1435 |
+
closeMemberModal();
|
| 1436 |
+
elements.memberForm.reset();
|
| 1437 |
+
|
| 1438 |
+
renderDashboard();
|
| 1439 |
+
updateCharts();
|
| 1440 |
+
|
| 1441 |
+
showNotification(`신규 수혜자 ${name}님이 성공적으로 데이터베이스에 추가되었습니다.`);
|
| 1442 |
+
});
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
function closeMemberModal() {
|
| 1446 |
+
elements.addMemberModal.classList.add('hidden');
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
// 10. Chart.js Dashboard and ESG rendering
|
| 1450 |
+
function initCharts() {
|
| 1451 |
+
// Dynamic refresh
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
function updateCharts() {
|
| 1455 |
+
const dashCanvas = document.getElementById('dashboardChart');
|
| 1456 |
+
if (dashCanvas) {
|
| 1457 |
+
if (state.charts.dashboard) state.charts.dashboard.destroy();
|
| 1458 |
+
|
| 1459 |
+
const listKey = state.currentMode === 'family' ? 'family' : state.currentFacility;
|
| 1460 |
+
const list = state.members[listKey];
|
| 1461 |
+
const normalCount = list.filter(m => m.type === '일반').length;
|
| 1462 |
+
const allergyCount = list.filter(m => m.type === '알레르기').length;
|
| 1463 |
+
const diseaseCount = list.filter(m => m.type === '질환식').length;
|
| 1464 |
+
|
| 1465 |
+
Chart.defaults.color = '#94a3b8';
|
| 1466 |
+
Chart.defaults.font.family = 'Outfit';
|
| 1467 |
+
|
| 1468 |
+
state.charts.dashboard = new Chart(dashCanvas, {
|
| 1469 |
+
type: 'doughnut',
|
| 1470 |
+
data: {
|
| 1471 |
+
labels: ['일반 급식군', '식품 알러지군', '만성 질환 식이군'],
|
| 1472 |
+
datasets: [{
|
| 1473 |
+
data: [normalCount, allergyCount, diseaseCount],
|
| 1474 |
+
backgroundColor: ['#10b981', '#f43f5e', '#8b5cf6'],
|
| 1475 |
+
borderWidth: 1,
|
| 1476 |
+
borderColor: 'rgba(255, 255, 255, 0.1)'
|
| 1477 |
+
}]
|
| 1478 |
+
},
|
| 1479 |
+
options: {
|
| 1480 |
+
responsive: true,
|
| 1481 |
+
maintainAspectRatio: false,
|
| 1482 |
+
plugins: {
|
| 1483 |
+
legend: {
|
| 1484 |
+
position: 'bottom',
|
| 1485 |
+
labels: {
|
| 1486 |
+
boxWidth: 12,
|
| 1487 |
+
padding: 10,
|
| 1488 |
+
font: { size: 11 }
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
},
|
| 1492 |
+
cutout: '65%'
|
| 1493 |
+
}
|
| 1494 |
+
});
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
const esgCanvas = document.getElementById('esgChart');
|
| 1498 |
+
if (esgCanvas) {
|
| 1499 |
+
if (state.charts.esg) state.charts.esg.destroy();
|
| 1500 |
+
|
| 1501 |
+
const lineLabel1 = state.currentMode === 'family' ? '자율 관리 환원 시간(H)' : '돌봄 환원 시간(H)';
|
| 1502 |
+
const lineLabel2 = state.currentMode === 'family' ? '가정 탄소 감축량(kg)' : '누적 탄소 절감(kg)';
|
| 1503 |
+
|
| 1504 |
+
state.charts.esg = new Chart(esgCanvas, {
|
| 1505 |
+
type: 'line',
|
| 1506 |
+
data: {
|
| 1507 |
+
labels: ['1월', '2월', '3월', '4월', '5월', '6월(예정)'],
|
| 1508 |
+
datasets: [
|
| 1509 |
+
{
|
| 1510 |
+
label: lineLabel1,
|
| 1511 |
+
data: [20, 35, 52, 68, 84, 105],
|
| 1512 |
+
borderColor: '#3b82f6',
|
| 1513 |
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
| 1514 |
+
tension: 0.4,
|
| 1515 |
+
fill: true
|
| 1516 |
+
},
|
| 1517 |
+
{
|
| 1518 |
+
label: lineLabel2,
|
| 1519 |
+
data: [50, 95, 140, 190, 240, 310],
|
| 1520 |
+
borderColor: '#10b981',
|
| 1521 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 1522 |
+
tension: 0.4,
|
| 1523 |
+
fill: true
|
| 1524 |
+
}
|
| 1525 |
+
]
|
| 1526 |
+
},
|
| 1527 |
+
options: {
|
| 1528 |
+
responsive: true,
|
| 1529 |
+
maintainAspectRatio: false,
|
| 1530 |
+
plugins: {
|
| 1531 |
+
legend: {
|
| 1532 |
+
position: 'top',
|
| 1533 |
+
labels: {
|
| 1534 |
+
boxWidth: 10,
|
| 1535 |
+
font: { size: 10 }
|
| 1536 |
+
}
|
| 1537 |
+
}
|
| 1538 |
+
},
|
| 1539 |
+
scales: {
|
| 1540 |
+
x: { grid: { display: false } },
|
| 1541 |
+
y: { grid: { color: 'rgba(255, 255, 255, 0.05)' } }
|
| 1542 |
+
}
|
| 1543 |
+
}
|
| 1544 |
+
});
|
| 1545 |
+
}
|
| 1546 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>CareTable - 취약계층 급식 위험 관리 및 평가 증빙 자동화 플랫폼</title>
|
| 7 |
+
<!-- Google Fonts -->
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 11 |
+
<!-- FontAwesome Icons -->
|
| 12 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 13 |
+
<!-- Chart.js CDN -->
|
| 14 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 15 |
+
<link rel="stylesheet" href="styles.css">
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<div class="app-container">
|
| 19 |
+
<!-- Sidebar Navigation -->
|
| 20 |
+
<aside class="sidebar">
|
| 21 |
+
<div class="sidebar-header">
|
| 22 |
+
<div class="logo-area">
|
| 23 |
+
<i class="fa-solid fa-wand-magic-sparkles logo-icon"></i>
|
| 24 |
+
<span class="logo-text">CareTable <span class="highlight">AI</span></span>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="badge-tag" id="main-badge">사보원 공공데이터 연계</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<nav class="sidebar-nav">
|
| 30 |
+
<button class="nav-btn active" data-tab="dashboard">
|
| 31 |
+
<i class="fa-solid fa-chart-pie"></i>
|
| 32 |
+
<span id="nav-dash-text">종합 대시보드</span>
|
| 33 |
+
</button>
|
| 34 |
+
<button class="nav-btn" data-tab="diet-analyzer">
|
| 35 |
+
<i class="fa-solid fa-calendar-days"></i>
|
| 36 |
+
<span>주간/월간 식단 AI 분석</span>
|
| 37 |
+
</button>
|
| 38 |
+
<button class="nav-btn" data-tab="vlm-scanner">
|
| 39 |
+
<i class="fa-solid fa-camera-retro"></i>
|
| 40 |
+
<span>VLM 배식 검증</span>
|
| 41 |
+
</button>
|
| 42 |
+
<button class="nav-btn" data-tab="report-generator" id="nav-report-btn">
|
| 43 |
+
<i class="fa-solid fa-file-shield"></i>
|
| 44 |
+
<span>평가 증빙 & ESG</span>
|
| 45 |
+
</button>
|
| 46 |
+
</nav>
|
| 47 |
+
|
| 48 |
+
<div class="sidebar-footer">
|
| 49 |
+
<!-- Mode Toggle -->
|
| 50 |
+
<div class="mode-switch-container">
|
| 51 |
+
<span class="mode-label"><i class="fa-solid fa-toggle-on"></i> 사용 모드 전환</span>
|
| 52 |
+
<div class="toggle-switch-wrapper">
|
| 53 |
+
<span class="toggle-mode-text" id="mode-text-left">기관</span>
|
| 54 |
+
<label class="switch">
|
| 55 |
+
<input type="checkbox" id="mode-toggle-checkbox">
|
| 56 |
+
<span class="slider round"></span>
|
| 57 |
+
</label>
|
| 58 |
+
<span class="toggle-mode-text" id="mode-text-right">가정</span>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="facility-selector" id="facility-selector-wrapper">
|
| 63 |
+
<label for="facility-select"><i class="fa-solid fa-house-medical"></i> 대상 시설 관리</label>
|
| 64 |
+
<select id="facility-select">
|
| 65 |
+
<option value="child" selected>튼튼어린이집 (보육시설)</option>
|
| 66 |
+
<option value="senior">행복실버요양원 (노인시설)</option>
|
| 67 |
+
</select>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="user-profile">
|
| 71 |
+
<div class="user-avatar" id="user-avatar-initial">복</div>
|
| 72 |
+
<div class="user-info">
|
| 73 |
+
<span class="user-name" id="user-display-name">박아름 사회복지사</span>
|
| 74 |
+
<span class="user-role" id="user-display-role">마포구 시설 운영대표</span>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</aside>
|
| 79 |
+
|
| 80 |
+
<!-- Main Content Area -->
|
| 81 |
+
<main class="main-content">
|
| 82 |
+
<!-- Top GNB -->
|
| 83 |
+
<header class="top-gnb">
|
| 84 |
+
<div class="page-title-area">
|
| 85 |
+
<h1 id="page-main-title">종합 대시보드</h1>
|
| 86 |
+
<p id="page-sub-title">시설 맞춤형 프로파일 및 취약계층 급식 위험 관리 요약</p>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="status-panel">
|
| 89 |
+
<button class="theme-toggle-btn" id="theme-toggle-btn">
|
| 90 |
+
<i class="fa-solid fa-moon"></i> <span id="theme-toggle-text">다크 모드</span>
|
| 91 |
+
</button>
|
| 92 |
+
<button class="config-btn" id="open-api-config-btn">
|
| 93 |
+
<i class="fa-solid fa-gears"></i> <span>오픈라우터 AI 연동</span>
|
| 94 |
+
</button>
|
| 95 |
+
<div class="api-status online" id="api-status-badge">
|
| 96 |
+
<span class="status-dot"></span>
|
| 97 |
+
<span id="api-status-text">AI: 시뮬레이션 모드</span>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="notification-bell">
|
| 100 |
+
<i class="fa-regular fa-bell"></i>
|
| 101 |
+
<span class="noti-count" id="noti-count-badge">2</span>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</header>
|
| 105 |
+
|
| 106 |
+
<!-- Dashboard Tab -->
|
| 107 |
+
<section id="dashboard" class="tab-content active-tab">
|
| 108 |
+
<!-- Info Cards Grid -->
|
| 109 |
+
<div class="stats-grid">
|
| 110 |
+
<div class="stat-card glass-card animate-fade-in">
|
| 111 |
+
<div class="card-icon-wrapper blue">
|
| 112 |
+
<i class="fa-solid fa-users"></i>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="stat-data">
|
| 115 |
+
<span class="stat-label" id="lbl-total-members">관리 대상자 수</span>
|
| 116 |
+
<h3 id="stat-total-members" class="stat-value">42명</h3>
|
| 117 |
+
<span class="stat-trend positive" id="lbl-total-desc"><i class="fa-solid fa-circle-info"></i> 사보원 데이터 기준</span>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="stat-card glass-card animate-fade-in" style="animation-delay: 0.1s;">
|
| 121 |
+
<div class="card-icon-wrapper red">
|
| 122 |
+
<i class="fa-solid fa-triangle-exclamation"></i>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="stat-data">
|
| 125 |
+
<span class="stat-label">위험 관리군 (알레르기/질환)</span>
|
| 126 |
+
<h3 id="stat-risk-members" class="stat-value">5명</h3>
|
| 127 |
+
<span class="stat-trend alert-color"><i class="fa-solid fa-circle-exclamation"></i> 개인별 집중 관리 요망</span>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="stat-card glass-card animate-fade-in" style="animation-delay: 0.2s;">
|
| 131 |
+
<div class="card-icon-wrapper green">
|
| 132 |
+
<i class="fa-solid fa-clock-rotate-left"></i>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="stat-data">
|
| 135 |
+
<span class="stat-label" id="lbl-saved-time">누적 행정 절감 시간</span>
|
| 136 |
+
<h3 id="stat-saved-time" class="stat-value">84시간</h3>
|
| 137 |
+
<span class="stat-trend positive" id="lbl-saved-desc"><i class="fa-solid fa-arrow-trend-up"></i> 돌봄 시간으로 환원됨</span>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
<div class="stat-card glass-card animate-fade-in" style="animation-delay: 0.3s;">
|
| 141 |
+
<div class="card-icon-wrapper yellow">
|
| 142 |
+
<i class="fa-solid fa-shield-halved"></i>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="stat-data">
|
| 145 |
+
<span class="stat-label">급식 안전 무결성 점수</span>
|
| 146 |
+
<h3 id="stat-safety-score" class="stat-value">100%</h3>
|
| 147 |
+
<span class="stat-trend positive"><i class="fa-solid fa-shield-check"></i> 사고 0건 달성</span>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- Middle Content Section -->
|
| 153 |
+
<div class="content-row">
|
| 154 |
+
<!-- Left: Facility Profile & Person management -->
|
| 155 |
+
<div class="panel-col glass-card">
|
| 156 |
+
<div class="panel-header">
|
| 157 |
+
<h2 class="panel-title" id="panel-member-title"><i class="fa-solid fa-id-card"></i> 대상자 집중 관리 명단</h2>
|
| 158 |
+
<button class="add-member-btn" id="add-member-btn"><i class="fa-solid fa-plus"></i> 대상 추가</button>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="table-container">
|
| 161 |
+
<table class="data-table" id="member-table">
|
| 162 |
+
<thead>
|
| 163 |
+
<tr>
|
| 164 |
+
<th>이름</th>
|
| 165 |
+
<th>연령/성별</th>
|
| 166 |
+
<th>위험 요인 (알레르기/질환)</th>
|
| 167 |
+
<th>맞춤 식이 지침</th>
|
| 168 |
+
<th>조작</th>
|
| 169 |
+
</tr>
|
| 170 |
+
</thead>
|
| 171 |
+
<tbody id="member-table-body">
|
| 172 |
+
<!-- Dynamic member data -->
|
| 173 |
+
</tbody>
|
| 174 |
+
</table>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<!-- Right: Quick Analytics -->
|
| 179 |
+
<div class="panel-col glass-card flex-col-35">
|
| 180 |
+
<div class="panel-header">
|
| 181 |
+
<h2 class="panel-title"><i class="fa-solid fa-chart-line"></i> 실시간 통계 분석</h2>
|
| 182 |
+
</div>
|
| 183 |
+
<div class="chart-container">
|
| 184 |
+
<canvas id="dashboardChart"></canvas>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="esg-score-box">
|
| 187 |
+
<div class="esg-title-row">
|
| 188 |
+
<span class="esg-title"><i class="fa-solid fa-leaf text-green"></i> CareTable ESG 혁신 지표</span>
|
| 189 |
+
<span class="esg-badge">A+ 등급</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="esg-progress-bar">
|
| 192 |
+
<div class="esg-progress-fill" style="width: 92%;"></div>
|
| 193 |
+
</div>
|
| 194 |
+
<p class="esg-desc" id="esg-bottom-desc">친환경 저탄소 식단 추천 및 페이퍼리스 행정 자동화를 통한 탄소 발자국 240kg CO2e 감축 달성</p>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
</section>
|
| 199 |
+
|
| 200 |
+
<!-- Diet Analyzer Tab -->
|
| 201 |
+
<section id="diet-analyzer" class="tab-content">
|
| 202 |
+
<div class="vertical-layout-wrapper">
|
| 203 |
+
<!-- Input Section -->
|
| 204 |
+
<div class="glass-card full-width-panel margin-bottom-24">
|
| 205 |
+
<div class="panel-header">
|
| 206 |
+
<h2 class="panel-title"><i class="fa-solid fa-calendar-plus"></i> 주간/월간 식단표 입력 (대량 식단 스캔)</h2>
|
| 207 |
+
<div class="quick-examples">
|
| 208 |
+
<span class="example-tag active" data-period="weekly">1주일 식단 템플릿</span>
|
| 209 |
+
<span class="example-tag" data-period="monthly">1개월 식단 템플릿</span>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="diet-input-area">
|
| 213 |
+
<label class="form-label">날짜(또는 요일)와 식단 내용을 입력하세요. 식재료는 쉼표로, 일자는 줄바꿈으로 구분합니다:</label>
|
| 214 |
+
<textarea id="diet-bulk-input" class="diet-textarea large-textarea" placeholder="[월] 쌀밥, 계란말이, 어묵볶음, 요구르트 [화] 잡곡밥, 된장국, 불고기, 고구마순나물, 생선구이 [수] 잡곡밥, 닭볶음탕, 오징어채무침, 땅콩조림, 우유"></textarea>
|
| 215 |
+
<div class="btn-row justify-between">
|
| 216 |
+
<div class="model-status-info">
|
| 217 |
+
<i class="fa-solid fa-robot"></i> <span id="analyzer-model-indicator">DeepSeek V4 Flash (무료) 모델 우선 작동</span>
|
| 218 |
+
</div>
|
| 219 |
+
<button id="analyze-diet-btn" class="primary-btn"><i class="fa-solid fa-wand-magic-sparkles"></i> AI 전체 식단 안전성 정밀 검사</button>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<!-- Analysis Loading State (hidden by default) -->
|
| 224 |
+
<div id="analyzer-loading" class="analyzer-loading hidden">
|
| 225 |
+
<div class="spinner"></div>
|
| 226 |
+
<p id="loading-text">OpenRouter AI 모델에 통신 중입니다. 실시간 식자재 영양 분석 및 위험 요인 매칭 중...</p>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<!-- Output Schedule View (hidden by default, shown after analysis) -->
|
| 231 |
+
<div id="schedule-result-view" class="hidden animate-fade-in">
|
| 232 |
+
<div class="result-header-summary glass-card margin-bottom-24">
|
| 233 |
+
<div class="summary-status-badge alert-red" id="schedule-status-banner">
|
| 234 |
+
<i class="fa-solid fa-circle-exclamation text-red animate-bounce"></i>
|
| 235 |
+
<div>
|
| 236 |
+
<h4 id="schedule-summary-title">위험 식단 감지됨</h4>
|
| 237 |
+
<p id="schedule-summary-desc">총 5일의 식단 중 2일의 식단에서 등록 수혜자 알레르기/질환 유발 성분이 발견되어 자동 대체식을 처방했습니다.</p>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
<div class="btn-row">
|
| 241 |
+
<button id="confirm-all-diet-btn" class="primary-btn"><i class="fa-solid fa-circle-check"></i> 전체 식단 확정 및 VLM 배식 검증 대기</button>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<!-- Calendar / Timelime Cards Grid -->
|
| 246 |
+
<div class="schedule-grid" id="schedule-cards-container">
|
| 247 |
+
<!-- Dynamic Cards generated via JS -->
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<!-- Empty State for results -->
|
| 252 |
+
<div id="schedule-empty-view" class="glass-card empty-state-view">
|
| 253 |
+
<i class="fa-solid fa-magnifying-glass-chart"></i>
|
| 254 |
+
<h3>식단 분석 대기 중</h3>
|
| 255 |
+
<p>위 텍스트 영역에 주간/월간 식단표를 넣고 분석 버튼을 누르면, 일자별 대체 식단표와 AI 안전 조리 지침이 타임라인 카드 뷰로 보기 좋게 자동 생성됩니다.</p>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</section>
|
| 259 |
+
|
| 260 |
+
<!-- VLM Verification Tab -->
|
| 261 |
+
<section id="vlm-scanner" class="tab-content">
|
| 262 |
+
<div class="content-row">
|
| 263 |
+
<!-- Scanner camera panel -->
|
| 264 |
+
<div class="panel-col glass-card">
|
| 265 |
+
<div class="panel-header">
|
| 266 |
+
<h2 class="panel-title"><i class="fa-solid fa-camera"></i> VLM(Vision-Language Model) 실시간 배식 모니터</h2>
|
| 267 |
+
<button class="add-member-btn" id="trigger-file-btn"><i class="fa-solid fa-upload"></i> 식판 사진 직접 업로드</button>
|
| 268 |
+
<input type="file" id="vlm-file-input" accept="image/*" class="hidden">
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div class="vlm-monitor-layout">
|
| 272 |
+
<!-- Left: Camera Box -->
|
| 273 |
+
<div class="camera-simulator-box">
|
| 274 |
+
<div class="camera-lens-overlay">
|
| 275 |
+
<div class="lens-crosshair"></div>
|
| 276 |
+
<div class="scanner-laser" id="scanner-laser"></div>
|
| 277 |
+
<div class="camera-badge">CAMERA LIVE</div>
|
| 278 |
+
</div>
|
| 279 |
+
<div class="image-viewport" id="image-viewport">
|
| 280 |
+
<div class="viewport-placeholder">
|
| 281 |
+
<i class="fa-solid fa-image"></i>
|
| 282 |
+
<p>아래에서 시뮬레이션 이미지를 선택하거나 직접 식판 사진을 업로드하세요</p>
|
| 283 |
+
</div>
|
| 284 |
+
<img id="vlm-target-image" src="" alt="식판 사진" class="hidden">
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
<!-- Right: Operations -->
|
| 289 |
+
<div class="vlm-ops-panel">
|
| 290 |
+
<h3>배식 모니터링 식판 이미지 매칭</h3>
|
| 291 |
+
<p class="vlm-desc">검증할 식판 사진을 준비해 주십시오. (성공/실패 데모를 즉시 실행할 수 있습니다.)</p>
|
| 292 |
+
<div class="sim-selector-grid">
|
| 293 |
+
<div class="sim-option-card" id="sim-opt-normal" data-type="normal">
|
| 294 |
+
<div class="sim-img-thumb-container">
|
| 295 |
+
<img src="assets/meal_normal.png" alt="일반 식단 (대체식 미적용)">
|
| 296 |
+
</div>
|
| 297 |
+
<div class="sim-info">
|
| 298 |
+
<span class="sim-title">일반 배식 식판</span>
|
| 299 |
+
<span class="sim-badge danger-badge">달걀말이/요구르트 포함</span>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
<div class="sim-option-card" id="sim-opt-alternative" data-type="alternative">
|
| 303 |
+
<div class="sim-img-thumb-container">
|
| 304 |
+
<img src="assets/meal_alternative.png" alt="대체 식단 (두부, 주스 적용)">
|
| 305 |
+
</div>
|
| 306 |
+
<div class="sim-info">
|
| 307 |
+
<span class="sim-title">대체 배식 식판</span>
|
| 308 |
+
<span class="sim-badge success-badge">두부구이/주스 변경완료</span>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="scan-action-row">
|
| 314 |
+
<button id="run-vlm-btn" class="primary-btn large-btn" disabled>
|
| 315 |
+
<i class="fa-solid fa-cube"></i> VLM 배식 사진 분석 & 검증 시작
|
| 316 |
+
</button>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<!-- VLM Verification Results -->
|
| 323 |
+
<div class="panel-col glass-card flex-col-40">
|
| 324 |
+
<div class="panel-header">
|
| 325 |
+
<h2 class="panel-title"><i class="fa-solid fa-circle-info"></i> AI Vision 매칭 및 실시간 분석 결과</h2>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<div id="vlm-empty-result" class="empty-state-view">
|
| 329 |
+
<i class="fa-solid fa-magnifying-glass-chart"></i>
|
| 330 |
+
<p>식판 사진을 선택하고 VLM 분석을 시작하면 배식 식단 매칭성, 유해 알레르기 물질 탐지, 배식 가부 판정 결과가 표시됩니다.</p>
|
| 331 |
+
</div>
|
| 332 |
+
|
| 333 |
+
<!-- Real VLM result panel (hidden by default) -->
|
| 334 |
+
<div id="vlm-result-panel" class="vlm-result-panel hidden animate-fade-in">
|
| 335 |
+
<div class="vlm-verdict-box" id="vlm-verdict-box">
|
| 336 |
+
<div class="verdict-icon" id="verdict-icon-container">
|
| 337 |
+
<!-- Dynamic verdict icon -->
|
| 338 |
+
</div>
|
| 339 |
+
<div class="verdict-text-area">
|
| 340 |
+
<span class="verdict-lbl">VLM 배식 검증 결과</span>
|
| 341 |
+
<h3 id="verdict-title">배식 보류 (REJECTED)</h3>
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
|
| 345 |
+
<div class="matching-details-list">
|
| 346 |
+
<div class="detail-item">
|
| 347 |
+
<span class="detail-label">식단 일치율 (Menu Matching)</span>
|
| 348 |
+
<div class="bar-progress-container">
|
| 349 |
+
<div class="bar-fill" id="vlm-match-rate" style="width: 80%;"></div>
|
| 350 |
+
</div>
|
| 351 |
+
<span class="detail-value" id="vlm-match-value">80%</span>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="detail-item">
|
| 354 |
+
<span class="detail-label">유해/금지 성분 유무 (Allergens)</span>
|
| 355 |
+
<span class="detail-value text-red" id="vlm-allergen-status">달걀, 우유 검출</span>
|
| 356 |
+
</div>
|
| 357 |
+
<div class="detail-item">
|
| 358 |
+
<span class="detail-label">배식 정량 및 형태</span>
|
| 359 |
+
<span class="detail-value" id="vlm-portion-status">정량 배식 (96%)</span>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<div class="vlm-reason-box">
|
| 364 |
+
<h4><i class="fa-solid fa-comment-dots"></i> AI Vision 분석 사유</h4>
|
| 365 |
+
<p id="vlm-analysis-reason">카메라 사진 분석 결과, 10시 방향에 계란말이 및 우측 하단에 우유가 포함된 요구르트병이 그대로 배치되어 있어 등록된 아동 '김민수'의 식판으로 적합하지 않습니다. 대체식(두부구이, 주스)으로의 교체를 요구하십시오.</p>
|
| 366 |
+
</div>
|
| 367 |
+
|
| 368 |
+
<div class="btn-row" id="vlm-action-footer">
|
| 369 |
+
<!-- dynamic pass / reject buttons -->
|
| 370 |
+
</div>
|
| 371 |
+
</div>
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
</section>
|
| 375 |
+
|
| 376 |
+
<!-- Report and ESG Tab -->
|
| 377 |
+
<section id="report-generator" class="tab-content">
|
| 378 |
+
<div class="content-row">
|
| 379 |
+
<!-- Left: Report preview list -->
|
| 380 |
+
<div class="panel-col glass-card flex-col-65">
|
| 381 |
+
<div class="panel-header">
|
| 382 |
+
<h2 class="panel-title"><i class="fa-solid fa-file-invoice"></i> 사회보장/가족 건강 데이터 증빙 센터</h2>
|
| 383 |
+
<div class="report-tab-selectors">
|
| 384 |
+
<button class="sub-tab-btn active" data-report="급식일지">급식 일지</button>
|
| 385 |
+
<button class="sub-tab-btn" data-report="알레르기대장">집중 케어 대장</button>
|
| 386 |
+
<button class="sub-tab-btn" data-report="대체식대장">대체 급식 관리대장</button>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
+
<!-- Report Render Area -->
|
| 391 |
+
<div class="report-document-container" id="report-print-area">
|
| 392 |
+
<div class="report-paper">
|
| 393 |
+
<!-- Dynamic Document Header -->
|
| 394 |
+
<div class="report-paper-header">
|
| 395 |
+
<h1 id="report-doc-title">급 식 일 지</h1>
|
| 396 |
+
<div class="report-meta-grid">
|
| 397 |
+
<div class="meta-cell"><strong>구분</strong>: <span id="r-facility-name">튼튼어린이집</span></div>
|
| 398 |
+
<div class="meta-cell"><strong>일자</strong>: <span id="r-date">2026년 06월 01일 (월)</span></div>
|
| 399 |
+
<div class="meta-cell"><strong>작성자</strong>: <span id="r-writer">박아름 사회복지사</span></div>
|
| 400 |
+
<div class="meta-cell"><strong>인증코드</strong>: <span>SSIS-2026-N45</span></div>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
|
| 404 |
+
<!-- Dynamic Document Body -->
|
| 405 |
+
<div id="report-doc-content">
|
| 406 |
+
<!-- Inject via JS -->
|
| 407 |
+
</div>
|
| 408 |
+
|
| 409 |
+
<div class="report-paper-footer">
|
| 410 |
+
<div class="sign-area">
|
| 411 |
+
<p id="report-confirm-statement">위와 같이 취약계층 급식 관리 및 대체 배식이 올바르게 수행되었으며, 배식 전 VLM(Vision-Language Model) 검증 절차를 완료하였음을 확인합니다.</p>
|
| 412 |
+
<div class="sign-seal">
|
| 413 |
+
<span id="report-signer-name">확인자: 시설대표 박아름 (서명/인)</span>
|
| 414 |
+
<div class="seal-stamp">CareTable 검증</div>
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
<div class="report-footer-tag" id="report-footer-desc">한국사회보장정보원 사회복지시설평가 증빙 표준 서식 (SSIS 14-2)</div>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
|
| 422 |
+
<div class="btn-row justify-end padding-top-15">
|
| 423 |
+
<button id="print-report-btn" class="primary-btn"><i class="fa-solid fa-print"></i> 공식 인쇄 (PDF/문서 저장)</button>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<!-- Right: ESG Innovation statistics -->
|
| 428 |
+
<div class="panel-col glass-card">
|
| 429 |
+
<div class="panel-header">
|
| 430 |
+
<h2 class="panel-title"><i class="fa-solid fa-leaf"></i> CareTable ESG 혁신 대시보드</h2>
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
<div class="esg-metric-grid">
|
| 434 |
+
<div class="esg-metric-card">
|
| 435 |
+
<div class="esg-val text-green" id="esg-co2">240.5kg</div>
|
| 436 |
+
<div class="esg-lbl">누적 탄소(CO2) 절감량</div>
|
| 437 |
+
<div class="esg-sub">종이 인쇄 감축 및 저탄소 식단 추천 효과</div>
|
| 438 |
+
</div>
|
| 439 |
+
<div class="esg-metric-card">
|
| 440 |
+
<div class="esg-val text-blue" id="esg-time">84.2시간</div>
|
| 441 |
+
<div class="esg-lbl" id="esg-saved-label">행정 시간 ➔ 직접 돌봄 시간 환원</div>
|
| 442 |
+
<div class="esg-sub" id="esg-saved-sub">시설 평가 문서 자동화에 따른 절감</div>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="esg-metric-card">
|
| 445 |
+
<div class="esg-val text-yellow" id="esg-local">45%</div>
|
| 446 |
+
<div class="esg-lbl">지역 농가 상생 재료 비중</div>
|
| 447 |
+
<div class="esg-sub">지자체 및 인근 로컬 푸드 매칭율</div>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
|
| 451 |
+
<div class="esg-chart-box">
|
| 452 |
+
<canvas id="esgChart"></canvas>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
</section>
|
| 457 |
+
</main>
|
| 458 |
+
</div>
|
| 459 |
+
|
| 460 |
+
<!-- Modals -->
|
| 461 |
+
<!-- OpenRouter API Config Modal -->
|
| 462 |
+
<div id="api-config-modal" class="modal hidden">
|
| 463 |
+
<div class="modal-content glass-card">
|
| 464 |
+
<div class="modal-header">
|
| 465 |
+
<h3><i class="fa-solid fa-robot"></i> OpenRouter AI 모델 설정</h3>
|
| 466 |
+
<span class="close-modal-btn" id="close-api-modal-btn">×</span>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="modal-body">
|
| 469 |
+
<form id="api-config-form">
|
| 470 |
+
<p class="modal-info-text">OpenRouter 무료 API Key를 등록하여 실시간 AI 위험 감지 및 맞춤 대체 레시피 분석을 구동할 수 있습니다. 키가 없어도 시뮬레이션 모드로 작동합니다.</p>
|
| 471 |
+
<div class="form-group">
|
| 472 |
+
<label for="api-key-input">OpenRouter API Key (OPENROUTER_API)</label>
|
| 473 |
+
<div class="password-input-wrapper">
|
| 474 |
+
<input type="password" id="api-key-input" placeholder="sk-or-v1-...">
|
| 475 |
+
<button type="button" id="toggle-key-visibility-btn"><i class="fa-solid fa-eye"></i></button>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
<div class="form-group">
|
| 479 |
+
<label for="api-model-select">사용할 AI 모델 (무료 모델)</label>
|
| 480 |
+
<select id="api-model-select">
|
| 481 |
+
<option value="deepseek/deepseek-v4-flash:free" selected>DeepSeek V4 Flash (무료)</option>
|
| 482 |
+
<option value="qwen/qwen3-next-80b-a3b-instruct:free">Qwen 3 Next 80B (무료)</option>
|
| 483 |
+
<option value="meta-llama/llama-3-8b-instruct:free">Llama 3 8B Instruct (무료)</option>
|
| 484 |
+
<option value="meta-llama/llama-3.3-70b-instruct:free">Llama 3.3 70B Instruct (무료)</option>
|
| 485 |
+
<option value="google/gemma-4-26b-a4b:free">Gemma 4 26B A4B (무료)</option>
|
| 486 |
+
<option value="nvidia/llama-3.1-nemotron-70b-instruct:free">Llama 3.1 Nemotron 70B (무료)</option>
|
| 487 |
+
</select>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="btn-row justify-end">
|
| 490 |
+
<button type="button" class="secondary-btn" id="reset-api-key-btn">키 삭제</button>
|
| 491 |
+
<button type="submit" class="primary-btn">연동 완료</button>
|
| 492 |
+
</div>
|
| 493 |
+
</form>
|
| 494 |
+
</div>
|
| 495 |
+
</div>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<!-- Add Member Modal -->
|
| 499 |
+
<div id="add-member-modal" class="modal hidden">
|
| 500 |
+
<div class="modal-content glass-card">
|
| 501 |
+
<div class="modal-header">
|
| 502 |
+
<h3 id="modal-member-title"><i class="fa-solid fa-user-plus"></i> 신규 관리 대상자 등록</h3>
|
| 503 |
+
<span class="close-modal-btn" id="close-member-modal-btn">×</span>
|
| 504 |
+
</div>
|
| 505 |
+
<div class="modal-body">
|
| 506 |
+
<form id="member-form">
|
| 507 |
+
<div class="form-group">
|
| 508 |
+
<label for="m-name">이름 (또는 별칭)</label>
|
| 509 |
+
<input type="text" id="m-name" required placeholder="예: 김동현">
|
| 510 |
+
</div>
|
| 511 |
+
<div class="form-group">
|
| 512 |
+
<label for="m-age">나이 (세)</label>
|
| 513 |
+
<input type="number" id="m-age" required placeholder="예: 6">
|
| 514 |
+
</div>
|
| 515 |
+
<div class="form-group">
|
| 516 |
+
<label for="m-gender">성별</label>
|
| 517 |
+
<select id="m-gender">
|
| 518 |
+
<option value="남">남성</option>
|
| 519 |
+
<option value="여">여성</option>
|
| 520 |
+
</select>
|
| 521 |
+
</div>
|
| 522 |
+
<div class="form-group">
|
| 523 |
+
<label for="m-risk-type">위험 분류</label>
|
| 524 |
+
<select id="m-risk-type">
|
| 525 |
+
<option value="알레르기">알레르기 (식품성)</option>
|
| 526 |
+
<option value="질환식">질환군 식이 (당뇨, 연하장애, 고혈압 등)</option>
|
| 527 |
+
<option value="일반">해당 없음 (일반 급식군)</option>
|
| 528 |
+
</select>
|
| 529 |
+
</div>
|
| 530 |
+
<div class="form-group">
|
| 531 |
+
<label for="m-detail">상세 요인 (식품명 또는 질환명)</label>
|
| 532 |
+
<input type="text" id="m-detail" required placeholder="예: 대두, 땅콩, 당뇨병 2단계, 유당불내증">
|
| 533 |
+
</div>
|
| 534 |
+
<div class="form-group">
|
| 535 |
+
<label for="m-instruction">맞춤 식사 조치 지침</label>
|
| 536 |
+
<textarea id="m-instruction" required placeholder="예: 콩기름/간장 사용 전면 제한, 알레르기 발생 시 대체 급식 및 구급약 준비"></textarea>
|
| 537 |
+
</div>
|
| 538 |
+
<div class="btn-row justify-end">
|
| 539 |
+
<button type="button" class="secondary-btn" id="cancel-member-btn">취소</button>
|
| 540 |
+
<button type="submit" class="primary-btn">등록 완료</button>
|
| 541 |
+
</div>
|
| 542 |
+
</form>
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
|
| 547 |
+
<!-- Script Source -->
|
| 548 |
+
<script src="app.js"></script>
|
| 549 |
+
</body>
|
| 550 |
+
</html>
|
styles.css
ADDED
|
@@ -0,0 +1,2139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CareMeal AI Premium Design Stylesheet */
|
| 2 |
+
|
| 3 |
+
/* Light Theme Variables (Default) */
|
| 4 |
+
:root {
|
| 5 |
+
--bg-main: #f3f4f6;
|
| 6 |
+
--bg-side: #ffffff;
|
| 7 |
+
--bg-card: rgba(255, 255, 255, 0.8);
|
| 8 |
+
--border-color: rgba(0, 0, 0, 0.08);
|
| 9 |
+
--border-color-active: rgba(59, 130, 246, 0.3);
|
| 10 |
+
|
| 11 |
+
--text-primary: #111827;
|
| 12 |
+
--text-secondary: #4b5563;
|
| 13 |
+
--text-muted: #9ca3af;
|
| 14 |
+
|
| 15 |
+
--color-blue: #2563eb;
|
| 16 |
+
--color-cyan: #0891b2;
|
| 17 |
+
--color-green: #059669;
|
| 18 |
+
--color-red: #dc2626;
|
| 19 |
+
--color-yellow: #d97706;
|
| 20 |
+
--color-purple: #7c3aed;
|
| 21 |
+
|
| 22 |
+
--gradient-primary: linear-gradient(135deg, #2563eb 0%, #0891b2 100%);
|
| 23 |
+
--gradient-success: linear-gradient(135deg, #059669 0%, #047857 100%);
|
| 24 |
+
--gradient-danger: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
|
| 25 |
+
--gradient-warning: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
| 26 |
+
|
| 27 |
+
--font-outfit: 'Outfit', 'Inter', 'Noto Sans KR', sans-serif;
|
| 28 |
+
--card-shadow: 0 10px 30px 0 rgba(31, 41, 55, 0.08);
|
| 29 |
+
--card-blur: blur(12px);
|
| 30 |
+
--transition-speed: 0.3s;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Dark Theme Variables (Activated via body.dark-mode) */
|
| 34 |
+
body.dark-mode {
|
| 35 |
+
--bg-main: #0a0d16;
|
| 36 |
+
--bg-side: #0f1322;
|
| 37 |
+
--bg-card: rgba(22, 28, 48, 0.6);
|
| 38 |
+
--border-color: rgba(255, 255, 255, 0.08);
|
| 39 |
+
--border-color-active: rgba(59, 130, 246, 0.4);
|
| 40 |
+
|
| 41 |
+
--text-primary: #f8fafc;
|
| 42 |
+
--text-secondary: #94a3b8;
|
| 43 |
+
--text-muted: #64748b;
|
| 44 |
+
|
| 45 |
+
--color-blue: #3b82f6;
|
| 46 |
+
--color-cyan: #06b6d4;
|
| 47 |
+
--color-green: #10b981;
|
| 48 |
+
--color-red: #f43f5e;
|
| 49 |
+
--color-yellow: #fbbf24;
|
| 50 |
+
--color-purple: #8b5cf6;
|
| 51 |
+
|
| 52 |
+
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%);
|
| 53 |
+
--gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
| 54 |
+
--gradient-danger: linear-gradient(135deg, #f43f5e 0%, #be123c 100%);
|
| 55 |
+
--gradient-warning: linear-gradient(135deg, #fbbf24 0%, #d97706 100%);
|
| 56 |
+
|
| 57 |
+
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* Reset & Global styles */
|
| 61 |
+
* {
|
| 62 |
+
margin: 0;
|
| 63 |
+
padding: 0;
|
| 64 |
+
box-sizing: border-box;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
body {
|
| 68 |
+
background-color: var(--bg-main);
|
| 69 |
+
color: var(--text-primary);
|
| 70 |
+
font-family: var(--font-outfit);
|
| 71 |
+
overflow-x: hidden;
|
| 72 |
+
height: 100vh;
|
| 73 |
+
transition: background-color var(--transition-speed), color var(--transition-speed);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* App Container Layout */
|
| 77 |
+
.app-container {
|
| 78 |
+
display: flex;
|
| 79 |
+
width: 100vw;
|
| 80 |
+
height: 100vh;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* Sidebar Styling */
|
| 84 |
+
.sidebar {
|
| 85 |
+
width: 280px;
|
| 86 |
+
background-color: var(--bg-side);
|
| 87 |
+
border-right: 1px solid var(--border-color);
|
| 88 |
+
display: flex;
|
| 89 |
+
flex-direction: column;
|
| 90 |
+
padding: 24px;
|
| 91 |
+
z-index: 10;
|
| 92 |
+
transition: background-color var(--transition-speed), border-color var(--transition-speed);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.sidebar-header {
|
| 96 |
+
margin-bottom: 30px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.logo-area {
|
| 100 |
+
display: flex;
|
| 101 |
+
align-items: center;
|
| 102 |
+
gap: 12px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.logo-icon {
|
| 106 |
+
font-size: 26px;
|
| 107 |
+
background: var(--gradient-primary);
|
| 108 |
+
-webkit-background-clip: text;
|
| 109 |
+
-webkit-text-fill-color: transparent;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.logo-text {
|
| 113 |
+
font-size: 22px;
|
| 114 |
+
font-weight: 800;
|
| 115 |
+
letter-spacing: -0.5px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.logo-text .highlight {
|
| 119 |
+
color: var(--color-cyan);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.badge-tag {
|
| 123 |
+
display: inline-block;
|
| 124 |
+
margin-top: 8px;
|
| 125 |
+
font-size: 11px;
|
| 126 |
+
font-weight: 600;
|
| 127 |
+
background: rgba(37, 99, 235, 0.08);
|
| 128 |
+
color: var(--color-blue);
|
| 129 |
+
padding: 3px 8px;
|
| 130 |
+
border-radius: 20px;
|
| 131 |
+
border: 1px solid rgba(37, 99, 235, 0.2);
|
| 132 |
+
text-align: center;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.sidebar-nav {
|
| 136 |
+
display: flex;
|
| 137 |
+
flex-direction: column;
|
| 138 |
+
gap: 8px;
|
| 139 |
+
flex-grow: 1;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.nav-btn {
|
| 143 |
+
display: flex;
|
| 144 |
+
align-items: center;
|
| 145 |
+
gap: 16px;
|
| 146 |
+
background: none;
|
| 147 |
+
border: 1px solid transparent;
|
| 148 |
+
color: var(--text-secondary);
|
| 149 |
+
padding: 14px 18px;
|
| 150 |
+
border-radius: 12px;
|
| 151 |
+
cursor: pointer;
|
| 152 |
+
font-family: var(--font-outfit);
|
| 153 |
+
font-size: 15px;
|
| 154 |
+
font-weight: 500;
|
| 155 |
+
text-align: left;
|
| 156 |
+
transition: all var(--transition-speed) ease;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.nav-btn i {
|
| 160 |
+
font-size: 18px;
|
| 161 |
+
transition: transform 0.2s ease;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.nav-btn:hover {
|
| 165 |
+
background-color: rgba(0, 0, 0, 0.03);
|
| 166 |
+
color: var(--text-primary);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
body.dark-mode .nav-btn:hover {
|
| 170 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.nav-btn.active {
|
| 174 |
+
background-color: rgba(37, 99, 235, 0.08);
|
| 175 |
+
color: var(--color-blue);
|
| 176 |
+
border-color: rgba(37, 99, 235, 0.15);
|
| 177 |
+
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.05);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
body.dark-mode .nav-btn.active {
|
| 181 |
+
background-color: rgba(59, 130, 246, 0.1);
|
| 182 |
+
color: var(--text-primary);
|
| 183 |
+
border-color: rgba(59, 130, 246, 0.25);
|
| 184 |
+
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.nav-btn.active i {
|
| 188 |
+
color: var(--color-cyan);
|
| 189 |
+
transform: scale(1.1);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.sidebar-footer {
|
| 193 |
+
border-top: 1px solid var(--border-color);
|
| 194 |
+
padding-top: 15px;
|
| 195 |
+
display: flex;
|
| 196 |
+
flex-direction: column;
|
| 197 |
+
gap: 15px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Toggle Switch Styles */
|
| 201 |
+
.mode-switch-container {
|
| 202 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 203 |
+
border: 1px solid var(--border-color);
|
| 204 |
+
border-radius: 10px;
|
| 205 |
+
padding: 12px;
|
| 206 |
+
display: flex;
|
| 207 |
+
flex-direction: column;
|
| 208 |
+
gap: 8px;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
body.dark-mode .mode-switch-container {
|
| 212 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.mode-label {
|
| 216 |
+
font-size: 11px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-weight: 600;
|
| 219 |
+
display: flex;
|
| 220 |
+
align-items: center;
|
| 221 |
+
gap: 6px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.toggle-switch-wrapper {
|
| 225 |
+
display: flex;
|
| 226 |
+
align-items: center;
|
| 227 |
+
justify-content: space-between;
|
| 228 |
+
gap: 10px;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.toggle-mode-text {
|
| 232 |
+
font-size: 12px;
|
| 233 |
+
font-weight: 600;
|
| 234 |
+
color: var(--text-muted);
|
| 235 |
+
transition: color 0.2s;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.toggle-mode-text.active-mode {
|
| 239 |
+
color: var(--color-cyan);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/* Switch slider */
|
| 243 |
+
.switch {
|
| 244 |
+
position: relative;
|
| 245 |
+
display: inline-block;
|
| 246 |
+
width: 48px;
|
| 247 |
+
height: 24px;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.switch input {
|
| 251 |
+
opacity: 0;
|
| 252 |
+
width: 0;
|
| 253 |
+
height: 0;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.slider {
|
| 257 |
+
position: absolute;
|
| 258 |
+
cursor: pointer;
|
| 259 |
+
top: 0;
|
| 260 |
+
left: 0;
|
| 261 |
+
right: 0;
|
| 262 |
+
bottom: 0;
|
| 263 |
+
background-color: rgba(0, 0, 0, 0.08);
|
| 264 |
+
transition: .4s;
|
| 265 |
+
border: 1px solid var(--border-color);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
body.dark-mode .slider {
|
| 269 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.slider:before {
|
| 273 |
+
position: absolute;
|
| 274 |
+
content: "";
|
| 275 |
+
height: 16px;
|
| 276 |
+
width: 16px;
|
| 277 |
+
left: 3px;
|
| 278 |
+
bottom: 3px;
|
| 279 |
+
background-color: var(--text-secondary);
|
| 280 |
+
transition: .4s;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
input:checked + .slider {
|
| 284 |
+
background-color: rgba(8, 145, 178, 0.15);
|
| 285 |
+
border-color: var(--color-cyan);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
body.dark-mode input:checked + .slider {
|
| 289 |
+
background-color: rgba(6, 182, 212, 0.2);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
input:checked + .slider:before {
|
| 293 |
+
transform: translateX(24px);
|
| 294 |
+
background-color: var(--color-cyan);
|
| 295 |
+
box-shadow: 0 0 6px var(--color-cyan);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.slider.round {
|
| 299 |
+
border-radius: 34px;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.slider.round:before {
|
| 303 |
+
border-radius: 50%;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.facility-selector {
|
| 307 |
+
display: flex;
|
| 308 |
+
flex-direction: column;
|
| 309 |
+
gap: 6px;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.facility-selector label {
|
| 313 |
+
font-size: 11px;
|
| 314 |
+
color: var(--text-secondary);
|
| 315 |
+
font-weight: 500;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.facility-selector select {
|
| 319 |
+
background-color: rgba(0, 0, 0, 0.03);
|
| 320 |
+
border: 1px solid var(--border-color);
|
| 321 |
+
color: var(--text-primary);
|
| 322 |
+
padding: 8px 12px;
|
| 323 |
+
border-radius: 8px;
|
| 324 |
+
font-family: var(--font-outfit);
|
| 325 |
+
font-size: 13px;
|
| 326 |
+
cursor: pointer;
|
| 327 |
+
outline: none;
|
| 328 |
+
transition: border-color 0.2s;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
body.dark-mode .facility-selector select {
|
| 332 |
+
background-color: rgba(255, 255, 255, 0.05);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.facility-selector select:focus {
|
| 336 |
+
border-color: var(--color-blue);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.user-profile {
|
| 340 |
+
display: flex;
|
| 341 |
+
align-items: center;
|
| 342 |
+
gap: 12px;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.user-avatar {
|
| 346 |
+
width: 40px;
|
| 347 |
+
height: 40px;
|
| 348 |
+
border-radius: 50%;
|
| 349 |
+
background: var(--gradient-primary);
|
| 350 |
+
display: flex;
|
| 351 |
+
align-items: center;
|
| 352 |
+
justify-content: center;
|
| 353 |
+
font-weight: 700;
|
| 354 |
+
color: white;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.user-info {
|
| 358 |
+
display: flex;
|
| 359 |
+
flex-direction: column;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.user-name {
|
| 363 |
+
font-size: 14px;
|
| 364 |
+
font-weight: 600;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.user-role {
|
| 368 |
+
font-size: 11px;
|
| 369 |
+
color: var(--text-muted);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/* Main Content Layout */
|
| 373 |
+
.main-content {
|
| 374 |
+
flex-grow: 1;
|
| 375 |
+
display: flex;
|
| 376 |
+
flex-direction: column;
|
| 377 |
+
height: 100vh;
|
| 378 |
+
overflow-y: auto;
|
| 379 |
+
background-image: radial-gradient(circle at top right, rgba(37, 99, 235, 0.05) 0%, rgba(243, 244, 246, 0) 60%);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
body.dark-mode .main-content {
|
| 383 |
+
background-image: radial-gradient(circle at top right, rgba(30, 41, 59, 0.3) 0%, rgba(10, 13, 22, 0) 60%);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/* Top GNB */
|
| 387 |
+
.top-gnb {
|
| 388 |
+
height: 80px;
|
| 389 |
+
min-height: 80px;
|
| 390 |
+
border-bottom: 1px solid var(--border-color);
|
| 391 |
+
display: flex;
|
| 392 |
+
align-items: center;
|
| 393 |
+
justify-content: space-between;
|
| 394 |
+
padding: 0 40px;
|
| 395 |
+
backdrop-filter: blur(10px);
|
| 396 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 397 |
+
position: sticky;
|
| 398 |
+
top: 0;
|
| 399 |
+
z-index: 9;
|
| 400 |
+
transition: background-color var(--transition-speed), border-color var(--transition-speed);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
body.dark-mode .top-gnb {
|
| 404 |
+
background-color: rgba(10, 13, 22, 0.8);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.page-title-area h1 {
|
| 408 |
+
font-size: 22px;
|
| 409 |
+
font-weight: 700;
|
| 410 |
+
letter-spacing: -0.5px;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.page-title-area p {
|
| 414 |
+
font-size: 13px;
|
| 415 |
+
color: var(--text-secondary);
|
| 416 |
+
margin-top: 2px;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.status-panel {
|
| 420 |
+
display: flex;
|
| 421 |
+
align-items: center;
|
| 422 |
+
gap: 16px;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* Theme Toggle GNB */
|
| 426 |
+
.theme-toggle-btn, .config-btn {
|
| 427 |
+
background-color: rgba(0, 0, 0, 0.03);
|
| 428 |
+
color: var(--text-primary);
|
| 429 |
+
border: 1px solid var(--border-color);
|
| 430 |
+
padding: 8px 14px;
|
| 431 |
+
border-radius: 8px;
|
| 432 |
+
font-family: var(--font-outfit);
|
| 433 |
+
font-size: 12px;
|
| 434 |
+
font-weight: 600;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
display: inline-flex;
|
| 437 |
+
align-items: center;
|
| 438 |
+
gap: 8px;
|
| 439 |
+
transition: all 0.2s ease;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
body.dark-mode .theme-toggle-btn, body.dark-mode .config-btn {
|
| 443 |
+
background-color: rgba(255, 255, 255, 0.05);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.theme-toggle-btn:hover, .config-btn:hover {
|
| 447 |
+
background-color: rgba(0, 0, 0, 0.06);
|
| 448 |
+
border-color: var(--color-blue);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
body.dark-mode .theme-toggle-btn:hover, body.dark-mode .config-btn:hover {
|
| 452 |
+
background-color: rgba(255, 255, 255, 0.08);
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.api-status {
|
| 456 |
+
display: flex;
|
| 457 |
+
align-items: center;
|
| 458 |
+
gap: 8px;
|
| 459 |
+
font-size: 12px;
|
| 460 |
+
color: var(--text-secondary);
|
| 461 |
+
background-color: rgba(0, 0, 0, 0.03);
|
| 462 |
+
padding: 6px 12px;
|
| 463 |
+
border-radius: 20px;
|
| 464 |
+
border: 1px solid var(--border-color);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
body.dark-mode .api-status {
|
| 468 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.status-dot {
|
| 472 |
+
width: 8px;
|
| 473 |
+
height: 8px;
|
| 474 |
+
border-radius: 50%;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.api-status.online .status-dot {
|
| 478 |
+
background-color: var(--color-green);
|
| 479 |
+
box-shadow: 0 0 8px var(--color-green);
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.api-status.live-connected .status-dot {
|
| 483 |
+
background-color: var(--color-cyan);
|
| 484 |
+
box-shadow: 0 0 8px var(--color-cyan);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.notification-bell {
|
| 488 |
+
position: relative;
|
| 489 |
+
font-size: 20px;
|
| 490 |
+
color: var(--text-secondary);
|
| 491 |
+
cursor: pointer;
|
| 492 |
+
width: 38px;
|
| 493 |
+
height: 38px;
|
| 494 |
+
display: flex;
|
| 495 |
+
align-items: center;
|
| 496 |
+
justify-content: center;
|
| 497 |
+
border-radius: 50%;
|
| 498 |
+
background-color: rgba(0, 0, 0, 0.03);
|
| 499 |
+
border: 1px solid var(--border-color);
|
| 500 |
+
transition: all 0.2s;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
body.dark-mode .notification-bell {
|
| 504 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.notification-bell:hover {
|
| 508 |
+
color: var(--text-primary);
|
| 509 |
+
background-color: rgba(0, 0, 0, 0.06);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
body.dark-mode .notification-bell:hover {
|
| 513 |
+
background-color: rgba(255, 255, 255, 0.08);
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.noti-count {
|
| 517 |
+
position: absolute;
|
| 518 |
+
top: -2px;
|
| 519 |
+
right: -2px;
|
| 520 |
+
background-color: var(--color-red);
|
| 521 |
+
color: white;
|
| 522 |
+
font-size: 10px;
|
| 523 |
+
font-weight: 700;
|
| 524 |
+
width: 16px;
|
| 525 |
+
height: 16px;
|
| 526 |
+
border-radius: 50%;
|
| 527 |
+
display: flex;
|
| 528 |
+
align-items: center;
|
| 529 |
+
justify-content: center;
|
| 530 |
+
border: 2px solid var(--bg-main);
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
/* Glass Card Styling */
|
| 534 |
+
.glass-card {
|
| 535 |
+
background: var(--bg-card);
|
| 536 |
+
border: 1px solid var(--border-color);
|
| 537 |
+
border-radius: 16px;
|
| 538 |
+
box-shadow: var(--card-shadow);
|
| 539 |
+
backdrop-filter: var(--card-blur);
|
| 540 |
+
-webkit-backdrop-filter: var(--card-blur);
|
| 541 |
+
padding: 24px;
|
| 542 |
+
transition: border-color 0.3s, box-shadow 0.3s, background-color var(--transition-speed);
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.glass-card:hover {
|
| 546 |
+
border-color: rgba(0, 0, 0, 0.12);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
body.dark-mode .glass-card:hover {
|
| 550 |
+
border-color: rgba(255, 255, 255, 0.12);
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
/* Tab Content Visibility */
|
| 554 |
+
.tab-content {
|
| 555 |
+
display: none;
|
| 556 |
+
padding: 30px 40px;
|
| 557 |
+
animation: fadeIn 0.4s ease-out forwards;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.tab-content.active-tab {
|
| 561 |
+
display: block;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
/* 1. Dashboard Tab Styles */
|
| 565 |
+
.stats-grid {
|
| 566 |
+
display: grid;
|
| 567 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 568 |
+
gap: 20px;
|
| 569 |
+
margin-bottom: 24px;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.stat-card {
|
| 573 |
+
display: flex;
|
| 574 |
+
align-items: center;
|
| 575 |
+
gap: 20px;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.card-icon-wrapper {
|
| 579 |
+
width: 54px;
|
| 580 |
+
height: 54px;
|
| 581 |
+
border-radius: 12px;
|
| 582 |
+
display: flex;
|
| 583 |
+
align-items: center;
|
| 584 |
+
justify-content: center;
|
| 585 |
+
font-size: 22px;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.card-icon-wrapper.blue {
|
| 589 |
+
background: rgba(37, 99, 235, 0.08);
|
| 590 |
+
color: var(--color-blue);
|
| 591 |
+
border: 1px solid rgba(37, 99, 235, 0.15);
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
body.dark-mode .card-icon-wrapper.blue {
|
| 595 |
+
background: rgba(59, 130, 246, 0.15);
|
| 596 |
+
border-color: rgba(59, 130, 246, 0.3);
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.card-icon-wrapper.red {
|
| 600 |
+
background: rgba(220, 38, 38, 0.08);
|
| 601 |
+
color: var(--color-red);
|
| 602 |
+
border: 1px solid rgba(220, 38, 38, 0.15);
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
body.dark-mode .card-icon-wrapper.red {
|
| 606 |
+
background: rgba(244, 63, 94, 0.15);
|
| 607 |
+
border-color: rgba(244, 63, 94, 0.3);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.card-icon-wrapper.green {
|
| 611 |
+
background: rgba(5, 150, 105, 0.08);
|
| 612 |
+
color: var(--color-green);
|
| 613 |
+
border: 1px solid rgba(5, 150, 105, 0.15);
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
body.dark-mode .card-icon-wrapper.green {
|
| 617 |
+
background: rgba(16, 185, 129, 0.15);
|
| 618 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.card-icon-wrapper.yellow {
|
| 622 |
+
background: rgba(217, 119, 6, 0.08);
|
| 623 |
+
color: var(--color-yellow);
|
| 624 |
+
border: 1px solid rgba(217, 119, 6, 0.15);
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
body.dark-mode .card-icon-wrapper.yellow {
|
| 628 |
+
background: rgba(251, 191, 36, 0.15);
|
| 629 |
+
border-color: rgba(251, 191, 36, 0.3);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.stat-data {
|
| 633 |
+
display: flex;
|
| 634 |
+
flex-direction: column;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.stat-label {
|
| 638 |
+
font-size: 13px;
|
| 639 |
+
color: var(--text-secondary);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.stat-value {
|
| 643 |
+
font-size: 26px;
|
| 644 |
+
font-weight: 700;
|
| 645 |
+
margin: 2px 0;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.stat-trend {
|
| 649 |
+
font-size: 11px;
|
| 650 |
+
display: flex;
|
| 651 |
+
align-items: center;
|
| 652 |
+
gap: 4px;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.stat-trend.positive {
|
| 656 |
+
color: var(--color-green);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.stat-trend.alert-color {
|
| 660 |
+
color: var(--color-red);
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
/* Content Columns & Table */
|
| 664 |
+
.content-row {
|
| 665 |
+
display: flex;
|
| 666 |
+
gap: 24px;
|
| 667 |
+
margin-bottom: 24px;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.panel-col {
|
| 671 |
+
flex: 1;
|
| 672 |
+
display: flex;
|
| 673 |
+
flex-direction: column;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.flex-col-35 {
|
| 677 |
+
flex: 0 0 35%;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.flex-col-40 {
|
| 681 |
+
flex: 0 0 40%;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.flex-col-65 {
|
| 685 |
+
flex: 0 0 65%;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.panel-header {
|
| 689 |
+
display: flex;
|
| 690 |
+
align-items: center;
|
| 691 |
+
justify-content: space-between;
|
| 692 |
+
margin-bottom: 20px;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.panel-title {
|
| 696 |
+
font-size: 16px;
|
| 697 |
+
font-weight: 600;
|
| 698 |
+
display: flex;
|
| 699 |
+
align-items: center;
|
| 700 |
+
gap: 10px;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.panel-title i {
|
| 704 |
+
color: var(--color-cyan);
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* Member Table Design */
|
| 708 |
+
.table-container {
|
| 709 |
+
overflow-x: auto;
|
| 710 |
+
flex-grow: 1;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.data-table {
|
| 714 |
+
width: 100%;
|
| 715 |
+
border-collapse: collapse;
|
| 716 |
+
text-align: left;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.data-table th, .data-table td {
|
| 720 |
+
padding: 14px 16px;
|
| 721 |
+
font-size: 13px;
|
| 722 |
+
border-bottom: 1px solid var(--border-color);
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.data-table th {
|
| 726 |
+
color: var(--text-secondary);
|
| 727 |
+
font-weight: 600;
|
| 728 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
body.dark-mode .data-table th {
|
| 732 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.data-table tbody tr {
|
| 736 |
+
transition: background-color 0.2s;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.data-table tbody tr:hover {
|
| 740 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
body.dark-mode .data-table tbody tr:hover {
|
| 744 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.risk-tag {
|
| 748 |
+
display: inline-block;
|
| 749 |
+
padding: 4px 10px;
|
| 750 |
+
border-radius: 20px;
|
| 751 |
+
font-size: 11px;
|
| 752 |
+
font-weight: 600;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.risk-tag.allergy {
|
| 756 |
+
background-color: rgba(220, 38, 38, 0.06);
|
| 757 |
+
color: #b91c1c;
|
| 758 |
+
border: 1px solid rgba(220, 38, 38, 0.15);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
body.dark-mode .risk-tag.allergy {
|
| 762 |
+
background-color: rgba(244, 63, 94, 0.12);
|
| 763 |
+
color: #fca5a5;
|
| 764 |
+
border-color: rgba(244, 63, 94, 0.2);
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
.risk-tag.disease {
|
| 768 |
+
background-color: rgba(124, 58, 237, 0.06);
|
| 769 |
+
color: #6d28d9;
|
| 770 |
+
border: 1px solid rgba(124, 58, 237, 0.15);
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
body.dark-mode .risk-tag.disease {
|
| 774 |
+
background-color: rgba(139, 92, 246, 0.12);
|
| 775 |
+
color: #c084fc;
|
| 776 |
+
border-color: rgba(139, 92, 246, 0.2);
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.status-badge {
|
| 780 |
+
display: inline-flex;
|
| 781 |
+
align-items: center;
|
| 782 |
+
gap: 5px;
|
| 783 |
+
font-size: 11px;
|
| 784 |
+
font-weight: 500;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.status-badge::before {
|
| 788 |
+
content: '';
|
| 789 |
+
width: 6px;
|
| 790 |
+
height: 6px;
|
| 791 |
+
border-radius: 50%;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.status-badge.safe {
|
| 795 |
+
color: var(--color-green);
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.status-badge.safe::before {
|
| 799 |
+
background-color: var(--color-green);
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.status-badge.care {
|
| 803 |
+
color: var(--color-yellow);
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.status-badge.care::before {
|
| 807 |
+
background-color: var(--color-yellow);
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
/* Remove/Delete Row icon */
|
| 811 |
+
.delete-member-btn {
|
| 812 |
+
background: none;
|
| 813 |
+
border: none;
|
| 814 |
+
color: var(--text-muted);
|
| 815 |
+
cursor: pointer;
|
| 816 |
+
font-size: 14px;
|
| 817 |
+
transition: color 0.2s;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.delete-member-btn:hover {
|
| 821 |
+
color: var(--color-red);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
/* Add buttons & Common Button Styles */
|
| 825 |
+
.add-member-btn, .primary-btn, .secondary-btn {
|
| 826 |
+
font-family: var(--font-outfit);
|
| 827 |
+
border-radius: 8px;
|
| 828 |
+
font-weight: 600;
|
| 829 |
+
cursor: pointer;
|
| 830 |
+
display: inline-flex;
|
| 831 |
+
align-items: center;
|
| 832 |
+
justify-content: center;
|
| 833 |
+
gap: 8px;
|
| 834 |
+
transition: all 0.2s ease;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.add-member-btn {
|
| 838 |
+
background: none;
|
| 839 |
+
border: 1px dashed var(--color-cyan);
|
| 840 |
+
color: var(--color-cyan);
|
| 841 |
+
padding: 6px 12px;
|
| 842 |
+
font-size: 12px;
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
.add-member-btn:hover {
|
| 846 |
+
background: rgba(8, 145, 178, 0.04);
|
| 847 |
+
transform: translateY(-1px);
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
body.dark-mode .add-member-btn:hover {
|
| 851 |
+
background: rgba(6, 182, 212, 0.08);
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
.primary-btn {
|
| 855 |
+
background: var(--gradient-primary);
|
| 856 |
+
color: white;
|
| 857 |
+
border: none;
|
| 858 |
+
padding: 12px 20px;
|
| 859 |
+
font-size: 14px;
|
| 860 |
+
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.2);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
body.dark-mode .primary-btn {
|
| 864 |
+
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3);
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.primary-btn:hover:not(:disabled) {
|
| 868 |
+
opacity: 0.95;
|
| 869 |
+
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.3);
|
| 870 |
+
transform: translateY(-1px);
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
.primary-btn:disabled {
|
| 874 |
+
background: var(--text-muted);
|
| 875 |
+
opacity: 0.4;
|
| 876 |
+
cursor: not-allowed;
|
| 877 |
+
box-shadow: none;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
.secondary-btn {
|
| 881 |
+
background: rgba(0, 0, 0, 0.03);
|
| 882 |
+
border: 1px solid var(--border-color);
|
| 883 |
+
color: var(--text-primary);
|
| 884 |
+
padding: 12px 20px;
|
| 885 |
+
font-size: 14px;
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
body.dark-mode .secondary-btn {
|
| 889 |
+
background: rgba(255, 255, 255, 0.05);
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
.secondary-btn:hover {
|
| 893 |
+
background: rgba(0, 0, 0, 0.06);
|
| 894 |
+
border-color: rgba(0, 0, 0, 0.15);
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
body.dark-mode .secondary-btn:hover {
|
| 898 |
+
background: rgba(255, 255, 255, 0.08);
|
| 899 |
+
border-color: rgba(255, 255, 255, 0.2);
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
/* Charts and ESG info on Dash */
|
| 903 |
+
.chart-container {
|
| 904 |
+
height: 180px;
|
| 905 |
+
position: relative;
|
| 906 |
+
margin-bottom: 20px;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.esg-score-box {
|
| 910 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 911 |
+
border: 1px solid var(--border-color);
|
| 912 |
+
border-radius: 12px;
|
| 913 |
+
padding: 16px;
|
| 914 |
+
display: flex;
|
| 915 |
+
flex-direction: column;
|
| 916 |
+
gap: 10px;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
body.dark-mode .esg-score-box {
|
| 920 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
.esg-title-row {
|
| 924 |
+
display: flex;
|
| 925 |
+
justify-content: space-between;
|
| 926 |
+
font-size: 13px;
|
| 927 |
+
font-weight: 600;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.esg-title {
|
| 931 |
+
display: flex;
|
| 932 |
+
align-items: center;
|
| 933 |
+
gap: 6px;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
.text-green {
|
| 937 |
+
color: var(--color-green);
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.text-blue {
|
| 941 |
+
color: var(--color-blue);
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.text-yellow {
|
| 945 |
+
color: var(--color-yellow);
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.esg-badge {
|
| 949 |
+
background-color: rgba(5, 150, 105, 0.08);
|
| 950 |
+
color: #047857;
|
| 951 |
+
padding: 2px 8px;
|
| 952 |
+
border-radius: 20px;
|
| 953 |
+
font-size: 11px;
|
| 954 |
+
border: 1px solid rgba(5, 150, 105, 0.2);
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
body.dark-mode .esg-badge {
|
| 958 |
+
background-color: rgba(16, 185, 129, 0.15);
|
| 959 |
+
color: #6ee7b7;
|
| 960 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
.esg-progress-bar {
|
| 964 |
+
width: 100%;
|
| 965 |
+
height: 8px;
|
| 966 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 967 |
+
border-radius: 4px;
|
| 968 |
+
overflow: hidden;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
body.dark-mode .esg-progress-bar {
|
| 972 |
+
background-color: rgba(255, 255, 255, 0.08);
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
.esg-progress-fill {
|
| 976 |
+
height: 100%;
|
| 977 |
+
background: var(--gradient-success);
|
| 978 |
+
border-radius: 4px;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
.esg-desc {
|
| 982 |
+
font-size: 11px;
|
| 983 |
+
color: var(--text-secondary);
|
| 984 |
+
line-height: 1.5;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
/* 2. Diet Analyzer Tab (Weekly/Monthly Schedule layout) */
|
| 988 |
+
.vertical-layout-wrapper {
|
| 989 |
+
display: flex;
|
| 990 |
+
flex-direction: column;
|
| 991 |
+
width: 100%;
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
.full-width-panel {
|
| 995 |
+
width: 100%;
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
.margin-bottom-24 {
|
| 999 |
+
margin-bottom: 24px;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.diet-textarea {
|
| 1003 |
+
width: 100%;
|
| 1004 |
+
height: 110px;
|
| 1005 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 1006 |
+
border: 1px solid var(--border-color);
|
| 1007 |
+
border-radius: 12px;
|
| 1008 |
+
color: var(--text-primary);
|
| 1009 |
+
padding: 16px;
|
| 1010 |
+
font-family: var(--font-outfit);
|
| 1011 |
+
font-size: 14px;
|
| 1012 |
+
line-height: 1.6;
|
| 1013 |
+
outline: none;
|
| 1014 |
+
resize: none;
|
| 1015 |
+
transition: border-color 0.2s, box-shadow 0.2s;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
body.dark-mode .diet-textarea {
|
| 1019 |
+
background-color: rgba(0, 0, 0, 0.2);
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
.diet-textarea.large-textarea {
|
| 1023 |
+
height: 180px;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
.diet-textarea:focus {
|
| 1027 |
+
border-color: var(--color-blue);
|
| 1028 |
+
box-shadow: 0 0 10px rgba(37, 99, 235, 0.1);
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
body.dark-mode .diet-textarea:focus {
|
| 1032 |
+
box-shadow: 0 0 10px rgba(59, 130, 246, 0.15);
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
.justify-between {
|
| 1036 |
+
justify-content: space-between;
|
| 1037 |
+
align-items: center;
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
.model-status-info {
|
| 1041 |
+
font-size: 12px;
|
| 1042 |
+
color: var(--text-secondary);
|
| 1043 |
+
display: flex;
|
| 1044 |
+
align-items: center;
|
| 1045 |
+
gap: 8px;
|
| 1046 |
+
background: rgba(0, 0, 0, 0.02);
|
| 1047 |
+
border: 1px solid var(--border-color);
|
| 1048 |
+
padding: 6px 12px;
|
| 1049 |
+
border-radius: 6px;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
body.dark-mode .model-status-info {
|
| 1053 |
+
background: rgba(255, 255, 255, 0.02);
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
.model-status-info i {
|
| 1057 |
+
color: var(--color-cyan);
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
.example-tag.active {
|
| 1061 |
+
background-color: rgba(8, 145, 178, 0.08);
|
| 1062 |
+
color: var(--text-primary);
|
| 1063 |
+
border-color: rgba(8, 145, 178, 0.25);
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
body.dark-mode .example-tag.active {
|
| 1067 |
+
background-color: rgba(6, 182, 212, 0.1);
|
| 1068 |
+
border-color: rgba(6, 182, 212, 0.3);
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
/* Scheduler Loading View */
|
| 1072 |
+
.analyzer-loading {
|
| 1073 |
+
display: flex;
|
| 1074 |
+
flex-direction: column;
|
| 1075 |
+
align-items: center;
|
| 1076 |
+
gap: 16px;
|
| 1077 |
+
padding: 30px 0;
|
| 1078 |
+
color: var(--text-secondary);
|
| 1079 |
+
font-size: 13px;
|
| 1080 |
+
border-top: 1px solid var(--border-color);
|
| 1081 |
+
margin-top: 20px;
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
.spinner {
|
| 1085 |
+
width: 32px;
|
| 1086 |
+
height: 32px;
|
| 1087 |
+
border: 3px solid rgba(37, 99, 235, 0.15);
|
| 1088 |
+
border-top: 3px solid var(--color-blue);
|
| 1089 |
+
border-radius: 50%;
|
| 1090 |
+
animation: spin 1s linear infinite;
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
/* Results header banner */
|
| 1094 |
+
.result-header-summary {
|
| 1095 |
+
display: flex;
|
| 1096 |
+
justify-content: space-between;
|
| 1097 |
+
align-items: center;
|
| 1098 |
+
gap: 20px;
|
| 1099 |
+
flex-wrap: wrap;
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
.summary-status-badge {
|
| 1103 |
+
display: flex;
|
| 1104 |
+
align-items: flex-start;
|
| 1105 |
+
gap: 16px;
|
| 1106 |
+
padding: 10px;
|
| 1107 |
+
flex: 1;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
.summary-status-badge.alert-red {
|
| 1111 |
+
background: rgba(220, 38, 38, 0.03);
|
| 1112 |
+
border-left: 4px solid var(--color-red);
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
body.dark-mode .summary-status-badge.alert-red {
|
| 1116 |
+
background: rgba(244, 63, 94, 0.05);
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
.summary-status-badge.safe-green {
|
| 1120 |
+
background: rgba(5, 150, 105, 0.03);
|
| 1121 |
+
border-left: 4px solid var(--color-green);
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
body.dark-mode .summary-status-badge.safe-green {
|
| 1125 |
+
background: rgba(16, 185, 129, 0.05);
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
.summary-status-badge i {
|
| 1129 |
+
font-size: 22px;
|
| 1130 |
+
margin-top: 2px;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
.summary-status-badge h4 {
|
| 1134 |
+
font-size: 14px;
|
| 1135 |
+
font-weight: 700;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.summary-status-badge p {
|
| 1139 |
+
font-size: 12px;
|
| 1140 |
+
color: var(--text-secondary);
|
| 1141 |
+
margin-top: 4px;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
/* Schedule Card Grid (Weekly / Monthly cards) */
|
| 1145 |
+
.schedule-grid {
|
| 1146 |
+
display: grid;
|
| 1147 |
+
grid-template-columns: 1fr;
|
| 1148 |
+
gap: 16px;
|
| 1149 |
+
margin-bottom: 30px;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
@media(min-width: 1024px) {
|
| 1153 |
+
.schedule-grid {
|
| 1154 |
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
| 1155 |
+
}
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
.schedule-card {
|
| 1159 |
+
background: rgba(255, 255, 255, 0.6);
|
| 1160 |
+
border: 1px solid var(--border-color);
|
| 1161 |
+
border-radius: 12px;
|
| 1162 |
+
overflow: hidden;
|
| 1163 |
+
transition: all 0.3s ease;
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
body.dark-mode .schedule-card {
|
| 1167 |
+
background: rgba(22, 28, 48, 0.4);
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
.schedule-card.has-risk {
|
| 1171 |
+
border-top: 3px solid var(--color-red);
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
.schedule-card.is-safe {
|
| 1175 |
+
border-top: 3px solid var(--color-green);
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.schedule-card:hover {
|
| 1179 |
+
transform: translateY(-2px);
|
| 1180 |
+
box-shadow: 0 8px 24px rgba(31, 41, 55, 0.08);
|
| 1181 |
+
border-color: rgba(0, 0, 0, 0.12);
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
body.dark-mode .schedule-card:hover {
|
| 1185 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
| 1186 |
+
border-color: rgba(255, 255, 255, 0.15);
|
| 1187 |
+
}
|
| 1188 |
+
|
| 1189 |
+
.card-header-toggle {
|
| 1190 |
+
padding: 18px 20px;
|
| 1191 |
+
display: flex;
|
| 1192 |
+
justify-content: space-between;
|
| 1193 |
+
align-items: center;
|
| 1194 |
+
cursor: pointer;
|
| 1195 |
+
user-select: none;
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
.header-day-info {
|
| 1199 |
+
display: flex;
|
| 1200 |
+
flex-direction: column;
|
| 1201 |
+
gap: 4px;
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
.day-title {
|
| 1205 |
+
font-size: 16px;
|
| 1206 |
+
font-weight: 700;
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.day-date {
|
| 1210 |
+
font-size: 11px;
|
| 1211 |
+
color: var(--text-muted);
|
| 1212 |
+
}
|
| 1213 |
+
|
| 1214 |
+
.status-indicator-box {
|
| 1215 |
+
display: flex;
|
| 1216 |
+
align-items: center;
|
| 1217 |
+
gap: 12px;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
.status-pill {
|
| 1221 |
+
font-size: 10px;
|
| 1222 |
+
font-weight: 700;
|
| 1223 |
+
padding: 2px 8px;
|
| 1224 |
+
border-radius: 4px;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
.status-pill.danger {
|
| 1228 |
+
background-color: rgba(220, 38, 38, 0.08);
|
| 1229 |
+
color: #dc2626;
|
| 1230 |
+
border: 1px solid rgba(220, 38, 38, 0.2);
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
body.dark-mode .status-pill.danger {
|
| 1234 |
+
background-color: rgba(244, 63, 94, 0.15);
|
| 1235 |
+
color: #fca5a5;
|
| 1236 |
+
border-color: rgba(244, 63, 94, 0.3);
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
.status-pill.success {
|
| 1240 |
+
background-color: rgba(5, 150, 105, 0.08);
|
| 1241 |
+
color: #059669;
|
| 1242 |
+
border: 1px solid rgba(5, 150, 105, 0.2);
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
body.dark-mode .status-pill.success {
|
| 1246 |
+
background-color: rgba(16, 185, 129, 0.15);
|
| 1247 |
+
color: #a7f3d0;
|
| 1248 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
.toggle-arrow-icon {
|
| 1252 |
+
color: var(--text-muted);
|
| 1253 |
+
font-size: 14px;
|
| 1254 |
+
transition: transform var(--transition-speed);
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.toggle-arrow-icon.rotate-icon {
|
| 1258 |
+
transform: rotate(180deg);
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
/* Accordion sliding effect */
|
| 1262 |
+
.accordion-content {
|
| 1263 |
+
max-height: 0;
|
| 1264 |
+
overflow: hidden;
|
| 1265 |
+
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
|
| 1266 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
body.dark-mode .accordion-content {
|
| 1270 |
+
background-color: rgba(0, 0, 0, 0.15);
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
.accordion-content.expanded {
|
| 1274 |
+
max-height: 2000px;
|
| 1275 |
+
transition: max-height 0.4s ease-in-out;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
.card-body-details {
|
| 1279 |
+
padding: 20px;
|
| 1280 |
+
border-top: 1px solid var(--border-color);
|
| 1281 |
+
display: flex;
|
| 1282 |
+
flex-direction: column;
|
| 1283 |
+
gap: 16px;
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
.menu-orig-box, .menu-alt-box, .recipe-guide-box {
|
| 1287 |
+
display: flex;
|
| 1288 |
+
flex-direction: column;
|
| 1289 |
+
gap: 6px;
|
| 1290 |
+
}
|
| 1291 |
+
|
| 1292 |
+
.menu-orig-box h5 {
|
| 1293 |
+
font-size: 12px;
|
| 1294 |
+
color: var(--color-red);
|
| 1295 |
+
text-transform: uppercase;
|
| 1296 |
+
letter-spacing: 0.5px;
|
| 1297 |
+
font-weight: 700;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
.menu-alt-box h5 {
|
| 1301 |
+
font-size: 12px;
|
| 1302 |
+
color: var(--color-green);
|
| 1303 |
+
text-transform: uppercase;
|
| 1304 |
+
letter-spacing: 0.5px;
|
| 1305 |
+
font-weight: 700;
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
.recipe-guide-box h5 {
|
| 1309 |
+
font-size: 12px;
|
| 1310 |
+
color: var(--color-yellow);
|
| 1311 |
+
text-transform: uppercase;
|
| 1312 |
+
letter-spacing: 0.5px;
|
| 1313 |
+
font-weight: 700;
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
.menu-content-text {
|
| 1317 |
+
font-size: 13px;
|
| 1318 |
+
line-height: 1.5;
|
| 1319 |
+
color: var(--text-primary);
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
.recipe-content-text {
|
| 1323 |
+
font-size: 12px;
|
| 1324 |
+
line-height: 1.6;
|
| 1325 |
+
color: var(--text-secondary);
|
| 1326 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 1327 |
+
padding: 12px;
|
| 1328 |
+
border-radius: 8px;
|
| 1329 |
+
border: 1px solid var(--border-color);
|
| 1330 |
+
}
|
| 1331 |
+
|
| 1332 |
+
body.dark-mode .recipe-content-text {
|
| 1333 |
+
background-color: rgba(0, 0, 0, 0.2);
|
| 1334 |
+
}
|
| 1335 |
+
|
| 1336 |
+
/* 3. VLM Verification Tab Styles */
|
| 1337 |
+
.vlm-monitor-layout {
|
| 1338 |
+
display: flex;
|
| 1339 |
+
flex-direction: column;
|
| 1340 |
+
gap: 24px;
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
.camera-simulator-box {
|
| 1344 |
+
width: 100%;
|
| 1345 |
+
height: 380px;
|
| 1346 |
+
background-color: #04060b;
|
| 1347 |
+
border-radius: 12px;
|
| 1348 |
+
border: 1px solid var(--border-color);
|
| 1349 |
+
position: relative;
|
| 1350 |
+
overflow: hidden;
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
.camera-lens-overlay {
|
| 1354 |
+
position: absolute;
|
| 1355 |
+
top: 0;
|
| 1356 |
+
left: 0;
|
| 1357 |
+
width: 100%;
|
| 1358 |
+
height: 100%;
|
| 1359 |
+
pointer-events: none;
|
| 1360 |
+
z-index: 2;
|
| 1361 |
+
border: 2px solid rgba(59, 130, 246, 0.15);
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
.lens-crosshair {
|
| 1365 |
+
position: absolute;
|
| 1366 |
+
top: 50%;
|
| 1367 |
+
left: 50%;
|
| 1368 |
+
width: 40px;
|
| 1369 |
+
height: 40px;
|
| 1370 |
+
transform: translate(-50%, -50%);
|
| 1371 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 1372 |
+
border-radius: 50%;
|
| 1373 |
+
}
|
| 1374 |
+
|
| 1375 |
+
.lens-crosshair::before, .lens-crosshair::after {
|
| 1376 |
+
content: '';
|
| 1377 |
+
position: absolute;
|
| 1378 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.lens-crosshair::before {
|
| 1382 |
+
top: 50%;
|
| 1383 |
+
left: -10px;
|
| 1384 |
+
width: 60px;
|
| 1385 |
+
height: 1px;
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
.lens-crosshair::after {
|
| 1389 |
+
left: 50%;
|
| 1390 |
+
top: -10px;
|
| 1391 |
+
width: 1px;
|
| 1392 |
+
height: 60px;
|
| 1393 |
+
}
|
| 1394 |
+
|
| 1395 |
+
.scanner-laser {
|
| 1396 |
+
position: absolute;
|
| 1397 |
+
top: 0;
|
| 1398 |
+
left: 0;
|
| 1399 |
+
width: 100%;
|
| 1400 |
+
height: 3px;
|
| 1401 |
+
background: linear-gradient(90deg, rgba(6, 182, 212, 0) 0%, rgba(6, 182, 212, 0.8) 50%, rgba(6, 182, 212, 0) 100%);
|
| 1402 |
+
box-shadow: 0 0 12px var(--color-cyan);
|
| 1403 |
+
display: none;
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
+
@keyframes scan {
|
| 1407 |
+
0% { top: 0%; }
|
| 1408 |
+
50% { top: 100%; }
|
| 1409 |
+
100% { top: 0%; }
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
.camera-badge {
|
| 1413 |
+
position: absolute;
|
| 1414 |
+
top: 16px;
|
| 1415 |
+
left: 16px;
|
| 1416 |
+
background-color: var(--color-red);
|
| 1417 |
+
color: white;
|
| 1418 |
+
font-size: 10px;
|
| 1419 |
+
font-weight: 700;
|
| 1420 |
+
padding: 3px 8px;
|
| 1421 |
+
border-radius: 4px;
|
| 1422 |
+
letter-spacing: 0.5px;
|
| 1423 |
+
animation: flash 1.5s infinite;
|
| 1424 |
+
}
|
| 1425 |
+
|
| 1426 |
+
.image-viewport {
|
| 1427 |
+
width: 100%;
|
| 1428 |
+
height: 100%;
|
| 1429 |
+
display: flex;
|
| 1430 |
+
align-items: center;
|
| 1431 |
+
justify-content: center;
|
| 1432 |
+
background-color: #020306;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
.viewport-placeholder {
|
| 1436 |
+
display: flex;
|
| 1437 |
+
flex-direction: column;
|
| 1438 |
+
align-items: center;
|
| 1439 |
+
gap: 12px;
|
| 1440 |
+
color: var(--text-muted);
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
.viewport-placeholder i {
|
| 1444 |
+
font-size: 40px;
|
| 1445 |
+
}
|
| 1446 |
+
|
| 1447 |
+
.viewport-placeholder p {
|
| 1448 |
+
font-size: 13px;
|
| 1449 |
+
}
|
| 1450 |
+
|
| 1451 |
+
#vlm-target-image {
|
| 1452 |
+
max-height: 100%;
|
| 1453 |
+
max-width: 100%;
|
| 1454 |
+
object-fit: contain;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
.vlm-ops-panel {
|
| 1458 |
+
display: flex;
|
| 1459 |
+
flex-direction: column;
|
| 1460 |
+
gap: 14px;
|
| 1461 |
+
}
|
| 1462 |
+
|
| 1463 |
+
.vlm-desc {
|
| 1464 |
+
font-size: 12px;
|
| 1465 |
+
color: var(--text-secondary);
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
.sim-selector-grid {
|
| 1469 |
+
display: grid;
|
| 1470 |
+
grid-template-columns: 1fr 1fr;
|
| 1471 |
+
gap: 16px;
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
.sim-option-card {
|
| 1475 |
+
border: 1px solid var(--border-color);
|
| 1476 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 1477 |
+
border-radius: 8px;
|
| 1478 |
+
cursor: pointer;
|
| 1479 |
+
overflow: hidden;
|
| 1480 |
+
transition: all 0.2s ease;
|
| 1481 |
+
}
|
| 1482 |
+
|
| 1483 |
+
body.dark-mode .sim-option-card {
|
| 1484 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
.sim-option-card:hover {
|
| 1488 |
+
border-color: rgba(0, 0, 0, 0.15);
|
| 1489 |
+
background-color: rgba(0, 0, 0, 0.04);
|
| 1490 |
+
}
|
| 1491 |
+
|
| 1492 |
+
body.dark-mode .sim-option-card:hover {
|
| 1493 |
+
border-color: rgba(255, 255, 255, 0.15);
|
| 1494 |
+
background-color: rgba(255, 255, 255, 0.04);
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
.sim-option-card.selected {
|
| 1498 |
+
border-color: var(--color-blue);
|
| 1499 |
+
background-color: rgba(37, 99, 235, 0.03);
|
| 1500 |
+
box-shadow: 0 0 10px rgba(37, 99, 235, 0.05);
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
body.dark-mode .sim-option-card.selected {
|
| 1504 |
+
background-color: rgba(59, 130, 246, 0.05);
|
| 1505 |
+
box-shadow: 0 0 10px rgba(59, 130, 246, 0.1);
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
.sim-img-thumb-container {
|
| 1509 |
+
height: 120px;
|
| 1510 |
+
background-color: black;
|
| 1511 |
+
display: flex;
|
| 1512 |
+
align-items: center;
|
| 1513 |
+
justify-content: center;
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
.sim-img-thumb-container img {
|
| 1517 |
+
max-height: 100%;
|
| 1518 |
+
max-width: 100%;
|
| 1519 |
+
object-fit: cover;
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.sim-info {
|
| 1523 |
+
padding: 10px;
|
| 1524 |
+
display: flex;
|
| 1525 |
+
flex-direction: column;
|
| 1526 |
+
gap: 6px;
|
| 1527 |
+
}
|
| 1528 |
+
|
| 1529 |
+
.sim-title {
|
| 1530 |
+
font-size: 12px;
|
| 1531 |
+
font-weight: 600;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.sim-badge {
|
| 1535 |
+
align-self: flex-start;
|
| 1536 |
+
font-size: 9px;
|
| 1537 |
+
font-weight: 700;
|
| 1538 |
+
padding: 2px 6px;
|
| 1539 |
+
border-radius: 4px;
|
| 1540 |
+
}
|
| 1541 |
+
|
| 1542 |
+
.danger-badge {
|
| 1543 |
+
background-color: rgba(220, 38, 38, 0.08);
|
| 1544 |
+
color: #dc2626;
|
| 1545 |
+
border: 1px solid rgba(220, 38, 38, 0.2);
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
body.dark-mode .danger-badge {
|
| 1549 |
+
background-color: rgba(244, 63, 94, 0.15);
|
| 1550 |
+
color: #fecaca;
|
| 1551 |
+
border-color: rgba(244, 63, 94, 0.3);
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
.success-badge {
|
| 1555 |
+
background-color: rgba(5, 150, 105, 0.08);
|
| 1556 |
+
color: #059669;
|
| 1557 |
+
border: 1px solid rgba(5, 150, 105, 0.2);
|
| 1558 |
+
}
|
| 1559 |
+
|
| 1560 |
+
body.dark-mode .success-badge {
|
| 1561 |
+
background-color: rgba(16, 185, 129, 0.15);
|
| 1562 |
+
color: #d1fae5;
|
| 1563 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
.scan-action-row {
|
| 1567 |
+
margin-top: 10px;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
.large-btn {
|
| 1571 |
+
width: 100%;
|
| 1572 |
+
padding: 14px;
|
| 1573 |
+
font-size: 15px;
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
/* VLM Verdict output panel */
|
| 1577 |
+
.vlm-result-panel {
|
| 1578 |
+
display: flex;
|
| 1579 |
+
flex-direction: column;
|
| 1580 |
+
gap: 20px;
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
.vlm-verdict-box {
|
| 1584 |
+
display: flex;
|
| 1585 |
+
align-items: center;
|
| 1586 |
+
gap: 16px;
|
| 1587 |
+
padding: 18px;
|
| 1588 |
+
border-radius: 12px;
|
| 1589 |
+
color: white;
|
| 1590 |
+
}
|
| 1591 |
+
|
| 1592 |
+
.vlm-verdict-box.rejected {
|
| 1593 |
+
background: var(--gradient-danger);
|
| 1594 |
+
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.2);
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
body.dark-mode .vlm-verdict-box.rejected {
|
| 1598 |
+
box-shadow: 0 4px 14px rgba(244, 63, 94, 0.2);
|
| 1599 |
+
}
|
| 1600 |
+
|
| 1601 |
+
.vlm-verdict-box.passed {
|
| 1602 |
+
background: var(--gradient-success);
|
| 1603 |
+
box-shadow: 0 4px 14px rgba(5, 150, 105, 0.2);
|
| 1604 |
+
}
|
| 1605 |
+
|
| 1606 |
+
body.dark-mode .vlm-verdict-box.passed {
|
| 1607 |
+
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.2);
|
| 1608 |
+
}
|
| 1609 |
+
|
| 1610 |
+
.verdict-icon {
|
| 1611 |
+
font-size: 28px;
|
| 1612 |
+
width: 48px;
|
| 1613 |
+
height: 48px;
|
| 1614 |
+
border-radius: 50%;
|
| 1615 |
+
background-color: rgba(255, 255, 255, 0.15);
|
| 1616 |
+
display: flex;
|
| 1617 |
+
align-items: center;
|
| 1618 |
+
justify-content: center;
|
| 1619 |
+
}
|
| 1620 |
+
|
| 1621 |
+
.verdict-text-area {
|
| 1622 |
+
display: flex;
|
| 1623 |
+
flex-direction: column;
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
.verdict-lbl {
|
| 1627 |
+
font-size: 11px;
|
| 1628 |
+
opacity: 0.8;
|
| 1629 |
+
}
|
| 1630 |
+
|
| 1631 |
+
.verdict-text-area h3 {
|
| 1632 |
+
font-size: 18px;
|
| 1633 |
+
font-weight: 700;
|
| 1634 |
+
}
|
| 1635 |
+
|
| 1636 |
+
.matching-details-list {
|
| 1637 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 1638 |
+
border: 1px solid var(--border-color);
|
| 1639 |
+
border-radius: 12px;
|
| 1640 |
+
padding: 18px;
|
| 1641 |
+
display: flex;
|
| 1642 |
+
flex-direction: column;
|
| 1643 |
+
gap: 14px;
|
| 1644 |
+
}
|
| 1645 |
+
|
| 1646 |
+
body.dark-mode .matching-details-list {
|
| 1647 |
+
background-color: rgba(0, 0, 0, 0.15);
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
.detail-item {
|
| 1651 |
+
display: flex;
|
| 1652 |
+
align-items: center;
|
| 1653 |
+
justify-content: space-between;
|
| 1654 |
+
font-size: 13px;
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
.detail-label {
|
| 1658 |
+
color: var(--text-secondary);
|
| 1659 |
+
}
|
| 1660 |
+
|
| 1661 |
+
.bar-progress-container {
|
| 1662 |
+
width: 80px;
|
| 1663 |
+
height: 6px;
|
| 1664 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 1665 |
+
border-radius: 3px;
|
| 1666 |
+
overflow: hidden;
|
| 1667 |
+
margin: 0 10px;
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
body.dark-mode .bar-progress-container {
|
| 1671 |
+
background-color: rgba(255, 255, 255, 0.08);
|
| 1672 |
+
}
|
| 1673 |
+
|
| 1674 |
+
.bar-fill {
|
| 1675 |
+
height: 100%;
|
| 1676 |
+
background-color: var(--color-blue);
|
| 1677 |
+
border-radius: 3px;
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
.detail-value {
|
| 1681 |
+
font-weight: 600;
|
| 1682 |
+
}
|
| 1683 |
+
|
| 1684 |
+
.vlm-reason-box {
|
| 1685 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 1686 |
+
border: 1px solid var(--border-color);
|
| 1687 |
+
border-radius: 12px;
|
| 1688 |
+
padding: 16px;
|
| 1689 |
+
display: flex;
|
| 1690 |
+
flex-direction: column;
|
| 1691 |
+
gap: 8px;
|
| 1692 |
+
}
|
| 1693 |
+
|
| 1694 |
+
body.dark-mode .vlm-reason-box {
|
| 1695 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 1696 |
+
}
|
| 1697 |
+
|
| 1698 |
+
.vlm-reason-box h4 {
|
| 1699 |
+
font-size: 12px;
|
| 1700 |
+
font-weight: 700;
|
| 1701 |
+
color: var(--text-secondary);
|
| 1702 |
+
display: flex;
|
| 1703 |
+
align-items: center;
|
| 1704 |
+
gap: 6px;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
.vlm-reason-box p {
|
| 1708 |
+
font-size: 12px;
|
| 1709 |
+
line-height: 1.5;
|
| 1710 |
+
color: var(--text-secondary);
|
| 1711 |
+
}
|
| 1712 |
+
|
| 1713 |
+
/* 4. Report and ESG Tab Styles */
|
| 1714 |
+
.report-tab-selectors {
|
| 1715 |
+
display: flex;
|
| 1716 |
+
gap: 6px;
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
.sub-tab-btn {
|
| 1720 |
+
background: rgba(0, 0, 0, 0.02);
|
| 1721 |
+
border: 1px solid var(--border-color);
|
| 1722 |
+
color: var(--text-secondary);
|
| 1723 |
+
padding: 6px 12px;
|
| 1724 |
+
font-size: 12px;
|
| 1725 |
+
border-radius: 6px;
|
| 1726 |
+
cursor: pointer;
|
| 1727 |
+
font-family: var(--font-outfit);
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
body.dark-mode .sub-tab-btn {
|
| 1731 |
+
background: rgba(255, 255, 255, 0.03);
|
| 1732 |
+
}
|
| 1733 |
+
|
| 1734 |
+
.sub-tab-btn:hover {
|
| 1735 |
+
color: var(--text-primary);
|
| 1736 |
+
}
|
| 1737 |
+
|
| 1738 |
+
.sub-tab-btn.active {
|
| 1739 |
+
background: rgba(8, 145, 178, 0.08);
|
| 1740 |
+
color: var(--text-primary);
|
| 1741 |
+
border-color: rgba(8, 145, 178, 0.25);
|
| 1742 |
+
}
|
| 1743 |
+
|
| 1744 |
+
body.dark-mode .sub-tab-btn.active {
|
| 1745 |
+
background: rgba(6, 182, 212, 0.1);
|
| 1746 |
+
border-color: rgba(6, 182, 212, 0.3);
|
| 1747 |
+
}
|
| 1748 |
+
|
| 1749 |
+
.report-document-container {
|
| 1750 |
+
background-color: #f8fafc;
|
| 1751 |
+
border-radius: 12px;
|
| 1752 |
+
padding: 24px;
|
| 1753 |
+
color: #1e293b;
|
| 1754 |
+
box-shadow: 0 10px 25px rgba(31, 41, 55, 0.1);
|
| 1755 |
+
max-height: 520px;
|
| 1756 |
+
overflow-y: auto;
|
| 1757 |
+
}
|
| 1758 |
+
|
| 1759 |
+
body.dark-mode .report-document-container {
|
| 1760 |
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
| 1761 |
+
}
|
| 1762 |
+
|
| 1763 |
+
.report-paper {
|
| 1764 |
+
background-color: white;
|
| 1765 |
+
padding: 30px;
|
| 1766 |
+
border: 1px solid #e2e8f0;
|
| 1767 |
+
border-radius: 4px;
|
| 1768 |
+
font-family: 'Noto Sans KR', sans-serif;
|
| 1769 |
+
}
|
| 1770 |
+
|
| 1771 |
+
.report-paper-header {
|
| 1772 |
+
text-align: center;
|
| 1773 |
+
border-bottom: 2px double #475569;
|
| 1774 |
+
padding-bottom: 16px;
|
| 1775 |
+
margin-bottom: 20px;
|
| 1776 |
+
}
|
| 1777 |
+
|
| 1778 |
+
.report-paper-header h1 {
|
| 1779 |
+
font-size: 20px;
|
| 1780 |
+
font-weight: 700;
|
| 1781 |
+
letter-spacing: 4px;
|
| 1782 |
+
margin-bottom: 14px;
|
| 1783 |
+
color: #0f172a;
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
.report-meta-grid {
|
| 1787 |
+
display: grid;
|
| 1788 |
+
grid-template-columns: 1fr 1fr;
|
| 1789 |
+
text-align: left;
|
| 1790 |
+
font-size: 11px;
|
| 1791 |
+
color: #475569;
|
| 1792 |
+
row-gap: 6px;
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
.report-table {
|
| 1796 |
+
width: 100%;
|
| 1797 |
+
border-collapse: collapse;
|
| 1798 |
+
margin-top: 14px;
|
| 1799 |
+
margin-bottom: 20px;
|
| 1800 |
+
}
|
| 1801 |
+
|
| 1802 |
+
.report-table th, .report-table td {
|
| 1803 |
+
border: 1px solid #cbd5e1;
|
| 1804 |
+
padding: 10px;
|
| 1805 |
+
font-size: 11px;
|
| 1806 |
+
text-align: center;
|
| 1807 |
+
}
|
| 1808 |
+
|
| 1809 |
+
.report-table th {
|
| 1810 |
+
background-color: #f1f5f9;
|
| 1811 |
+
color: #1e293b;
|
| 1812 |
+
font-weight: 700;
|
| 1813 |
+
}
|
| 1814 |
+
|
| 1815 |
+
.report-table td.align-left {
|
| 1816 |
+
text-align: left;
|
| 1817 |
+
}
|
| 1818 |
+
|
| 1819 |
+
.report-paper-body p {
|
| 1820 |
+
font-size: 12px;
|
| 1821 |
+
line-height: 1.6;
|
| 1822 |
+
margin-bottom: 12px;
|
| 1823 |
+
}
|
| 1824 |
+
|
| 1825 |
+
.report-paper-footer {
|
| 1826 |
+
border-top: 1px solid #cbd5e1;
|
| 1827 |
+
padding-top: 16px;
|
| 1828 |
+
margin-top: 30px;
|
| 1829 |
+
font-size: 11px;
|
| 1830 |
+
color: #475569;
|
| 1831 |
+
}
|
| 1832 |
+
|
| 1833 |
+
.sign-area {
|
| 1834 |
+
display: flex;
|
| 1835 |
+
flex-direction: column;
|
| 1836 |
+
gap: 16px;
|
| 1837 |
+
}
|
| 1838 |
+
|
| 1839 |
+
.sign-seal {
|
| 1840 |
+
display: flex;
|
| 1841 |
+
justify-content: flex-end;
|
| 1842 |
+
align-items: center;
|
| 1843 |
+
gap: 12px;
|
| 1844 |
+
font-weight: 700;
|
| 1845 |
+
position: relative;
|
| 1846 |
+
}
|
| 1847 |
+
|
| 1848 |
+
.seal-stamp {
|
| 1849 |
+
width: 48px;
|
| 1850 |
+
height: 48px;
|
| 1851 |
+
border: 2px solid #ef4444;
|
| 1852 |
+
color: #ef4444;
|
| 1853 |
+
border-radius: 50%;
|
| 1854 |
+
font-size: 8px;
|
| 1855 |
+
display: flex;
|
| 1856 |
+
align-items: center;
|
| 1857 |
+
justify-content: center;
|
| 1858 |
+
transform: rotate(-15deg);
|
| 1859 |
+
font-weight: 800;
|
| 1860 |
+
letter-spacing: 0.5px;
|
| 1861 |
+
opacity: 0.8;
|
| 1862 |
+
}
|
| 1863 |
+
|
| 1864 |
+
.report-footer-tag {
|
| 1865 |
+
font-size: 9px;
|
| 1866 |
+
color: #94a3b8;
|
| 1867 |
+
text-align: center;
|
| 1868 |
+
margin-top: 20px;
|
| 1869 |
+
}
|
| 1870 |
+
|
| 1871 |
+
.padding-top-15 {
|
| 1872 |
+
padding-top: 15px;
|
| 1873 |
+
}
|
| 1874 |
+
|
| 1875 |
+
/* ESG metrics style page */
|
| 1876 |
+
.esg-metric-grid {
|
| 1877 |
+
display: grid;
|
| 1878 |
+
grid-template-columns: 1fr;
|
| 1879 |
+
gap: 16px;
|
| 1880 |
+
margin-bottom: 24px;
|
| 1881 |
+
}
|
| 1882 |
+
|
| 1883 |
+
.esg-metric-card {
|
| 1884 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 1885 |
+
border: 1px solid var(--border-color);
|
| 1886 |
+
border-radius: 12px;
|
| 1887 |
+
padding: 18px;
|
| 1888 |
+
display: flex;
|
| 1889 |
+
flex-direction: column;
|
| 1890 |
+
}
|
| 1891 |
+
|
| 1892 |
+
body.dark-mode .esg-metric-card {
|
| 1893 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 1894 |
+
}
|
| 1895 |
+
|
| 1896 |
+
.esg-val {
|
| 1897 |
+
font-size: 22px;
|
| 1898 |
+
font-weight: 800;
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
.esg-lbl {
|
| 1902 |
+
font-size: 12px;
|
| 1903 |
+
font-weight: 600;
|
| 1904 |
+
margin: 4px 0;
|
| 1905 |
+
}
|
| 1906 |
+
|
| 1907 |
+
.esg-sub {
|
| 1908 |
+
font-size: 10px;
|
| 1909 |
+
color: var(--text-secondary);
|
| 1910 |
+
}
|
| 1911 |
+
|
| 1912 |
+
.esg-chart-box {
|
| 1913 |
+
background-color: rgba(0, 0, 0, 0.01);
|
| 1914 |
+
border: 1px solid var(--border-color);
|
| 1915 |
+
border-radius: 12px;
|
| 1916 |
+
padding: 16px;
|
| 1917 |
+
height: 180px;
|
| 1918 |
+
position: relative;
|
| 1919 |
+
}
|
| 1920 |
+
|
| 1921 |
+
body.dark-mode .esg-chart-box {
|
| 1922 |
+
background-color: rgba(255, 255, 255, 0.02);
|
| 1923 |
+
}
|
| 1924 |
+
|
| 1925 |
+
/* Password Input field for OpenRouter API */
|
| 1926 |
+
.password-input-wrapper {
|
| 1927 |
+
display: flex;
|
| 1928 |
+
position: relative;
|
| 1929 |
+
width: 100%;
|
| 1930 |
+
}
|
| 1931 |
+
|
| 1932 |
+
.password-input-wrapper input {
|
| 1933 |
+
width: 100%;
|
| 1934 |
+
padding-right: 46px !important;
|
| 1935 |
+
}
|
| 1936 |
+
|
| 1937 |
+
.password-input-wrapper button {
|
| 1938 |
+
position: absolute;
|
| 1939 |
+
right: 5px;
|
| 1940 |
+
top: 50%;
|
| 1941 |
+
transform: translateY(-50%);
|
| 1942 |
+
background: none;
|
| 1943 |
+
border: none;
|
| 1944 |
+
color: var(--text-secondary);
|
| 1945 |
+
cursor: pointer;
|
| 1946 |
+
font-size: 14px;
|
| 1947 |
+
padding: 8px;
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
+
.password-input-wrapper button:hover {
|
| 1951 |
+
color: var(--text-primary);
|
| 1952 |
+
}
|
| 1953 |
+
|
| 1954 |
+
/* Modal styles */
|
| 1955 |
+
.modal {
|
| 1956 |
+
position: fixed;
|
| 1957 |
+
top: 0;
|
| 1958 |
+
left: 0;
|
| 1959 |
+
width: 100vw;
|
| 1960 |
+
height: 100vh;
|
| 1961 |
+
background-color: rgba(0, 0, 0, 0.4);
|
| 1962 |
+
backdrop-filter: blur(4px);
|
| 1963 |
+
display: flex;
|
| 1964 |
+
align-items: center;
|
| 1965 |
+
justify-content: center;
|
| 1966 |
+
z-index: 100;
|
| 1967 |
+
animation: fadeIn 0.2s ease-out;
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
+
body.dark-mode .modal {
|
| 1971 |
+
background-color: rgba(0, 0, 0, 0.6);
|
| 1972 |
+
}
|
| 1973 |
+
|
| 1974 |
+
.modal-content {
|
| 1975 |
+
width: 480px;
|
| 1976 |
+
max-width: 90%;
|
| 1977 |
+
padding: 30px;
|
| 1978 |
+
background: #ffffff;
|
| 1979 |
+
color: #111827;
|
| 1980 |
+
}
|
| 1981 |
+
|
| 1982 |
+
body.dark-mode .modal-content {
|
| 1983 |
+
background: var(--bg-card);
|
| 1984 |
+
color: var(--text-primary);
|
| 1985 |
+
}
|
| 1986 |
+
|
| 1987 |
+
.modal-header {
|
| 1988 |
+
display: flex;
|
| 1989 |
+
align-items: center;
|
| 1990 |
+
justify-content: space-between;
|
| 1991 |
+
margin-bottom: 24px;
|
| 1992 |
+
}
|
| 1993 |
+
|
| 1994 |
+
.modal-header h3 {
|
| 1995 |
+
font-size: 16px;
|
| 1996 |
+
font-weight: 700;
|
| 1997 |
+
}
|
| 1998 |
+
|
| 1999 |
+
.close-modal-btn {
|
| 2000 |
+
font-size: 24px;
|
| 2001 |
+
cursor: pointer;
|
| 2002 |
+
color: var(--text-secondary);
|
| 2003 |
+
transition: color 0.2s;
|
| 2004 |
+
}
|
| 2005 |
+
|
| 2006 |
+
.close-modal-btn:hover {
|
| 2007 |
+
color: var(--text-primary);
|
| 2008 |
+
}
|
| 2009 |
+
|
| 2010 |
+
.member-form {
|
| 2011 |
+
display: flex;
|
| 2012 |
+
flex-direction: column;
|
| 2013 |
+
gap: 16px;
|
| 2014 |
+
}
|
| 2015 |
+
|
| 2016 |
+
.form-group {
|
| 2017 |
+
display: flex;
|
| 2018 |
+
flex-direction: column;
|
| 2019 |
+
gap: 6px;
|
| 2020 |
+
margin-bottom: 16px;
|
| 2021 |
+
}
|
| 2022 |
+
|
| 2023 |
+
.form-group label {
|
| 2024 |
+
font-size: 12px;
|
| 2025 |
+
color: var(--text-secondary);
|
| 2026 |
+
font-weight: 600;
|
| 2027 |
+
}
|
| 2028 |
+
|
| 2029 |
+
.form-group input, .form-group select, .form-group textarea {
|
| 2030 |
+
background-color: rgba(0, 0, 0, 0.02);
|
| 2031 |
+
border: 1px solid var(--border-color);
|
| 2032 |
+
border-radius: 8px;
|
| 2033 |
+
color: var(--text-primary);
|
| 2034 |
+
padding: 10px 14px;
|
| 2035 |
+
font-family: var(--font-outfit);
|
| 2036 |
+
font-size: 13px;
|
| 2037 |
+
outline: none;
|
| 2038 |
+
transition: border-color 0.2s;
|
| 2039 |
+
}
|
| 2040 |
+
|
| 2041 |
+
body.dark-mode .form-group input, body.dark-mode .form-group select, body.dark-mode .form-group textarea {
|
| 2042 |
+
background-color: rgba(0, 0, 0, 0.2);
|
| 2043 |
+
}
|
| 2044 |
+
|
| 2045 |
+
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
| 2046 |
+
border-color: var(--color-blue);
|
| 2047 |
+
}
|
| 2048 |
+
|
| 2049 |
+
.form-group textarea {
|
| 2050 |
+
height: 80px;
|
| 2051 |
+
resize: none;
|
| 2052 |
+
}
|
| 2053 |
+
|
| 2054 |
+
.modal-info-text {
|
| 2055 |
+
font-size: 12px;
|
| 2056 |
+
color: var(--text-secondary);
|
| 2057 |
+
line-height: 1.5;
|
| 2058 |
+
margin-bottom: 20px;
|
| 2059 |
+
}
|
| 2060 |
+
|
| 2061 |
+
/* Animations */
|
| 2062 |
+
@keyframes fadeIn {
|
| 2063 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 2064 |
+
to { opacity: 1; transform: translateY(0); }
|
| 2065 |
+
}
|
| 2066 |
+
|
| 2067 |
+
@keyframes spin {
|
| 2068 |
+
0% { transform: rotate(0deg); }
|
| 2069 |
+
100% { transform: rotate(360deg); }
|
| 2070 |
+
}
|
| 2071 |
+
|
| 2072 |
+
@keyframes flash {
|
| 2073 |
+
0%, 100% { opacity: 1; }
|
| 2074 |
+
50% { opacity: 0.4; }
|
| 2075 |
+
}
|
| 2076 |
+
|
| 2077 |
+
.animate-bounce {
|
| 2078 |
+
animation: bounce 2s infinite;
|
| 2079 |
+
}
|
| 2080 |
+
|
| 2081 |
+
@keyframes bounce {
|
| 2082 |
+
0%, 100% { transform: translateY(0); }
|
| 2083 |
+
50% { transform: translateY(-5px); }
|
| 2084 |
+
}
|
| 2085 |
+
|
| 2086 |
+
/* Toast Notifications */
|
| 2087 |
+
.toast-notification {
|
| 2088 |
+
position: fixed;
|
| 2089 |
+
bottom: 30px;
|
| 2090 |
+
right: 30px;
|
| 2091 |
+
z-index: 1000;
|
| 2092 |
+
display: flex;
|
| 2093 |
+
align-items: center;
|
| 2094 |
+
gap: 12px;
|
| 2095 |
+
padding: 16px 24px;
|
| 2096 |
+
background: #ffffff !important;
|
| 2097 |
+
border: 1px solid rgba(37, 99, 235, 0.3) !important;
|
| 2098 |
+
color: #111827;
|
| 2099 |
+
transform: translateY(100px);
|
| 2100 |
+
opacity: 0;
|
| 2101 |
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 2102 |
+
}
|
| 2103 |
+
|
| 2104 |
+
body.dark-mode .toast-notification {
|
| 2105 |
+
background: rgba(15, 23, 42, 0.95) !important;
|
| 2106 |
+
border: 1px solid rgba(59, 130, 246, 0.4) !important;
|
| 2107 |
+
color: white;
|
| 2108 |
+
}
|
| 2109 |
+
|
| 2110 |
+
.toast-notification.show {
|
| 2111 |
+
transform: translateY(0);
|
| 2112 |
+
opacity: 1;
|
| 2113 |
+
}
|
| 2114 |
+
|
| 2115 |
+
/* Print CSS Styles */
|
| 2116 |
+
@media print {
|
| 2117 |
+
body * {
|
| 2118 |
+
visibility: hidden;
|
| 2119 |
+
}
|
| 2120 |
+
#report-print-area, #report-print-area * {
|
| 2121 |
+
visibility: visible;
|
| 2122 |
+
}
|
| 2123 |
+
#report-print-area {
|
| 2124 |
+
position: absolute;
|
| 2125 |
+
left: 0;
|
| 2126 |
+
top: 0;
|
| 2127 |
+
width: 100%;
|
| 2128 |
+
background-color: white !important;
|
| 2129 |
+
box-shadow: none;
|
| 2130 |
+
padding: 0;
|
| 2131 |
+
color: black !important;
|
| 2132 |
+
max-height: none;
|
| 2133 |
+
overflow: visible;
|
| 2134 |
+
}
|
| 2135 |
+
.report-paper {
|
| 2136 |
+
border: none;
|
| 2137 |
+
padding: 0;
|
| 2138 |
+
}
|
| 2139 |
+
}
|