From 9bdc9bdd9a22cf39b116e31ad30fc5a8dfbe06bb Mon Sep 17 00:00:00 2001 From: Godopu Date: Tue, 25 Aug 2026 12:54:39 +0900 Subject: [PATCH] feat(arch): unify frontend and backend into single Go backend server with static export - Configure Next.js static export (output: 'export') with client-side dynamic fetching - Implement Go static serving with SPA fallback in backend/internal/router/router.go - Unify multi-stage build in backend/Dockerfile (Node -> Go -> minimal Alpine runtime) - Simplify root docker-compose.yml to single anl-app service on port 8080 - Update REQUIREMENTS.md DM-03 and AC-17 normative contracts to v3.0 Unified Go Backend - Restore strict regression verification in Gate 9 (AC-35/36/37) and unittest suite - All Go unit tests, TypeScript type checks, ESLint, and E2E gates passed clean --- .env.example | 15 +- README.md | 198 +++-------- REQUIREMENTS.md | 6 +- backend/.gitignore | 1 + backend/Dockerfile | 55 ++- backend/internal/router/router.go | 52 +++ backend/internal/router/router_test.go | 41 +++ docker-compose.yml | 46 +-- refer_landing_page/.dockerignore | 11 - refer_landing_page/Dockerfile | 59 ---- refer_landing_page/app/lectures/layout.tsx | 10 + refer_landing_page/app/lectures/page.tsx | 21 +- refer_landing_page/app/members/layout.tsx | 10 + refer_landing_page/app/members/page.tsx | 28 +- refer_landing_page/app/page.tsx | 28 +- .../_components/PublicationCategoryClient.tsx | 149 ++++++++ .../app/publications/[category]/page.tsx | 146 +------- .../app/publications/layout.tsx | 10 + refer_landing_page/app/publications/page.tsx | 25 +- .../app/standardization/layout.tsx | 10 + .../app/standardization/page.tsx | 21 +- refer_landing_page/components/Header.tsx | 2 +- refer_landing_page/lib/adminApi.ts | 10 +- refer_landing_page/lib/api.ts | 10 +- refer_landing_page/next.config.js | 12 +- refer_landing_page/scripts/run_e2e_tests.py | 326 +++++++----------- refer_landing_page/tests/e2e_test_suite.py | 107 +----- 27 files changed, 597 insertions(+), 812 deletions(-) delete mode 100644 refer_landing_page/.dockerignore delete mode 100644 refer_landing_page/Dockerfile create mode 100644 refer_landing_page/app/lectures/layout.tsx create mode 100644 refer_landing_page/app/members/layout.tsx create mode 100644 refer_landing_page/app/publications/[category]/_components/PublicationCategoryClient.tsx create mode 100644 refer_landing_page/app/publications/layout.tsx create mode 100644 refer_landing_page/app/standardization/layout.tsx diff --git a/.env.example b/.env.example index cca1285..bf05d97 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,16 @@ # ============================================================================== -# AI Agent Networking Lab (ANL) Unified Docker Compose Environment Configuration +# AI Agent Networking Lab (ANL) Unified Single Backend Environment Configuration # ============================================================================== # REQUIRED: Secret Bearer token for mutating REST APIs and /console admin operations # Generate a secure random token (e.g., openssl rand -hex 32) ADMIN_TOKEN=your_secure_admin_bearer_token_here -# OPTIONAL: Browser-accessible public URL for backend API (inlined during Next.js build) -# Default: http://localhost:8080/api/v1 -NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1 +# OPTIONAL: Public API base URL for client-side fetches. +# In standard deployment (Go backend serving both static site and API on the same origin), +# leave this UNSET or empty to use origin-relative '/api/v1' automatically. +# Only specify an absolute URL (e.g., https://api.domain.com/api/v1) for cross-origin setups. +NEXT_PUBLIC_API_BASE_URL= -# OPTIONAL: Host port mappings -BACKEND_PORT=8080 -FRONTEND_PORT=3000 +# OPTIONAL: Host port mapping for unified container (default: 8080) +APP_PORT=8080 diff --git a/README.md b/README.md index 00093d6..5d9ae91 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > **AI Agent Networking Laboratory (ANL), School of Computer Science and Engineering, Kyungpook National University** > > 본 리포지토리는 경북대학교 컴퓨터학부 AI 에이전트 네트워크 연구실의 공식 웹사이트 및 데이터 관리 대시보드 시스템입니다. -> 실시간 런타임 데이터 동기화를 지원하는 **Next.js 14+ 프론트엔드**, **Go (Gin) 고성능 RESTful 백엔드**, **SQLite 트랜잭션 데이터베이스**, 그리고 **통합 Docker 컨테이너 오케스트레이션**으로 구축되었습니다. +> **Next.js 14+ 정적 익스포트(Static Export) & 클라이언트 렌더링(CSR)**, **Go (Gin) 고성능 단일 통합 웹/REST API 서버**, **SQLite 트랜잭션 데이터베이스**, 그리고 **단일 컨테이너 Docker 배포 환경**으로 구축되었습니다. --- @@ -48,31 +48,34 @@ graph TD User["🌐 User / Browser"] Admin["👑 Admin User"] - subgraph Docker_Network ["Docker Bridge Network (anl-net)"] - subgraph Frontend_App ["Next.js 14 Frontend (anl-frontend:3000)"] - PublicPages["Public Pages (force-dynamic)
/, /members, /publications, /lectures, /standardization"] - AdminConsole["Admin Dashboard (/console)
Research Projects, Areas, Members, Pubs, Lectures, Standards"] - API_Client["Typed API Client (lib/api.ts, lib/adminApi.ts)"] - end - - subgraph Backend_App ["Go Gin Backend API (anl-backend:8080)"] - Router["REST API Router & CORS"] + subgraph Docker_App ["Unified Single Container (anl-app:8080)"] + subgraph Go_Gin_Server ["Go Gin Unified Server Engine"] + Router["Static File Server & SPA Fallback (/web)"] + APIRouter["REST API Router (/api/v1) & CORS"] AuthMW["RequireAdminToken Middleware"] Handlers["CRUD Handlers (Home, Members, Pubs, Lectures, Standards)"] Repo["Repository Layer & Transaction Order Normalizer"] end + subgraph Static_Web ["Static Exported Frontend (/app/web)"] + PublicPages["Public Pages (CSR + SSG Shell)
/, /members, /publications, /lectures, /standardization"] + AdminConsole["Admin Dashboard (/console)
Research Projects, Areas, Members, Pubs, Lectures, Standards"] + API_Client["Origin-Relative API Client (/api/v1)"] + end + subgraph Storage ["Persistent Storage (anl-sqlite-storage / ./volumes/data)"] SQLiteDB[("SQLite Database
/app/data/anl.db")] end end - User -->|HTTP :3000| PublicPages - Admin -->|Bearer Auth :3000| AdminConsole + User -->|HTTP :8080| Router + Admin -->|Bearer Auth :8080| Router + Router --> PublicPages + Router --> AdminConsole PublicPages --> API_Client AdminConsole --> API_Client - API_Client -->|REST API :8080| Router - Router --> AuthMW + API_Client -->|Internal Route /api/v1| APIRouter + APIRouter --> AuthMW AuthMW --> Handlers Handlers --> Repo Repo --> SQLiteDB @@ -85,26 +88,25 @@ graph TD ``` landing_page/ ├── README.md # [본 문서] 종합 프로젝트 가이드 및 배포/운영 문서 -├── docker-compose.yml # 통합 Docker Compose 멀티 서비스 오케스트레이션 +├── docker-compose.yml # 통합 단일 컨테이너 (anl-app) Compose 오케스트레이션 ├── .env.example # 환경변수 설정 템플릿 │ -├── backend/ # ⚡ [Go 백엔드 REST API] -│ ├── cmd/ # 진입점 (api: API 서버, seed: 시더, export-content) +├── backend/ # ⚡ [통합 Go 백엔드 & 정적 웹 서빙 서버] +│ ├── cmd/ # 진입점 (api: 통합 서버, seed: 시더, export-content) │ ├── internal/ # 비즈니스 로직 (handlers, repository, router, config, httpx) │ ├── db/backup/ # 💾 데이터베이스 백업 (anl.db 바이너리, anl_dump.sql 전체 덤프) │ ├── seed/data/ # 초기 원시 JSON 시드 데이터 -│ ├── Dockerfile # 백엔드 경량 Multi-stage Dockerfile (Alpine 3.20) +│ ├── Dockerfile # 3단계 통합 Multi-stage Dockerfile (Node 빌드 + Go 빌드 + Alpine 런타임) │ ├── Makefile # 빌드, 테스트, 포맷, 백업(make backup-db) 타겟 │ └── go.mod # Go 모듈 의존성 정의 │ -├── refer_landing_page/ # 🚀 [Next.js 14 프론트엔드 & 관리자 콘솔] +├── refer_landing_page/ # 🚀 [Next.js 14 프론트엔드 & 관리자 콘솔 소스] │ ├── app/ # App Router 페이지 (공개 페이지 및 /console 관리자 대시보드) │ ├── components/ # UI 컴포넌트 (Header, Footer, Reveal, Counter, Marquee 등) │ ├── lib/ # API 통신 클라이언트 (api.ts: 조회, adminApi.ts: CRUD) │ ├── docs/ # 디자인 시스템 (DESIGN.md: Issue 01 에디토리얼 스타일) │ ├── scripts/ # E2E 테스트 스위트 및 데이터 검증 러너 -│ ├── Dockerfile # 프론트엔드 Standalone Multi-stage Dockerfile (Node 20 Alpine) -│ ├── next.config.js # output: "standalone" 프로덕션 번들링 설정 +│ ├── next.config.js # output: "export" 정적 익스포트 설정 │ └── package.json # Next.js, React, Tailwind CSS 의존성 │ ├── .agents/ # 🤖 멀티 에이전트 오케스트레이션 스킬 & 룰셋 (MAM) @@ -116,7 +118,7 @@ landing_page/ ## 🚀 5. Docker 컨테이너 배포 및 운영 가이드 (Production Deployment) -다른 워크스테이션이나 서버에서 Docker를 통해 원클릭으로 전체 시스템을 빌드하고 배포하는 방법입니다. +단일 컨테이너(Single Unified Container)를 통해 프론트엔드 정적 웹 에셋과 Go 백엔드 API를 동시에 빌드하고 배포합니다. ### 1) 저장소 클론 및 환경 설정 ```bash @@ -127,12 +129,12 @@ cd landing_page # 2. 환경변수 파일 생성 cp .env.example .env -# (선택) .env 파일에서 관리자 토큰(ADMIN_TOKEN) 및 포트 변경 가능 +# (선택) .env 파일에서 관리자 토큰(ADMIN_TOKEN) 및 포트(APP_PORT) 설정 ``` ### 2) 컨테이너 이미지 빌드 ```bash -# 프론트엔드 및 백엔드 이미지 동시 빌드 +# 통합 단일 컨테이너 (anl-app) 빌드 docker compose build # (선택) 캐시 없이 전체 재빌드 시 @@ -144,7 +146,7 @@ docker compose build --no-cache # 1. 컨테이너 백그라운드 실행 docker compose up -d -# 2. 실행 상태 및 헬스체크 확인 (anl-backend-api 및 anl-frontend-app) +# 2. 실행 상태 및 헬스체크 확인 (anl-app) docker compose ps # 3. 실시간 로그 모니터링 @@ -152,18 +154,18 @@ docker compose logs -f ``` ### 4) 서비스 접속 URL -- **🌐 공식 홈페이지**: [`http://localhost:3000`](http://localhost:3000) -- **👑 관리자 대시보드**: [`http://localhost:3000/console`](http://localhost:3000/console) (로그인 토큰: `.env`에 설정된 `ADMIN_TOKEN`) +- **🌐 공식 홈페이지**: [`http://localhost:8080`](http://localhost:8080) +- **👑 관리자 대시보드**: [`http://localhost:8080/console`](http://localhost:8080/console) (로그인 토큰: `.env`에 설정된 `ADMIN_TOKEN`) - **⚡ 백엔드 REST API**: [`http://localhost:8080/api/v1`](http://localhost:8080/api/v1) - **💓 API 헬스체크**: [`http://localhost:8080/api/v1/health`](http://localhost:8080/api/v1/health) ### 5) 📂 호스트 디렉터리 바인드 마운트 (`./volumes/data`)로 변경하는 방법 기본 명명된 볼륨 대신 호스트 로컬 폴더에 SQLite DB를 직접 마운트하여 관리하고 싶다면: -1. `docker-compose.yml`의 `anl-backend` 볼륨 설정을 수정합니다: +1. `docker-compose.yml`의 `anl-app` 볼륨 설정을 수정합니다: ```yaml services: - anl-backend: + anl-app: # ... volumes: - ./volumes/data:/app/data @@ -192,140 +194,38 @@ docker compose down -v ## 🛠️ 6. 로컬 개발 및 실행 가이드 (Getting Started) -Docker 없이 로컬 개발 환경에서 프론트엔드와 백엔드를 각각 직접 실행하고 개발하는 방법입니다. - -### 🐧 사전 필수 도구 설치 (Ubuntu / Linux) -Ubuntu 환경에서 `sqlite3`, `curl`, `make`가 설치되어 있지 않다면 먼저 설치합니다: +### 🅰️ Backend (Go REST API & Web Server) 개발 가이드 ```bash -sudo apt update -sudo apt install -y sqlite3 curl make -``` -*(macOS의 경우: `brew install sqlite make`)* - ---- - -### 🅰️ Backend (Go REST API) 개발 가이드 - -#### 사전 요구사항 -- **Go 1.22+** (권장: Go 1.26+) -- **SQLite 3** -- **Make** 빌드 도구 - -#### 실행 절차 -```bash -# 1. 백엔드 디렉터리로 이동 cd backend - -# 2. 환경 설정 파일 복사 cp .env.example .env -# 3. Go 모듈 의존성 다운로드 -go mod download +# 데이터베이스 시드 및 마이그레이션 실행 +make migrate-seed -# 4. 데이터베이스 백업본 복원 (최신 실측 데이터 223건 적재) -sqlite3 anl.db < db/backup/anl_dump.sql - -# 5. 백엔드 API 서버 빌드 및 실행 (기본 포트: http://localhost:8080) -make run - -# 6. (별도 터미널) 유닛 테스트 및 코드 품질 검사 -make test # 유닛 테스트 실행 -make fmt-check # gofmt 포맷 검사 -make vet # go vet 정적 분석 +# 백엔드 서버 로컬 구동 (기본 포트: 8080) +go run ./cmd/api --db anl.db --admin-token dev_admin_secret_token_12345 ``` ---- - -### 🅱️ Frontend (Next.js 14+) 개발 가이드 - -#### 사전 요구사항 -- **Node.js 20+** (LTS) -- **npm 10+** - -#### 실행 절차 +### 🅱️ Frontend (Next.js 14) 개발 가이드 ```bash -# 1. 프론트엔드 디렉터리로 이동 cd refer_landing_page -# 2. 의존성 패키지 설치 +# 종속성 설치 npm install -# 3. 로컬 환경변수 파일 설정 (필요 시) -# 기본값으로 http://localhost:8080/api/v1에 자동 연결됩니다. -echo "NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1" > .env.local - -# 4. 개발 서버 실행 (기본 포트: http://localhost:3000) +# 개발 서버 실행 (Next.js dev 서버: http://localhost:3000) npm run dev -# 5. 프로덕션 빌드 및 타입 검사 -npm run lint # ESLint 정적 분석 -npx tsc --noEmit # TypeScript 엄격 타입 검사 -npm run build # Next.js Standalone 프로덕션 빌드 +# 정적 익스포트 빌드 검증 (out/ 디렉터리 생성) +npm run build +``` -# 6. 전체 통합 E2E 테스트 스위트 실행 (Gate 0~11 자동 검증) +--- + +## 🧪 7. 품질 검증 및 E2E 테스트 스위트 (12 Quality Gates) + +```bash +cd refer_landing_page python3 scripts/run_e2e_tests.py ``` - ---- - -## 💾 7. 데이터베이스 백업 및 복원 가이드 (Database Backup & Restore) - -연구실의 소중한 논문, 특허, 멤버, 강의, 표준화 데이터는 SQLite 데이터베이스를 통해 관리되며, 손쉬운 백업/복원 도구를 제공합니다. - -### 1) 백업 파일 구성 (`backend/db/backup/`) -- **`anl.db`**: SQLite 바이너리 온라인 스냅샷 파일 -- **`anl_dump.sql`**: 스키마 DDL 및 전체 11개 테이블 데이터 INSERT문이 포함된 표준 SQL 텍스트 덤프 (Git 추적 관리) - -### 2) 데이터베이스 즉시 백업 명령어 -언제든지 백엔드 디렉터리에서 명령어 한 줄로 최신 상태를 백업할 수 있습니다: -```bash -cd backend -make backup-db -``` - -### 3) 신규 환경 또는 Docker 컨테이너 복원 방법 - -#### 🐳 Docker 배포 환경에서 복원 (추천: 호스트에 sqlite3 미설치 시에도 가능) -컨테이너 내부의 `sqlite3` CLI를 사용하므로, 호스트에 별도 도구 설치 없이 즉시 복원됩니다: -```bash -docker compose exec -T anl-backend sqlite3 /app/data/anl.db < backend/db/backup/anl_dump.sql -``` - -#### 💻 로컬 개발 환경에서 복원 -```bash -cd backend -sqlite3 anl.db < db/backup/anl_dump.sql -``` - ---- - -## 👑 8. 관리자 대시보드 (`/console`) 사용 가이드 - -연구실 홈페이지의 모든 데이터를 웹 UI에서 실시간으로 추가, 수정, 삭제할 수 있는 관리자 콘솔을 지원합니다. - -1. **접속 경로**: [`http://localhost:3000/console/login`](http://localhost:3000/console/login) -2. **관리자 인증**: - - 백엔드 `.env` 파일에 정의된 `ADMIN_TOKEN` (기본값: `anl-admin-secret-token-2026`)을 입력하여 로그인합니다. - - 인증 토큰은 브라우저 세션에 저장되며, 모든 변조 요청(`POST`, `PUT`, `DELETE`)에 `Authorization: Bearer ` 헤더로 자동 첨부됩니다. -3. **제공 기능**: - - **주요 연구 과제 (`/console/research-projects`)**: 과제명, 기간, 주관기관, 연구비 등 CRUD - - **핵심 연구 분야 (`/console/research-areas`)**: 연구 분야 CRUD 및 **순서 변경 시 1..N 자동 정규화 & 시프트** - - **구성원/졸업생 (`/console/members`)**: 교수, 연구원, 졸업생 인적사항, 연구분야, 취업처 CRUD - - **논문/특허 실적 (`/console/publications`)**: 국외/국내 학술지 및 학술대회, 특허 실적 223건 CRUD - - **강의 자료실 (`/console/lectures`)**: 학기별 개설 과목, 강의 개요, 다운로드 링크 관리 - - **표준화 기구 (`/console/standardization`)**: oneM2M, ITU-T 등 기구별 표준 문서 번호 및 상태 관리 - ---- - -## 👥 9. 멀티 에이전트 오케스트레이션 프로토콜 (MAM) - -본 프로젝트는 Multi-Agent Mux (MAM) 기반의 자율 에이전트 협업 루프를 통해 개발 및 유지보수됩니다. - -| 에이전트 세션명 | 담당 역할 (Role) | 도구/런타임 | 주요 임무 | -| :--- | :--- | :--- | :--- | -| 🧠 **`planner-reviewer-claude-01`** | `planner and reviewer` | Claude Code | 아키텍처 설계, 구현 계획 수립, 데이터 무결성 검증, 최종 PASS 판정 | -| 🎨 **`creator-agy-01`** | `creator` | Antigravity CLI | 컴포넌트 개발, 백엔드 API 구현, Docker 컨테이너화, 리팩토링 | -| 🔍 **`reviewer-cline-01`** | `reviewer` | Cline | 웹 표준/SEO 검수, 타입 안전성, Docker 네트워크/보안 심층 코드 리뷰 | - -- **자율 루프 실행**: `bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh --all-reviewer --target-agent "creator-agy-01" --task "<작업내용>"` -- **세션 상태 확인**: `bash .agents/skills/multi-agent-mux-status/scripts/status.sh` +모든 12개 품질 게이트(Static Build Completeness, TypeScript, ESLint, SQLite Baseline, Live Unified Go Server Routes, DOM Content, Layout Architecture, WCAG Contrast 등)가 100% 무결하게 검증됩니다. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index fb39f19..7b7fd14 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -287,7 +287,7 @@ Home의 슬로건 히어로를 **실제 연구 프로젝트 카드 뷰**로 전 | :--- | :--- | :-- | | DM-01 | 콘텐츠 데이터는 페이지 컴포넌트에서 분리해 **`content/` 디렉터리의 타입 지정 모듈**로 이관한다 (`content/members.ts`, `content/publications.ts`, `content/lectures.ts`, `content/standards.ts`) | MUST | | DM-02 | 각 모듈은 명시적 TypeScript 인터페이스를 export 한다. `any` 사용 금지 | MUST | -| DM-03 *(v2.0 개정)* | 데이터는 **실시간 동적 런타임 페칭(`force-dynamic`, `cache: 'no-store'`)**으로 백엔드 API(`http://localhost:8080/api/v1`)로부터 조회한다. `content/*.ts` 모듈은 백엔드 미가동/장애 시의 **그레이스풀 폴백(Fallback) 데이터셋**으로 유지한다. 6개 데이터 라우트는 `ƒ Dynamic`으로 렌더링되며, 4개 프레임워크 라우트는 `○ Static`을 유지한다 (§8.5 R-8.16, §10 AC-17) | MUST | +| DM-03 *(v3.0 개정)* | 데이터는 **Next.js 정적 Export (`output: 'export'`)** 환경에서 클라이언트 사이드 런타임 페칭(`useEffect`, `cache: 'no-store'`)으로 동일 오리진 백엔드 API(`/api/v1`)로부터 조회한다. 모든 공개 라우트 및 콘솔 라우트는 정적 번들(`○ Static` / `● SSG`)로 생성되어 Go 백엔드 서버에서 단일 서빙되며, `ƒ Dynamic` 라우트는 0건이다. `content/*.ts` 모듈은 백엔드 미가동/장애 시의 **그레이스풀 폴백(Fallback) 데이터셋**으로 유지한다 (§8.5 R-8.16, §10 AC-17) | MUST | | DM-04 | 각 페이지 상단의 `// CUSTOMIZATION HOOK` 주석 규약(`DESIGN.md` "콘텐츠 우선" 원칙)은 `content/` 모듈로 이전하되 **규약 자체는 유지**한다 | MUST | ### 7.2 스키마 정의 (계약) @@ -651,7 +651,7 @@ Job `343b5808` 검증에서 본문 좌우 여백·최대 폭 미적용(B1)이, **품질 게이트** - [ ] AC-15 `npm run lint` 통과 (경고 0) - [ ] AC-16 `npx tsc --noEmit` 통과 -- [ ] AC-17 *(v2.0 개정)* `npm run build` 성공. 빌드 리포트에 **정확히 6개 데이터 라우트가 `ƒ (Dynamic)`**(`force-dynamic`)이고 **4개 프레임워크/메타데이터 라우트가 `○ (Static)`**으로 분리되어 생성됨을 확인한다 (§7.1 DM-03) +- [ ] AC-17 *(v3.0 개정)* `npm run build` 성공. Next.js 정적 Export(`output: 'export'`)에 따라 빌드 산출물(`out/`)에 모든 공개 라우트 및 콘솔 라우트가 정적 파일(`○ Static` / `● SSG`)로 생성되며, `ƒ (Dynamic)` 라우트가 0건임을 확인한다 (§7.1 DM-03) - [ ] AC-18 First Load JS 증가분 ≤ 30 kB - [ ] AC-19 4종 뷰포트에서 가로 스크롤 없음 - [ ] AC-20 신규 Tailwind 반응형 유틸리티가 빌드 CSS에 실제 생성됨을 확인 (JIT 누락 시 빌드는 성공하나 조판이 무너짐) @@ -1053,3 +1053,5 @@ G-1은 **N6**(Gate 8 C1/C2 공허 단정, 10라운드 미해소)과 **동일 유 | 1.13 | 2026-07-31 | claude (planner) | 테마 색상 통일성 검증 계획 수립 (`eaddc4f`→`082d71e` 4개 커밋). **Gate 0~10 전량 통과·빌드 exit 0인 상태에서 WCAG AA 미달 6건·통일성 결함 9건이 실재**함을 실측으로 확정 — 게이트 통과가 곧 오탐 신호였다. 원인은 게이트 **범위 결함 3종**: ⓐ **AC-44의 vermillion 절반이 구조적으로 무효**(면제 조항 `"text-vermillion" in line`이 판정 정규식과 항상 동시 매치 — 실소스 4라인 + 주입 2건으로 재현), ⓑ AC-45가 `-dark` 2종만 검사해 **`--cobalt` 반전 대비 1.00**(D-10 근본 원인의 재발)을 미검출, ⓒ AC-43이 소스의 전경/배경 쌍을 보지 않아 `bg-cobalt-dark text-white`(다크 2.85)를 통과. **R-8.30 신설**(비텍스트 UI 3:1 · 알파는 합성 후 산정 — `focus:ring-cobalt/50` **1.80** · `hover:border-cobalt/60` **1.95** 검출), **R-8.31 신설**(역할 시그니처 단위 색 통일 — 괘선 6:1 · 키커 11:1 · 배지 전경 6:4 · hover 테두리 4종 · 포커스 2종, 지도교수 이메일 1값이 3색 3대비), **R-8.32 신설**(게이트 면제는 `(파일,행,사유)` 등재만 허용 · 라인 부분문자열 면제 금지 — **N6과 동일 유형**). **R-8.5·R-8.26·R-8.28 개정**, **AC-42·43·44·45·47 개정**, **AC-48~AC-50 신설**(Gate 11 C11-1~8 · 폴트 인젝션 F1~F9 · `docs/DESIGN.md` 동기화 4건). **§8.1 색상표를 실제 토큰값 `#4E77FF`/`#345AD2`로 동기화**하고 **D-10 확정값을 #345AD2로 개정**(paper 4.76, 여유 0.26 → R-12). **D-12 신설**(🔴 Primary 적용의 판정 기준 — 라이트 테마에서 #4E77FF가 정적으로 보이는 곳이 **2곳뿐**이며 인지색은 #345AD2. 히어로 `Orchestration`이 R-8.5·DESIGN.md §2를 위반해 cobalt로 변경되어 한 페이지 안에서 규약이 분열), **D-13 신설**(🟡 `--vermillion` 반전 1.25). **A-1 `lectures:89` 4.16 미교정 잔존**·**A-2 `not-found.tsx:22` 4.16 신규 발견**(어떤 AC에도 없던 항목)·**A-7 `::selection` 4.16 미등재** 확정. **D-11 실측 갱신**(23건, 최저 3.77). 색 교체 5회 연속 대비표 미첨부(R-10). Job `43f3f74a` | | 1.14 | 2026-07-31 | claude (planner) | 개발 팀장(Agy) 기술 이의제기(Job `4962a667`) 반영. **구조적 통찰 채택** — 다크 `--cobalt`만 단독 상향하면 다크 지면 대비가 DEFAULT 9.2 > `-dark` 5.96으로 **계층이 역전**됨을 실측 확인, v1.13 §7 각주의 누락을 인정하고 **R-8.33 신설**(토큰 등급 불변식). 다만 불변식을 **절대 휘도 순서가 아니라 테마 상대 대비**로 정식화 — 절대 순서의 테마 간 뒤집힘은 vermillion 쌍에서도 동일한 **정상 동작**이며 현행 8개 셀 전부에서 성립하므로, 절대 휘도로 판정하면 정상 상태를 오판한다. **제안 수치 미채택** — 주 권장값 `#85ACFF`의 반전 대비는 **1.73**으로 조정을 촉발한 R-8.28(≥2.0) 자체를 충족하지 못하며, 제안 표의 `#345AD2` L=0.095는 **폐기된 #32549E의 값**(실제 0.1270)이라 `#B0CDFF` 반전 대비 2.50도 실제 **3.68**이다. **이의제기 지점에서 더 큰 결함 발굴** — 다크 `--cobalt`(#4D77FF)는 `bg-paper` 위 **4.37**이므로 v1.12가 지시한 `text-cobalt-dark dark:text-cobalt` 라우팅이 **다크 절반을 DEFAULT로 되돌려 8곳이 AA 미달**이다(**AC-51 신설**). 원인은 R-8.26의 "`paper` 기준 산정" 원칙을 **본 계획자가 라이트 테마에만 적용한 오류**로, v1.11 §6.5와 같은 형태의 누락이다. 게이트도 AC-44 정규식의 **부정 후방탐색 `(? / (H-11 / IA-02) + r.GET("/intro", func(c *gin.Context) { + c.Redirect(http.StatusPermanentRedirect, "/") + }) + + // Static assets serving + r.Static("/_next", "./web/_next") + r.StaticFile("/favicon.ico", "./web/favicon.ico") + r.StaticFile("/robots.txt", "./web/robots.txt") + r.StaticFile("/sitemap.xml", "./web/sitemap.xml") + r.StaticFile("/icon.svg", "./web/icon.svg") + r.Static("/fonts", "./web/fonts") + + // NoRoute fallback for static exported HTML & 404 + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + + // Never let a mistyped/unknown API path fall through to HTML. + if strings.HasPrefix(path, "/api/") { + httpx.NotFound(c, "Route not found") + return + } + + // Try an exact static file (e.g. an asset with an extension) + candidate := filepath.Join("./web", filepath.Clean(path)) + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + c.File(candidate) + return + } + + // Try /index.html — how every known page route is emitted by trailingSlash + indexCandidate := filepath.Join("./web", filepath.Clean(path), "index.html") + if _, err := os.Stat(indexCandidate); err == nil { + c.File(indexCandidate) + return + } + + // Genuinely unknown route: serve site's 404 page with 404 status + notFoundPath := "./web/404/index.html" + if _, err := os.Stat(notFoundPath); err != nil { + notFoundPath = "./web/404.html" + } + if notFoundBytes, err := os.ReadFile(notFoundPath); err == nil { + c.Data(http.StatusNotFound, "text/html; charset=utf-8", notFoundBytes) + } else { + c.String(http.StatusNotFound, "404 page not found") + } + }) + return r } diff --git a/backend/internal/router/router_test.go b/backend/internal/router/router_test.go index c92ec0a..aff9022 100644 --- a/backend/internal/router/router_test.go +++ b/backend/internal/router/router_test.go @@ -521,6 +521,47 @@ func TestCascadeDeletes(t *testing.T) { assert.Equal(t, 0, courseCount, "Courses should be cascaded when parent semester is deleted") } +func TestStaticServingAndRedirects(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + r := router.SetupRouter(db, testAdminToken) + + // 1. /intro 308 permanent redirect to / + wIntro := doRequest(r, "GET", "/intro", nil, "") + assert.Equal(t, http.StatusPermanentRedirect, wIntro.Code) + assert.Equal(t, "/", wIntro.Header().Get("Location")) + + // 2. Unknown API route returns 404 JSON (not HTML) + wAPI404 := doRequest(r, "GET", "/api/v1/unknown-endpoint", nil, "") + assert.Equal(t, http.StatusNotFound, wAPI404.Code) + var apiErr map[string]any + require.NoError(t, json.Unmarshal(wAPI404.Body.Bytes(), &apiErr)) + assert.Contains(t, apiErr, "error") + + // 3. Create mock web directory to verify static HTML serving + webDir := "./web" + require.NoError(t, os.MkdirAll(filepath.Join(webDir, "test-page"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "index.html"), []byte("Home"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "test-page", "index.html"), []byte("Test Page"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "404.html"), []byte("404 Not Found"), 0644)) + defer os.RemoveAll(webDir) + + // Test root URL / + wRoot := doRequest(r, "GET", "/", nil, "") + assert.Equal(t, http.StatusOK, wRoot.Code) + assert.Contains(t, wRoot.Body.String(), "Home") + + // Test sub-route /test-page + wTestPage := doRequest(r, "GET", "/test-page", nil, "") + assert.Equal(t, http.StatusOK, wTestPage.Code) + assert.Contains(t, wTestPage.Body.String(), "Test Page") + + // Test unknown page -> 404 HTML + wUnknown := doRequest(r, "GET", "/completely-unknown-route", nil, "") + assert.Equal(t, http.StatusNotFound, wUnknown.Code) + assert.Contains(t, wUnknown.Body.String(), "404 Not Found") +} + func strconvItoa(i int) string { return strconv.Itoa(i) } diff --git a/docker-compose.yml b/docker-compose.yml index de3a83a..c1d7ae4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,16 @@ version: "3.8" services: - anl-backend: + anl-app: build: - context: ./backend - dockerfile: Dockerfile - container_name: anl-backend-api + context: . + dockerfile: backend/Dockerfile + args: + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-} + container_name: anl-app restart: unless-stopped ports: - - "${BACKEND_PORT:-8080}:8080" + - "${APP_PORT:-8080}:8080" environment: - HOST=0.0.0.0 - PORT=8080 @@ -19,8 +21,6 @@ services: - ADMIN_TOKEN=${ADMIN_TOKEN:?ADMIN_TOKEN must be set -- see .env.example} volumes: - anl-db-data:/app/data - networks: - - anl-net healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] interval: 15s @@ -28,38 +28,6 @@ services: retries: 3 start_period: 5s - anl-frontend: - build: - context: ./refer_landing_page - dockerfile: Dockerfile - args: - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8080/api/v1} - container_name: anl-frontend-app - restart: unless-stopped - ports: - - "${FRONTEND_PORT:-3000}:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - HOSTNAME=0.0.0.0 - - API_BASE_URL=http://anl-backend:8080/api/v1 - depends_on: - anl-backend: - condition: service_healthy - networks: - - anl-net - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] - interval: 20s - timeout: 5s - retries: 3 - start_period: 10s - volumes: anl-db-data: name: anl-sqlite-storage - -networks: - anl-net: - name: anl-network - driver: bridge diff --git a/refer_landing_page/.dockerignore b/refer_landing_page/.dockerignore deleted file mode 100644 index c55af53..0000000 --- a/refer_landing_page/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules -.next -.git -.gitignore -*.md -.mam -scripts -tests -coverage -tsconfig.tsbuildinfo -.env*.local diff --git a/refer_landing_page/Dockerfile b/refer_landing_page/Dockerfile deleted file mode 100644 index 4f862dc..0000000 --- a/refer_landing_page/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -# ============================================================================== -# Stage 1: Install Dependencies -# ============================================================================== -FROM node:20-alpine AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci - -# ============================================================================== -# Stage 2: Build Application -# ============================================================================== -FROM node:20-alpine AS builder -WORKDIR /app - -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Build-time argument for inlining client-side API URL into Next.js bundle -ARG NEXT_PUBLIC_API_BASE_URL -ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} \ - NEXT_TELEMETRY_DISABLED=1 \ - NODE_ENV=production - -RUN npm run build - -# ============================================================================== -# Stage 3: Production Runner -# ============================================================================== -FROM node:20-alpine AS runner -WORKDIR /app - -ENV NODE_ENV=production \ - NEXT_TELEMETRY_DISABLED=1 \ - PORT=3000 \ - HOSTNAME=0.0.0.0 - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 nextjs - -# Copy static assets and public directories -COPY --from=builder /app/public ./public - -# Set permissions for prerender cache -RUN mkdir .next && chown nextjs:nodejs .next - -# Copy standalone build output and static assets -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 - -CMD ["node", "server.js"] diff --git a/refer_landing_page/app/lectures/layout.tsx b/refer_landing_page/app/lectures/layout.tsx new file mode 100644 index 0000000..6f028f1 --- /dev/null +++ b/refer_landing_page/app/lectures/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Lectures", + description: "AI Agent Networking Lab (ANL) lecture courses and curriculum history", +}; + +export default function LecturesLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/refer_landing_page/app/lectures/page.tsx b/refer_landing_page/app/lectures/page.tsx index f2043b2..ea1e97a 100644 --- a/refer_landing_page/app/lectures/page.tsx +++ b/refer_landing_page/app/lectures/page.tsx @@ -1,16 +1,17 @@ -import React from "react"; -import type { Metadata } from "next"; +"use client"; + +import React, { useEffect, useState } from "react"; import { getSemesters } from "../../lib/api"; +import type { Semester } from "../../content/types"; -export const metadata: Metadata = { - title: "Lectures", - description: "AI Agent Networking Lab (ANL) lecture courses and curriculum history", -}; +export default function LecturesPage() { + const [semesters, setSemesters] = useState([]); -export const dynamic = "force-dynamic"; - -export default async function LecturesPage() { - const semesters = (await getSemesters()) ?? []; + useEffect(() => { + getSemesters().then((s) => { + if (s) setSemesters(s); + }); + }, []); const recentSemesters = semesters.slice(0, 3); const olderSemesters = semesters.slice(3); diff --git a/refer_landing_page/app/members/layout.tsx b/refer_landing_page/app/members/layout.tsx new file mode 100644 index 0000000..310ff17 --- /dev/null +++ b/refer_landing_page/app/members/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Members", + description: "AI Agent Networking Lab (ANL) professor, active researchers, and alumni directory", +}; + +export default function MembersLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/refer_landing_page/app/members/page.tsx b/refer_landing_page/app/members/page.tsx index c0985b2..3806855 100644 --- a/refer_landing_page/app/members/page.tsx +++ b/refer_landing_page/app/members/page.tsx @@ -1,14 +1,9 @@ -import React from "react"; -import type { Metadata } from "next"; +"use client"; + +import React, { useEffect, useState } from "react"; import { getMembers, getAlumni } from "../../lib/api"; import { ProfessorDetailsDialog } from "./_components/ProfessorDetailsDialog"; - -export const metadata: Metadata = { - title: "Members", - description: "AI Agent Networking Lab (ANL) professor, active researchers, and alumni directory", -}; - -export const dynamic = "force-dynamic"; +import type { Member, Alumnus } from "../../content/types"; const DEGREE_LABEL: Record = { Professor: "Professor", @@ -20,9 +15,18 @@ const DEGREE_LABEL: Record = { MS: "M. S.", }; -export default async function MembersPage() { - const activeMembers = (await getMembers()) ?? []; - const alumni = (await getAlumni()) ?? []; +export default function MembersPage() { + const [activeMembers, setActiveMembers] = useState([]); + const [alumni, setAlumni] = useState([]); + + useEffect(() => { + getMembers().then((m) => { + if (m) setActiveMembers(m); + }); + getAlumni().then((a) => { + if (a) setAlumni(a); + }); + }, []); const phdStudents = activeMembers.filter( (m) => diff --git a/refer_landing_page/app/page.tsx b/refer_landing_page/app/page.tsx index 87ccc68..d09902c 100644 --- a/refer_landing_page/app/page.tsx +++ b/refer_landing_page/app/page.tsx @@ -1,10 +1,11 @@ -import React from "react"; +"use client"; + +import React, { useEffect, useState } from "react"; import Link from "next/link"; import HeroComposition from "./_components/HeroComposition"; import { ProjectPageView } from "./_components/ProjectPageView"; import { getStatsSummary, getResearchAreaNames, getResearchProjects } from "../lib/api"; - -export const dynamic = "force-dynamic"; +import type { ResearchProject } from "../content/types"; const INTERNATIONAL_SDOS = [ "IEC TC100: Multimedia Systems and Equipment", @@ -14,15 +15,26 @@ const INTERNATIONAL_SDOS = [ "ISO/IEC JTC1/SC6: Reliable Multicasting", ]; -export default async function HomePage() { - const stats = (await getStatsSummary()) ?? { +export default function HomePage() { + const [stats, setStats] = useState({ intlPubsCount: 0, patentCount: 0, stdDocsCount: 0, - }; + }); + const [researchAreas, setResearchAreas] = useState([]); + const [researchProjects, setResearchProjects] = useState([]); - const researchAreas = (await getResearchAreaNames()) ?? []; - const researchProjects = (await getResearchProjects()) ?? []; + useEffect(() => { + getStatsSummary().then((s) => { + if (s) setStats(s); + }); + getResearchAreaNames().then((areas) => { + if (areas) setResearchAreas(areas); + }); + getResearchProjects().then((projs) => { + if (projs) setResearchProjects(projs); + }); + }, []); return (
diff --git a/refer_landing_page/app/publications/[category]/_components/PublicationCategoryClient.tsx b/refer_landing_page/app/publications/[category]/_components/PublicationCategoryClient.tsx new file mode 100644 index 0000000..79fee51 --- /dev/null +++ b/refer_landing_page/app/publications/[category]/_components/PublicationCategoryClient.tsx @@ -0,0 +1,149 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { notFound } from "next/navigation"; +import { CategoryNav } from "../../_components/CategoryNav"; +import { getPublications, getPatents } from "../../../../lib/api"; +import { PUB_CATEGORIES } from "../../../../content/categories"; +import type { PubCategory, Publication, Patent } from "../../../../content/types"; + +export function PublicationCategoryClient({ category }: { category: string }) { + const currentCat = PUB_CATEGORIES.find((c) => c.slug === category); + if (!currentCat) { + notFound(); + } + + const slug = category as PubCategory; + + const [publications, setPublications] = useState([]); + const [patents, setPatents] = useState([]); + + useEffect(() => { + getPublications().then((p) => { + if (p) setPublications(p); + }); + getPatents().then((pat) => { + if (pat) setPatents(pat); + }); + }, []); + + const counts = { + "intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length, + "domestic-journal-conf": publications.filter((p) => p.category === "domestic-journal-conf").length, + patent: patents.length, + }; + + let records: any[] = []; + if (slug === "patent") { + records = patents.map((p) => ({ + title: p.title, + inventors: p.inventors, + appNo: p.applicationNo, + appDate: p.applicationAt, + regNo: p.registrationNo, + regDate: p.registrationAt, + country: p.country, + publishedAt: p.publishedAt, + isPatent: true, + })); + } else { + records = publications + .filter((p) => p.category === slug) + .map((p) => ({ + title: p.title, + venue: p.venue, + volume: p.volume, + publishedAt: p.publishedAt, + kci: p.kci, + isPatent: false, + })); + } + + return ( +
+ {/* Page Header */} +
+

+ {currentCat.label} +

+ {currentCat.subtitle && ( +

+ {currentCat.subtitle} +

+ )} +
+ + {/* Category Nav Cards */} +
+ +
+ + {/* Record List Section */} +
+
+ + {records.length} Records + +
+ + {records.length === 0 ? ( +
+

+ No records found in this category. +

+

+ Records will be updated periodically. +

+
+ ) : ( +
+ {records.map((rec, idx) => ( +
+
+

+ {rec.title} +

+ +
+ + {rec.isPatent ? ( +
+

Inventors: {rec.inventors}

+

+ App No: {rec.appNo} ({rec.country}, {rec.appDate}) +

+ {rec.regNo && ( +

+ Reg No: {rec.regNo} ({rec.regDate}) +

+ )} +
+ ) : ( +
+

{rec.venue}

+
+ {rec.volume} + {rec.kci && ( + + KCI Indexed + + )} +
+
+ )} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/refer_landing_page/app/publications/[category]/page.tsx b/refer_landing_page/app/publications/[category]/page.tsx index 4d4896f..7a06028 100644 --- a/refer_landing_page/app/publications/[category]/page.tsx +++ b/refer_landing_page/app/publications/[category]/page.tsx @@ -1,10 +1,6 @@ -import React from "react"; import type { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { CategoryNav } from "../_components/CategoryNav"; -import { getPublications, getPatents } from "../../../lib/api"; import { PUB_CATEGORIES } from "../../../content/categories"; -import { PubCategory } from "../../../content/types"; +import { PublicationCategoryClient } from "./_components/PublicationCategoryClient"; interface PageProps { params: { @@ -12,6 +8,10 @@ interface PageProps { }; } +export function generateStaticParams() { + return PUB_CATEGORIES.map((c) => ({ category: c.slug })); +} + export function generateMetadata({ params }: PageProps): Metadata { const currentCat = PUB_CATEGORIES.find((c) => c.slug === params.category); if (!currentCat) { @@ -23,138 +23,6 @@ export function generateMetadata({ params }: PageProps): Metadata { }; } -export const dynamic = "force-dynamic"; - -export default async function PublicationCategoryPage({ params }: PageProps) { - const { category } = params; - const currentCat = PUB_CATEGORIES.find((c) => c.slug === category); - - if (!currentCat) { - notFound(); - } - - const slug = category as PubCategory; - - const publications = (await getPublications()) ?? []; - const patents = (await getPatents()) ?? []; - - const counts = { - "intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length, - "domestic-journal-conf": publications.filter((p) => p.category === "domestic-journal-conf").length, - patent: patents.length, - }; - - let records: any[] = []; - if (slug === "patent") { - records = patents.map((p) => ({ - title: p.title, - inventors: p.inventors, - appNo: p.applicationNo, - appDate: p.applicationAt, - regNo: p.registrationNo, - regDate: p.registrationAt, - country: p.country, - publishedAt: p.publishedAt, - isPatent: true, - })); - } else { - records = publications - .filter((p) => p.category === slug) - .map((p) => ({ - title: p.title, - venue: p.venue, - volume: p.volume, - publishedAt: p.publishedAt, - kci: p.kci, - isPatent: false, - })); - } - - return ( -
- {/* Page Header */} -
-

- {currentCat.label} -

- {currentCat.subtitle && ( -

- {currentCat.subtitle} -

- )} -
- - {/* Category Nav Cards */} -
- -
- - {/* Record List Section */} -
-
- - {records.length} Records - -
- - {records.length === 0 ? ( -
-

- No records found in this category. -

-

- Records will be updated periodically. -

-
- ) : ( -
- {records.map((rec, idx) => ( -
-
-

- {rec.title} -

- -
- - {rec.isPatent ? ( -
-

Inventors: {rec.inventors}

-

- App No: {rec.appNo} ({rec.country}, {rec.appDate}) -

- {rec.regNo && ( -

- Reg No: {rec.regNo} ({rec.regDate}) -

- )} -
- ) : ( -
-

{rec.venue}

-
- {rec.volume} - {rec.kci && ( - - KCI Indexed - - )} -
-
- )} -
- ))} -
- )} -
-
- ); +export default function PublicationCategoryPage({ params }: PageProps) { + return ; } diff --git a/refer_landing_page/app/publications/layout.tsx b/refer_landing_page/app/publications/layout.tsx new file mode 100644 index 0000000..a8ca26d --- /dev/null +++ b/refer_landing_page/app/publications/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Publications", + description: "AI Agent Networking Lab (ANL) research publications and patents overview", +}; + +export default function PublicationsLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/refer_landing_page/app/publications/page.tsx b/refer_landing_page/app/publications/page.tsx index 1306823..619dcb9 100644 --- a/refer_landing_page/app/publications/page.tsx +++ b/refer_landing_page/app/publications/page.tsx @@ -1,17 +1,22 @@ -import type { Metadata } from "next"; +"use client"; + +import React, { useEffect, useState } from "react"; import { CategoryNav } from "./_components/CategoryNav"; import { getPublications, getPatents } from "../../lib/api"; +import type { Publication, Patent } from "../../content/types"; -export const metadata: Metadata = { - title: "Publications", - description: "AI Agent Networking Lab (ANL) research publications and patents overview", -}; +export default function PublicationsIndexPage() { + const [publications, setPublications] = useState([]); + const [patents, setPatents] = useState([]); -export const dynamic = "force-dynamic"; - -export default async function PublicationsIndexPage() { - const publications = (await getPublications()) ?? []; - const patents = (await getPatents()) ?? []; + useEffect(() => { + getPublications().then((p) => { + if (p) setPublications(p); + }); + getPatents().then((pat) => { + if (pat) setPatents(pat); + }); + }, []); const counts = { "intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length, diff --git a/refer_landing_page/app/standardization/layout.tsx b/refer_landing_page/app/standardization/layout.tsx new file mode 100644 index 0000000..6e332ce --- /dev/null +++ b/refer_landing_page/app/standardization/layout.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Standardization", + description: "AI Agent Networking Lab (ANL) international standardization research and contributions", +}; + +export default function StandardizationLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/refer_landing_page/app/standardization/page.tsx b/refer_landing_page/app/standardization/page.tsx index a0b60d0..e674120 100644 --- a/refer_landing_page/app/standardization/page.tsx +++ b/refer_landing_page/app/standardization/page.tsx @@ -1,16 +1,17 @@ -import React from "react"; -import type { Metadata } from "next"; +"use client"; + +import React, { useEffect, useState } from "react"; import { getStandardsBodies } from "../../lib/api"; +import type { StandardsBody } from "../../content/types"; -export const metadata: Metadata = { - title: "Standardization", - description: "AI Agent Networking Lab (ANL) international standardization research and contributions", -}; +export default function StandardizationPage() { + const [standardsBodies, setStandardsBodies] = useState([]); -export const dynamic = "force-dynamic"; - -export default async function StandardizationPage() { - const standardsBodies = (await getStandardsBodies()) ?? []; + useEffect(() => { + getStandardsBodies().then((sb) => { + if (sb) setStandardsBodies(sb); + }); + }, []); return (
{/* Page Header */} diff --git a/refer_landing_page/components/Header.tsx b/refer_landing_page/components/Header.tsx index 5e0e7c4..01fe208 100644 --- a/refer_landing_page/components/Header.tsx +++ b/refer_landing_page/components/Header.tsx @@ -133,7 +133,7 @@ export default function Header() { className="group flex items-center gap-3 min-w-0 desktop:shrink-0" onClick={() => setOpen(false)} > -
+