Compare commits

...
10 Commits
Author SHA1 Message Date
Godopu dadd14feb1 test(e2e,verify): enhance live API verification and dynamic port binding in E2E runner
- Update verify_counts.py to validate SQLite database records and live Go REST API endpoints directly
- Harden E2E test runner with dynamic free port binding (get_free_port) to prevent port collisions
- Add exact regex matching for dynamic (ƒ) and static (○) route table allocations
- Ignore root build binary artifacts in backend/.gitignore
2026-08-24 20:31:42 +09:00
Godopu f96f687dd1 feat(frontend): purge all static content fallback files and enforce strict backend-only dynamic rendering
- Delete all legacy static content data files (members.ts, publications.ts, lectures.ts, standards.ts, projects.ts)
- Remove all fallback imports and static fallbacks across all page components
- Enforce strict dynamic runtime fetching where pages render empty datasets when Go backend server is unavailable
2026-08-24 20:10:25 +09:00
Godopu 5041ab1ea8 feat(frontend): convert to live real-time dynamic runtime fetching with force-dynamic and cache no-store
- Set cache: 'no-store' in lib/api.ts to trigger real-time REST API requests on every page load
- Add export const dynamic = 'force-dynamic' across all 6 app pages (/home, /members, /publications, /lectures, /standardization)
- Update REQUIREMENTS.md and E2E test runner to validate live server routes and dynamic runtime fetching
- Pass all 12 quality gates cleanly
2026-08-24 19:25:26 +09:00
Godopu 9cb6258d3d feat(frontend): connect Next.js server components to Go backend REST API with resilient fallback
- Implement typed REST API client in refer_landing_page/lib/api.ts
- Support API_BASE_URL environment variable with graceful fallback to static content
- Connect Home page (projects, areas, stats), Members page (active, alumni), Publications (highlights, list, patents), Lectures, and Standardization pages to backend endpoints
- Pass all 12 SSG build, TypeScript, ESLint, and E2E quality gates
2026-08-24 18:40:46 +09:00
Godopu fb6881070c feat(backend,docker,docs): implement Go/Gin/SQLite REST API server, Docker containerization, and architecture specifications
- Implement standalone Go backend API server using Gin and pure-Go SQLite (modernc.org/sqlite)
- Add 11-table schema DDL migrations (000001_init.up.sql) based on DATA_TABLE.md
- Add seed data ingestion tool (cmd/seed) and static TypeScript content exporter (cmd/export-content)
- Add configuration loader (internal/config) supporting environment variables and .env files (HOST, PORT, DB_PATH, GIN_MODE, CORS_ALLOW_ORIGINS, AUTO_MIGRATE)
- Add lightweight multi-stage Dockerfile and docker-compose.yml with persistent SQLite volume
- Add comprehensive architecture specification (ARCHITECTURE.md) and REST API interface specification (API_INTERFACE.md)
- Harmonize frontend publications page to consume isHighlight flag from database-synced content
2026-08-24 17:34:51 +09:00
Godopu 75bf334fec fix(home): update project metadata label from Standards Track to Organization 2026-08-24 16:17:03 +09:00
Godopu f34e9bad57 refactor(pages,a11y,seo): enhance subpage metadata, semantic HTML, accessibility, and E2E regression tests
- Standardize Next.js Metadata and dynamic generateMetadata across all subpages
- Fix duplicate main landmark in publications page by replacing nested main with div
- Unify Members page spacing (space-y-10 sm:space-y-14, pt-6 md:pt-8) and responsive H2s
- Add aria-label and focus ring to CategoryNav View All links for a11y
- Derive dynamic course history semester ranges in Lectures page
- Add 3 comprehensive E2E test cases (test_08, test_09, test_10) for highlights, active border, CITE brand links, and English H1 headers
2026-08-24 16:10:11 +09:00
Godopu b1640cdac4 feat(publications,standardization,ui): harmonize typography, active category border, and 5 representative highlights
- Refine Publications overview & category detail pages with clean typography and spacing
- Add active border line (border-2 border-cobalt-dark) on active CategoryNav card
- Replace 10 highlights with 5 designated representative publications
- Add Patent subtitle '(IoT Architecture & Protocols, etc ...)'
- Update Footer link label from 'College of IT Engineering' to 'CITE'
- Clean up Standardization & Lectures page header kickers and spacing
2026-08-24 15:55:59 +09:00
Godopu a2b626b192 feat(theme,ui): apply pure white background in light mode and improve accessibility
- Update light mode theme tokens to pure white (#ffffff) background and soft gray borders
- Add supports-[height:100dvh] fallback to ProfessorDetailsDialog max-height
- Add aria-hidden to decorative arrows in Header mobile navigation
2026-08-22 22:10:09 +09:00
Godopu 44c21ab59e feat(members,header): overhaul members page with CV dialog and english-ify navigation
- Rewrite Members page to English: LAB MEMBERS, Prof. Seok-Joo Koh, Active Researchers, and Graduates
- Add ProfessorDetailsDialog component with comprehensive CV modal
- Standardize degree course label mappings (Ph. D., M. S.)
- Translate mobile navigation panel and header aria-labels to English
- Update E2E test suite assertions for the updated members structure
2026-08-21 23:16:06 +09:00
71 changed files with 7807 additions and 2491 deletions
+30
View File
@@ -80,6 +80,10 @@
#default: hermes
# MQTT_CLIENT_ID_PREFIX=hermes
# MQTT keepalive interval (seconds). Used by paho-mqtt client connections.
#default: 60
# MQTT_KEEPALIVE=60
# Log level for MAM runtime components (DEBUG, INFO, WARN, ERROR).
#default: INFO
# MAM_LOG_LEVEL=INFO
@@ -112,6 +116,32 @@
#default: <cwd>/.mam/delegate_job_logs
# DELEGATE_JOB_LOGS_DIR=/path/to/workspace/.mam/delegate_job_logs
# Max attempts to poll for pane renderer quiescence in send_keys_safe.
#default: 20
# SKS_QUIESCENT_TRIES=20
# Interval (seconds) between pane quiescence capture polls.
#default: 0.5
# SKS_QUIESCENT_INTERVAL=0.5
# Consecutive empty captures to conclude unobservable/headless mode early.
#default: 3
# SKS_EMPTY_GIVEUP=3
# Minimum columns a pane must retain after a vertical split (2xK layout engine).
#default: 60
# MAM_MIN_PANE_COLS=60
# Minimum rows a pane must retain after a horizontal split (2xK layout engine).
#default: 20
# MAM_MIN_PANE_ROWS=20
# Maximum number of columns a workspace may grow to before the engine reports
# 'overflow' (which makes lib.sh create a fresh workspace instead of splitting).
# Applies to both measured (GUI) and headless 0x0 layouts.
#default: (unset -> no column cap)
# MAM_MAX_PANE_COLS=3
# ==============================================================================
# deploy / distribution source (for forks/mirrors)
# ==============================================================================
+6 -4
View File
@@ -68,7 +68,9 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
Read [MULTI_AGENT_RULES.md](.agents/MULTI_AGENT_RULES.md) (or [Korean version](.agents/MULTI_AGENT_RULES.ko.md)) and [multi-agent-mux-loop/SKILL.md](.agents/skills/multi-agent-mux-loop/SKILL.md) first before working and follow the instructions for orchestration and collaboration.
<!-- BEGIN MAM ORCHESTRATION -->
# 🤖 Multi-Agent Orchestration Guidelines
Please refer to [.agents/MULTI_AGENT_RULES.md](file://./.agents/MULTI_AGENT_RULES.md) for detailed collaborative rules and state flow constraints.
<!-- END MAM ORCHESTRATION -->
## 5. Orchestrator Scope Guard (O-3)
**Normal vs. Orchestration Modes**
- **Normal Mode**: The orchestrator acts as Main Creator and may edit files directly.
- **Loop Active Mode (`/multi-agent-mux-loop`)**: Direct file mutation is intercepted by `.agents/hooks.json`. Do not seek workarounds when a tool call is denied — delegate the task immediately via `run_loop.sh`.
+366
View File
@@ -0,0 +1,366 @@
# AI Agent Networking Lab (ANL) — REST API Interface Specification
This document defines the RESTful API endpoints, request parameters, response schemas, and data structures provided by the ANL Go/Gin backend server.
---
## 1. General API Conventions
### 1.1 Base URL
- **Local Development**: `http://localhost:8080/api/v1`
- **Protocol**: HTTP/1.1 (JSON Content-Type: `application/json; charset=utf-8`)
### 1.2 Standard Response Envelope
All API endpoints strictly follow the uniform JSON response envelope structure.
#### Success Response (`200 OK`)
```json
{
"data": <Payload Object | Array>
}
```
#### Error Response (`4xx / 5xx`)
```json
{
"error": {
"message": "Human-readable error description"
}
}
```
### 1.3 HTTP Status Codes
| Status Code | Description |
|---|---|
| `200 OK` | The request succeeded and returned the requested data. |
| `400 Bad Request` | Invalid query parameter (e.g., unsupported publication category). |
| `404 Not Found` | The requested resource was not found. |
| `500 Internal Server Error` | Unexpected server-side failure during database query or processing. |
---
## 2. API Endpoints Specification
### 2.1 System & Health
#### `GET /api/v1/health`
Checks backend API service health and database availability.
- **Response Payload**:
```json
{
"data": {
"service": "anl-backend-api",
"status": "ok"
}
}
```
---
## 2.2 Home & Overview
#### `GET /api/v1/research-projects`
Retrieves the list of all active research projects.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"slug": "iot-standards",
"title": "AIoT & AI-RAN",
"abstract": "AIoT & AI-RAN investigates agent-based IoT architectures in which autonomous AI agents are distributed across sensor, edge, and radio access layers. The project studies agent-to-agent coordination over high-performance transport (gRPC/QUIC) bridged with constrained edge protocols (MQTT/CoAP), targeting AI-RAN designs for distributed autonomous control.",
"keywords": [
"AIoT",
"agent-based IoT",
"AI-RAN",
"constrained edge protocols",
"distributed autonomous control"
],
"organization": "National Research Foundation of Korea (NRF)",
"standards_org": "NRF",
"period": "2026 ~ Present",
"funder": null,
"created_at": "2026-08-24T16:47:14Z",
"updated_at": "2026-08-24T16:47:14Z"
}
]
}
```
#### `GET /api/v1/research-areas`
Retrieves the list of 14 core research areas ordered by `display_order`.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"name_en": "Quick UDP Internet Connection (QUIC)",
"name_kr": null,
"display_order": 1
},
{
"id": 2,
"name_en": "QUIC-based Mobility and Multi-Streaming Support",
"name_kr": null,
"display_order": 2
}
]
}
```
#### `GET /api/v1/stats/summary`
Retrieves aggregated top-level metrics for the homepage hero section.
- **Response Payload**:
```json
{
"data": {
"intl_publications": 68,
"standardization_docs": 44,
"patents": 75
}
}
```
---
## 2.3 Members & Alumni
#### `GET /api/v1/members`
Retrieves all 16 active members (including the professor and graduate researchers) ordered by `display_order`.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"name_ko": "고석주",
"name_en": "Seok-Joo Koh",
"degree": "Professor",
"affiliation": "School of Computer Science and Engineering",
"email": "sjkoh@knu.ac.kr",
"is_advisor": true,
"display_order": 1,
"created_at": "2026-08-24T16:47:14Z"
},
{
"id": 2,
"name_ko": "김경호",
"name_en": "Kyeong-Ho Kim",
"degree": "PhDStudent",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 2,
"created_at": "2026-08-24T16:47:14Z"
}
]
}
```
#### `GET /api/v1/alumni`
Retrieves all 60 alumni sorted chronologically by `graduated_at DESC`.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"name_ko": "도경진",
"name_en": "Gyeong-Jin Do",
"degree": "MS",
"graduated_at": "2024-02",
"major": "School of Electronic and Electrical Engineering",
"current_position": "LIG Nex1, Senior Researcher",
"created_at": "2026-08-24T16:47:14Z"
}
]
}
```
---
## 2.4 Publications & Intellectual Property
#### `GET /api/v1/publications`
Retrieves list of academic publications. Supports filtering by publication category.
- **Query Parameters**:
- `category` (optional, string): Filter by category. Allowed values: `intl-journal-conf`, `domestic-journal-conf`.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"category": "intl-journal-conf",
"title": "Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G",
"authors": "Seok-Joo Koh, ...",
"venue": "IEEE Transactions on Network and Service Management",
"volume": "Vol. 23, pp. 4490~4505, April 2026",
"published_at": "2026-04",
"kci": false,
"doi": "10.1109/TNSM.2026.10908861",
"is_highlight": true,
"created_at": "2026-08-24T16:47:14Z"
}
]
}
```
#### `GET /api/v1/publications/highlights`
Retrieves the **5 designated top representative publications** (`WHERE is_highlight = 1`).
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"category": "intl-journal-conf",
"title": "Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G",
"authors": "Seok-Joo Koh, ...",
"venue": "IEEE Transactions on Network and Service Management",
"volume": "Vol. 23, pp. 4490~4505, April 2026",
"published_at": "2026-04",
"kci": false,
"doi": "10.1109/TNSM.2026.10908861",
"is_highlight": true,
"created_at": "2026-08-24T16:47:14Z"
},
{
"id": 2,
"category": "intl-journal-conf",
"title": "Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G",
"authors": "Seok-Joo Koh, ...",
"venue": "IEEE Transactions on Consumer Electronics",
"volume": "Vol. 71, pp. 1120~1135, Jan. 2026",
"published_at": "2026-01",
"kci": false,
"doi": "10.1109/TCE.2025.3524673",
"is_highlight": true,
"created_at": "2026-08-24T16:47:14Z"
}
]
}
```
#### `GET /api/v1/patents`
Retrieves all 75 registered and pending patents sorted by `published_at DESC`.
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"title": "다중 인터페이스 기반 QUIC 핸드오버 지원 장치 및 방법",
"inventors": "고석주, 김경호",
"application_no": "10-2024-0012345",
"application_at": "2024-01-15",
"registration_no": "10-2567890",
"registration_at": "2025-05-20",
"country": "KR",
"published_at": "2025-05-20",
"created_at": "2026-08-24T16:47:14Z"
}
]
}
```
---
## 2.5 Lectures & Curriculum
#### `GET /api/v1/semesters`
Retrieves all 45 academic semesters with nested course lists, ordered by `year DESC` and `term` (Fall before Spring).
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"year": 2026,
"term": "Spring",
"courses": [
{
"id": 1,
"semester_id": 1,
"code": "COMP0411",
"name_en": "Computer Networks",
"name_kr": null,
"note": "Undergraduate Core",
"display_order": 1
}
]
},
{
"id": 2,
"year": 2025,
"term": "Fall",
"courses": [
{
"id": 2,
"semester_id": 2,
"code": "COMP0754",
"name_en": "Advanced Internet Protocol",
"name_kr": null,
"note": "Graduate Course",
"display_order": 1
}
]
}
]
}
```
---
## 2.6 Standardization
#### `GET /api/v1/standards-bodies`
Retrieves all 6 standardization bodies with 3-tier hierarchy (`Body → Project (WG) → Documents[]`).
- **Response Payload**:
```json
{
"data": [
{
"id": 1,
"org": "IEC TC100",
"full_name": "International Electrotechnical Commission: Technical Committee 100",
"scope": "international",
"period": "2018 ~ Present",
"role": "Project Editor / Active Contributor",
"display_order": 1,
"projects": [
{
"wg": "WG 12",
"name": "MCM: Multilateral and Collaborative Metaverse",
"documents": [
{
"id": 1,
"body_id": 1,
"wg": "WG 12",
"project_name": "MCM: Multilateral and Collaborative Metaverse",
"title": "Conceptual model for multilateral and collaborative metaverse systems",
"doc_ref": "IEC 63430 ED1",
"status": "IS",
"published_at": "2026-03"
}
]
}
]
}
]
}
```
+102
View File
@@ -0,0 +1,102 @@
# AI Agent Networking Lab (ANL) — System Architecture Specification
## 1. System Overview
AI Agent Networking Lab (ANL) 시스템은 연구실의 학술 실적(논문, 특허), 연구 과제, 구성원, 강의, 표준화 기고서 등의 데이터를 효율적으로 관리하고 제공하기 위한 **하이브리드 데이터 아키텍처(Decoupled Static-Export & Headless API Architecture)**로 설계되었습니다.
Next.js 기반의 고성능 정적 웹사이트(SSG)와 Go/Gin 및 SQLite 기반의 독립형 헤드리스(Headless) 백엔드 API 서버를 분리하여, **완벽한 정적 빌드 성능(0 Dynamic Route)**과 **단일 진실 공급원(Single Source of Truth) 기반의 데이터 관리 편의성**을 동시에 달성합니다.
---
## 2. High-Level Architecture Diagram
```mermaid
graph TD
subgraph "Storage & Ingestion Layer"
SEED["Seed Data JSON<br/>(backend/seed/data/*.json)"] -->|cmd/seed| SQLITE[("SQLite Database<br/>(anl.db - WAL Mode)")]
SQLITE -->|cmd/export-content| TS_CONTENT["Static TypeScript Content<br/>(refer_landing_page/content/*.ts)"]
end
subgraph "Backend API Serving Layer (Go / Gin)"
API_MAIN["API Server Binary<br/>(backend/cmd/api)"] --> ROUTER["Gin Engine & Router<br/>(internal/router)"]
ROUTER --> HANDLERS["Domain Handlers<br/>(internal/handlers)"]
HANDLERS --> REPO["Repository Layer<br/>(internal/repository)"]
REPO -->|database/sql + modernc.org/sqlite| SQLITE
end
subgraph "Frontend Presentation Layer (Next.js 14 SSG)"
TS_CONTENT --> NEXT_BUILD["Next.js Static Build Engine<br/>(○ Static / ● SSG)"]
NEXT_BUILD --> SSG_PAGES["Static HTML Routes<br/>(/, /members, /publications, etc.)"]
ADMIN["Future Admin Dashboard<br/>(CRUD Client)"] -.->|REST API /api/v1| ROUTER
CLIENT_BROWSER["End Users / Browsers"] -->|Fast CDN / Nginx Serving| SSG_PAGES
end
```
---
## 3. Core Architectural Principles
### 3.1 Single Source of Truth (SSOT)
- 모든 연구실 데이터(과제, 멤버, 논문, 특허, 강의, 표준화)는 **SQLite 데이터베이스(`anl.db`)**를 단일 원천으로 관리합니다.
- 데이터 갱신 시 `cmd/export-content` 도구를 통해 프론트엔드의 `content/*.ts` 파일로 동기화되어 코드와 데이터의 정합성을 보장합니다.
### 3.2 Zero-CGO Pure Go & Portability
- SQLite 드라이버로 **`modernc.org/sqlite`**를 채택하여 CGO 의존성을 완전히 제거했습니다.
- C 컴파일러 없이도 단일 실행 바이너리(`bin/api`, `bin/seed`, `bin/export-content`)로 컴파일되며, macOS, Linux, Docker 컨테이너 등 어떤 환경에서도 즉시 구동됩니다.
### 3.3 High-Performance Read Optimization (WAL Mode)
- 랜딩페이지 특성상 읽기(Read) 요청이 99% 이상이므로, SQLite 연결 시 **WAL(Write-Ahead Logging)** 모드와 `busy_timeout=5000`을 적용하여 동시 다발적인 읽기 트랜잭션에서 잠금 경합 없는 고속 처리를 보장합니다.
### 3.4 Strict Static Build Compliance (DM-03 / AC-17)
- 프론트엔드 빌드 시 런타임 API 호출 의존성을 배제하여 Next.js 정적 사전 렌더링(`○ Static` / `● SSG`) 원칙을 100% 준수합니다.
---
## 4. Layered Module Structure
```
backend/
├── cmd/
│ ├── api/main.go # Gin HTTP REST API 서버 엔드포인트 (:8080)
│ ├── seed/main.go # SQLite DDL 마이그레이션 및 JSON 시드 데이터 인제스트 도구
│ └── export-content/main.go # SQLite DB 데이터를 프론트엔드 content/*.ts 파일로 내보내는 도구
├── internal/
│ ├── db/
│ │ ├── db.go # SQLite 커넥션 풀 관리 및 PRAGMA 설정 (WAL, foreign_keys)
│ │ └── migrations/ # golang-migrate SQL DDL 스크립트 (000001_init.up.sql / down.sql)
│ ├── models/ # 도메인별 데이터 구조체 모델
│ │ ├── home.go # ResearchProject, ResearchArea, StatsSummary
│ │ ├── members.go # Member, Alumnus
│ │ ├── publications.go # Publication, Patent
│ │ ├── lectures.go # Semester, Course
│ │ └── standardization.go # StandardsBody, StandardProject, StandardDocument
│ ├── repository/ # database/sql 기반 비즈니스 쿼리 계층 (N+1 방지 일괄 쿼리 최적화)
│ ├── handlers/ # Gin HTTP 요청 바인딩 및 JSON 응답 핸들러
│ ├── router/ # API 라우트 등록, CORS 미들웨어 및 통합 테스트
│ └── httpx/ # 표준 JSON 응답 봉투 ({ data: ... } / { error: ... })
├── seed/data/ # 손실 없이 추출된 원본 JSON 데이터셋
└── Makefile # 빌드, 테스트, 시드, 실행 자동화 타겟
```
---
## 5. Data Flow Workflows
### 5.1 Initial Seeding & Database Bootstrap
1. `make seed` 실행
2. `cmd/seed`가 SQLite 데이터베이스 파일(`anl.db`)을 생성
3. 임베디드된 마이그레이션 DDL(`000001_init.up.sql`)을 실행하여 11개 테이블과 인덱스 생성
4. `seed/data/*.json` 파일들을 읽어 트랜잭션 내에서 일괄 삽입
5. 각 테이블별 행(Row) 개수 및 5대 대표 논문(`is_highlight=1`) 정합성을 자동 검증
### 5.2 Content Export & Frontend Build
1. 데이터베이스 변경 후 `make export-content` 실행
2. `cmd/export-content`가 SQLite에서 최신 데이터를 조회
3. `refer_landing_page/content/*.ts` 파일들을 타입 안전한 TypeScript 상수로 덮어쓰기 생성
4. Next.js 빌드(`npm run build`)를 통해 15개 정적 HTML 페이지가 100% SSG로 생성
### 5.3 Live REST API Serving
1. `make run` 또는 `./bin/api` 실행 (기본 포트 `:8080`)
2. 클라이언트가 `/api/v1/*` 엔드포인트로 HTTP GET 요청
3. Gin 라우터가 요청을 파싱하고 적절한 도메인 핸들러로 전달
4. 리포지토리 계층이 파라미터화된 SQL 쿼리로 SQLite 조회 후 일관된 JSON Envelope 형식으로 반환
+119
View File
@@ -0,0 +1,119 @@
# AI Agent Networking Lab (ANL) — Backend Database Schema Specification
This document defines the SQLite database schema and REST API entities to decouple data from the frontend landing page.
---
## 1. `/home` (Home & Overview)
### 1.1 `research_projects` (연구 과제)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `slug` (TEXT, UNIQUE, NOT NULL) — e.g. `iot-standards`, `ccis`, `vlc-iot`
- `title` (TEXT, NOT NULL) — e.g. `AIoT & AI-RAN`, `MCM`, `MVQM`
- `abstract` (TEXT, NOT NULL)
- `keywords` (TEXT, JSON Array) — e.g. `["AIoT", "agent-based IoT", "AI-RAN"]`
- `organization` (TEXT) — e.g. `National Research Foundation of Korea (NRF)`, `IEC/TC 100/WG 12 supported by KEIT`
- `standards_org` (TEXT) — e.g. `NRF`, `IEC TC100`
- `period` (TEXT) — e.g. `2026 ~ Present`
- `funder` (TEXT, OPTIONAL)
- `created_at` (DATETIME)
- `updated_at` (DATETIME)
### 1.2 `research_areas` (연구 분야)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `name_en` (TEXT, NOT NULL) — e.g. `AI Agent Networking`, `QUIC & Transport Protocol`
- `name_kr` (TEXT) — e.g. `AI 에이전트 네트워킹`
- `display_order` (INTEGER, DEFAULT 0)
---
## 2. `/members` (구성원 및 졸업생)
### 2.1 `members` (현재 연구원)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `name_ko` (TEXT, NOT NULL) — e.g. `고석주`
- `name_en` (TEXT, NOT NULL) — e.g. `Seok-Joo Koh`
- `degree` (TEXT, NOT NULL) — `Professor`, `PhD`, `PhDCandidate`, `PhDStudent`, `MSCandidate`, `MSStudent`
- `affiliation` (TEXT) — e.g. `School of Computer Science and Engineering`
- `email` (TEXT)
- `is_advisor` (BOOLEAN, DEFAULT FALSE)
- `display_order` (INTEGER, DEFAULT 0)
- `created_at` (DATETIME)
### 2.2 `alumni` (졸업생)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `name_ko` (TEXT, NOT NULL)
- `name_en` (TEXT, NOT NULL)
- `degree` (TEXT, NOT NULL) — `PhD`, `MS`
- `graduated_at` (TEXT, NOT NULL) — `YYYY-MM` (e.g. `2024-02`)
- `major` (TEXT) — e.g. `Computer Science and Engineering`
- `current_position` (TEXT) — e.g. `Samsung Electronics, Senior Researcher`
- `created_at` (DATETIME)
---
## 3. `/publications` (연구 실적 & 지식재산권)
### 3.1 `publications` (학술 논문)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `category` (TEXT, NOT NULL) — `intl-journal-conf`, `domestic-journal-conf`
- `title` (TEXT, NOT NULL)
- `authors` (TEXT) — e.g. `Seok-Joo Koh, ...`
- `venue` (TEXT, NOT NULL) — e.g. `IEEE Transactions on Network and Service Management`
- `volume` (TEXT) — e.g. `Vol. 23, pp. 4490~4505, April 2026`
- `published_at` (TEXT, NOT NULL) — `YYYY-MM` or `YYYY-MM-DD`
- `kci` (BOOLEAN, DEFAULT FALSE)
- `doi` (TEXT)
- `is_highlight` (BOOLEAN, DEFAULT FALSE) — For Top Representative Highlights
- `created_at` (DATETIME)
### 3.2 `patents` (특허)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `title` (TEXT, NOT NULL)
- `inventors` (TEXT, NOT NULL)
- `application_no` (TEXT, NOT NULL)
- `application_at` (TEXT, NOT NULL) — `YYYY-MM-DD`
- `registration_no` (TEXT)
- `registration_at` (TEXT) — `YYYY-MM-DD`
- `country` (TEXT, NOT NULL) — e.g. `KR`, `US`
- `published_at` (TEXT, NOT NULL)
- `created_at` (DATETIME)
---
## 4. `/lectures` (강의)
### 4.1 `semesters` (학기)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `year` (INTEGER, NOT NULL) — e.g. `2026`
- `term` (TEXT, NOT NULL) — `Spring`, `Fall`
### 4.2 `courses` (개설 과목)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `semester_id` (INTEGER, NOT NULL, FK -> semesters.id)
- `code` (TEXT, NOT NULL) — e.g. `COMP0411`
- `name_en` (TEXT, NOT NULL) — e.g. `Computer Networks`
- `name_kr` (TEXT)
- `note` (TEXT) — e.g. `Graduate Course`
---
## 5. `/standardization` (국제 표준화)
### 5.1 `standards_bodies` (표준화 기구)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `org` (TEXT, NOT NULL) — e.g. `IEC TC100`, `ITU-T SG17`
- `full_name` (TEXT, NOT NULL)
- `scope` (TEXT, NOT NULL) — `international`, `domestic`
- `period` (TEXT, NOT NULL) — e.g. `2018 ~ Present`
- `role` (TEXT) — e.g. `Project Editor`
### 5.2 `standard_documents` (표준 문서 및 기고서)
- `id` (INTEGER, PK, AUTOINCREMENT)
- `body_id` (INTEGER, NOT NULL, FK -> standards_bodies.id)
- `wg` (TEXT, NOT NULL) — e.g. `WG 12`
- `project_name` (TEXT, NOT NULL)
- `title` (TEXT, NOT NULL)
- `doc_ref` (TEXT, NOT NULL) — e.g. `IEC 63430 ED1`
- `status` (TEXT, NOT NULL) — `IS`, `TS`, `TR`, `CDV`, `WDTR`, `InProgress`
- `published_at` (TEXT)
+7 -6
View File
@@ -4,10 +4,10 @@
| 항목 | 내용 |
| :--- | :--- |
| **문서 ID** | `REQ-ANL-WEB-001` |
| **버전** | `1.14` |
| **작성일** | 2026-07-30 |
| **작성자** | claude (`canary-projects-landing-page-creator-claude`) · Role: planner |
| **상태** | 승인 (Approved) — v1.5에서 **D-01** 확정(A·B안 기각 / `intl-conf` 폐기 · 3개 카테고리), v1.6에서 **D-06** 확정(레이아웃 컨테이너 3계층 분리). v1.7에서 **§6.1.2 Research Project PageView** 신설 — 신규 미결 **D-07**(🔴 Funder 실데이터 부재)·**D-08**(🟡 프로젝트 집합 범위)은 각 기본값으로 구현을 차단하지 않는다. 잔여 미결 D-02·D-03·D-05는 🟡 등급으로 각 기본값이 적용되어 승인을 보류하지 않는다. v1.9에서 **§8.7 색 토큰 알파 계약**(R-8.21~R-8.24)을 신설하고 **D-09**(Publications 인덱스 라우트)를 확정했다. v1.10에서 개발 팀장(Agy) 이의제기를 반영해 **D-09를 Overview 뷰로 재확정**하고 R-8.25·AC-41을 신설했다. v1.11에서 강조색 교체 커밋(`eaddc4f`)의 대비 붕괴를 규명해 **§8.7에 R-8.26~R-8.28**(강조색 대비 바닥값 · 색값 변경 재측정 의무 · 명암 반전 계약)을 신설하고 **D-10을 확정**(`--cobalt` #4A7CE8 유지 + `--cobalt-dark` #32549E 재조정), **AC-43~AC-46**(대비 게이트 Gate 10 및 그 폴트 인젝션)을 신설했다. 신규 미결 **D-11**(🟡 `text-vermillion` 소형 텍스트 30여 곳)은 현행 유지를 기본값으로 하여 구현을 차단하지 않는다. v1.12에서 개발 팀장(Agy) 이의제기(Job `7f136931`)를 반영해 **D-10의 배지·칩 전경/배경을 재확정**(`bg-cobalt text-ink` 폐기 → `bg-{accent}-dark text-ivory`)하고 **R-8.29**(반전 토큰 쌍의 `dark:` 불필요 원칙)·**AC-47**(팔레트 외 색 상한)을 신설했다. v1.13에서 Primary #4E77FF 전면 적용 커밋군(`ca4a7ee`~`082d71e`)을 검증해 **Gate 0~10 전량 통과 상태에서 AA 미달 6건·통일성 결함 9건**을 실측 확정하고, 게이트 범위 결함 3종에 대해 **R-8.30~R-8.32**(비텍스트 UI 대비 · 역할 단위 색 통일 · 게이트 면제 형식 제한)와 **AC-48~AC-50**(Gate 11 · 폴트 인젝션 F1~F9 · `docs/DESIGN.md` 동기화)을 신설했다. **§8.1을 실제 토큰값으로 동기화**하고 **D-10 확정값을 #345AD2로 개정**했다. 신규 미결 **D-12**(🔴 Primary 적용의 판정 기준 및 히어로 `Orchestration` 시맨틱 규약)는 **A안+B안 병행**을 기본값으로 하여 구현을 차단하지 않으며, **D-13**(🟡 `--vermillion` 반전 1.25)은 현행 유지 + 화이트리스트 등재를 기본값으로 한다. v1.14에서 개발 팀장(Agy) 이의제기(Job `4962a667`)를 반영해 **R-8.33**(토큰 등급 불변식)을 신설하고 **R-8.29를 강화**(강조색 `dark:` 변형 금지 · 현행 위반 38건)했으며, 다크 테마 `bg-paper` 소형 텍스트 **8곳의 AA 미달(4.37)****G-7**(AC-44 정규식이 다크 절반을 설계상 면제)을 확정해 **AC-51**을 신설했다. 신규 미결 **D-14**(🔴 `--cobalt` 반전 1.00)는 **A안(브랜드 고정 예외)** 을 기본값으로 하되 규격 완화를 포함하므로 **cline·GM 심사 전에는 집행하지 않는다** |
| **버전** | `2.0` |
| **작성일** | 2026-08-24 |
| **작성자** | agy (`herdr:creator-agy-01`) / claude (`herdr:planner-reviewer-claude-01`) · Role: worker / planner |
| **상태** | 승인 (Approved) — v2.0에서 **DM-03 및 AC-17 개정**(Go/Gin 백엔드 REST API 연동 및 `force-dynamic` + `cache: 'no-store'` 기반 실시간 동적 런타임 페칭 도입, 6개 데이터 라우트 `ƒ Dynamic` 및 4개 프레임워크 라우트 `○ Static` 분리 확정). |
| **대상 독자** | General Manager, Developer Team Leader(Agy), Reviewer Team Leader(Cline), 지도교수 |
| **선행 문서** | [`README.md`](./README.md), [`.agents/MULTI_AGENT_RULES.md`](./.agents/MULTI_AGENT_RULES.md), [`refer_landing_page/docs/DESIGN.md`](./refer_landing_page/docs/DESIGN.md) |
| **적용 범위** | `refer_landing_page/` (Next.js 14 App Router 애플리케이션) 전체 |
@@ -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 *(v1.1 문구 정정)* | 데이터는 **빌드 타임 정적 상수**로 유지한다. 런타임 fetch·외부 API·DB를 도입하지 않는다. 전 라우트가 **빌드 타임 정적 프리렌더**(`○ Static` 또는 `● SSG`)를 유지해야 하며, `ƒ Dynamic` 라우트가 발생해서는 안 된다 (§8.5 R-8.16) | 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-04 | 각 페이지 상단의 `// CUSTOMIZATION HOOK` 주석 규약(`DESIGN.md` "콘텐츠 우선" 원칙)은 `content/` 모듈로 이전하되 **규약 자체는 유지**한다 | MUST |
### 7.2 스키마 정의 (계약)
@@ -648,7 +648,7 @@ Job `343b5808` 검증에서 본문 좌우 여백·최대 폭 미적용(B1)이,
**품질 게이트**
- [ ] AC-15 `npm run lint` 통과 (경고 0)
- [ ] AC-16 `npx tsc --noEmit` 통과
- [ ] AC-17 *(v1.1 개정)* `npm run build` 성공. 빌드 리포트에 **`ƒ (Dynamic)` 라우트가 0건**이고 전 라우트가 `○ (Static)` 또는 `● (SSG)`이다 — `●`도 빌드 타임 정적 프리렌더이므로 충족으로 판정한다 (§8.5 R-8.16)
- [ ] AC-17 *(v2.0 개정)* `npm run build` 성공. 빌드 리포트에 **정확히 6개 데이터 라우트가 `ƒ (Dynamic)`**(`force-dynamic`)이고 **4개 프레임워크/메타데이터 라우트가 `○ (Static)`**으로 분리되어 생성됨을 확인한다 (§7.1 DM-03)
- [ ] AC-18 First Load JS 증가분 ≤ 30 kB
- [ ] AC-19 4종 뷰포트에서 가로 스크롤 없음
- [ ] AC-20 신규 Tailwind 반응형 유틸리티가 빌드 CSS에 실제 생성됨을 확인 (JIT 누락 시 빌드는 성공하나 조판이 무너짐)
@@ -1049,3 +1049,4 @@ G-1은 **N6**(Gate 8 C1/C2 공허 단정, 10라운드 미해소)과 **동일 유
| 1.12 | 2026-07-31 | claude (planner) | 개발 팀장(Agy) 기술 이의제기(Job `7f136931`) 반영. **D-10 배지·칩 결정 재확정** — v1.11의 `bg-cobalt text-ink dark:text-ivory`(5.03 / 4.82)는 채택안 C의 원칙(작은 요소는 `-dark` 라우팅)을 **소형 텍스트 12곳에만 적용하고 동일 크기 등급의 상태 칩 2곳에는 적용하지 않은 일관성 결함**이었음을 인정하고 폐기, **`bg-{accent}-dark text-ivory`** 로 교체(6.38~6.43 / 6.57~6.72). 이의제기의 권장 전경 `text-white dark:text-ivory`(7.25)는 **미채택** — `-dark` 배경에서는 `ivory`만으로 양 테마가 통과해 **팔레트 외 색 `white`와 `dark:` 변형이 모두 불필요**하다(**R-8.29 신설**). 아울러 권장안을 그대로 적용하면 `standardization:87-89` 삼항식의 나란한 두 칩 상대휘도 대비가 **1.19 → 1.54로 악화**됨을 실측해, vermillion 칩도 `-dark`로 통일하여 **1.01** 균형을 달성(**R-8.26에 배지·칩 라우팅 및 짝 칩 등급 일치 조항 추가**). 배지류를 **부류 A(`text-[0.65rem]` 상태 칩 4곳) / 부류 B(인터랙티브 내비·토글 5곳)** 로 분리해 **AC-42 개정**, `-dark`가 **배경**으로 쓰이는 조합을 **AC-43 검사 대상에 추가**(v1.11 목록은 전경만 다루어 칩 배경을 놓쳤다), **AC-47 신설**(팔레트 외 색 상한 8건 화이트리스트), **AC-46에 폴트 인젝션 ⑤·⑥ 추가**(폐기된 v1.11 칩 패턴 및 `white` 증가 검출). 부류 A vermillion 칩 2곳의 배경이 #D9342B → #A8231C로 어두워지는 **시각 변경은 GM 승인 사항으로 명시**하되 차단 사유가 아님을 기록. Job `3d3b823a` |
| 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 정규식의 **부정 후방탐색 `(?<!dark:)`로 다크 절반을 설계상 면제**하고 있었다(**G-7** — G-1·G-2·G-3에 이은 네 번째 "검사를 절반만 한다" 유형). **처방을 토큰 조정에서 `dark:` 변형 제거로 재설계** — `-dark`는 반전 토큰이므로 `dark:` 없이 4개 셀 전부(5.26/4.76/6.57/5.96)를 통과한다. **R-8.29 강화**(강조색 `dark:` 금지 · 정본 패턴 3종 · 현행 위반 **38건**), **R-8.26·AC-43·AC-44·AC-48(C11-9)·AC-49(F10·F11, 총 11건) 개정**. **D-14 신설**(🔴 `--cobalt` 반전 1.00 — A안 브랜드 고정 예외 권고 / B안 대칭 상향 정정 수치 `#A8BCFF`+`#C7D4FF`). A안은 본 계획자가 작성한 R-8.28의 완화를 포함하므로 **cline·GM 별도 심사 대상**으로 명시했고, 안전망 유지 근거로 `#1B3A8A` 역주입 시 AC-43 즉시 실패(1.80)를 제시했다. Job `d35bc3e7` |
| 2.0 | 2026-08-24 | claude (planner) / agy (creator) | **실시간 동적 런타임 페칭 아키텍처 도입 (DM-03·AC-17·R-08 개정)**. Go/Gin 백엔드 REST API(`http://localhost:8080/api/v1`) 연동 및 `force-dynamic` + `cache: 'no-store'` 적용으로 전 페이지 실시간 데이터 바인딩 지원. 6개 데이터 라우트를 `ƒ (Dynamic)`으로, 4개 프레임워크 라우트를 `○ (Static)`으로 분리. 기존 `content/*.ts` 정적 상수를 장애 시 그레이스풀 폴백 데이터셋으로 유지. E2E 테스트 게이트 2, 4, 7, 8을 라이브 서버 기반 검증으로 리팩터링. Job `258f6ff7` |
+93 -11
View File
@@ -1,16 +1,98 @@
`body > header > div.hidden.border-b.border-line.desktop\:block > div > span:nth-child(3)`
- "Daegu / South Korea" 로 수정
`body > main > div > section.space-y-3 > div > span.font-mono.text-xs.tracking-widest.uppercase.text-cobalt-dark.font-bold`
- 삭제
`body > footer > div.container-content.grid.gap-12.py-16.lg\:grid-cols-12 > div.lg\:col-span-5 > h3``body > footer > div.container-content.grid.gap-12.py-16.lg\:grid-cols-12 > div.lg\:col-span-5 > h3` 앞에 favicon logo 넣어서 `body > header > div.container-content.flex.h-\[4\.25rem\].items-center.justify-between.gap-4.my-2.sm\:my-3 > a`와 같이 변경해줘.
`body > main > div > section.space-y-3 > h1`
- 연구실 구성원 -> "LAB MEMBERS"
`body > footer > div.border-t.border-line > div > p.kicker.text-ink-mute`
-거해줘.
`body > main > div > section.space-y-3 > p`
-
`body > footer > div.container-content.grid.gap-12.py-16.lg\:grid-cols-12 > div.lg\:col-span-4 > address`
- 교수 정보, 이메일 주소, 주소 순으로 배치
`body > main > div > section:nth-child(2) > div.font-mono.text-xs.text-cobalt-dark.uppercase.tracking-widest.font-bold`
- 삭제
Footer 레이아웃을 조금 더 구조적이고 모던하게 보이도록 레이아웃을 변경
`body > main > div > section:nth-child(2) > div.font-mono.text-xs.text-cobalt-dark.uppercase.tracking-widest.font-bold`
- "고석주 교수" -> "Prof. Seok-Joo Koh"로 변경
`body > main > div > section.relative > div.grid.grid-cols-1.lg\:grid-cols-12.gap-8.lg\:items-center > div.lg\:col-span-7.space-y-6.lg\:pt-4 > div`
- International Journals & Conference, Standardization Contributions, Patent 순으로 배치
- "PATENT" -> "PATENT\n(IoT Architecture \& Protocols) 수정
`body > main > div > section:nth-child(2) > div.bg-paper.p-8.rounded-lg.border.border-line.grid.grid-cols-1.lg\:grid-cols-12.gap-8.items-center > div.lg\:col-span-8.space-y-4 > div.flex.items-baseline.gap-3 > span`
- 삭제
`body > main > div > section:nth-child(2) > div.bg-paper.p-8.rounded-lg.border.border-line.grid.grid-cols-1.lg\:grid-cols-12.gap-8.items-center > div.lg\:col-span-8.space-y-4 > p`
- 다음 세부 커리어를 기반으로 설명 작성 (영어로) 및 details 링크 연결.
- details 링크 클릭 시 modal dialog를 통해 아래 세부 내용 보여주기
```
# Education
- KAIST, Management Science, B. S., 1992. 2.
- KAIST, Management Science, M. S., 1994. 2.
- KAIST, Industrial Engineering, Ph. D., 1998. 8.
# Career
2024. 3 - Present
Dean
College of IT Engineering (CITE)
Kyungpook National University (KNU)
2015. 12 - Present
Director
Institute of Software Education (SWEDU)
Kyungpook National University (KNU)
2004. 3 - Present
Professor
School of Computer Science and Engineering (CSE)
College of IT Engineering (CITE)
Kyungpook National University (KNU)
1998. 8 - 2004. 2
Senior Researcher
Protocol Engineering Center (PEC)
Electronics Telecommunications Research Institute (ETRI)
# International Standardization
- ISO/IEC JTC1 SC6/WG7: Reliable/Mobile Multicasting (1999 ~ 2019): Project Editor
- ISO/IEC JTC1 SC41/WG5: Internet-of-Things (IoT) Applications (2020 ~ Present): Project Editor
- ITU-T SG13: Mobility Management (1999 ~ 2015): Project Editor
- ITU-T SG20: VLC-based IoT (2018 ~ 2020): Project Editor
- IEC TC100: Multimedia Systems and Equipment (2018 ~ Present): Project Editor, TA15 TAM, WG11 Convenor
# Membership on Academic Society
- KIPS (Korea Information Processing Society)
- KICS (Korean Institute of Communications and Information Sciences)
- KIISE (Korean Institute of Information Scientists and Engineers)
- IEEE (Institute of Electrical and Electronics Engineers)
```
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.flex.justify-between.items-end > div > div`
- 삭제
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.flex.justify-between.items-end > div > h2`
- "재학 연구원" -> Active Researchers
- 글자색 변경: primary color
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div:nth-child(2)`
- PhD -> Ph. D
- PhDCandidate -> Ph. D. Candidate
- PhDStudent -> Ph. D. Student
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.space-y-4.pt-4`
- MSCandidate -> M. S. Candidate
- MSStudent -> M. S. Student
`body > main > div > section:nth-child(4) > div > div > div`
- 삭제
`body > main > div > section:nth-child(4) > div > div > h2`
- 졸업생 목록 -> "Graduates"
`body > main > div > section:nth-child(4) > details > summary > div > span`
- 영어로 작성
`body > main > div > section:nth-child(4) > details > summary > div > p`
- 영어로 작성
`body > main > div > section:nth-child(4) > details > summary > span > span.group-open\:hidden`
- 전체 보기 -> Open
- 삭제된 component들의 크기를 반영하여 화면 비율 조정
+19
View File
@@ -0,0 +1,19 @@
# ==============================================================================
# AI Agent Networking Lab (ANL) Backend API Server Configuration
# ==============================================================================
# Server binding host and port
HOST=0.0.0.0
PORT=8080
# SQLite database file path (Inside container: /app/data/anl.db)
DB_PATH=anl.db
# Gin runtime mode: release | debug | test
GIN_MODE=release
# Allowed CORS Origins (Comma separated, or * for all)
CORS_ALLOW_ORIGINS=*
# Automatically run SQL schema migrations on startup (true | false)
AUTO_MIGRATE=true
+15
View File
@@ -0,0 +1,15 @@
# Binaries and build outputs
/bin/
/export-content
/api
/seed
# SQLite databases and WAL journal files
*.db
*.db-wal
*.db-shm
# Environment files
.env
.env.*
!.env.example
+63
View File
@@ -0,0 +1,63 @@
# ==============================================================================
# Stage 1: Build the Go Binaries
# ==============================================================================
FROM golang:1.26-alpine AS builder
WORKDIR /app
# Install git and certificates
RUN apk add --no-cache git ca-certificates tzdata
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy backend source code
COPY . .
# Build pure-Go static binaries
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/api ./cmd/api && \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/seed ./cmd/seed && \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/export-content ./cmd/export-content
# ==============================================================================
# Stage 2: Minimal Runtime Image
# ==============================================================================
FROM alpine:3.20
WORKDIR /app
# Install ca-certificates and sqlite cli for maintenance
RUN apk add --no-cache ca-certificates tzdata sqlite curl && \
addgroup -S appgroup && adduser -S appuser -G appgroup
# Create persistent database storage directory
RUN mkdir -p /app/data && chown -R appuser:appgroup /app/data
# Copy built binaries from builder
COPY --from=builder /bin/api /app/bin/api
COPY --from=builder /bin/seed /app/bin/seed
COPY --from=builder /bin/export-content /app/bin/export-content
# Copy seed data
COPY --from=builder /app/seed /app/seed
# Copy environment template
COPY --from=builder /app/.env.example /app/.env
# Set environment defaults for container
ENV HOST=0.0.0.0 \
PORT=8080 \
DB_PATH=/app/data/anl.db \
GIN_MODE=release \
CORS_ALLOW_ORIGINS=* \
AUTO_MIGRATE=true
EXPOSE 8080
USER appuser
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/api/v1/health || exit 1
ENTRYPOINT ["/app/bin/api"]
+41
View File
@@ -0,0 +1,41 @@
.PHONY: all build run seed export-content test fmt fmt-check vet clean
BIN_DIR := bin
DB_PATH := anl.db
all: fmt-check vet build test
build:
@mkdir -p $(BIN_DIR)
@echo "Building api server..."
go build -o $(BIN_DIR)/api ./cmd/api
@echo "Building seeder..."
go build -o $(BIN_DIR)/seed ./cmd/seed
@echo "Building content exporter..."
go build -o $(BIN_DIR)/export-content ./cmd/export-content
run: build
./$(BIN_DIR)/api --db $(DB_PATH)
seed: build
./$(BIN_DIR)/seed --db $(DB_PATH) --seed-dir seed/data
export-content: build
@echo "⚠️ Notice: Frontend has migrated to real-time dynamic runtime fetching (force-dynamic)."
@echo "export-content is deprecated to prevent re-introducing static content data files."
./$(BIN_DIR)/export-content --db $(DB_PATH) --out-dir ../refer_landing_page/content
fmt:
gofmt -w .
fmt-check:
@test -z "$$(gofmt -l .)" || (echo "gofmt check failed on files:" && gofmt -l . && exit 1)
vet:
go vet ./...
test: fmt-check vet
go test -v ./...
clean:
rm -rf $(BIN_DIR) *.db*
+129
View File
@@ -0,0 +1,129 @@
# AI Agent Networking Lab (ANL) — Go/Gin/SQLite Backend API
This module implements the standalone backend REST API server, SQLite database storage, seed ingestion pipeline, and static content exporter for the ANL landing page.
---
## 🏛 Architecture Overview
```
+-------------------------------------------------------------+
| SQLite (anl.db) |
| 11 Relational Tables (Projects, Members, Pubs, Lectures) |
+-------------------------------------------------------------+
▲ ▲
│ (cmd/seed) │ (SQL query)
│ │
+-------------------------------+ +-------------------------+
| seed/data/*.json | | Gin REST API Server |
| (extracted from content/*.ts) | | (cmd/api, :8080) |
+-------------------------------+ +-------------------------+
+-------------------------+
| Content Exporter |
| (cmd/export-content) |
+-------------------------+
+-------------------------+
| Next.js Static SSG |
| (refer_landing_page/) |
+-------------------------+
```
---
## 🚀 Quick Start
### 1. Build Binaries
```bash
make build
```
Creates `bin/api`, `bin/seed`, and `bin/export-content`.
### 2. Run Migrations & Seed Database
```bash
make seed
```
Creates `anl.db`, executes SQL DDL migrations, inserts seed data, and verifies all 11 table row counts and the 5 designated highlights.
### 3. Run Backend API Server
```bash
make run
# Or with options:
./bin/api --port 8080 --db anl.db
```
Server starts at `http://localhost:8080/api/v1`.
### 4. Run Unit & Integration Tests
```bash
make test
```
### 5. Export Content to Next.js Frontend
```bash
make export-content
```
Regenerates `refer_landing_page/content/*.ts` from SQLite with typed `isHighlight` metadata.
---
## 📡 REST API Endpoints (`/api/v1`)
| Method | Path | Description |
|---|---|---|
| `GET` | `/api/v1/health` | Service health status check |
| `GET` | `/api/v1/research-projects` | 3 research project entities |
| `GET` | `/api/v1/research-areas` | 14 core research areas ordered by `display_order` |
| `GET` | `/api/v1/stats/summary` | Aggregated counts (`intl_publications`, `standardization_docs`, `patents`) |
| `GET` | `/api/v1/members` | 16 active members (with `is_advisor` flag) |
| `GET` | `/api/v1/alumni` | 60 alumni ordered chronologically |
| `GET` | `/api/v1/publications` | 148 publications (supports `?category=intl-journal-conf` or `domestic-journal-conf`) |
| `GET` | `/api/v1/publications/highlights` | 5 designated top representative publications (`WHERE is_highlight = 1`) |
| `GET` | `/api/v1/patents` | 75 intellectual property patents |
| `GET` | `/api/v1/semesters` | 45 semesters with nested courses, ordered by year DESC and term |
| `GET` | `/api/v1/standards-bodies` | 6 standards bodies with nested projects and documents (deterministic ordering) |
---
## ⚙️ Environment Configuration (`.env`)
Copy `.env.example` to `.env` to customize runtime settings:
```bash
cp .env.example .env
```
| Variable | Default | Description |
|---|---|---|
| `HOST` | `0.0.0.0` | Binding host IP address |
| `PORT` | `8080` | Server listening port |
| `DB_PATH` | `anl.db` | SQLite database file path |
| `GIN_MODE` | `release` | Gin runtime mode (`release`, `debug`, `test`) |
| `CORS_ALLOW_ORIGINS` | `*` | Allowed CORS origins (e.g. `https://anl.knu.ac.kr`) |
| `AUTO_MIGRATE` | `true` | Apply database migrations automatically on startup |
---
## 🐳 Docker & Containerization
### Build & Run Container
```bash
docker build -t anl-backend-api .
docker run -p 8080:8080 -v anl-data:/app/data anl-backend-api
```
### Docker Compose
Run the full backend service with automated healthchecks and volume persistence from the repository root:
```bash
docker compose up -d
```
---
## 📦 Database Schema & Specifications
* **Architecture**: See [ARCHITECTURE.md](../ARCHITECTURE.md)
* **REST API Interfaces**: See [API_INTERFACE.md](../API_INTERFACE.md)
* **Database Schema Spec**: See [DATA_TABLE.md](../DATA_TABLE.md) and [000001_init.up.sql](internal/db/migrations/000001_init.up.sql)
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"flag"
"log"
"git.godopu.com/lab/landing_page/backend/internal/config"
"git.godopu.com/lab/landing_page/backend/internal/db"
"git.godopu.com/lab/landing_page/backend/internal/router"
"github.com/gin-gonic/gin"
)
func main() {
// Load environment variables & .env
cfg := config.Load()
// Optional CLI flags override environment defaults
portFlag := flag.Int("port", 0, "Port for the Gin server to listen on (overrides PORT)")
hostFlag := flag.String("host", "", "Host for the Gin server to bind on (overrides HOST)")
dbPathFlag := flag.String("db", "", "Path to SQLite database file (overrides DB_PATH)")
ginModeFlag := flag.String("mode", "", "Gin mode: release, debug, test (overrides GIN_MODE)")
flag.Parse()
if *portFlag > 0 {
cfg.Port = *portFlag
}
if *hostFlag != "" {
cfg.Host = *hostFlag
}
if *dbPathFlag != "" {
cfg.DBPath = *dbPathFlag
}
if *ginModeFlag != "" {
cfg.GinMode = *ginModeFlag
}
gin.SetMode(cfg.GinMode)
log.Printf("Connecting to SQLite database at %s...", cfg.DBPath)
database, err := db.Connect(cfg.DBPath)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer database.Close()
if cfg.AutoMigrate {
log.Println("Ensuring database schema migrations are applied...")
if err := db.RunMigrations(database); err != nil {
log.Fatalf("Migration failed: %v", err)
}
}
r := router.SetupRouter(database, cfg.CORSAllowOrigins)
addr := cfg.Address()
log.Printf("Starting ANL Backend API server on http://%s/api/v1 (mode: %s)...", addr, cfg.GinMode)
if err := r.Run(addr); err != nil {
log.Fatalf("Server failed to run: %v", err)
}
}
+352
View File
@@ -0,0 +1,352 @@
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"git.godopu.com/lab/landing_page/backend/internal/config"
"git.godopu.com/lab/landing_page/backend/internal/db"
"git.godopu.com/lab/landing_page/backend/internal/repository"
)
func marshalJSONNoEscapeHTML(data any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(data); err != nil {
return nil, err
}
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
func formatTSFile(importStmt, exportConstName, typeAnnotation string, data any) ([]byte, error) {
jsonData, err := marshalJSONNoEscapeHTML(data)
if err != nil {
return nil, fmt.Errorf("marshal JSON: %w", err)
}
header := "// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\n"
content := fmt.Sprintf("%s%s\n\nexport const %s: %s = %s;\n", header, importStmt, exportConstName, typeAnnotation, string(jsonData))
return []byte(content), nil
}
func writeFileSafe(path string, data []byte) error {
return os.WriteFile(path, data, 0644)
}
func main() {
config.LoadEnvFile()
defaultDB := "anl.db"
if envDB := os.Getenv("DB_PATH"); envDB != "" {
defaultDB = envDB
}
dbPath := flag.String("db", defaultDB, "Path to SQLite database file")
targetDir := flag.String("out-dir", "../refer_landing_page/content", "Target content directory")
force := flag.Bool("force", false, "Force export content files despite dynamic runtime architecture")
flag.Parse()
if !*force {
log.Printf("⚠️ NOTICE: Frontend architecture has migrated to real-time dynamic runtime fetching (force-dynamic).")
log.Printf("Exporting static TypeScript files to %s is DEPRECATED to prevent re-introducing static fallback leaks.", *targetDir)
log.Printf("If you really need to export fallback files, pass the --force flag.")
return
}
log.Printf("Exporting content from SQLite (%s) to TypeScript files in %s (forced)...", *dbPath, *targetDir)
database, err := db.Connect(*dbPath)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer database.Close()
ctx := context.Background()
homeRepo := repository.NewHomeRepository(database)
membersRepo := repository.NewMembersRepository(database)
pubsRepo := repository.NewPublicationsRepository(database)
lecturesRepo := repository.NewLecturesRepository(database)
standardsRepo := repository.NewStandardizationRepository(database)
// 1. Projects
projects, err := homeRepo.GetResearchProjects(ctx)
if err != nil {
log.Fatalf("Get research projects: %v", err)
}
type TSProject struct {
Slug string `json:"slug"`
Title string `json:"title"`
Abstract string `json:"abstract"`
Keywords []string `json:"keywords"`
StandardsTrack *string `json:"standardsTrack,omitempty"`
StandardsOrg *string `json:"standardsOrg,omitempty"`
Period *string `json:"period,omitempty"`
Deliverables []any `json:"deliverables"`
Funder *string `json:"funder,omitempty"`
}
var tsProjects []TSProject
for _, p := range projects {
tsProjects = append(tsProjects, TSProject{
Slug: p.Slug,
Title: p.Title,
Abstract: p.Abstract,
Keywords: p.Keywords,
StandardsTrack: p.Organization,
StandardsOrg: p.StandardsOrg,
Period: p.Period,
Deliverables: []any{},
Funder: p.Funder,
})
}
pBytes, err := formatTSFile(`import { ResearchProject } from "./types";`, "researchProjects", "ResearchProject[]", tsProjects)
if err != nil {
log.Fatalf("Format projects.ts failed: %v", err)
}
if err := writeFileSafe(filepath.Join(*targetDir, "projects.ts"), pBytes); err != nil {
log.Fatalf("Write projects.ts failed: %v", err)
}
log.Println("✓ Exported projects.ts")
// 2. Members & Alumni
members, err := membersRepo.GetMembers(ctx)
if err != nil {
log.Fatalf("Get members: %v", err)
}
type TSMember struct {
Ko string `json:"ko"`
En string `json:"en"`
Degree string `json:"degree"`
Affiliation *string `json:"affiliation,omitempty"`
}
var tsMembers []TSMember
for _, m := range members {
tsMembers = append(tsMembers, TSMember{
Ko: m.NameKo,
En: m.NameEn,
Degree: m.Degree,
Affiliation: m.Affiliation,
})
}
alumni, err := membersRepo.GetAlumni(ctx)
if err != nil {
log.Fatalf("Get alumni: %v", err)
}
type TSAlumnus struct {
Ko string `json:"ko"`
En string `json:"en"`
Degree string `json:"degree"`
GraduatedAt string `json:"graduatedAt"`
Major *string `json:"major,omitempty"`
CurrentPosition *string `json:"currentPosition,omitempty"`
}
var tsAlumni []TSAlumnus
for _, a := range alumni {
tsAlumni = append(tsAlumni, TSAlumnus{
Ko: a.NameKo,
En: a.NameEn,
Degree: a.Degree,
GraduatedAt: a.GraduatedAt,
Major: a.Major,
CurrentPosition: a.CurrentPosition,
})
}
mJSON, err := marshalJSONNoEscapeHTML(tsMembers)
if err != nil {
log.Fatalf("Marshal members failed: %v", err)
}
aJSON, err := marshalJSONNoEscapeHTML(tsAlumni)
if err != nil {
log.Fatalf("Marshal alumni failed: %v", err)
}
membersTS := fmt.Sprintf("// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\nimport { Member, Alumnus } from \"./types\";\n\nexport const activeMembers: Member[] = %s;\n\nexport const alumni: Alumnus[] = %s;\n", string(mJSON), string(aJSON))
if err := writeFileSafe(filepath.Join(*targetDir, "members.ts"), []byte(membersTS)); err != nil {
log.Fatalf("Write members.ts failed: %v", err)
}
log.Println("✓ Exported members.ts")
// 3. Publications & Patents
pubs, err := pubsRepo.GetPublications(ctx, "")
if err != nil {
log.Fatalf("Get publications: %v", err)
}
type TSPublication struct {
Category string `json:"category"`
Title string `json:"title"`
Authors *string `json:"authors,omitempty"`
Venue string `json:"venue"`
Volume *string `json:"volume,omitempty"`
PublishedAt string `json:"publishedAt"`
KCI bool `json:"kci,omitempty"`
DOI *string `json:"doi,omitempty"`
IsHighlight bool `json:"isHighlight,omitempty"`
}
var tsPubs []TSPublication
for _, p := range pubs {
tsPubs = append(tsPubs, TSPublication{
Category: p.Category,
Title: p.Title,
Authors: p.Authors,
Venue: p.Venue,
Volume: p.Volume,
PublishedAt: p.PublishedAt,
KCI: p.KCI,
DOI: p.DOI,
IsHighlight: p.IsHighlight,
})
}
patents, err := pubsRepo.GetPatents(ctx)
if err != nil {
log.Fatalf("Get patents: %v", err)
}
type TSPatent struct {
Category string `json:"category"`
Title string `json:"title"`
Inventors string `json:"inventors"`
ApplicationNo string `json:"applicationNo"`
ApplicationAt string `json:"applicationAt"`
RegistrationNo *string `json:"registrationNo,omitempty"`
RegistrationAt *string `json:"registrationAt,omitempty"`
Country string `json:"country"`
PublishedAt string `json:"publishedAt"`
}
var tsPatents []TSPatent
for _, pat := range patents {
tsPatents = append(tsPatents, TSPatent{
Category: "patent",
Title: pat.Title,
Inventors: pat.Inventors,
ApplicationNo: pat.ApplicationNo,
ApplicationAt: pat.ApplicationAt,
RegistrationNo: pat.RegistrationNo,
RegistrationAt: pat.RegistrationAt,
Country: pat.Country,
PublishedAt: pat.PublishedAt,
})
}
pubsJSON, err := marshalJSONNoEscapeHTML(tsPubs)
if err != nil {
log.Fatalf("Marshal publications failed: %v", err)
}
patentsJSON, err := marshalJSONNoEscapeHTML(tsPatents)
if err != nil {
log.Fatalf("Marshal patents failed: %v", err)
}
pubsTS := fmt.Sprintf("// AUTO-GENERATED by backend/cmd/export-content — DO NOT EDIT BY HAND\nimport { Publication, Patent } from \"./types\";\n\nexport const publications: Publication[] = %s;\n\nexport const patents: Patent[] = %s;\n", string(pubsJSON), string(patentsJSON))
if err := writeFileSafe(filepath.Join(*targetDir, "publications.ts"), []byte(pubsTS)); err != nil {
log.Fatalf("Write publications.ts failed: %v", err)
}
log.Println("✓ Exported publications.ts (with isHighlight metadata & unescaped HTML entities)")
// 4. Lectures
semesters, err := lecturesRepo.GetSemestersWithCourses(ctx)
if err != nil {
log.Fatalf("Get semesters: %v", err)
}
type TSCourse struct {
Code string `json:"code"`
En string `json:"en"`
Note *string `json:"note,omitempty"`
}
type TSSemester struct {
Term string `json:"term"`
Year int `json:"year"`
Courses []TSCourse `json:"courses"`
}
var tsSemesters []TSSemester
for _, s := range semesters {
var tsCourses []TSCourse
for _, c := range s.Courses {
tsCourses = append(tsCourses, TSCourse{
Code: c.Code,
En: c.NameEn,
Note: c.Note,
})
}
tsSemesters = append(tsSemesters, TSSemester{
Term: s.Term,
Year: s.Year,
Courses: tsCourses,
})
}
lecturesBytes, err := formatTSFile(`import { Semester } from "./types";`, "semesters", "Semester[]", tsSemesters)
if err != nil {
log.Fatalf("Format lectures.ts failed: %v", err)
}
if err := writeFileSafe(filepath.Join(*targetDir, "lectures.ts"), lecturesBytes); err != nil {
log.Fatalf("Write lectures.ts failed: %v", err)
}
log.Println("✓ Exported lectures.ts")
// 5. Standards
standards, err := standardsRepo.GetStandardsBodiesWithProjects(ctx)
if err != nil {
log.Fatalf("Get standards bodies: %v", err)
}
type TSDoc struct {
Title string `json:"title"`
Ref string `json:"ref"`
Status string `json:"status"`
PublishedAt *string `json:"publishedAt,omitempty"`
}
type TSStandardProject struct {
WG string `json:"wg"`
Name string `json:"name"`
Documents []TSDoc `json:"documents"`
}
type TSStandardsBody struct {
Org string `json:"org"`
Full string `json:"full"`
Scope string `json:"scope"`
Period string `json:"period"`
Role *string `json:"role,omitempty"`
Projects []TSStandardProject `json:"projects"`
}
var tsStandards []TSStandardsBody
for _, b := range standards {
var tsProjs []TSStandardProject
for _, p := range b.Projects {
var tsDocs []TSDoc
for _, d := range p.Documents {
tsDocs = append(tsDocs, TSDoc{
Title: d.Title,
Ref: d.DocRef,
Status: d.Status,
PublishedAt: d.PublishedAt,
})
}
tsProjs = append(tsProjs, TSStandardProject{
WG: p.WG,
Name: p.Name,
Documents: tsDocs,
})
}
tsStandards = append(tsStandards, TSStandardsBody{
Org: b.Org,
Full: b.FullName,
Scope: b.Scope,
Period: b.Period,
Role: b.Role,
Projects: tsProjs,
})
}
standardsBytes, err := formatTSFile(`import { StandardsBody } from "./types";`, "standardsBodies", "StandardsBody[]", tsStandards)
if err != nil {
log.Fatalf("Format standards.ts failed: %v", err)
}
if err := writeFileSafe(filepath.Join(*targetDir, "standards.ts"), standardsBytes); err != nil {
log.Fatalf("Write standards.ts failed: %v", err)
}
log.Println("✓ Exported standards.ts")
log.Println("=== All content successfully exported from SQLite! ===")
}
+345
View File
@@ -0,0 +1,345 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"git.godopu.com/lab/landing_page/backend/internal/config"
"git.godopu.com/lab/landing_page/backend/internal/db"
)
type SeedProject struct {
Slug string `json:"slug"`
Title string `json:"title"`
Abstract string `json:"abstract"`
Keywords []string `json:"keywords"`
StandardsTrack *string `json:"standardsTrack"`
StandardsOrg *string `json:"standardsOrg"`
Period *string `json:"period"`
Funder *string `json:"funder"`
}
type SeedArea struct {
NameEn string `json:"name_en"`
NameKr *string `json:"name_kr"`
DisplayOrder int `json:"display_order"`
}
type SeedMember struct {
NameKo string `json:"name_ko"`
NameEn string `json:"name_en"`
Degree string `json:"degree"`
Affiliation *string `json:"affiliation"`
Email *string `json:"email"`
IsAdvisor bool `json:"is_advisor"`
DisplayOrder int `json:"display_order"`
}
type SeedAlumnus struct {
NameKo string `json:"name_ko"`
NameEn string `json:"name_en"`
Degree string `json:"degree"`
GraduatedAt string `json:"graduated_at"`
Major *string `json:"major"`
CurrentPosition *string `json:"current_position"`
}
type SeedPublication struct {
Category string `json:"category"`
Title string `json:"title"`
Authors *string `json:"authors"`
Venue string `json:"venue"`
Volume *string `json:"volume"`
PublishedAt string `json:"published_at"`
KCI bool `json:"kci"`
DOI *string `json:"doi"`
IsHighlight bool `json:"is_highlight"`
}
type SeedPatent struct {
Title string `json:"title"`
Inventors string `json:"inventors"`
ApplicationNo string `json:"application_no"`
ApplicationAt string `json:"application_at"`
RegistrationNo *string `json:"registration_no"`
RegistrationAt *string `json:"registration_at"`
Country string `json:"country"`
PublishedAt string `json:"published_at"`
}
type SeedCourse struct {
Code string `json:"code"`
En string `json:"en"`
Note *string `json:"note"`
}
type SeedSemester struct {
Term string `json:"term"`
Year int `json:"year"`
Courses []SeedCourse `json:"courses"`
}
type SeedDoc struct {
Title string `json:"title"`
Ref string `json:"ref"`
Status string `json:"status"`
PublishedAt *string `json:"publishedAt"`
}
type SeedStandardProject struct {
WG string `json:"wg"`
Name string `json:"name"`
Documents []SeedDoc `json:"documents"`
}
type SeedStandardsBody struct {
Org string `json:"org"`
Full string `json:"full"`
Scope string `json:"scope"`
Period string `json:"period"`
Role *string `json:"role"`
Projects []SeedStandardProject `json:"projects"`
}
func readJSONFile[T any](path string) (T, error) {
var result T
data, err := os.ReadFile(path)
if err != nil {
return result, fmt.Errorf("read file %s: %w", path, err)
}
if err := json.Unmarshal(data, &result); err != nil {
return result, fmt.Errorf("unmarshal %s: %w", path, err)
}
return result, nil
}
func main() {
config.LoadEnvFile()
defaultDB := "anl.db"
if envDB := os.Getenv("DB_PATH"); envDB != "" {
defaultDB = envDB
}
dbPath := flag.String("db", defaultDB, "SQLite database file path")
seedDir := flag.String("seed-dir", "seed/data", "Directory containing seed JSON files")
flag.Parse()
log.Printf("Connecting to SQLite database at %s...", *dbPath)
database, err := db.Connect(*dbPath)
if err != nil {
log.Fatalf("Database connection failed: %v", err)
}
defer database.Close()
log.Println("Applying schema migrations...")
if err := db.RunMigrations(database); err != nil {
log.Fatalf("Migration failed: %v", err)
}
tx, err := database.Begin()
if err != nil {
log.Fatalf("Begin transaction failed: %v", err)
}
defer tx.Rollback()
// Clear existing records in reverse FK order
tables := []string{
"standard_documents", "standards_bodies",
"courses", "semesters",
"patents", "publications",
"alumni", "members",
"research_areas", "research_projects",
}
for _, t := range tables {
if _, err := tx.Exec("DELETE FROM " + t); err != nil {
log.Fatalf("Failed to clear table %s: %v", t, err)
}
}
// 1. Research Projects
projects, err := readJSONFile[[]SeedProject](filepath.Join(*seedDir, "research_projects.json"))
if err != nil {
log.Fatalf("Failed to read research projects: %v", err)
}
for _, p := range projects {
kwJSON, _ := json.Marshal(p.Keywords)
_, err := tx.Exec(`
INSERT INTO research_projects (slug, title, abstract, keywords, organization, standards_org, period, funder)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, p.Slug, p.Title, p.Abstract, string(kwJSON), p.StandardsTrack, p.StandardsOrg, p.Period, p.Funder)
if err != nil {
log.Fatalf("Insert research project (%s) failed: %v", p.Slug, err)
}
}
// 2. Research Areas
areas, err := readJSONFile[[]SeedArea](filepath.Join(*seedDir, "research_areas.json"))
if err != nil {
log.Fatalf("Failed to read research areas: %v", err)
}
for _, a := range areas {
_, err := tx.Exec(`
INSERT INTO research_areas (name_en, name_kr, display_order)
VALUES (?, ?, ?)
`, a.NameEn, a.NameKr, a.DisplayOrder)
if err != nil {
log.Fatalf("Insert research area failed: %v", err)
}
}
// 3. Members
members, err := readJSONFile[[]SeedMember](filepath.Join(*seedDir, "members.json"))
if err != nil {
log.Fatalf("Failed to read members: %v", err)
}
for _, m := range members {
_, err := tx.Exec(`
INSERT INTO members (name_ko, name_en, degree, affiliation, email, is_advisor, display_order)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, m.NameKo, m.NameEn, m.Degree, m.Affiliation, m.Email, m.IsAdvisor, m.DisplayOrder)
if err != nil {
log.Fatalf("Insert member (%s) failed: %v", m.NameEn, err)
}
}
// 4. Alumni
alumni, err := readJSONFile[[]SeedAlumnus](filepath.Join(*seedDir, "alumni.json"))
if err != nil {
log.Fatalf("Failed to read alumni: %v", err)
}
for _, a := range alumni {
_, err := tx.Exec(`
INSERT INTO alumni (name_ko, name_en, degree, graduated_at, major, current_position)
VALUES (?, ?, ?, ?, ?, ?)
`, a.NameKo, a.NameEn, a.Degree, a.GraduatedAt, a.Major, a.CurrentPosition)
if err != nil {
log.Fatalf("Insert alumnus (%s) failed: %v", a.NameEn, err)
}
}
// 5. Publications
pubs, err := readJSONFile[[]SeedPublication](filepath.Join(*seedDir, "publications.json"))
if err != nil {
log.Fatalf("Failed to read publications: %v", err)
}
for _, p := range pubs {
_, err := tx.Exec(`
INSERT INTO publications (category, title, authors, venue, volume, published_at, kci, doi, is_highlight)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, p.Category, p.Title, p.Authors, p.Venue, p.Volume, p.PublishedAt, p.KCI, p.DOI, p.IsHighlight)
if err != nil {
log.Fatalf("Insert publication (%s) failed: %v", p.Title, err)
}
}
// 6. Patents
patents, err := readJSONFile[[]SeedPatent](filepath.Join(*seedDir, "patents.json"))
if err != nil {
log.Fatalf("Failed to read patents: %v", err)
}
for _, pat := range patents {
_, err := tx.Exec(`
INSERT INTO patents (title, inventors, application_no, application_at, registration_no, registration_at, country, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, pat.Title, pat.Inventors, pat.ApplicationNo, pat.ApplicationAt, pat.RegistrationNo, pat.RegistrationAt, pat.Country, pat.PublishedAt)
if err != nil {
log.Fatalf("Insert patent (%s) failed: %v", pat.Title, err)
}
}
// 7. Semesters & Courses
semesters, err := readJSONFile[[]SeedSemester](filepath.Join(*seedDir, "lectures.json"))
if err != nil {
log.Fatalf("Failed to read lectures: %v", err)
}
for _, s := range semesters {
res, err := tx.Exec(`INSERT INTO semesters (year, term) VALUES (?, ?)`, s.Year, s.Term)
if err != nil {
log.Fatalf("Insert semester (%d %s) failed: %v", s.Year, s.Term, err)
}
semID, _ := res.LastInsertId()
for cIdx, c := range s.Courses {
_, err := tx.Exec(`
INSERT INTO courses (semester_id, code, name_en, name_kr, note, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, semID, c.Code, c.En, nil, c.Note, cIdx+1)
if err != nil {
log.Fatalf("Insert course (%s) failed: %v", c.Code, err)
}
}
}
// 8. Standards Bodies & Documents
standards, err := readJSONFile[[]SeedStandardsBody](filepath.Join(*seedDir, "standards.json"))
if err != nil {
log.Fatalf("Failed to read standards: %v", err)
}
for bIdx, b := range standards {
res, err := tx.Exec(`
INSERT INTO standards_bodies (org, full_name, scope, period, role, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, b.Org, b.Full, b.Scope, b.Period, b.Role, bIdx+1)
if err != nil {
log.Fatalf("Insert standards body (%s) failed: %v", b.Org, err)
}
bodyID, _ := res.LastInsertId()
for _, p := range b.Projects {
for _, d := range p.Documents {
_, err := tx.Exec(`
INSERT INTO standard_documents (body_id, wg, project_name, title, doc_ref, status, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, bodyID, p.WG, p.Name, d.Title, d.Ref, d.Status, d.PublishedAt)
if err != nil {
log.Fatalf("Insert standard document (%s) failed: %v", d.Title, err)
}
}
}
}
if err := tx.Commit(); err != nil {
log.Fatalf("Transaction commit failed: %v", err)
}
log.Println("Transaction committed successfully.")
// Verify Counts
log.Println("=== Verifying Seeding Counts ===")
checkCount := func(table string, expected int) {
var cnt int
if err := database.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&cnt); err != nil {
log.Fatalf("Count check failed for %s: %v", table, err)
}
if cnt != expected {
log.Fatalf("Count mismatch for %s: expected %d, got %d", table, expected, cnt)
}
log.Printf("✓ %-20s : %3d rows (matches expected)", table, cnt)
}
checkCount("research_projects", 3)
checkCount("research_areas", 14)
checkCount("members", 16)
checkCount("alumni", 60)
checkCount("publications", 148)
checkCount("patents", 75)
checkCount("semesters", 45)
checkCount("courses", 132)
checkCount("standards_bodies", 6)
checkCount("standard_documents", 44)
// Explicit highlight assertion
var hlCount int
if err := database.QueryRow("SELECT COUNT(*) FROM publications WHERE is_highlight = 1").Scan(&hlCount); err != nil {
log.Fatalf("Highlight count query failed: %v", err)
}
if hlCount != 5 {
log.Fatalf("Highlight count mismatch: expected exactly 5, got %d", hlCount)
}
log.Printf("✓ %-20s : %3d rows (exactly 5 highlights)", "publications(highlight)", hlCount)
log.Println("=== Seeding & Verification Completed Successfully! ===")
}
+49
View File
@@ -0,0 +1,49 @@
module git.godopu.com/lab/landing_page/backend
go 1.26.3
require (
github.com/gin-gonic/gin v1.12.0
github.com/stretchr/testify v1.12.1
modernc.org/sqlite v1.57.0
)
require (
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.61.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.2 // indirect
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.30.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
modernc.org/libc v1.75.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
)
+136
View File
@@ -0,0 +1,136 @@
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA=
github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc=
github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU=
golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.75.4 h1:EHQJNYDC6LiDIOqM76862xe4frbDc7IzEOZTtGxZV8Q=
modernc.org/libc v1.75.4/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+104
View File
@@ -0,0 +1,104 @@
package config
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// Config holds all server configuration derived from environment variables and flags.
type Config struct {
Port int
Host string
DBPath string
GinMode string
CORSAllowOrigins string
AutoMigrate bool
}
// LoadEnvFile reads a simple .env file if it exists and loads variables into environment without overriding existing ones.
func LoadEnvFile(filenames ...string) {
if len(filenames) == 0 {
filenames = []string{".env", "../.env"}
}
for _, filename := range filenames {
file, err := os.Open(filename)
if err != nil {
continue
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
// Strip optional surrounding quotes
val = strings.Trim(val, `"'`)
// Only set if not already present in environment
if _, exists := os.LookupEnv(key); !exists {
_ = os.Setenv(key, val)
}
}
_ = file.Close()
}
}
// Load reads and parses all configuration settings from the environment.
func Load() *Config {
LoadEnvFile()
cfg := &Config{
Port: 8080,
Host: "0.0.0.0",
DBPath: "anl.db",
GinMode: "release",
CORSAllowOrigins: "*",
AutoMigrate: true,
}
if portStr := os.Getenv("PORT"); portStr != "" {
if p, err := strconv.Atoi(portStr); err == nil && p > 0 {
cfg.Port = p
}
}
if hostStr := os.Getenv("HOST"); hostStr != "" {
cfg.Host = hostStr
}
if dbPathStr := os.Getenv("DB_PATH"); dbPathStr != "" {
cfg.DBPath = dbPathStr
}
if ginModeStr := os.Getenv("GIN_MODE"); ginModeStr != "" {
cfg.GinMode = ginModeStr
}
if corsStr := os.Getenv("CORS_ALLOW_ORIGINS"); corsStr != "" {
cfg.CORSAllowOrigins = corsStr
}
if autoMigrateStr := os.Getenv("AUTO_MIGRATE"); autoMigrateStr != "" {
cfg.AutoMigrate = strings.ToLower(autoMigrateStr) != "false" && autoMigrateStr != "0"
}
return cfg
}
// Address returns the combined host:port address string.
func (c *Config) Address() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
+80
View File
@@ -0,0 +1,80 @@
package config_test
import (
"os"
"path/filepath"
"testing"
"git.godopu.com/lab/landing_page/backend/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigDefaults(t *testing.T) {
// Clear env
_ = os.Unsetenv("PORT")
_ = os.Unsetenv("HOST")
_ = os.Unsetenv("DB_PATH")
_ = os.Unsetenv("GIN_MODE")
_ = os.Unsetenv("CORS_ALLOW_ORIGINS")
_ = os.Unsetenv("AUTO_MIGRATE")
cfg := config.Load()
assert.Equal(t, 8080, cfg.Port)
assert.Equal(t, "0.0.0.0", cfg.Host)
assert.Equal(t, "anl.db", cfg.DBPath)
assert.Equal(t, "release", cfg.GinMode)
assert.Equal(t, "*", cfg.CORSAllowOrigins)
assert.True(t, cfg.AutoMigrate)
assert.Equal(t, "0.0.0.0:8080", cfg.Address())
}
func TestConfigEnvOverrides(t *testing.T) {
_ = os.Setenv("PORT", "9090")
_ = os.Setenv("HOST", "127.0.0.1")
_ = os.Setenv("DB_PATH", "/tmp/custom.db")
_ = os.Setenv("GIN_MODE", "debug")
_ = os.Setenv("CORS_ALLOW_ORIGINS", "https://anl.knu.ac.kr")
_ = os.Setenv("AUTO_MIGRATE", "false")
defer func() {
_ = os.Unsetenv("PORT")
_ = os.Unsetenv("HOST")
_ = os.Unsetenv("DB_PATH")
_ = os.Unsetenv("GIN_MODE")
_ = os.Unsetenv("CORS_ALLOW_ORIGINS")
_ = os.Unsetenv("AUTO_MIGRATE")
}()
cfg := config.Load()
assert.Equal(t, 9090, cfg.Port)
assert.Equal(t, "127.0.0.1", cfg.Host)
assert.Equal(t, "/tmp/custom.db", cfg.DBPath)
assert.Equal(t, "debug", cfg.GinMode)
assert.Equal(t, "https://anl.knu.ac.kr", cfg.CORSAllowOrigins)
assert.False(t, cfg.AutoMigrate)
assert.Equal(t, "127.0.0.1:9090", cfg.Address())
}
func TestLoadEnvFile(t *testing.T) {
tmpDir := t.TempDir()
envPath := filepath.Join(tmpDir, ".env")
envContent := `
# Sample comment
TEST_CUSTOM_KEY="hello_world"
TEST_PORT_KEY=8888
INVALID_LINE_WITHOUT_EQUALS
`
err := os.WriteFile(envPath, []byte(envContent), 0644)
require.NoError(t, err)
_ = os.Unsetenv("TEST_CUSTOM_KEY")
_ = os.Unsetenv("TEST_PORT_KEY")
defer func() {
_ = os.Unsetenv("TEST_CUSTOM_KEY")
_ = os.Unsetenv("TEST_PORT_KEY")
}()
config.LoadEnvFile(envPath)
assert.Equal(t, "hello_world", os.Getenv("TEST_CUSTOM_KEY"))
assert.Equal(t, "8888", os.Getenv("TEST_PORT_KEY"))
}
+50
View File
@@ -0,0 +1,50 @@
package db
import (
"database/sql"
_ "embed"
"fmt"
"strings"
_ "modernc.org/sqlite"
)
//go:embed migrations/000001_init.up.sql
var initMigrationUp string
//go:embed migrations/000001_init.down.sql
var initMigrationDown string
// Connect opens an SQLite database connection with appropriate pragmas
func Connect(dbPath string) (*sql.DB, error) {
dsn := dbPath
if !strings.Contains(dsn, "?") {
dsn += "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
// RunMigrations applies up migrations
func RunMigrations(db *sql.DB) error {
if _, err := db.Exec(initMigrationUp); err != nil {
return fmt.Errorf("failed to apply migration up: %w", err)
}
return nil
}
// RollbackMigrations applies down migrations
func RollbackMigrations(db *sql.DB) error {
if _, err := db.Exec(initMigrationDown); err != nil {
return fmt.Errorf("failed to apply migration down: %w", err)
}
return nil
}
@@ -0,0 +1,10 @@
DROP TABLE IF EXISTS standard_documents;
DROP TABLE IF EXISTS standards_bodies;
DROP TABLE IF EXISTS courses;
DROP TABLE IF EXISTS semesters;
DROP TABLE IF EXISTS patents;
DROP TABLE IF EXISTS publications;
DROP TABLE IF EXISTS alumni;
DROP TABLE IF EXISTS members;
DROP TABLE IF EXISTS research_areas;
DROP TABLE IF EXISTS research_projects;
@@ -0,0 +1,114 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS research_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
abstract TEXT NOT NULL,
keywords TEXT NOT NULL DEFAULT '[]',
organization TEXT,
standards_org TEXT,
period TEXT,
funder TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS research_areas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name_en TEXT NOT NULL,
name_kr TEXT,
display_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name_ko TEXT NOT NULL,
name_en TEXT NOT NULL,
degree TEXT NOT NULL CHECK (degree IN ('Professor','PhD','PhDCandidate','PhDStudent','MSCandidate','MSStudent')),
affiliation TEXT,
email TEXT,
is_advisor BOOLEAN NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS alumni (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name_ko TEXT NOT NULL,
name_en TEXT NOT NULL,
degree TEXT NOT NULL CHECK (degree IN ('PhD','MS')),
graduated_at TEXT NOT NULL,
major TEXT,
current_position TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS publications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL CHECK (category IN ('intl-journal-conf','domestic-journal-conf')),
title TEXT NOT NULL,
authors TEXT,
venue TEXT NOT NULL,
volume TEXT,
published_at TEXT NOT NULL,
kci BOOLEAN NOT NULL DEFAULT 0,
doi TEXT,
is_highlight BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_publications_category ON publications(category);
CREATE INDEX IF NOT EXISTS idx_publications_highlight ON publications(is_highlight) WHERE is_highlight = 1;
CREATE TABLE IF NOT EXISTS patents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
inventors TEXT NOT NULL,
application_no TEXT NOT NULL,
application_at TEXT NOT NULL,
registration_no TEXT,
registration_at TEXT,
country TEXT NOT NULL,
published_at TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS semesters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
year INTEGER NOT NULL,
term TEXT NOT NULL CHECK (term IN ('Spring','Fall')),
UNIQUE(year, term)
);
CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
semester_id INTEGER NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
code TEXT NOT NULL,
name_en TEXT NOT NULL,
name_kr TEXT,
note TEXT,
display_order INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_courses_semester ON courses(semester_id);
CREATE TABLE IF NOT EXISTS standards_bodies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org TEXT NOT NULL,
full_name TEXT NOT NULL,
scope TEXT NOT NULL CHECK (scope IN ('international','domestic')),
period TEXT NOT NULL,
role TEXT,
display_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS standard_documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
body_id INTEGER NOT NULL REFERENCES standards_bodies(id) ON DELETE CASCADE,
wg TEXT NOT NULL,
project_name TEXT NOT NULL,
title TEXT NOT NULL,
doc_ref TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('IS','TS','TR','CDV','WDTR','InProgress')),
published_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_stddocs_body ON standard_documents(body_id);
+42
View File
@@ -0,0 +1,42 @@
package handlers
import (
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type HomeHandler struct {
repo *repository.HomeRepository
}
func NewHomeHandler(repo *repository.HomeRepository) *HomeHandler {
return &HomeHandler{repo: repo}
}
func (h *HomeHandler) GetResearchProjects(c *gin.Context) {
projects, err := h.repo.GetResearchProjects(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve research projects: "+err.Error())
return
}
httpx.OK(c, projects)
}
func (h *HomeHandler) GetResearchAreas(c *gin.Context) {
areas, err := h.repo.GetResearchAreas(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve research areas: "+err.Error())
return
}
httpx.OK(c, areas)
}
func (h *HomeHandler) GetStatsSummary(c *gin.Context) {
stats, err := h.repo.GetStatsSummary(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve statistics summary: "+err.Error())
return
}
httpx.OK(c, stats)
}
+24
View File
@@ -0,0 +1,24 @@
package handlers
import (
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type LecturesHandler struct {
repo *repository.LecturesRepository
}
func NewLecturesHandler(repo *repository.LecturesRepository) *LecturesHandler {
return &LecturesHandler{repo: repo}
}
func (h *LecturesHandler) GetSemesters(c *gin.Context) {
semesters, err := h.repo.GetSemestersWithCourses(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve semesters: "+err.Error())
return
}
httpx.OK(c, semesters)
}
+33
View File
@@ -0,0 +1,33 @@
package handlers
import (
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type MembersHandler struct {
repo *repository.MembersRepository
}
func NewMembersHandler(repo *repository.MembersRepository) *MembersHandler {
return &MembersHandler{repo: repo}
}
func (h *MembersHandler) GetMembers(c *gin.Context) {
members, err := h.repo.GetMembers(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve members: "+err.Error())
return
}
httpx.OK(c, members)
}
func (h *MembersHandler) GetAlumni(c *gin.Context) {
alumni, err := h.repo.GetAlumni(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve alumni: "+err.Error())
return
}
httpx.OK(c, alumni)
}
+48
View File
@@ -0,0 +1,48 @@
package handlers
import (
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type PublicationsHandler struct {
repo *repository.PublicationsRepository
}
func NewPublicationsHandler(repo *repository.PublicationsRepository) *PublicationsHandler {
return &PublicationsHandler{repo: repo}
}
func (h *PublicationsHandler) GetPublications(c *gin.Context) {
category := c.Query("category")
if category != "" && category != "intl-journal-conf" && category != "domestic-journal-conf" {
httpx.BadRequest(c, "Invalid category parameter: must be 'intl-journal-conf' or 'domestic-journal-conf'")
return
}
pubs, err := h.repo.GetPublications(c.Request.Context(), category)
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve publications: "+err.Error())
return
}
httpx.OK(c, pubs)
}
func (h *PublicationsHandler) GetHighlights(c *gin.Context) {
highlights, err := h.repo.GetHighlights(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve highlights: "+err.Error())
return
}
httpx.OK(c, highlights)
}
func (h *PublicationsHandler) GetPatents(c *gin.Context) {
patents, err := h.repo.GetPatents(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve patents: "+err.Error())
return
}
httpx.OK(c, patents)
}
@@ -0,0 +1,24 @@
package handlers
import (
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type StandardizationHandler struct {
repo *repository.StandardizationRepository
}
func NewStandardizationHandler(repo *repository.StandardizationRepository) *StandardizationHandler {
return &StandardizationHandler{repo: repo}
}
func (h *StandardizationHandler) GetStandardsBodies(c *gin.Context) {
bodies, err := h.repo.GetStandardsBodiesWithProjects(c.Request.Context())
if err != nil {
httpx.InternalServerError(c, "Failed to retrieve standards bodies: "+err.Error())
return
}
httpx.OK(c, bodies)
}
+45
View File
@@ -0,0 +1,45 @@
package httpx
import (
"net/http"
"github.com/gin-gonic/gin"
)
type ErrorDetail struct {
Message string `json:"message"`
}
type ErrorResponse struct {
Error ErrorDetail `json:"error"`
}
type SuccessResponse struct {
Data any `json:"data"`
}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, SuccessResponse{
Data: data,
})
}
func Error(c *gin.Context, status int, msg string) {
c.JSON(status, ErrorResponse{
Error: ErrorDetail{
Message: msg,
},
})
}
func BadRequest(c *gin.Context, msg string) {
Error(c, http.StatusBadRequest, msg)
}
func NotFound(c *gin.Context, msg string) {
Error(c, http.StatusNotFound, msg)
}
func InternalServerError(c *gin.Context, msg string) {
Error(c, http.StatusInternalServerError, msg)
}
+30
View File
@@ -0,0 +1,30 @@
package models
import "time"
type ResearchProject struct {
ID int `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Abstract string `json:"abstract"`
Keywords []string `json:"keywords"`
Organization *string `json:"organization,omitempty"`
StandardsOrg *string `json:"standards_org,omitempty"`
Period *string `json:"period,omitempty"`
Funder *string `json:"funder,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ResearchArea struct {
ID int `json:"id"`
NameEn string `json:"name_en"`
NameKr *string `json:"name_kr,omitempty"`
DisplayOrder int `json:"display_order"`
}
type StatsSummary struct {
IntlPublications int `json:"intl_publications"`
StandardizationDocs int `json:"standardization_docs"`
Patents int `json:"patents"`
}
+24
View File
@@ -0,0 +1,24 @@
package models
type Course struct {
ID int `json:"id"`
SemesterID int `json:"semester_id"`
Code string `json:"code"`
NameEn string `json:"name_en"`
NameKr *string `json:"name_kr,omitempty"`
Note *string `json:"note,omitempty"`
DisplayOrder int `json:"display_order"`
}
type Semester struct {
ID int `json:"id"`
Year int `json:"year"`
Term string `json:"term"`
}
type SemesterWithCourses struct {
ID int `json:"id"`
Year int `json:"year"`
Term string `json:"term"`
Courses []Course `json:"courses"`
}
+26
View File
@@ -0,0 +1,26 @@
package models
import "time"
type Member struct {
ID int `json:"id"`
NameKo string `json:"name_ko"`
NameEn string `json:"name_en"`
Degree string `json:"degree"`
Affiliation *string `json:"affiliation,omitempty"`
Email *string `json:"email,omitempty"`
IsAdvisor bool `json:"is_advisor"`
DisplayOrder int `json:"display_order"`
CreatedAt time.Time `json:"created_at"`
}
type Alumnus struct {
ID int `json:"id"`
NameKo string `json:"name_ko"`
NameEn string `json:"name_en"`
Degree string `json:"degree"`
GraduatedAt string `json:"graduated_at"`
Major *string `json:"major,omitempty"`
CurrentPosition *string `json:"current_position,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
+30
View File
@@ -0,0 +1,30 @@
package models
import "time"
type Publication struct {
ID int `json:"id"`
Category string `json:"category"`
Title string `json:"title"`
Authors *string `json:"authors,omitempty"`
Venue string `json:"venue"`
Volume *string `json:"volume,omitempty"`
PublishedAt string `json:"published_at"`
KCI bool `json:"kci"`
DOI *string `json:"doi,omitempty"`
IsHighlight bool `json:"is_highlight"`
CreatedAt time.Time `json:"created_at"`
}
type Patent struct {
ID int `json:"id"`
Title string `json:"title"`
Inventors string `json:"inventors"`
ApplicationNo string `json:"application_no"`
ApplicationAt string `json:"application_at"`
RegistrationNo *string `json:"registration_no,omitempty"`
RegistrationAt *string `json:"registration_at,omitempty"`
Country string `json:"country"`
PublishedAt string `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
}
@@ -0,0 +1,39 @@
package models
type StandardDocument struct {
ID int `json:"id"`
BodyID int `json:"body_id"`
WG string `json:"wg"`
ProjectName string `json:"project_name"`
Title string `json:"title"`
DocRef string `json:"doc_ref"`
Status string `json:"status"`
PublishedAt *string `json:"published_at,omitempty"`
}
type StandardProjectGroup struct {
WG string `json:"wg"`
Name string `json:"name"`
Documents []StandardDocument `json:"documents"`
}
type StandardsBody struct {
ID int `json:"id"`
Org string `json:"org"`
FullName string `json:"full_name"`
Scope string `json:"scope"`
Period string `json:"period"`
Role *string `json:"role,omitempty"`
DisplayOrder int `json:"display_order"`
}
type StandardsBodyWithProjects struct {
ID int `json:"id"`
Org string `json:"org"`
FullName string `json:"full_name"`
Scope string `json:"scope"`
Period string `json:"period"`
Role *string `json:"role,omitempty"`
DisplayOrder int `json:"display_order"`
Projects []StandardProjectGroup `json:"projects"`
}
+100
View File
@@ -0,0 +1,100 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"git.godopu.com/lab/landing_page/backend/internal/models"
)
type HomeRepository struct {
db *sql.DB
}
func NewHomeRepository(db *sql.DB) *HomeRepository {
return &HomeRepository{db: db}
}
func (r *HomeRepository) GetResearchProjects(ctx context.Context) ([]models.ResearchProject, error) {
query := `
SELECT id, slug, title, abstract, keywords, organization, standards_org, period, funder, created_at, updated_at
FROM research_projects
ORDER BY id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query research projects: %w", err)
}
defer rows.Close()
var projects []models.ResearchProject
for rows.Next() {
var p models.ResearchProject
var kwJSON string
err := rows.Scan(
&p.ID, &p.Slug, &p.Title, &p.Abstract, &kwJSON,
&p.Organization, &p.StandardsOrg, &p.Period, &p.Funder,
&p.CreatedAt, &p.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("scan research project: %w", err)
}
if kwJSON != "" {
_ = json.Unmarshal([]byte(kwJSON), &p.Keywords)
}
if p.Keywords == nil {
p.Keywords = []string{}
}
projects = append(projects, p)
}
if err := rows.Err(); err != nil {
return nil, err
}
return projects, nil
}
func (r *HomeRepository) GetResearchAreas(ctx context.Context) ([]models.ResearchArea, error) {
query := `
SELECT id, name_en, name_kr, display_order
FROM research_areas
ORDER BY display_order ASC, id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query research areas: %w", err)
}
defer rows.Close()
var areas []models.ResearchArea
for rows.Next() {
var a models.ResearchArea
if err := rows.Scan(&a.ID, &a.NameEn, &a.NameKr, &a.DisplayOrder); err != nil {
return nil, fmt.Errorf("scan research area: %w", err)
}
areas = append(areas, a)
}
return areas, rows.Err()
}
func (r *HomeRepository) GetStatsSummary(ctx context.Context) (*models.StatsSummary, error) {
var summary models.StatsSummary
err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM publications WHERE category = 'intl-journal-conf'").Scan(&summary.IntlPublications)
if err != nil {
return nil, fmt.Errorf("count intl publications: %w", err)
}
err = r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM standard_documents").Scan(&summary.StandardizationDocs)
if err != nil {
return nil, fmt.Errorf("count standard documents: %w", err)
}
err = r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM patents").Scan(&summary.Patents)
if err != nil {
return nil, fmt.Errorf("count patents: %w", err)
}
return &summary, nil
}
+71
View File
@@ -0,0 +1,71 @@
package repository
import (
"context"
"database/sql"
"fmt"
"git.godopu.com/lab/landing_page/backend/internal/models"
)
type LecturesRepository struct {
db *sql.DB
}
func NewLecturesRepository(db *sql.DB) *LecturesRepository {
return &LecturesRepository{db: db}
}
func (r *LecturesRepository) GetSemestersWithCourses(ctx context.Context) ([]models.SemesterWithCourses, error) {
// Query semesters ordered by year DESC and Fall before Spring within same year
semQuery := `
SELECT id, year, term
FROM semesters
ORDER BY year DESC, CASE term WHEN 'Fall' THEN 1 WHEN 'Spring' THEN 2 ELSE 3 END, id DESC
`
semRows, err := r.db.QueryContext(ctx, semQuery)
if err != nil {
return nil, fmt.Errorf("query semesters: %w", err)
}
defer semRows.Close()
var semesters []models.SemesterWithCourses
semMap := make(map[int]int) // semester_id -> index in slice
for semRows.Next() {
var s models.SemesterWithCourses
if err := semRows.Scan(&s.ID, &s.Year, &s.Term); err != nil {
return nil, fmt.Errorf("scan semester: %w", err)
}
s.Courses = []models.Course{}
semMap[s.ID] = len(semesters)
semesters = append(semesters, s)
}
if err := semRows.Err(); err != nil {
return nil, err
}
// Query courses ordered by display_order ASC, id ASC
courseQuery := `
SELECT id, semester_id, code, name_en, name_kr, note, display_order
FROM courses
ORDER BY semester_id ASC, display_order ASC, id ASC
`
courseRows, err := r.db.QueryContext(ctx, courseQuery)
if err != nil {
return nil, fmt.Errorf("query courses: %w", err)
}
defer courseRows.Close()
for courseRows.Next() {
var c models.Course
if err := courseRows.Scan(&c.ID, &c.SemesterID, &c.Code, &c.NameEn, &c.NameKr, &c.Note, &c.DisplayOrder); err != nil {
return nil, fmt.Errorf("scan course: %w", err)
}
if idx, exists := semMap[c.SemesterID]; exists {
semesters[idx].Courses = append(semesters[idx].Courses, c)
}
}
return semesters, courseRows.Err()
}
+69
View File
@@ -0,0 +1,69 @@
package repository
import (
"context"
"database/sql"
"fmt"
"git.godopu.com/lab/landing_page/backend/internal/models"
)
type MembersRepository struct {
db *sql.DB
}
func NewMembersRepository(db *sql.DB) *MembersRepository {
return &MembersRepository{db: db}
}
func (r *MembersRepository) GetMembers(ctx context.Context) ([]models.Member, error) {
query := `
SELECT id, name_ko, name_en, degree, affiliation, email, is_advisor, display_order, created_at
FROM members
ORDER BY display_order ASC, id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query members: %w", err)
}
defer rows.Close()
var members []models.Member
for rows.Next() {
var m models.Member
if err := rows.Scan(
&m.ID, &m.NameKo, &m.NameEn, &m.Degree,
&m.Affiliation, &m.Email, &m.IsAdvisor, &m.DisplayOrder, &m.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan member: %w", err)
}
members = append(members, m)
}
return members, rows.Err()
}
func (r *MembersRepository) GetAlumni(ctx context.Context) ([]models.Alumnus, error) {
query := `
SELECT id, name_ko, name_en, degree, graduated_at, major, current_position, created_at
FROM alumni
ORDER BY graduated_at DESC, id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query alumni: %w", err)
}
defer rows.Close()
var alumni []models.Alumnus
for rows.Next() {
var a models.Alumnus
if err := rows.Scan(
&a.ID, &a.NameKo, &a.NameEn, &a.Degree,
&a.GraduatedAt, &a.Major, &a.CurrentPosition, &a.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan alumnus: %w", err)
}
alumni = append(alumni, a)
}
return alumni, rows.Err()
}
+102
View File
@@ -0,0 +1,102 @@
package repository
import (
"context"
"database/sql"
"fmt"
"git.godopu.com/lab/landing_page/backend/internal/models"
)
type PublicationsRepository struct {
db *sql.DB
}
func NewPublicationsRepository(db *sql.DB) *PublicationsRepository {
return &PublicationsRepository{db: db}
}
func (r *PublicationsRepository) GetPublications(ctx context.Context, category string) ([]models.Publication, error) {
query := `
SELECT id, category, title, authors, venue, volume, published_at, kci, doi, is_highlight, created_at
FROM publications
`
var args []any
if category != "" {
query += " WHERE category = ?"
args = append(args, category)
}
query += " ORDER BY published_at DESC, id ASC"
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("query publications: %w", err)
}
defer rows.Close()
var pubs []models.Publication
for rows.Next() {
var p models.Publication
if err := rows.Scan(
&p.ID, &p.Category, &p.Title, &p.Authors,
&p.Venue, &p.Volume, &p.PublishedAt, &p.KCI, &p.DOI, &p.IsHighlight, &p.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan publication: %w", err)
}
pubs = append(pubs, p)
}
return pubs, rows.Err()
}
func (r *PublicationsRepository) GetHighlights(ctx context.Context) ([]models.Publication, error) {
query := `
SELECT id, category, title, authors, venue, volume, published_at, kci, doi, is_highlight, created_at
FROM publications
WHERE is_highlight = 1
ORDER BY published_at DESC, id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query highlights: %w", err)
}
defer rows.Close()
var pubs []models.Publication
for rows.Next() {
var p models.Publication
if err := rows.Scan(
&p.ID, &p.Category, &p.Title, &p.Authors,
&p.Venue, &p.Volume, &p.PublishedAt, &p.KCI, &p.DOI, &p.IsHighlight, &p.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan highlight: %w", err)
}
pubs = append(pubs, p)
}
return pubs, rows.Err()
}
func (r *PublicationsRepository) GetPatents(ctx context.Context) ([]models.Patent, error) {
query := `
SELECT id, title, inventors, application_no, application_at, registration_no, registration_at, country, published_at, created_at
FROM patents
ORDER BY published_at DESC, id ASC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query patents: %w", err)
}
defer rows.Close()
var patents []models.Patent
for rows.Next() {
var p models.Patent
if err := rows.Scan(
&p.ID, &p.Title, &p.Inventors, &p.ApplicationNo,
&p.ApplicationAt, &p.RegistrationNo, &p.RegistrationAt, &p.Country, &p.PublishedAt, &p.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan patent: %w", err)
}
patents = append(patents, p)
}
return patents, rows.Err()
}
@@ -0,0 +1,95 @@
package repository
import (
"context"
"database/sql"
"fmt"
"git.godopu.com/lab/landing_page/backend/internal/models"
)
type StandardizationRepository struct {
db *sql.DB
}
func NewStandardizationRepository(db *sql.DB) *StandardizationRepository {
return &StandardizationRepository{db: db}
}
func (r *StandardizationRepository) GetStandardsBodiesWithProjects(ctx context.Context) ([]models.StandardsBodyWithProjects, error) {
// Query bodies ordered by display_order
bodyQuery := `
SELECT id, org, full_name, scope, period, role, display_order
FROM standards_bodies
ORDER BY display_order ASC, id ASC
`
bodyRows, err := r.db.QueryContext(ctx, bodyQuery)
if err != nil {
return nil, fmt.Errorf("query standards bodies: %w", err)
}
defer bodyRows.Close()
var bodies []models.StandardsBodyWithProjects
bodyMap := make(map[int]int) // body_id -> index in bodies
for bodyRows.Next() {
var b models.StandardsBodyWithProjects
if err := bodyRows.Scan(&b.ID, &b.Org, &b.FullName, &b.Scope, &b.Period, &b.Role, &b.DisplayOrder); err != nil {
return nil, fmt.Errorf("scan standards body: %w", err)
}
b.Projects = []models.StandardProjectGroup{}
bodyMap[b.ID] = len(bodies)
bodies = append(bodies, b)
}
if err := bodyRows.Err(); err != nil {
return nil, err
}
// Query documents ordered by body_id, and preserves curated insertion order with MIN(id) ASC
docQuery := `
SELECT id, body_id, wg, project_name, title, doc_ref, status, published_at
FROM standard_documents
ORDER BY body_id ASC, id ASC
`
docRows, err := r.db.QueryContext(ctx, docQuery)
if err != nil {
return nil, fmt.Errorf("query standard documents: %w", err)
}
defer docRows.Close()
// Group documents into projects under each body preserving curated order
type projectKey struct {
bodyID int
wg string
projectName string
}
projectMap := make(map[projectKey]int) // projectKey -> index in bodies[bIdx].Projects
for docRows.Next() {
var d models.StandardDocument
if err := docRows.Scan(&d.ID, &d.BodyID, &d.WG, &d.ProjectName, &d.Title, &d.DocRef, &d.Status, &d.PublishedAt); err != nil {
return nil, fmt.Errorf("scan standard document: %w", err)
}
bIdx, bodyExists := bodyMap[d.BodyID]
if !bodyExists {
continue
}
key := projectKey{bodyID: d.BodyID, wg: d.WG, projectName: d.ProjectName}
pIdx, projExists := projectMap[key]
if !projExists {
pIdx = len(bodies[bIdx].Projects)
bodies[bIdx].Projects = append(bodies[bIdx].Projects, models.StandardProjectGroup{
WG: d.WG,
Name: d.ProjectName,
Documents: []models.StandardDocument{},
})
projectMap[key] = pIdx
}
bodies[bIdx].Projects[pIdx].Documents = append(bodies[bIdx].Projects[pIdx].Documents, d)
}
return bodies, docRows.Err()
}
+76
View File
@@ -0,0 +1,76 @@
package router
import (
"database/sql"
"net/http"
"git.godopu.com/lab/landing_page/backend/internal/handlers"
"git.godopu.com/lab/landing_page/backend/internal/httpx"
"git.godopu.com/lab/landing_page/backend/internal/repository"
"github.com/gin-gonic/gin"
)
func SetupRouter(db *sql.DB, corsOrigins ...string) *gin.Engine {
r := gin.Default()
allowOrigin := "*"
if len(corsOrigins) > 0 && corsOrigins[0] != "" {
allowOrigin = corsOrigins[0]
}
// CORS Middleware
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
// Repositories
homeRepo := repository.NewHomeRepository(db)
membersRepo := repository.NewMembersRepository(db)
pubsRepo := repository.NewPublicationsRepository(db)
lecturesRepo := repository.NewLecturesRepository(db)
standardsRepo := repository.NewStandardizationRepository(db)
// Handlers
homeHandler := handlers.NewHomeHandler(homeRepo)
membersHandler := handlers.NewMembersHandler(membersRepo)
pubsHandler := handlers.NewPublicationsHandler(pubsRepo)
lecturesHandler := handlers.NewLecturesHandler(lecturesRepo)
standardsHandler := handlers.NewStandardizationHandler(standardsRepo)
// API v1 Group
v1 := r.Group("/api/v1")
{
v1.GET("/health", func(c *gin.Context) {
httpx.OK(c, gin.H{"status": "ok", "service": "anl-backend-api"})
})
// Home
v1.GET("/research-projects", homeHandler.GetResearchProjects)
v1.GET("/research-areas", homeHandler.GetResearchAreas)
v1.GET("/stats/summary", homeHandler.GetStatsSummary)
// Members & Alumni
v1.GET("/members", membersHandler.GetMembers)
v1.GET("/alumni", membersHandler.GetAlumni)
// Publications & Patents
v1.GET("/publications", pubsHandler.GetPublications)
v1.GET("/publications/highlights", pubsHandler.GetHighlights)
v1.GET("/patents", pubsHandler.GetPatents)
// Lectures
v1.GET("/semesters", lecturesHandler.GetSemesters)
// Standardization
v1.GET("/standards-bodies", standardsHandler.GetStandardsBodies)
}
return r
}
+446
View File
@@ -0,0 +1,446 @@
package router_test
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"git.godopu.com/lab/landing_page/backend/internal/db"
"git.godopu.com/lab/landing_page/backend/internal/models"
"git.godopu.com/lab/landing_page/backend/internal/router"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
_ "modernc.org/sqlite"
)
func setupTestDB(t *testing.T) *sql.DB {
// In-memory or temporary file sqlite DB
tmpDB := filepath.Join(t.TempDir(), "test.db")
database, err := db.Connect(tmpDB)
require.NoError(t, err)
err = db.RunMigrations(database)
require.NoError(t, err)
// Seed test data from ../../seed/data
seedDir := filepath.Join("..", "..", "seed", "data")
seedDatabase(t, database, seedDir)
return database
}
func seedDatabase(t *testing.T, database *sql.DB, seedDir string) {
tx, err := database.BeginTx(context.Background(), nil)
require.NoError(t, err)
defer tx.Rollback()
// 1. Projects
pData, err := os.ReadFile(filepath.Join(seedDir, "research_projects.json"))
require.NoError(t, err)
var projects []struct {
Slug string `json:"slug"`
Title string `json:"title"`
Abstract string `json:"abstract"`
Keywords []string `json:"keywords"`
StandardsTrack *string `json:"standardsTrack"`
StandardsOrg *string `json:"standardsOrg"`
Period *string `json:"period"`
Funder *string `json:"funder"`
}
require.NoError(t, json.Unmarshal(pData, &projects))
for _, p := range projects {
kwJSON, _ := json.Marshal(p.Keywords)
_, err := tx.Exec(`
INSERT INTO research_projects (slug, title, abstract, keywords, organization, standards_org, period, funder)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, p.Slug, p.Title, p.Abstract, string(kwJSON), p.StandardsTrack, p.StandardsOrg, p.Period, p.Funder)
require.NoError(t, err)
}
// 2. Areas
aData, err := os.ReadFile(filepath.Join(seedDir, "research_areas.json"))
require.NoError(t, err)
var areas []models.ResearchArea
require.NoError(t, json.Unmarshal(aData, &areas))
for _, a := range areas {
_, err := tx.Exec(`INSERT INTO research_areas (name_en, name_kr, display_order) VALUES (?, ?, ?)`, a.NameEn, a.NameKr, a.DisplayOrder)
require.NoError(t, err)
}
// 3. Members
mData, err := os.ReadFile(filepath.Join(seedDir, "members.json"))
require.NoError(t, err)
var members []models.Member
require.NoError(t, json.Unmarshal(mData, &members))
for _, m := range members {
_, err := tx.Exec(`
INSERT INTO members (name_ko, name_en, degree, affiliation, email, is_advisor, display_order)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, m.NameKo, m.NameEn, m.Degree, m.Affiliation, m.Email, m.IsAdvisor, m.DisplayOrder)
require.NoError(t, err)
}
// 4. Alumni
alumniData, err := os.ReadFile(filepath.Join(seedDir, "alumni.json"))
require.NoError(t, err)
var alumni []models.Alumnus
require.NoError(t, json.Unmarshal(alumniData, &alumni))
for _, a := range alumni {
_, err := tx.Exec(`
INSERT INTO alumni (name_ko, name_en, degree, graduated_at, major, current_position)
VALUES (?, ?, ?, ?, ?, ?)
`, a.NameKo, a.NameEn, a.Degree, a.GraduatedAt, a.Major, a.CurrentPosition)
require.NoError(t, err)
}
// 5. Publications
pubData, err := os.ReadFile(filepath.Join(seedDir, "publications.json"))
require.NoError(t, err)
var pubs []models.Publication
require.NoError(t, json.Unmarshal(pubData, &pubs))
for _, p := range pubs {
_, err := tx.Exec(`
INSERT INTO publications (category, title, authors, venue, volume, published_at, kci, doi, is_highlight)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, p.Category, p.Title, p.Authors, p.Venue, p.Volume, p.PublishedAt, p.KCI, p.DOI, p.IsHighlight)
require.NoError(t, err)
}
// 6. Patents
patData, err := os.ReadFile(filepath.Join(seedDir, "patents.json"))
require.NoError(t, err)
var patents []models.Patent
require.NoError(t, json.Unmarshal(patData, &patents))
for _, pat := range patents {
_, err := tx.Exec(`
INSERT INTO patents (title, inventors, application_no, application_at, registration_no, registration_at, country, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, pat.Title, pat.Inventors, pat.ApplicationNo, pat.ApplicationAt, pat.RegistrationNo, pat.RegistrationAt, pat.Country, pat.PublishedAt)
require.NoError(t, err)
}
// 7. Semesters & Courses
lecData, err := os.ReadFile(filepath.Join(seedDir, "lectures.json"))
require.NoError(t, err)
var semesters []struct {
Term string `json:"term"`
Year int `json:"year"`
Courses []struct {
Code string `json:"code"`
En string `json:"en"`
Note *string `json:"note"`
} `json:"courses"`
}
require.NoError(t, json.Unmarshal(lecData, &semesters))
for _, s := range semesters {
res, err := tx.Exec(`INSERT INTO semesters (year, term) VALUES (?, ?)`, s.Year, s.Term)
require.NoError(t, err)
semID, _ := res.LastInsertId()
for cIdx, c := range s.Courses {
_, err := tx.Exec(`
INSERT INTO courses (semester_id, code, name_en, name_kr, note, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, semID, c.Code, c.En, nil, c.Note, cIdx+1)
require.NoError(t, err)
}
}
// 8. Standards
stdData, err := os.ReadFile(filepath.Join(seedDir, "standards.json"))
require.NoError(t, err)
var standards []struct {
Org string `json:"org"`
Full string `json:"full"`
Scope string `json:"scope"`
Period string `json:"period"`
Role *string `json:"role"`
Projects []struct {
WG string `json:"wg"`
Name string `json:"name"`
Documents []struct {
Title string `json:"title"`
Ref string `json:"ref"`
Status string `json:"status"`
PublishedAt *string `json:"publishedAt"`
} `json:"documents"`
} `json:"projects"`
}
require.NoError(t, json.Unmarshal(stdData, &standards))
for bIdx, b := range standards {
res, err := tx.Exec(`
INSERT INTO standards_bodies (org, full_name, scope, period, role, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, b.Org, b.Full, b.Scope, b.Period, b.Role, bIdx+1)
require.NoError(t, err)
bodyID, _ := res.LastInsertId()
for _, p := range b.Projects {
for _, d := range p.Documents {
_, err := tx.Exec(`
INSERT INTO standard_documents (body_id, wg, project_name, title, doc_ref, status, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, bodyID, p.WG, p.Name, d.Title, d.Ref, d.Status, d.PublishedAt)
require.NoError(t, err)
}
}
}
require.NoError(t, tx.Commit())
}
type APIResponse[T any] struct {
Data T `json:"data"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
func executeRequest(r http.Handler, method, target string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, target, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func TestHealthEndpoint(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/health")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[map[string]any]
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, "ok", resp.Data["status"])
assert.Equal(t, "anl-backend-api", resp.Data["service"])
}
func TestResearchProjects(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/research-projects")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[[]models.ResearchProject]
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Len(t, resp.Data, 3)
assert.Equal(t, "iot-standards", resp.Data[0].Slug)
assert.Equal(t, "AIoT & AI-RAN", resp.Data[0].Title)
}
func TestResearchAreas(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/research-areas")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[[]models.ResearchArea]
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Len(t, resp.Data, 14)
assert.Equal(t, 1, resp.Data[0].DisplayOrder)
assert.Equal(t, "Quick UDP Internet Connection (QUIC)", resp.Data[0].NameEn)
}
func TestStatsSummary(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/stats/summary")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[models.StatsSummary]
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, 68, resp.Data.IntlPublications)
assert.Equal(t, 44, resp.Data.StandardizationDocs)
assert.Equal(t, 75, resp.Data.Patents)
}
func TestMembersAndAlumni(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
// Members
wMem := executeRequest(r, "GET", "/api/v1/members")
assert.Equal(t, http.StatusOK, wMem.Code)
var respMem APIResponse[[]models.Member]
require.NoError(t, json.Unmarshal(wMem.Body.Bytes(), &respMem))
assert.Len(t, respMem.Data, 16)
assert.True(t, respMem.Data[0].IsAdvisor)
assert.Equal(t, "Seok-Joo Koh", respMem.Data[0].NameEn)
// Alumni
wAlumni := executeRequest(r, "GET", "/api/v1/alumni")
assert.Equal(t, http.StatusOK, wAlumni.Code)
var respAlumni APIResponse[[]models.Alumnus]
require.NoError(t, json.Unmarshal(wAlumni.Body.Bytes(), &respAlumni))
assert.Len(t, respAlumni.Data, 60)
assert.GreaterOrEqual(t, respAlumni.Data[0].GraduatedAt, respAlumni.Data[1].GraduatedAt)
}
func TestPublicationsAndHighlights(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
// Full Publications
wAll := executeRequest(r, "GET", "/api/v1/publications")
assert.Equal(t, http.StatusOK, wAll.Code)
var respAll APIResponse[[]models.Publication]
require.NoError(t, json.Unmarshal(wAll.Body.Bytes(), &respAll))
assert.Len(t, respAll.Data, 148)
// Category filter: intl
wIntl := executeRequest(r, "GET", "/api/v1/publications?category=intl-journal-conf")
assert.Equal(t, http.StatusOK, wIntl.Code)
var respIntl APIResponse[[]models.Publication]
require.NoError(t, json.Unmarshal(wIntl.Body.Bytes(), &respIntl))
assert.Len(t, respIntl.Data, 68)
// Category filter: domestic
wDom := executeRequest(r, "GET", "/api/v1/publications?category=domestic-journal-conf")
assert.Equal(t, http.StatusOK, wDom.Code)
var respDom APIResponse[[]models.Publication]
require.NoError(t, json.Unmarshal(wDom.Body.Bytes(), &respDom))
assert.Len(t, respDom.Data, 80)
// Invalid category
wInvalid := executeRequest(r, "GET", "/api/v1/publications?category=unknown")
assert.Equal(t, http.StatusBadRequest, wInvalid.Code)
// Highlights (asserts exactly the 5 known representative titles)
wHL := executeRequest(r, "GET", "/api/v1/publications/highlights")
assert.Equal(t, http.StatusOK, wHL.Code)
var respHL APIResponse[[]models.Publication]
require.NoError(t, json.Unmarshal(wHL.Body.Bytes(), &respHL))
assert.Len(t, respHL.Data, 5)
expectedTitles := map[string]bool{
"Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G": true,
"Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G": true,
"mQUIC: Use of QUIC for Handover Support with Connection Migration in Wireless/Mobile Networks": true,
"Use of QUIC for CoAP Transport in IoT Networks": true,
"Use of QUIC for Mobile-Oriented Future Internet (Q-MOFI)": true,
}
for _, hl := range respHL.Data {
assert.True(t, hl.IsHighlight)
assert.True(t, expectedTitles[hl.Title], "Unexpected highlight title: %s", hl.Title)
}
// Patents
wPat := executeRequest(r, "GET", "/api/v1/patents")
assert.Equal(t, http.StatusOK, wPat.Code)
var respPat APIResponse[[]models.Patent]
require.NoError(t, json.Unmarshal(wPat.Body.Bytes(), &respPat))
assert.Len(t, respPat.Data, 75)
}
func TestSemestersAndCourses(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/semesters")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[[]models.SemesterWithCourses]
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Len(t, resp.Data, 45)
// Check semester ordering (Year DESC, Fall before Spring within same year)
first := resp.Data[0]
assert.Equal(t, 2026, first.Year)
assert.Equal(t, "Spring", first.Term)
assert.NotEmpty(t, first.Courses)
// In 2025: Fall must come before Spring
var s2025Fall, s2025Spring int
for idx, s := range resp.Data {
if s.Year == 2025 && s.Term == "Fall" {
s2025Fall = idx
}
if s.Year == 2025 && s.Term == "Spring" {
s2025Spring = idx
}
}
assert.Less(t, s2025Fall, s2025Spring, "2025 Fall should precede 2025 Spring in chronological descending order")
}
func TestStandardsBodiesAndHierarchy(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
w := executeRequest(r, "GET", "/api/v1/standards-bodies")
assert.Equal(t, http.StatusOK, w.Code)
var resp APIResponse[[]models.StandardsBodyWithProjects]
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Len(t, resp.Data, 6)
// First body: IEC TC100
iec := resp.Data[0]
assert.Equal(t, "IEC TC100", iec.Org)
require.NotEmpty(t, iec.Projects)
// Verify project ordering within IEC TC100 (deterministic order: WG12 before WG11(Convenor))
assert.Equal(t, "WG12", iec.Projects[0].WG)
assert.Equal(t, "WG11(Convenor)", iec.Projects[1].WG)
assert.NotEmpty(t, iec.Projects[0].Documents)
}
func TestJSONKeyCasingSnakeCase(t *testing.T) {
database := setupTestDB(t)
defer database.Close()
r := router.SetupRouter(database)
// Test Members endpoint raw JSON map
wMem := executeRequest(r, "GET", "/api/v1/members")
assert.Equal(t, http.StatusOK, wMem.Code)
var rawMem map[string]any
require.NoError(t, json.Unmarshal(wMem.Body.Bytes(), &rawMem))
dataList, ok := rawMem["data"].([]any)
require.True(t, ok)
require.NotEmpty(t, dataList)
firstMember, ok := dataList[0].(map[string]any)
require.True(t, ok)
// Verify snake_case keys exist and camelCase keys do not
assert.Contains(t, firstMember, "name_ko")
assert.Contains(t, firstMember, "name_en")
assert.Contains(t, firstMember, "is_advisor")
assert.Contains(t, firstMember, "display_order")
assert.NotContains(t, firstMember, "nameKo")
assert.NotContains(t, firstMember, "isAdvisor")
// Test Highlights endpoint raw JSON map
wHL := executeRequest(r, "GET", "/api/v1/publications/highlights")
assert.Equal(t, http.StatusOK, wHL.Code)
var rawHL map[string]any
require.NoError(t, json.Unmarshal(wHL.Body.Bytes(), &rawHL))
hlList, ok := rawHL["data"].([]any)
require.True(t, ok)
require.NotEmpty(t, hlList)
firstHL, ok := hlList[0].(map[string]any)
require.True(t, ok)
assert.Contains(t, firstHL, "is_highlight")
assert.Contains(t, firstHL, "published_at")
assert.NotContains(t, firstHL, "isHighlight")
assert.NotContains(t, firstHL, "publishedAt")
}
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
Exports curated data from refer_landing_page/content/*.ts and app/page.tsx into backend/seed/data/*.json.
Ensures is_highlight is properly tagged on the 5 representative publications.
"""
import json
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
CONTENT_DIR = os.path.join(REPO_ROOT, "refer_landing_page", "content")
SEED_DATA_DIR = os.path.join(REPO_ROOT, "backend", "seed", "data")
PAGE_TSX = os.path.join(REPO_ROOT, "refer_landing_page", "app", "page.tsx")
os.makedirs(SEED_DATA_DIR, exist_ok=True)
HIGHLIGHT_TITLES = {
"Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G",
"Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G",
"mQUIC: Use of QUIC for Handover Support with Connection Migration in Wireless/Mobile Networks",
"Use of QUIC for CoAP Transport in IoT Networks",
"Use of QUIC for Mobile-Oriented Future Internet (Q-MOFI)",
}
def clean_json_str(s):
# Remove trailing commas before ] or }
s = re.sub(r',\s*\]', ']', s)
s = re.sub(r',\s*\}', '}', s)
return s
def extract_json_array_from_ts(filepath, var_name):
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Match export const <var_name>[: Type] = [ ... ];
pattern = rf'export\s+const\s+{var_name}(?:\s*:\s*[^=]+)?\s*=\s*(\[\s*[\s\S]*?\s*\]);'
match = re.search(pattern, content)
if not match:
raise ValueError(f"Could not find array literal for '{var_name}' in {filepath}")
array_str = clean_json_str(match.group(1))
return json.loads(array_str)
def main():
print("=== Extracting TypeScript content to JSON for SQLite seeding ===")
# 1. Projects
projects_file = os.path.join(CONTENT_DIR, "projects.ts")
projects = extract_json_array_from_ts(projects_file, "researchProjects")
with open(os.path.join(SEED_DATA_DIR, "research_projects.json"), "w", encoding="utf-8") as f:
json.dump(projects, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(projects)} research projects")
# 2. Research Areas (from app/page.tsx)
with open(PAGE_TSX, "r", encoding="utf-8") as f:
page_src = f.read()
areas_match = re.search(r'const\s+RESEARCH_AREAS(?:\s*:\s*[^=]+)?\s*=\s*(\[\s*[\s\S]*?\s*\]);', page_src)
if not areas_match:
raise ValueError("Could not find RESEARCH_AREAS in app/page.tsx")
areas_str = clean_json_str(areas_match.group(1))
areas_list = json.loads(areas_str)
research_areas = [{"name_en": name, "name_kr": None, "display_order": i + 1} for i, name in enumerate(areas_list)]
with open(os.path.join(SEED_DATA_DIR, "research_areas.json"), "w", encoding="utf-8") as f:
json.dump(research_areas, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(research_areas)} research areas")
# 3. Members & Alumni
members_file = os.path.join(CONTENT_DIR, "members.ts")
active_members = extract_json_array_from_ts(members_file, "activeMembers")
alumni = extract_json_array_from_ts(members_file, "alumni")
# Format active members with is_advisor
formatted_members = []
for i, m in enumerate(active_members):
formatted_members.append({
"name_ko": m["ko"],
"name_en": m["en"],
"degree": m["degree"],
"affiliation": m.get("affiliation"),
"email": "sjkoh@knu.ac.kr" if m["degree"] == "Professor" else None,
"is_advisor": m["degree"] == "Professor",
"display_order": i + 1,
})
with open(os.path.join(SEED_DATA_DIR, "members.json"), "w", encoding="utf-8") as f:
json.dump(formatted_members, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(formatted_members)} active members")
formatted_alumni = []
for a in alumni:
formatted_alumni.append({
"name_ko": a["ko"],
"name_en": a["en"],
"degree": a["degree"],
"graduated_at": a["graduatedAt"],
"major": a.get("major"),
"current_position": a.get("currentPosition"),
})
with open(os.path.join(SEED_DATA_DIR, "alumni.json"), "w", encoding="utf-8") as f:
json.dump(formatted_alumni, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(formatted_alumni)} alumni")
# 4. Publications & Patents
pubs_file = os.path.join(CONTENT_DIR, "publications.ts")
raw_pubs = extract_json_array_from_ts(pubs_file, "publications")
raw_patents = extract_json_array_from_ts(pubs_file, "patents")
matched_highlights = []
formatted_pubs = []
for p in raw_pubs:
title_clean = p["title"].strip()
is_hl = title_clean in HIGHLIGHT_TITLES
if is_hl:
matched_highlights.append(title_clean)
formatted_pubs.append({
"category": p["category"],
"title": p["title"],
"authors": p.get("authors"),
"venue": p["venue"],
"volume": p.get("volume"),
"published_at": p["publishedAt"],
"kci": bool(p.get("kci", False)),
"doi": p.get("doi"),
"is_highlight": is_hl,
})
if len(matched_highlights) != 5:
raise ValueError(f"Highlight matching error: expected exactly 5 matched highlights, found {len(matched_highlights)}: {matched_highlights}")
with open(os.path.join(SEED_DATA_DIR, "publications.json"), "w", encoding="utf-8") as f:
json.dump(formatted_pubs, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(formatted_pubs)} publications (exactly {len(matched_highlights)} highlights tagged)")
formatted_patents = []
for pat in raw_patents:
formatted_patents.append({
"title": pat["title"],
"inventors": pat["inventors"],
"application_no": pat["applicationNo"],
"application_at": pat["applicationAt"],
"registration_no": pat.get("registrationNo"),
"registration_at": pat.get("registrationAt"),
"country": pat["country"],
"published_at": pat["publishedAt"],
})
with open(os.path.join(SEED_DATA_DIR, "patents.json"), "w", encoding="utf-8") as f:
json.dump(formatted_patents, f, indent=2, ensure_ascii=False)
print(f"✓ Extracted {len(formatted_patents)} patents")
# 5. Lectures (Semesters & Courses)
lectures_file = os.path.join(CONTENT_DIR, "lectures.ts")
raw_semesters = extract_json_array_from_ts(lectures_file, "semesters")
with open(os.path.join(SEED_DATA_DIR, "lectures.json"), "w", encoding="utf-8") as f:
json.dump(raw_semesters, f, indent=2, ensure_ascii=False)
total_courses = sum(len(s.get("courses", [])) for s in raw_semesters)
print(f"✓ Extracted {len(raw_semesters)} semesters with {total_courses} courses")
# 6. Standardization
standards_file = os.path.join(CONTENT_DIR, "standards.ts")
raw_standards = extract_json_array_from_ts(standards_file, "standardsBodies")
with open(os.path.join(SEED_DATA_DIR, "standards.json"), "w", encoding="utf-8") as f:
json.dump(raw_standards, f, indent=2, ensure_ascii=False)
total_docs = sum(
sum(len(p.get("documents", [])) for p in b.get("projects", []))
for b in raw_standards
)
print(f"✓ Extracted {len(raw_standards)} standards bodies with {total_docs} documents")
print("=== Extraction completed successfully ===")
if __name__ == "__main__":
main()
+482
View File
@@ -0,0 +1,482 @@
[
{
"name_ko": "김동주",
"name_en": "Dongju Kim",
"degree": "PhD",
"graduated_at": "2026-08",
"major": "Computer Science and Engineering",
"current_position": "He is now with Daegu Catholic University (대구가톨릭대학교) as Professor"
},
{
"name_ko": "황정미",
"name_en": "Jung-Mi Hwang",
"degree": "MS",
"graduated_at": "2026-08",
"major": "Information Science",
"current_position": "She is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "배드로",
"name_en": "Dro Bae",
"degree": "MS",
"graduated_at": "2026-02",
"major": "Industrial Engineering",
"current_position": "He is now with SK AX"
},
{
"name_ko": "권세기",
"name_en": "Se-Gi Kwon",
"degree": "MS",
"graduated_at": "2026-02",
"major": "Information Security",
"current_position": "He is now with J Solution (제이솔루션) as CEO"
},
{
"name_ko": "조세현",
"name_en": "Se-Hyun Cho",
"degree": "PhD",
"graduated_at": "2025-08",
"major": "Information Security",
"current_position": "He is now with Cyber Security Center"
},
{
"name_ko": "김윤성",
"name_en": "Yun-Seong Kim",
"degree": "MS",
"graduated_at": "2025-08",
"major": "Data Convergence Computing",
"current_position": "He is now with SL (에스엘)"
},
{
"name_ko": "김수진",
"name_en": "Su-Jin Kim",
"degree": "MS",
"graduated_at": "2025-08",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "김상태",
"name_en": "Sang-Tae Kim",
"degree": "MS",
"graduated_at": "2025-08",
"major": "Industrial Engineering",
"current_position": "He is now with Oh-Sung Electronis (오성전자)"
},
{
"name_ko": "김규범",
"name_en": "Gyu-Bum Kim",
"degree": "MS",
"graduated_at": "2025-08",
"major": "Computer Science and Engineering",
"current_position": "He is now with Korea Real Estate Board (한국부동산원)"
},
{
"name_ko": "정중화",
"name_en": "Joong-Hwa Jung",
"degree": "PhD",
"graduated_at": "2025-02",
"major": null,
"current_position": "He is now with Lychee AI Coding Academy (리치AI코딩학원) as CEO"
},
{
"name_ko": "임도현",
"name_en": "Dohyeon Lim",
"degree": "MS",
"graduated_at": "2025-02",
"major": "Computer Science and Engineering",
"current_position": "He is now with TakenSoft (테이큰소프트)"
},
{
"name_ko": "김소용",
"name_en": "So-Yong Kim",
"degree": "PhD",
"graduated_at": "2024-08",
"major": null,
"current_position": "He is now with Catholic University of Leuven (UCLouvain) in Belgium"
},
{
"name_ko": "김민지",
"name_en": "Min-Ji Kim",
"degree": "MS",
"graduated_at": "2024-02",
"major": null,
"current_position": "She is now with LG Electronics"
},
{
"name_ko": "안은지",
"name_en": "Eun-Ji Ahn",
"degree": "MS",
"graduated_at": "2024-02",
"major": null,
"current_position": "S is now with National Security Research Institute (국가보안연구소)"
},
{
"name_ko": "오동규",
"name_en": "Dong-Kyu Oh",
"degree": "MS",
"graduated_at": "2024-02",
"major": "Industrial Engineering",
"current_position": "He is now with Human-Plus (휴먼플러스)"
},
{
"name_ko": "김재성",
"name_en": "Jae-Seong Kim",
"degree": "MS",
"graduated_at": "2023-08",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "최동규",
"name_en": "Dong-Kyu Choi",
"degree": "PhD",
"graduated_at": "2023-02",
"major": null,
"current_position": "He is now with ISL in KNU"
},
{
"name_ko": "최진원",
"name_en": "Jin-Won Choi",
"degree": "MS",
"graduated_at": "2023-02",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "김지은",
"name_en": "Ji-Eun Kim",
"degree": "MS",
"graduated_at": "2023-02",
"major": "Information Science",
"current_position": "She is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "김철민",
"name_en": "Cheol-Min Kim",
"degree": "PhD",
"graduated_at": "2022-08",
"major": null,
"current_position": "He is now with KETI (한국전자기술연구원)"
},
{
"name_ko": "김경식",
"name_en": "Kyung-Sik Kim",
"degree": "MS",
"graduated_at": "2022-02",
"major": null,
"current_position": "He is now with Hyundai Autoever (현대오토에버)"
},
{
"name_ko": "김근수",
"name_en": "Keun-Soo Kim",
"degree": "MS",
"graduated_at": "2022-02",
"major": null,
"current_position": "He is now with Shinhan Bank (신한은행)"
},
{
"name_ko": "정성우",
"name_en": "Seong-Woo Jeong",
"degree": "MS",
"graduated_at": "2022-02",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "최민철",
"name_en": "Min-Cheol Choi",
"degree": "MS",
"graduated_at": "2022-02",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "장준희",
"name_en": "Jun-Hee Jang",
"degree": "MS",
"graduated_at": "2021-08",
"major": "Computer Science and Engineering",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "Muhammad Hafidh Firmansyah",
"name_en": "Muhammad Hafidh Firmansyah",
"degree": "MS",
"graduated_at": "2021-08",
"major": null,
"current_position": "He is now with State Polytechnic University of Jember in Indonesia"
},
{
"name_ko": "장강민",
"name_en": "Kang-Min Jang",
"degree": "MS",
"graduated_at": "2021-02",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "이홍근",
"name_en": "Hong-Keun Lee",
"degree": "MS",
"graduated_at": "2021-02",
"major": "Information Science",
"current_position": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"name_ko": "남혜빈",
"name_en": "Hyebeen Nam",
"degree": "MS",
"graduated_at": "2021-02",
"major": null,
"current_position": "She is now with ISL in KNU"
},
{
"name_ko": "최낙중",
"name_en": "Nak-Jung Choi",
"degree": "PhD",
"graduated_at": "2020-02",
"major": null,
"current_position": "He is now with Agency for Defense Development (국방과학연구소)"
},
{
"name_ko": "정민우",
"name_en": "Min-Woo Jung",
"degree": "MS",
"graduated_at": "2019-02",
"major": null,
"current_position": "He is now with LG Electronics (LG전자)"
},
{
"name_ko": "강형우",
"name_en": "Hyung-Woo Kang",
"degree": "PhD",
"graduated_at": "2018-08",
"major": null,
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "최예찬",
"name_en": "Ye-Chan Choi",
"degree": "MS",
"graduated_at": "2018-02",
"major": null,
"current_position": "He is now with LG-CNS"
},
{
"name_ko": "최상일",
"name_en": "Sang-Il Choi",
"degree": "PhD",
"graduated_at": "2017-02",
"major": null,
"current_position": "He is now with Daegu Catholic University (대구가톨릭대학교) as a professor"
},
{
"name_ko": "박진호",
"name_en": "Jin-Ho Park",
"degree": "MS",
"graduated_at": "2016-08",
"major": null,
"current_position": "He is now with BiThumb (빗썸코리아)"
},
{
"name_ko": "석성윤",
"name_en": "Sung-Yoon Seok",
"degree": "MS",
"graduated_at": "2016-08",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김진규",
"name_en": "Jin-Kyu Kim",
"degree": "MS",
"graduated_at": "2016-08",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김재철",
"name_en": "Jae-Cheol Kim",
"degree": "MS",
"graduated_at": "2016-08",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김지인",
"name_en": "Ji-In Kim",
"degree": "PhD",
"graduated_at": "2016-02",
"major": null,
"current_position": "He is now with FAM Tech. (팸텍) as CEO"
},
{
"name_ko": "김우주",
"name_en": "Woo-Ju Kim",
"degree": "MS",
"graduated_at": "2016-02",
"major": null,
"current_position": "He is now with ZCube (지큐브)"
},
{
"name_ko": "박정섭",
"name_en": "Jung-Sub Park",
"degree": "MS",
"graduated_at": "2015-08",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "최종명",
"name_en": "Jong-Myoung Choi",
"degree": "MS",
"graduated_at": "2015-02",
"major": null,
"current_position": "He is now with NP Communications"
},
{
"name_ko": "이종관",
"name_en": "Jong-Kwan Lee",
"degree": "MS",
"graduated_at": "2014-02",
"major": "Samsung Electronics Program",
"current_position": "He is now with Hanwha Ocean (한화오션)"
},
{
"name_ko": "이상헌",
"name_en": "Sang-Hun Lee",
"degree": "MS",
"graduated_at": "2014-02",
"major": null,
"current_position": "He is now with Inno Wireless"
},
{
"name_ko": "민경욱",
"name_en": "Kyeong-Wook Min",
"degree": "MS",
"graduated_at": "2013-02",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김상헌",
"name_en": "Sang-Heon Kim",
"degree": "MS",
"graduated_at": "2013-02",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "Moneeb Gohar",
"name_en": "Moneeb Gohar",
"degree": "PhD",
"graduated_at": "2012-08",
"major": null,
"current_position": "He is now with Department of Computer Science, Bahria University (at Islamabad) in Pakistan"
},
{
"name_ko": "박재완",
"name_en": "Jae-Wan Park",
"degree": "MS",
"graduated_at": "2012-02",
"major": null,
"current_position": "He is now with Ministry of Justice (Immigration Information Center, 법무부)"
},
{
"name_ko": "이재경",
"name_en": "Jae-Kyoung Lee",
"degree": "MS",
"graduated_at": "2012-02",
"major": null,
"current_position": "She is now with Korea Transportation Satefy Authority (한국교통안전공단)"
},
{
"name_ko": "하원기",
"name_en": "Won-Ki Ha",
"degree": "MS",
"graduated_at": "2012-02",
"major": "Samsung Electronics Program",
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김근희",
"name_en": "Keun-Hee Kim",
"degree": "MS",
"graduated_at": "2012-02",
"major": "Samsung Electronics Program",
"current_position": "She is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "권순홍",
"name_en": "Soon-Hong Kwon",
"degree": "MS",
"graduated_at": "2010-02",
"major": null,
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "김동필",
"name_en": "Dong-Phil Kim",
"degree": "PhD",
"graduated_at": "2009-02",
"major": null,
"current_position": "He is now with National Security Research Institute (국가보안연구소)"
},
{
"name_ko": "최린",
"name_en": "Lin Cui",
"degree": "PhD",
"graduated_at": "2009-02",
"major": null,
"current_position": "He is now with Tianjin University of Technology and Education in China"
},
{
"name_ko": "이동화",
"name_en": "Dong-Hwa Lee",
"degree": "MS",
"graduated_at": "2009-02",
"major": null,
"current_position": "He is now with Samsung Electronics (삼성전자)"
},
{
"name_ko": "박재성",
"name_en": "Jae-Sung Park",
"degree": "MS",
"graduated_at": "2008-08",
"major": null,
"current_position": "He is now with Gom&Company (곰앤컴퍼니)"
},
{
"name_ko": "주수경",
"name_en": "Su-Kyoung Ju",
"degree": "MS",
"graduated_at": "2008-02",
"major": null,
"current_position": "She is now with Youngjin Univ. (영진전문대학교)"
},
{
"name_ko": "윤성식",
"name_en": "Sung-Shik Yoon",
"degree": "MS",
"graduated_at": "2007-08",
"major": null,
"current_position": "He is now with Jo-Il Textile Processing Company (조일가공) as CEO"
},
{
"name_ko": "김상태",
"name_en": "Sang-Tae Kim",
"degree": "MS",
"graduated_at": "2007-02",
"major": null,
"current_position": "He is now with LG Electronics (LG전자)"
},
{
"name_ko": "하종식",
"name_en": "Jong-Shik Ha",
"degree": "MS",
"graduated_at": "2007-02",
"major": null,
"current_position": "He is now with Samsung Electronics (삼성전자)"
}
]
@@ -1,7 +1,4 @@
// AUTO-GENERATED by scripts/extract/generate_all.py DO NOT EDIT BY HAND
import { Semester } from "./types";
export const semesters: Semester[] = [
[
{
"term": "Spring",
"year": 2026,
@@ -801,4 +798,4 @@ export const semesters: Semester[] = [
}
]
}
];
]
+146
View File
@@ -0,0 +1,146 @@
[
{
"name_ko": "고석주",
"name_en": "Seok-Joo Koh",
"degree": "Professor",
"affiliation": null,
"email": "sjkoh@knu.ac.kr",
"is_advisor": true,
"display_order": 1
},
{
"name_ko": "최동규",
"name_en": "Dong-Kyu Choi",
"degree": "PhD",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 2
},
{
"name_ko": "남혜빈",
"name_en": "Hye-Been Nam",
"degree": "PhDCandidate",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 3
},
{
"name_ko": "임도현",
"name_en": "Dohyeon Lim",
"degree": "PhDStudent",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 4
},
{
"name_ko": "최준혁",
"name_en": "Jun-Hyeok Choi",
"degree": "PhDStudent",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 5
},
{
"name_ko": "신광선",
"name_en": "Gwang-Seon Shin",
"degree": "MSCandidate",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 6
},
{
"name_ko": "이환웅",
"name_en": "Hwan-Woong Lee",
"degree": "MSStudent",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 7
},
{
"name_ko": "Hoang Anh Dang",
"name_en": "Hoang Anh Dang",
"degree": "MSStudent",
"affiliation": null,
"email": null,
"is_advisor": false,
"display_order": 8
},
{
"name_ko": "김민희",
"name_en": "Minhee Kim",
"degree": "PhDCandidate",
"affiliation": "School of Computer Science and Engineering",
"email": null,
"is_advisor": false,
"display_order": 9
},
{
"name_ko": "김근솔",
"name_en": "Keun-Sol Kim",
"degree": "PhDCandidate",
"affiliation": "Department of Information Security",
"email": null,
"is_advisor": false,
"display_order": 10
},
{
"name_ko": "민성준",
"name_en": "Sung-Jun Min",
"degree": "PhDCandidate",
"affiliation": "Department of Information Science",
"email": null,
"is_advisor": false,
"display_order": 11
},
{
"name_ko": "최진원",
"name_en": "Jin-Won Choi",
"degree": "PhDStudent",
"affiliation": "Department of Science and Technology",
"email": null,
"is_advisor": false,
"display_order": 12
},
{
"name_ko": "정기수",
"name_en": "Ki-Soo Jung",
"degree": "PhDStudent",
"affiliation": "Department of Science and Technology",
"email": null,
"is_advisor": false,
"display_order": 13
},
{
"name_ko": "이미주",
"name_en": "Mi-Joo Lee",
"degree": "PhDStudent",
"affiliation": "Department of ICT Convergence",
"email": null,
"is_advisor": false,
"display_order": 14
},
{
"name_ko": "박유나",
"name_en": "Yunah Park",
"degree": "MSCandidate",
"affiliation": "Department of Information Security",
"email": null,
"is_advisor": false,
"display_order": 15
},
{
"name_ko": "박채영",
"name_en": "Chae-Young Park",
"degree": "MSCandidate",
"affiliation": "Department of Information Science",
"email": null,
"is_advisor": false,
"display_order": 16
}
]
+752
View File
@@ -0,0 +1,752 @@
[
{
"title": "국내 지역별 태양광 발전량 및 전력 단가 예측 웹 서비스 제공 장치 및 그 방법",
"inventors": "고석주, 김경훈, 김나현, 정수인, 홍일표",
"application_no": "2025-0200788",
"application_at": "2025-12-16",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2025-12"
},
{
"title": "사물인터넷 네트워크 환경에서 QUIC 기반으로 CoAP 메시지를 중계하는 시스템",
"inventors": "고석주, 정중화, 남혜빈, 최동규, 김민지",
"application_no": "2023-0180751",
"application_at": "2023-12-13",
"registration_no": "2963109",
"registration_at": "2026-05-06",
"country": "한국",
"published_at": "2023-12"
},
{
"title": "엣지 컴퓨팅을 위한 사물인터넷 서비스 시스템",
"inventors": "고석주, 정중화, 남혜빈, 최동규",
"application_no": "2023-0161365",
"application_at": "2023-11-20",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2023-11"
},
{
"title": "무선 네트워크 환경에서 핸드오버 및 커넥션 마이그레이션을 지원하는 장치 및 방법",
"inventors": "고석주, 김소용, 모닙 고하르, 최동규",
"application_no": "2023-0063455",
"application_at": "2023-05-17",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2023-05"
},
{
"title": "제한된 통신 환경에서 브로커 서버를 이용한 스마트 약상자 제어 시스템 및 방법",
"inventors": "고석주, 김근수, 김철민, 김경식, 나재욱, 박진호",
"application_no": "2020-0164156",
"application_at": "2020-11-30",
"registration_no": "2384614",
"registration_at": "2022-04-05",
"country": "한국",
"published_at": "2020-11"
},
{
"title": "단방향 가시광 통신 환경에서 데이터 전송 신뢰성 개선을 위한 방법 및 이를 이용한 하이브리드",
"inventors": "고석주, 김소용, 최동규, 남혜빈, 정중화, 김창묵",
"application_no": "2020-0164152",
"application_at": "2020-11-30",
"registration_no": "2420614",
"registration_at": "2022-07-08",
"country": "한국",
"published_at": "2020-11"
},
{
"title": "사물 인터넷 서비스를 제공하기 위한 QUIC-Proxy를 이용한 데이터 전달 방법 및 장치",
"inventors": "고석주, 김소용, 최동규, 남혜빈, 정중화",
"application_no": "2020-0164141",
"application_at": "2020-11-30",
"registration_no": "2345473",
"registration_at": "2021-12-27",
"country": "한국",
"published_at": "2020-11"
},
{
"title": "금연구역 관리 서비스 방법 및 시스템",
"inventors": "고석주, 이정우, 이용호, 권오상, 정서영, 김경식",
"application_no": "2020-0101365",
"application_at": "2020-08-12",
"registration_no": "2375686",
"registration_at": "2022-03-14",
"country": "한국",
"published_at": "2020-08"
},
{
"title": "사물 인터넷 환경에서 CoAP 기반의 데이터 스트리밍 방법 및 통신 시스템",
"inventors": "고석주, 정중화, 최동규, 남혜빈, 이채현",
"application_no": "2020-0094058",
"application_at": "2020-07-28",
"registration_no": "2375703",
"registration_at": "2022-03-14",
"country": "한국",
"published_at": "2020-07"
},
{
"title": "인공지능 기반 적외선 카메라 센싱 시스템 및 방법",
"inventors": "고석주, 홍성기, 김희원, 박명훈, 권민철",
"application_no": "2019-0174964",
"application_at": "2019-12-26",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2019-12"
},
{
"title": "폭력 행위 관리 시스템 및 방법",
"inventors": "고석주, 김동욱, 윤서원, 구영준, 성경화, 경예지",
"application_no": "2019-0122161",
"application_at": "2019-10-02",
"registration_no": "2264275",
"registration_at": "2021-06-07",
"country": "한국",
"published_at": "2019-10"
},
{
"title": "차량 인포테인먼트 관리 시스템",
"inventors": "고석주, 최동규, 정중화, 남혜빈, 손종명",
"application_no": "2019-0120044",
"application_at": "2019-09-27",
"registration_no": "2410024",
"registration_at": "2022-06-13",
"country": "한국",
"published_at": "2019-09"
},
{
"title": "차량 인포테인먼트 마스터 장치 및 이를 포함하는 차량 인포테인먼트 통합 관리 시스템",
"inventors": "고석주, 최동규, 정중화, 남혜빈, 신호경",
"application_no": "2019-0059551",
"application_at": "2019-05-21",
"registration_no": "2251310",
"registration_at": "2021-05-06",
"country": "한국",
"published_at": "2019-05"
},
{
"title": "CoAP 기반의 센싱 정보 스트리밍 방법",
"inventors": "고석주, 최동규, 정중화, 남혜빈",
"application_no": "2018-0168580",
"application_at": "2018-12-24",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-12"
},
{
"title": "이미지에서 기계학습 기법을 활용한 특정 부품영역 탐지 방법",
"inventors": "고석주, 김대기, 김동인, 방종원, 우진철, 정중화",
"application_no": "2018-0167974",
"application_at": "2018-12-21",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-12"
},
{
"title": "블록체인 기반의 재정장부 관리 시스템",
"inventors": "고석주, 신승민, 서상민, 송동훈, 최효선, 그레고즈 리뼤시치",
"application_no": "2018-0167970",
"application_at": "2018-12-21",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-12"
},
{
"title": "블록체인을 이용한 게임정보 처리 방법",
"inventors": "고석주, 정재훈, 서창호, 노경환, 원응호",
"application_no": "2018-0167951",
"application_at": "2018-12-21",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-12"
},
{
"title": "가시광 통신을 이용한 서비스 제공 시스템 및 그 방법",
"inventors": "고석주, 김철민, 김소용, 모닙고하르",
"application_no": "2018-0155013",
"application_at": "2018-12-05",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-12"
},
{
"title": "차량 인포테인먼트 시스템",
"inventors": "고석주, 최동규, 정중화, 정민우",
"application_no": "2018-0083515",
"application_at": "2018-07-18",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-07"
},
{
"title": "CoAP 기반의 사물인터넷 기술을 이용한 센서 관리 방법 및 이를 이용한 시스템",
"inventors": "고석주, 최동규, 정중화, 정민우",
"application_no": "2018-0082485",
"application_at": "2018-07-16",
"registration_no": "2071974",
"registration_at": "2020-01-23",
"country": "한국",
"published_at": "2018-07"
},
{
"title": "가시광 통신을 이용한 무선 인터넷 비밀번호 관리 시스템 및 방법",
"inventors": "김소용, 김철민, 고석주",
"application_no": "2018-0082463",
"application_at": "2018-07-16",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-07"
},
{
"title": "시각 장애인을 위한 가시광 통신 기반의 교통 신호 전달 시스템 및 방법",
"inventors": "권경동, 금동우, 황지영, 채윤창, 김철민, 김소용, 고석주",
"application_no": "2018-0079319",
"application_at": "2018-07-09",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2018-07"
},
{
"title": "데이터 전달 장치, 방법과 그를 이용한 사물 인터넷 시스템, 데이터 전달 방법을 실행하기 위한 프로그램이",
"inventors": "고석주, 최동규, 정중화",
"application_no": "2017-0153908",
"application_at": "2017-11-17",
"registration_no": "1969652",
"registration_at": "2019-04-10",
"country": "한국",
"published_at": "2017-11"
},
{
"title": "분산된 게시-구독 기법을 이용한 CoAP 기반 사물 인터넷 시스템의 작동 방법",
"inventors": "고석주, 정중화, 최동규",
"application_no": "2017-0153357",
"application_at": "2017-11-16",
"registration_no": "2031726",
"registration_at": "2019-10-07",
"country": "한국",
"published_at": "2017-11"
},
{
"title": "비콘 기반 식당 정보 서비스 시스템 및 방법",
"inventors": "고석주, 안창준, 김혜경, 송명근, 최예찬, 허동",
"application_no": "2017-0087265",
"application_at": "2017-07-10",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2017-07"
},
{
"title": "사물 인터넷 네트워크에서의 모바일 단말 이동성 제어 장치 및 방법",
"inventors": "고석주, 최상일",
"application_no": "2017-0064039",
"application_at": "2017-05-24",
"registration_no": "1847081",
"registration_at": "2018-04-03",
"country": "한국",
"published_at": "2017-05"
},
{
"title": "데이터 전달 장치, 방법 및 그를 이용한 사물인터넷 시스템",
"inventors": "고석주, 정중화, 강형우, 최동규",
"application_no": "2016-0175882",
"application_at": "2016-12-21",
"registration_no": "1972470",
"registration_at": "2019-04-19",
"country": "한국",
"published_at": "2016-12"
},
{
"title": "저전력 근거리 통신을 이용한 개인 정보 교환 방법 및 시스템, 이를 수행하기 위한 기록매체",
"inventors": "고석주, 김지인, 김지희, 김송아, 박지혜, 이승일, 서대화",
"application_no": "2016-0157411",
"application_at": "2016-11-24",
"registration_no": "1784309",
"registration_at": "2017-09-27",
"country": "한국",
"published_at": "2016-11"
},
{
"title": "가시광 통신 기기 관리 방법 및 장치 (with 유양디앤유)",
"inventors": "김상옥, 유병오, 윤상호, 노승완, 고석주, 최상일",
"application_no": "2016-0157606",
"application_at": "2016-11-24",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2016-11"
},
{
"title": "가시광 통신 방법 및 장치 (with 유양디앤유)",
"inventors": "김상옥, 유병오, 윤상호, 노승완, 고석주, 최상일",
"application_no": "2016-0157576",
"application_at": "2016-11-24",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2016-11"
},
{
"title": "이동 단말 및 관리 서버의 제어 방법",
"inventors": "고석주, 최상일",
"application_no": "2016-0051126",
"application_at": "2016-04-26",
"registration_no": "1836116",
"registration_at": "2018-03-02",
"country": "한국",
"published_at": "2016-04"
},
{
"title": "무선 통신을 이용한 범용 안내 서비스 제공 방법 및 시스템",
"inventors": "고석주, 장호영, 이종훈, 박찬석, 서민영",
"application_no": "2016-0013614",
"application_at": "2016-02-03",
"registration_no": "1716661",
"registration_at": "2017-03-09",
"country": "한국",
"published_at": "2016-02"
},
{
"title": "디지털 도어락 시스템 및 그 제어 방법",
"inventors": "고석주, 권다영, 노혜성, 이수정",
"application_no": "2015-0163735",
"application_at": "2015-11-23",
"registration_no": "1814555",
"registration_at": "2017-12-27",
"country": "한국",
"published_at": "2015-11"
},
{
"title": "무인 지상차량을 이용한 주차차량 관리 시스템",
"inventors": "김인한, 권혜련, 이은섭, 고석주",
"application_no": "2015-0008735",
"application_at": "2015-01-19",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2015-01"
},
{
"title": "ESL 신호를 이용하는 측위 방법, 이를 수행하기 위한 기록 매체, 단말기 및 시스템",
"inventors": "고석주, 김지인, 이민형, 김종근, 박지수, 조정근, 이승일, 서대화",
"application_no": "2014-0180037",
"application_at": "2014-12-15",
"registration_no": "1596320",
"registration_at": "2016-02-16",
"country": "한국",
"published_at": "2014-12"
},
{
"title": "ESL 신호를 이용하여 위치 기반 서비스를 제공하는 스마트 장치",
"inventors": "고석주, 김지인, 이민형, 김종근, 박지수, 조정근, 이승일, 서대화",
"application_no": "2014-0180036",
"application_at": "2014-12-15",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2014-12"
},
{
"title": "3차원 형상 복원을 위한 볼륨 카빙 장치 및 그 방법",
"inventors": "고석주, 하태윤, 서대화, 정귀영, 이상은, 김지인, 김한별",
"application_no": "2014-0167063",
"application_at": "2014-11-27",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2014-11"
},
{
"title": "립모션 기기를 이용한 수화 번역 시스템 및 그 방법",
"inventors": "고석주, 조재현, 서대화, 이동훈, 이상은, 김지인, 하대규",
"application_no": "2014-0166166",
"application_at": "2014-11-26",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2014-11"
},
{
"title": "립모션 기기의 수화 번역 정확도 향상을 위한 수화 번역 시스템 및 그 방법",
"inventors": "고석주, 조재현, 서대화, 이동훈, 이상은, 김지인, 하대규",
"application_no": "2014-0166165",
"application_at": "2014-11-26",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2014-11"
},
{
"title": "저전력 근거리 통신을 이용한 개인 정보 교환 방법 및 시스템, 이를 수행하기 위한 기록매체",
"inventors": "고석주, 김지희, 서대화, 박지혜, 이승일, 김지인, 김송아",
"application_no": "2014-0164802",
"application_at": "2014-11-24",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2014-11"
},
{
"title": "차량 분산 방법 및 장치, 이를 수행하기 위한 기록매체",
"inventors": "고석주, 김지인, 엄진욱, 김민규, 홍석진, 배정규, 서대화",
"application_no": "2014-0161975",
"application_at": "2014-11-19",
"registration_no": "1612047",
"registration_at": "2016-04-06",
"country": "한국",
"published_at": "2014-11"
},
{
"title": "신규 액세스 포인트의 위치 인식 방법 및 이를 이용하는 서버",
"inventors": "고석주, 김우주, 이민형, 하태윤, 심대섭, 조정근, 강보영, 서대화",
"application_no": "2013-0167207",
"application_at": "2013-12-30",
"registration_no": "1568365",
"registration_at": "2015-11-05",
"country": "한국",
"published_at": "2013-12"
},
{
"title": "디바이스 탐색 장치 및 디바이스 탐색 방법",
"inventors": "고석주, 이상헌, 최상일",
"application_no": "2013-0140737",
"application_at": "2013-11-19",
"registration_no": "1405248",
"registration_at": "2015-04-17",
"country": "한국",
"published_at": "2013-11"
},
{
"title": "오픈플로우 통신 시스템 및 방법",
"inventors": "고석주, 김지인, 최낙중",
"application_no": "2013-0140736",
"application_at": "2013-11-19",
"registration_no": "1525047",
"registration_at": "2015-05-27",
"country": "한국",
"published_at": "2013-11"
},
{
"title": "이동통신 시스템의 추적 영역 코드 구성 장치 및 방법",
"inventors": "고석주, 강형우, 백태산, 강현구",
"application_no": "2013-0140734",
"application_at": "2013-11-19",
"registration_no": "1538453",
"registration_at": "2015-07-15",
"country": "한국",
"published_at": "2013-11"
},
{
"title": "프로세스 모니터링과 키보드 잠금을 이용한 프로세스 관리 방법 및 프로세스 관리 장치",
"inventors": "고석주, 박진호, 이재휘",
"application_no": "2013-0108587",
"application_at": "2013-09-10",
"registration_no": "1515493",
"registration_at": "2015-04-21",
"country": "한국",
"published_at": "2013-09"
},
{
"title": "공유기 탐지 장치 및 공유기 탐지 방법",
"inventors": "고석주, 박진호, 김철민",
"application_no": "2013-0091483",
"application_at": "2013-08-01",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2013-08"
},
{
"title": "호스트 식별 프로토콜 네트워크 환경의 통신 시스템 및 방법",
"inventors": "고석주, 최상일",
"application_no": "2012-0154836",
"application_at": "2012-12-27",
"registration_no": "1405248",
"registration_at": "2014-06-02",
"country": "한국",
"published_at": "2012-12"
},
{
"title": "액세스 라우터 및 그를 이용한 핸드오버 제어 방법",
"inventors": "고석주, 최낙중, 김지인",
"application_no": "2012-0154835",
"application_at": "2012-12-27",
"registration_no": "1447104",
"registration_at": "2014-09-26",
"country": "한국",
"published_at": "2012-12"
},
{
"title": "호스트 식별 프로토콜 네트워크 환경의 이동통신 시스템 및 방법",
"inventors": "고석주, 김지인, 이상헌",
"application_no": "2012-0146740",
"application_at": "2012-12-14",
"registration_no": "1459628",
"registration_at": "2014-11-03",
"country": "한국",
"published_at": "2012-12"
},
{
"title": "라우터의 호스트 위치 관리 방법",
"inventors": "고석주, 김지인",
"application_no": "2012-0030058",
"application_at": "2012-03-23",
"registration_no": "1356721",
"registration_at": "2014-01-20",
"country": "한국",
"published_at": "2012-03"
},
{
"title": "분산형 구조를 이용한 데이터 통신 방법",
"inventors": "고석주, 모닙고하르, 이재경",
"application_no": "2011-0111384",
"application_at": "2011-10-28",
"registration_no": "1311864",
"registration_at": "2013-09-17",
"country": "한국",
"published_at": "2011-10"
},
{
"title": "라우터, 그것을 포함하는 통신 네트워크 시스템 및 그것의 이동성 제어 방법",
"inventors": "고석주, 최상일",
"application_no": "2011-0107533",
"application_at": "2011-10-20",
"registration_no": "1329331",
"registration_at": "2013-11-07",
"country": "한국",
"published_at": "2011-10"
},
{
"title": "모바일 액세스 게이트웨이 및 이를 이용한 이동성 제어 방법",
"inventors": "고석주, 김지인",
"application_no": "2011-0057608",
"application_at": "2011-06-14",
"registration_no": "1223047",
"registration_at": "2013-01-10",
"country": "한국",
"published_at": "2011-06"
},
{
"title": "멀티홈잉 환경에서 프록시 모바일 인터넷 프로토콜을 사용하는 이동통신 시스템 및 그것의 핸드오버 방법",
"inventors": "고석주, 김지인",
"application_no": "2011-0001905",
"application_at": "2011-01-07",
"registration_no": "1189140",
"registration_at": "2012-10-02",
"country": "한국",
"published_at": "2011-01"
},
{
"title": "멀티캐스트 방법 및 억세스 게이트웨이",
"inventors": "고석주, 모닙고하르, 이재경",
"application_no": "2010-0137897",
"application_at": "2010-12-29",
"registration_no": "1200407",
"registration_at": "2012-11-06",
"country": "한국",
"published_at": "2010-12"
},
{
"title": "프록시 모바일 인터넷 프로토콜을 사용하는 이동통신 시스템 및 그것의 핸드오버 방법",
"inventors": "고석주, 김지인",
"application_no": "2010-0108058",
"application_at": "2010-11-02",
"registration_no": "1258238",
"registration_at": "2013-04-19",
"country": "한국",
"published_at": "2010-11"
},
{
"title": "이동 단말, 통신 네트워크 및 그것의 이동성 제어 방법",
"inventors": "고석주, 최상일",
"application_no": "2010-0107701",
"application_at": "2010-11-01",
"registration_no": "1177354",
"registration_at": "2012-08-21",
"country": "한국",
"published_at": "2010-11"
},
{
"title": "빠른 핸드오버를 지원하기 위한 이동통신 시스템 및 방법",
"inventors": "고석주, 모닙고하르, 박재완",
"application_no": "2010-0039688",
"application_at": "2010-04-28",
"registration_no": "1091397",
"registration_at": "2011-12-01",
"country": "한국",
"published_at": "2010-04"
},
{
"title": "이동 단말간 데이터 전송 시스템 및 그 방법",
"inventors": "고석주, 권순홍",
"application_no": "2009-0048797",
"application_at": "2009-06-02",
"registration_no": "1078156",
"registration_at": "2011-10-24",
"country": "한국",
"published_at": "2009-06"
},
{
"title": "프록시 모바일 IPv6망내 이동단말간 통신 경로 최적화 시스템 및 그 방법",
"inventors": "고석주, 김지인",
"application_no": "2009-0048796",
"application_at": "2009-06-02",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2009-06"
},
{
"title": "이종 무선망간 수직 핸드오버를 위한 이동 SCTP의 적응적 혼잡 제어 방법 및 장치",
"inventors": "고석주, 김동필",
"application_no": "2009-0013072",
"application_at": "2009-02-17",
"registration_no": "1048251",
"registration_at": "2011-07-04",
"country": "한국",
"published_at": "2009-02"
},
{
"title": "서비스 품질 향상을 위한 PR-SCTP 기반 실시간 멀티미디어 데이터 전송 방법",
"inventors": "고석주, 김상태",
"application_no": "2008-0135342",
"application_at": "2008-12-29",
"registration_no": "1040780",
"registration_at": "2011-06-03",
"country": "한국",
"published_at": "2008-12"
},
{
"title": "이종 무선망간의 수직적 핸드오버를 위한 데이터 전송 방법 및 장치",
"inventors": "고석주, 김동필",
"application_no": "2008-0083995",
"application_at": "2008-08-27",
"registration_no": "0980592",
"registration_at": "2010-08-31",
"country": "한국",
"published_at": "2008-08"
},
{
"title": "무선통신용 멀티캐스트 핸드오버 방법",
"inventors": "고석주, 박재성",
"application_no": "2008-0077918",
"application_at": "2008-08-08",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2008-08"
},
{
"title": "무선 인터넷 망에서의 바이캐스팅을 이용한 SIP 핸드오버 방법 및 이동 단말",
"inventors": "고석주, 이동화",
"application_no": "2008-0052456",
"application_at": "2008-06-04",
"registration_no": "0987555",
"registration_at": "2010-10-06",
"country": "한국",
"published_at": "2008-06"
},
{
"title": "무선 인터넷 환경에서의 바이캐스팅 기반 SCTP 핸드오버 방법 및 이동 단말",
"inventors": "고석주, 권순홍, 김동필",
"application_no": "2008-0049203",
"application_at": "2008-05-27",
"registration_no": "0980582",
"registration_at": "2010-08-31",
"country": "한국",
"published_at": "2008-05"
},
{
"title": "Proxy MIP 기반 무선 인터넷 망에서의 바이캐스팅을 이용한 핸드오버 방법 및 장치",
"inventors": "고석주, 김지인",
"application_no": "2008-0049153",
"application_at": "2008-05-27",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2008-05"
},
{
"title": "윈도우 기반의 이동 단말에서 SCTPLIB를 이용한 mSCTP 핸드오버 방법",
"inventors": "김용진, 고석주, 이동화, 김상태",
"application_no": "2007-0139730",
"application_at": "2007-12-28",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2007-12"
},
{
"title": "전송 처리율 향상을 위한 SCTP 우선경로 설정 방법",
"inventors": "고석주, 주수경",
"application_no": "2007-0102031",
"application_at": "2007-10-10",
"registration_no": null,
"registration_at": null,
"country": "한국",
"published_at": "2007-10"
},
{
"title": "무선 네트워크에서 패킷의 손상정보를 사용하는 TCP 혼잡제어 방법",
"inventors": "고석주, 최린, 이동화, 김용진",
"application_no": "2007-0061209",
"application_at": "2007-06-21",
"registration_no": "0870619",
"registration_at": "2008-11-19",
"country": "한국",
"published_at": "2007-06"
},
{
"title": "이동 단말의 SCTP 핸드오버와 모바일 아이피의 연동 장치 및 그 방법",
"inventors": "고석주, 김동필",
"application_no": "2007-0050648",
"application_at": "2007-05-25",
"registration_no": "0880112",
"registration_at": "2009-01-15",
"country": "한국",
"published_at": "2007-05"
},
{
"title": "청크첵섬을 사용하는 무선 인터넷 에스씨티피 송수신 시스템 및 방법",
"inventors": "김용진, 정종일, 고석주",
"application_no": "2006-0117121",
"application_at": "2006-11-24",
"registration_no": "0780921",
"registration_at": "2007-11-23",
"country": "한국",
"published_at": "2006-11"
},
{
"title": "데이터 링크계층의 링크 정보를 이용하여 핸드오버를 수행하는 단말장치",
"inventors": "고석주, 김동필",
"application_no": "2005-0110213",
"application_at": "2005-11-17",
"registration_no": "0685740",
"registration_at": "2007-02-15",
"country": "한국",
"published_at": "2005-11"
},
{
"title": "SCTP 기반의 핸드오버 기능을 구비한 단말장치 및 핸드오버 방법",
"inventors": "고석주, 김동필",
"application_no": "US 11-915457 (미국, 2007.11.26)",
"application_at": "2005-05-27",
"registration_no": "US 8,644,248 (미국, 2014.2.4)",
"registration_at": "2007-01-26",
"country": "한국",
"published_at": "2005-05"
}
]
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
[
{
"name_en": "Quick UDP Internet Connection (QUIC)",
"name_kr": null,
"display_order": 1
},
{
"name_en": "QUIC-based Mobility and Multi-Streaming Support",
"name_kr": null,
"display_order": 2
},
{
"name_en": "Services and Protocols for Internet-of-Things (IoT)",
"name_kr": null,
"display_order": 3
},
{
"name_en": "Constrained Application Protocol (CoAP)",
"name_kr": null,
"display_order": 4
},
{
"name_en": "In-Vehicle Infotainment (IVI)",
"name_kr": null,
"display_order": 5
},
{
"name_en": "Optical Wireless or Visible Light Communications (OWC/VLC)",
"name_kr": null,
"display_order": 6
},
{
"name_en": "Lighting Control Networks (PLASA E1.45) for LED-based VLC",
"name_kr": null,
"display_order": 7
},
{
"name_en": "Mobility Management: Distributed Mobility Control",
"name_kr": null,
"display_order": 8
},
{
"name_en": "Multicast Routing Protocols and Reliable Multicasting",
"name_kr": null,
"display_order": 9
},
{
"name_en": "mobile SCTP (mSCTP): Soft Handover in Transport Layer",
"name_kr": null,
"display_order": 10
},
{
"name_en": "Stream Control Transmission Protocol (SCTP)",
"name_kr": null,
"display_order": 11
},
{
"name_en": "Telecommunication Optimization: Network Planning and Management",
"name_kr": null,
"display_order": 12
},
{
"name_en": "TAC/TAL Configuration for Paging and Location Management",
"name_kr": null,
"display_order": 13
},
{
"name_en": "Tabu Search and Genetic Algorithm",
"name_kr": null,
"display_order": 14
}
]
@@ -1,7 +1,4 @@
// AUTO-GENERATED by scripts/extract/generate_all.py DO NOT EDIT BY HAND
import { ResearchProject } from "./types";
export const researchProjects: ResearchProject[] = [
[
{
"slug": "iot-standards",
"title": "AIoT & AI-RAN",
@@ -50,4 +47,4 @@ export const researchProjects: ResearchProject[] = [
"period": "2026 ~ Present",
"deliverables": []
}
];
]
@@ -1,7 +1,4 @@
// AUTO-GENERATED by scripts/extract/generate_all.py DO NOT EDIT BY HAND
import { StandardsBody } from "./types";
export const standardsBodies: StandardsBody[] = [
[
{
"org": "IEC TC100",
"full": "Multimedia Systems and Equipment",
@@ -384,4 +381,4 @@ export const standardsBodies: StandardsBody[] = [
}
]
}
];
]
+27
View File
@@ -0,0 +1,27 @@
services:
anl-backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: anl-backend-api
restart: unless-stopped
ports:
- "8080:8080"
environment:
- HOST=0.0.0.0
- PORT=8080
- DB_PATH=/app/data/anl.db
- GIN_MODE=release
- CORS_ALLOW_ORIGINS=*
- AUTO_MIGRATE=true
volumes:
- anl-db-data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"]
interval: 30s
timeout: 3s
retries: 3
volumes:
anl-db-data:
name: anl-sqlite-storage
+6
View File
@@ -0,0 +1,6 @@
# ==============================================================================
# AI Agent Networking Lab (ANL) Next.js Frontend Configuration
# ==============================================================================
# Backend REST API Base URL (used at build time / SSR)
API_BASE_URL=http://localhost:8080/api/v1
@@ -1,9 +1,14 @@
import React from "react";
import { researchProjects } from "../../content/projects";
import type { ResearchProject } from "../../content/types";
import ProjectPageViewControls from "./ProjectPageViewControls";
export function ProjectPageView() {
const slugs = researchProjects.map((p) => p.slug);
interface ProjectPageViewProps {
projects?: ResearchProject[];
}
export function ProjectPageView({ projects: projectsProp }: ProjectPageViewProps = {}) {
const projects = projectsProp ?? [];
const slugs = projects.map((p) => p.slug);
const trackId = "research-projects-track";
return (
@@ -21,7 +26,7 @@ export function ProjectPageView() {
</h2>
</div>
<p className="text-xs text-ink-mute font-mono">
{researchProjects.length} core international standardization research projects
{projects.length} core international standardization research projects
</p>
</div>
@@ -34,13 +39,13 @@ export function ProjectPageView() {
aria-label="연구 프로젝트 목록"
className="flex gap-6 overflow-x-auto snap-x snap-mandatory scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden desktop:grid desktop:grid-cols-3 desktop:overflow-visible focus:outline-none focus:ring-1 focus:ring-cobalt/50 rounded"
>
{researchProjects.map((proj, idx) => (
{projects.map((proj, idx) => (
<article
key={proj.slug}
id={`project-${proj.slug}`}
role="group"
aria-roledescription="slide"
aria-label={`${researchProjects.length}개 중 ${idx + 1}번째 프로젝트`}
aria-label={`${projects.length}개 중 ${idx + 1}번째 프로젝트`}
className="snap-start shrink-0 min-w-0 w-full md:w-[calc(50%-12px)] desktop:w-full flex flex-col justify-between bg-paper p-6 rounded-lg border border-line hover:border-cobalt/60 transition-colors space-y-5 break-words [word-break:keep-all]"
>
<div className="space-y-4">
@@ -76,7 +81,7 @@ export function ProjectPageView() {
{/* Metadata Footer: Standards Track & Deliverables */}
<div className="space-y-3 pt-3 border-t border-line/60 text-xs font-mono">
{/* Standards Track (or Funder if present, never placeholder if undefined) */}
{/* Standards Track / Organization */}
{proj.funder ? (
<div className="flex items-baseline justify-between gap-2">
<span className="text-ink-mute">Funder:</span>
@@ -84,7 +89,7 @@ export function ProjectPageView() {
</div>
) : proj.standardsTrack ? (
<div className="flex items-baseline justify-between gap-2">
<span className="text-ink-mute">Standards Track:</span>
<span className="text-ink-mute">Organization:</span>
<span className="text-cobalt-dark font-bold">{proj.standardsTrack}</span>
</div>
) : proj.standardsOrg ? (
+6 -6
View File
@@ -31,7 +31,7 @@
:root {
color-scheme: light;
/* Offline System & CSS Font Variables */
/* Offline System & CSS Font Variables */
--font-geist: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-geist-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--font-display: 'Fraunces', Georgia, Cambria, 'Times New Roman', Times, serif;
@@ -41,10 +41,10 @@
--ink-rgb: 10 10 10;
--ink-soft-rgb: 38 36 32;
--ink-mute-rgb: 107 101 91;
--ivory-rgb: 245 241 232;
--paper-rgb: 236 230 214;
--line-rgb: 216 209 192;
--ink-mute-rgb: 107 114 128;
--ivory-rgb: 255 255 255;
--paper-rgb: 248 249 250;
--line-rgb: 229 231 235;
--vermillion-rgb: 217 52 43;
--vermillion-dark-rgb: 168 35 28;
--cobalt-rgb: 78 119 255;
@@ -61,7 +61,7 @@
--cobalt: rgb(var(--cobalt-rgb));
--cobalt-dark: rgb(var(--cobalt-dark-rgb));
--grain-color: rgba(10, 10, 10, 0.022);
--grain-color: rgba(10, 10, 10, 0.015);
/* CUSTOMIZATION HOOK — MOTION: global reveal timing */
--reveal-duration: 600ms;
+50 -47
View File
@@ -1,61 +1,63 @@
import React from "react";
import { semesters } from "../../content/lectures";
import type { Metadata } from "next";
import { getSemesters } from "../../lib/api";
export const metadata: Metadata = {
title: "Lectures",
description: "AI Agent Networking Lab (ANL) lecture courses and curriculum history",
};
export const dynamic = "force-dynamic";
export default async function LecturesPage() {
const semesters = (await getSemesters()) ?? [];
export default function LecturesPage() {
const recentSemesters = semesters.slice(0, 3);
const olderSemesters = semesters.slice(3);
const oldestSem = olderSemesters[olderSemesters.length - 1];
const newestOlderSem = olderSemesters[0];
return (
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
{/* Header */}
<section className="space-y-3">
<div className="flex items-center gap-3">
<span className="h-px w-8 bg-cobalt-dark"></span>
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
Academic Courses
</span>
</div>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
LECTURES
</h1>
<p className="text-ink-mute text-base max-w-2xl">
2016 2026 20 / .
</p>
</section>
{/* Recent 3 Semesters */}
<section className="space-y-6 pt-6 md:pt-10 border-t border-line">
<div className="flex justify-between items-end">
<div>
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
Recent Courses ({recentSemesters.length} Semesters)
</div>
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
</h2>
</div>
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-center">
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
Recent Courses
</h2>
<span className="font-mono text-xs text-ink-mute hidden sm:inline">
Latest 3 Semesters
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 sm:gap-6">
{recentSemesters.map((sem, idx) => (
<div
key={idx}
className="bg-paper p-6 rounded-lg border border-line space-y-4 flex flex-col justify-between"
className="bg-paper p-5 sm:p-6 rounded-lg border border-line space-y-4 flex flex-col justify-between shadow-sm"
>
<div className="space-y-3">
<div className="flex justify-between items-center border-b border-line pb-2">
<span className="font-serif text-xl font-normal text-ink">
<div className="flex justify-between items-center border-b border-line pb-2.5">
<span className="font-serif text-lg sm:text-xl font-normal text-ink">
{sem.term} {sem.year}
</span>
<span className="font-mono text-xs text-vermillion-dark font-bold">
<span className="font-mono text-[0.7rem] uppercase tracking-wider text-vermillion-dark font-bold px-2 py-0.5 rounded bg-vermillion-dark/10">
Latest
</span>
</div>
<ul className="space-y-2 pt-1">
<ul className="space-y-2.5 pt-1">
{sem.courses.map((c, cIdx) => (
<li key={cIdx} className="space-y-0.5">
<div className="font-serif text-base text-ink">
<div className="font-serif text-sm sm:text-base text-ink leading-snug">
{c.en}
</div>
<div className="font-mono text-xs text-ink-mute">
@@ -66,8 +68,9 @@ export default function LecturesPage() {
</ul>
</div>
<div className="pt-3 border-t border-line/60 font-mono text-xs text-ink-mute">
Course Count: {sem.courses.length}
<div className="pt-3 border-t border-line/60 font-mono text-xs text-ink-mute flex justify-between items-center">
<span>Total Offerings</span>
<span className="font-semibold text-ink">{sem.courses.length} Courses</span>
</div>
</div>
))}
@@ -75,30 +78,30 @@ export default function LecturesPage() {
</section>
{/* Older Semesters (Native <details>/<summary> progressive disclosure) */}
<section className="space-y-6 pt-6 border-t border-line">
<details className="group border border-line rounded-lg bg-paper p-6 transition-all duration-200">
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<details className="group border border-line rounded-lg bg-paper p-5 sm:p-6 transition-all duration-200">
<summary className="flex justify-between items-center cursor-pointer list-none select-none">
<div className="space-y-1">
<span className="font-serif text-xl font-normal text-ink">
({olderSemesters.length} )
<span className="font-serif text-lg sm:text-xl font-normal text-ink">
Browse all semesters
</span>
<p className="text-xs text-ink-mute font-mono">
2016 Fall ~ 2024 Spring
Course history from {oldestSem ? `${oldestSem.year} ${oldestSem.term}` : "2004 Spring"} to {newestOlderSem ? `${newestOlderSem.year} ${newestOlderSem.term}` : "2024 Fall"} ({olderSemesters.length} Semesters)
</p>
</div>
<span className="font-mono text-xs text-vermillion-dark font-bold border border-vermillion-dark px-3 py-1.5 rounded group-open:bg-vermillion-dark group-open:text-white dark:group-open:text-ivory transition-colors">
<span className="group-open:hidden"> </span>
<span className="hidden group-open:inline"> </span>
<span className="font-mono text-xs text-vermillion-dark font-bold border border-vermillion-dark px-3.5 py-1.5 rounded group-open:bg-vermillion-dark group-open:text-white dark:group-open:text-ivory transition-colors">
<span className="group-open:hidden">Open </span>
<span className="hidden group-open:inline">Close </span>
</span>
</summary>
<div className="mt-6 pt-6 border-t border-line grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="mt-6 pt-6 border-t border-line grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 sm:gap-6">
{olderSemesters.map((sem, idx) => (
<div
key={idx}
className="bg-ivory p-5 rounded border border-line space-y-3"
className="bg-ivory p-5 rounded-lg border border-line space-y-3 shadow-sm hover:border-cobalt-dark/40 transition-colors"
>
<div className="font-serif text-lg font-normal text-ink border-b border-line pb-1.5 flex justify-between">
<div className="font-serif text-base sm:text-lg font-normal text-ink border-b border-line pb-2 flex justify-between items-center">
<span>
{sem.term} {sem.year}
</span>
@@ -110,7 +113,7 @@ export default function LecturesPage() {
<ul className="space-y-2">
{sem.courses.map((c, cIdx) => (
<li key={cIdx} className="space-y-0.5">
<div className="font-serif text-sm text-ink">
<div className="font-serif text-sm text-ink leading-snug">
{c.en}
</div>
<div className="font-mono text-[0.7rem] text-ink-mute">
@@ -0,0 +1,179 @@
"use client";
import React, { useRef } from "react";
// CUSTOMIZATION HOOK: Detailed career & background data for Prof. Seok-Joo Koh
const EDUCATION = [
"KAIST, Management Science, B. S., 1992. 2.",
"KAIST, Management Science, M. S., 1994. 2.",
"KAIST, Industrial Engineering, Ph. D., 1998. 8.",
];
const CAREER = [
{
period: "2024. 3 - Present",
role: "Dean",
org: "College of IT Engineering (CITE)",
inst: "Kyungpook National University (KNU)",
},
{
period: "2015. 12 - Present",
role: "Director",
org: "Institute of Software Education (SWEDU)",
inst: "Kyungpook National University (KNU)",
},
{
period: "2004. 3 - Present",
role: "Professor",
org: "School of Computer Science and Engineering (CSE), College of IT Engineering (CITE)",
inst: "Kyungpook National University (KNU)",
},
{
period: "1998. 8 - 2004. 2",
role: "Senior Researcher",
org: "Protocol Engineering Center (PEC)",
inst: "Electronics Telecommunications Research Institute (ETRI)",
},
];
const STANDARDIZATION = [
"ISO/IEC JTC1 SC6/WG7: Reliable/Mobile Multicasting (1999 ~ 2019): Project Editor",
"ISO/IEC JTC1 SC41/WG5: Internet-of-Things (IoT) Applications (2020 ~ Present): Project Editor",
"ITU-T SG13: Mobility Management (1999 ~ 2015): Project Editor",
"ITU-T SG20: VLC-based IoT (2018 ~ 2020): Project Editor",
"IEC TC100: Multimedia Systems and Equipment (2018 ~ Present): Project Editor, TA15 TAM, WG11 Convenor",
];
const MEMBERSHIPS = [
"KIPS (Korea Information Processing Society)",
"KICS (Korean Institute of Communications and Information Sciences)",
"KIISE (Korean Institute of Information Scientists and Engineers)",
"IEEE (Institute of Electrical and Electronics Engineers)",
];
export function ProfessorDetailsDialog() {
const dialogRef = useRef<HTMLDialogElement>(null);
const handleOpen = () => {
dialogRef.current?.showModal();
};
const handleClose = () => {
dialogRef.current?.close();
};
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
if (e.target === dialogRef.current) {
dialogRef.current?.close();
}
};
return (
<>
<button
type="button"
onClick={handleOpen}
className="inline-flex items-center gap-1 font-mono text-xs font-semibold text-cobalt-dark hover:underline focus:outline-none focus:ring-1 focus:ring-cobalt-dark rounded"
aria-haspopup="dialog"
>
<span>Details </span>
</button>
<dialog
ref={dialogRef}
aria-labelledby="prof-dialog-title"
onClick={handleBackdropClick}
className="bg-paper text-ink border border-line rounded-lg p-0 max-w-2xl w-[92vw] max-h-[85vh] supports-[height:100dvh]:max-h-[85dvh] backdrop:bg-ink/50 backdrop:backdrop-blur-sm open:flex open:flex-col shadow-2xl m-auto"
>
{/* Modal Header */}
<div className="flex items-center justify-between border-b border-line p-6 bg-paper sticky top-0 z-10">
<div>
<span className="font-mono text-xs text-cobalt-dark uppercase tracking-wider font-bold">
Curriculum Vitae
</span>
<h2
id="prof-dialog-title"
className="font-serif text-2xl font-normal text-ink mt-0.5"
>
Prof. Seok-Joo Koh
</h2>
</div>
<button
type="button"
onClick={handleClose}
aria-label="Close dialog"
className="p-1.5 rounded border border-line hover:bg-ivory text-ink-mute hover:text-ink transition-colors font-mono text-xs"
>
Close
</button>
</div>
{/* Modal Scrollable Content */}
<div className="p-6 sm:p-8 space-y-8 overflow-y-auto overscroll-contain text-sm">
{/* Education */}
<section className="space-y-3">
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
Education
</h3>
<ul className="space-y-2 text-ink/90 font-sans">
{EDUCATION.map((edu, idx) => (
<li key={idx} className="flex items-start gap-2.5">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
<span>{edu}</span>
</li>
))}
</ul>
</section>
{/* Career */}
<section className="space-y-3">
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
Career
</h3>
<div className="space-y-4">
{CAREER.map((car, idx) => (
<div key={idx} className="border-l-2 border-line pl-3 space-y-0.5">
<div className="font-mono text-xs text-ink-mute">{car.period}</div>
<div className="font-medium text-ink">
{car.role} · <span className="text-ink-mute font-normal">{car.org}</span>
</div>
<div className="text-xs text-ink-soft">{car.inst}</div>
</div>
))}
</div>
</section>
{/* International Standardization */}
<section className="space-y-3">
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
International Standardization
</h3>
<ul className="space-y-2.5 text-ink/90 font-sans">
{STANDARDIZATION.map((std, idx) => (
<li key={idx} className="flex items-start gap-2.5 leading-relaxed">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
<span>{std}</span>
</li>
))}
</ul>
</section>
{/* Membership on Academic Society */}
<section className="space-y-3">
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
Membership on Academic Society
</h3>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-ink/90 font-sans">
{MEMBERSHIPS.map((mem, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
<span>{mem}</span>
</li>
))}
</ul>
</section>
</div>
</dialog>
</>
);
}
+63 -59
View File
@@ -1,74 +1,84 @@
import React from "react";
import { activeMembers, alumni } from "../../content/members";
import type { Metadata } from "next";
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";
const DEGREE_LABEL: Record<string, string> = {
Professor: "Professor",
PhD: "Ph. D.",
PhDCandidate: "Ph. D. Candidate",
PhDStudent: "Ph. D. Student",
MSCandidate: "M. S. Candidate",
MSStudent: "M. S. Student",
MS: "M. S.",
};
export default async function MembersPage() {
const activeMembers = (await getMembers()) ?? [];
const alumni = (await getAlumni()) ?? [];
export default function MembersPage() {
const professor = activeMembers.find((m) => m.degree === "Professor");
const phdStudents = activeMembers.filter(
(m) => m.degree === "PhD" || m.degree === "PhDCandidate" || m.degree === "PhDStudent"
(m) =>
m.degree === "PhD" ||
m.degree === "PhDCandidate" ||
m.degree === "PhDStudent"
);
const msStudents = activeMembers.filter(
(m) => m.degree === "MSCandidate" || m.degree === "MSStudent"
);
return (
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
{/* Header */}
<section className="space-y-3">
<div className="flex items-center gap-3">
<span className="h-px w-8 bg-cobalt-dark"></span>
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
People &amp; Researchers
</span>
</div>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
LAB MEMBERS
</h1>
<p className="text-ink-mute text-base max-w-2xl">
16, IT 60 .
</p>
</section>
{/* Professor Section */}
<section className="space-y-6 pt-6 md:pt-10 border-t border-line">
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
Faculty
</div>
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="bg-paper p-8 rounded-lg border border-line grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
<div className="lg:col-span-8 space-y-4">
<div className="flex items-baseline gap-3">
<h2 className="font-serif text-3xl font-normal text-ink">
{professor?.ko || "고석주"}
</h2>
<span className="font-mono text-sm text-ink-mute">
{professor?.en || "Seok-Joo Koh"}, Professor
</span>
<h2 className="font-serif text-3xl font-normal text-ink">
Prof. Seok-Joo Koh
</h2>
<div className="text-sm text-ink-mute leading-relaxed space-y-2">
<p>
Professor at the School of Computer Science and Engineering, Kyungpook National University, and Dean of the College of IT Engineering. He directs the AI Agent Networking Laboratory (ANL), leading research on QUIC/gRPC transport, A2A/ACP protocols, MCP &amp; SKILL architectures, and multi-agent orchestration, and serves as Project Editor in ISO/IEC JTC1, ITU-T, and IEC TC100.
</p>
<div>
<ProfessorDetailsDialog />
</div>
</div>
<p className="text-sm text-ink-mute leading-relaxed">
· AI (ANL) .
QUIC/gRPC , A2A/ACP , MCP &amp; SKILL .
</p>
<div className="pt-4 border-t border-line/60 flex flex-wrap gap-4 text-xs font-mono">
<a
href="mailto:sjkoh@knu.ac.kr"
className="text-cobalt-dark hover:underline"
className="text-cobalt-dark hover:underline font-bold"
>
sjkoh@knu.ac.kr
</a>
<span className="text-ink-mute"></span>
<span className="text-ink-mute">|</span>
<a
href="https://github.com/sjkoh12"
target="_blank"
rel="noreferrer"
className="text-cobalt-dark hover:underline"
rel="noopener noreferrer"
className="text-ink hover:text-cobalt-dark hover:underline"
>
GitHub @sjkoh12
GitHub Profile
</a>
</div>
</div>
<div className="lg:col-span-4 bg-ivory p-6 rounded border border-line text-xs space-y-2 font-mono">
<div className="text-ink font-bold border-b border-line pb-1">
Affiliation & Office
Affiliation &amp; Office
</div>
<p className="text-ink-mute">Kyungpook National University</p>
<p className="text-ink-mute">IT Building #5 Room 527</p>
@@ -78,14 +88,11 @@ export default function MembersPage() {
</section>
{/* Active Researchers Section */}
<section className="space-y-8 pt-6 md:pt-10 border-t border-line">
<section className="space-y-8 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-end">
<div>
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
Active Researchers ({activeMembers.length} Members)
</div>
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
Active Researchers
</h2>
</div>
</div>
@@ -106,7 +113,7 @@ export default function MembersPage() {
{mem.ko}
</span>
<span className="font-mono text-xs text-cobalt-dark">
{mem.degree}
{DEGREE_LABEL[mem.degree] || mem.degree}
</span>
</div>
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
@@ -136,7 +143,7 @@ export default function MembersPage() {
{mem.ko}
</span>
<span className="font-mono text-xs text-cobalt-dark">
{mem.degree}
{DEGREE_LABEL[mem.degree] || mem.degree}
</span>
</div>
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
@@ -152,14 +159,11 @@ export default function MembersPage() {
</section>
{/* Alumni Section (Native <details>/<summary> progressive disclosure) */}
<section className="space-y-6 pt-6 md:pt-10 border-t border-line">
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-end">
<div>
<div className="font-mono text-xs text-ink-mute uppercase tracking-widest font-bold">
Alumni Inventory ({alumni.length} Graduates)
</div>
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
Graduates
</h2>
</div>
</div>
@@ -168,15 +172,15 @@ export default function MembersPage() {
<summary className="flex justify-between items-center cursor-pointer list-none select-none">
<div className="space-y-1">
<span className="font-serif text-xl font-normal text-ink">
{alumni.length}
Browse all graduates
</span>
<p className="text-xs text-ink-mute font-mono">
2007 2026 (Ph.D.) (M.S.)
Ph. D. and M. S. graduates conferred from 2007 to 2026
</p>
</div>
<span className="font-mono text-xs text-vermillion-dark font-bold border border-vermillion-dark px-3 py-1.5 rounded group-open:bg-vermillion-dark group-open:text-white dark:group-open:text-ivory transition-colors">
<span className="group-open:hidden"> </span>
<span className="hidden group-open:inline"> </span>
<span className="group-open:hidden">Open </span>
<span className="hidden group-open:inline">Close </span>
</span>
</summary>
@@ -191,7 +195,7 @@ export default function MembersPage() {
{alum.ko} ({alum.en})
</span>
<span className="font-mono text-xs text-ink-mute">
{alum.degree}
{DEGREE_LABEL[alum.degree] || alum.degree}
</span>
</div>
<div className="font-mono text-xs text-ink-mute flex justify-between">
+18 -36
View File
@@ -2,26 +2,9 @@ import React from "react";
import Link from "next/link";
import HeroComposition from "./_components/HeroComposition";
import { ProjectPageView } from "./_components/ProjectPageView";
import { publications, patents } from "../content/publications";
import { standardsBodies } from "../content/standards";
import { getStatsSummary, getResearchAreaNames, getResearchProjects } from "../lib/api";
// CUSTOMIZATION HOOK: 14 Core Research Areas
const RESEARCH_AREAS: string[] = [
"Quick UDP Internet Connection (QUIC)",
"QUIC-based Mobility and Multi-Streaming Support",
"Services and Protocols for Internet-of-Things (IoT)",
"Constrained Application Protocol (CoAP)",
"In-Vehicle Infotainment (IVI)",
"Optical Wireless or Visible Light Communications (OWC/VLC)",
"Lighting Control Networks (PLASA E1.45) for LED-based VLC",
"Mobility Management: Distributed Mobility Control",
"Multicast Routing Protocols and Reliable Multicasting",
"mobile SCTP (mSCTP): Soft Handover in Transport Layer",
"Stream Control Transmission Protocol (SCTP)",
"Telecommunication Optimization: Network Planning and Management",
"TAC/TAL Configuration for Paging and Location Management",
"Tabu Search and Genetic Algorithm",
];
export const dynamic = "force-dynamic";
const INTERNATIONAL_SDOS = [
"IEC TC100: Multimedia Systems and Equipment",
@@ -31,16 +14,15 @@ const INTERNATIONAL_SDOS = [
"ISO/IEC JTC1/SC6: Reliable Multicasting",
];
export default function HomePage() {
const intlPubsCount = publications.filter(
(p) => p.category === "intl-journal-conf"
).length;
const patentCount = patents.length;
const stdDocsCount = standardsBodies.reduce(
(sum, b) =>
sum + b.projects.reduce((pSum, p) => pSum + p.documents.length, 0),
0
);
export default async function HomePage() {
const stats = (await getStatsSummary()) ?? {
intlPubsCount: 0,
patentCount: 0,
stdDocsCount: 0,
};
const researchAreas = (await getResearchAreaNames()) ?? [];
const researchProjects = (await getResearchProjects()) ?? [];
return (
<div className="container-content space-y-16 md:space-y-24 pt-10 md:pt-16 pb-16 md:pb-24">
@@ -66,7 +48,7 @@ export default function HomePage() {
<div className="pt-6 border-t border-line grid grid-cols-3 gap-4">
<div>
<div className="font-serif text-3xl text-cobalt-dark font-normal">
{intlPubsCount}
{stats.intlPubsCount}
</div>
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
International Journals &amp; Conference
@@ -74,7 +56,7 @@ export default function HomePage() {
</div>
<div>
<div className="font-serif text-3xl text-cobalt-dark font-normal">
{stdDocsCount}
{stats.stdDocsCount}
</div>
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
Standardization Contributions
@@ -82,11 +64,11 @@ export default function HomePage() {
</div>
<div>
<div className="font-serif text-3xl text-cobalt-dark font-normal">
{patentCount}
{stats.patentCount}
</div>
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
Patent<br />
<span className="text-[0.68rem] tracking-normal font-sans normal-case text-ink-mute/80">
<span className="text-[0.68rem] tracking-normal font-sans normal-case text-ink-mute">
(IoT Architecture &amp; Protocols)
</span>
</div>
@@ -101,7 +83,7 @@ export default function HomePage() {
</section>
{/* Research Project PageView Component */}
<ProjectPageView />
<ProjectPageView projects={researchProjects} />
{/* 14 Research Areas Grid */}
<section className="space-y-8 pt-16 md:pt-20 border-t border-line">
@@ -118,12 +100,12 @@ export default function HomePage() {
</h2>
</div>
<p className="text-xs text-ink-mute font-mono">
{RESEARCH_AREAS.length} fundamental networking protocols and AI system domains
{researchAreas.length} fundamental networking protocols and AI system domains
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{RESEARCH_AREAS.map((area, idx) => {
{researchAreas.map((area, idx) => {
const no = String(idx + 1).padStart(2, "0");
const isOdd = (idx + 1) % 2 !== 0;
@@ -1,7 +1,8 @@
import React from "react";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { CategoryNav } from "../_components/CategoryNav";
import { publications, patents } from "../../../content/publications";
import { getPublications, getPatents } from "../../../lib/api";
import { PUB_CATEGORIES } from "../../../content/categories";
import { PubCategory } from "../../../content/types";
@@ -11,14 +12,20 @@ interface PageProps {
};
}
// SSG static pre-rendering (● SSG) for Next.js 14
export function generateStaticParams() {
return PUB_CATEGORIES.map((cat) => ({
category: cat.slug,
}));
export function generateMetadata({ params }: PageProps): Metadata {
const currentCat = PUB_CATEGORIES.find((c) => c.slug === params.category);
if (!currentCat) {
return { title: "Publications" };
}
return {
title: `${currentCat.label} | Publications`,
description: `AI Agent Networking Lab (ANL) ${currentCat.label} research inventory and records`,
};
}
export default function PublicationCategoryPage({ params }: PageProps) {
export const dynamic = "force-dynamic";
export default async function PublicationCategoryPage({ params }: PageProps) {
const { category } = params;
const currentCat = PUB_CATEGORIES.find((c) => c.slug === category);
@@ -28,6 +35,15 @@ export default function PublicationCategoryPage({ params }: PageProps) {
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) => ({
@@ -55,44 +71,39 @@ export default function PublicationCategoryPage({ params }: PageProps) {
}
return (
<div className="container-content space-y-8 md:space-y-12 pt-10 md:pt-16 pb-16 md:pb-24">
{/* Header */}
<section className="space-y-3">
<div className="flex items-center gap-3">
<span className="h-px w-8 bg-cobalt-dark"></span>
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
Publications Category
</span>
</div>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section className="space-y-1">
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
{currentCat.label}
</h1>
<p className="text-ink-mute text-base max-w-2xl">
{currentCat.desc} {records.length} .
</p>
{currentCat.subtitle && (
<p className="font-mono text-sm text-ink-mute">
{currentCat.subtitle}
</p>
)}
</section>
{/* Category Nav */}
<CategoryNav currentCategory={slug} />
{/* Category Nav Cards */}
<section className="pt-6 md:pt-8 border-t border-line">
<CategoryNav currentCategory={slug} counts={counts} />
</section>
{/* Record List Section */}
<section className="space-y-6">
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-center border-b border-line pb-3">
<span className="font-mono text-xs text-ink-mute uppercase">
Showing {records.length} Pre-rendered Records
</span>
<span className="font-mono text-xs text-vermillion-dark font-bold">
SSG Static HTML
{records.length} Records
</span>
</div>
{records.length === 0 ? (
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
<p className="font-serif text-lg text-ink">
0.
No records found in this category.
</p>
<p className="text-xs text-ink-mute font-mono">
(D-01 )
Records will be updated periodically.
</p>
</div>
) : (
@@ -116,13 +127,13 @@ export default function PublicationCategoryPage({ params }: PageProps) {
{rec.isPatent ? (
<div className="space-y-1 text-xs text-ink-mute font-mono">
<p className="text-ink">: {rec.inventors}</p>
<p className="text-ink">Inventors: {rec.inventors}</p>
<p>
: {rec.appNo} ({rec.country}, {rec.appDate})
App No: {rec.appNo} ({rec.country}, {rec.appDate})
</p>
{rec.regNo && (
<p className="text-cobalt-dark font-bold">
: {rec.regNo} ({rec.regDate})
Reg No: {rec.regNo} ({rec.regDate})
</p>
)}
</div>
@@ -133,7 +144,7 @@ export default function PublicationCategoryPage({ params }: PageProps) {
<span>{rec.volume}</span>
{rec.kci && (
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
KCI
KCI Indexed
</span>
)}
</div>
@@ -3,28 +3,108 @@ import Link from "next/link";
import { PUB_CATEGORIES } from "../../../content/categories";
import { PubCategory } from "../../../content/types";
interface CategoryNavProps {
currentCategory?: PubCategory;
export interface CategoryCounts {
"intl-journal-conf"?: number;
"domestic-journal-conf"?: number;
patent?: number;
}
export function CategoryNav({ currentCategory }: CategoryNavProps) {
interface CategoryNavProps {
currentCategory?: PubCategory;
counts?: CategoryCounts;
}
export function CategoryNav({ currentCategory, counts }: CategoryNavProps) {
const getCount = (slug: string) => {
if (counts?.[slug as keyof CategoryCounts] !== undefined) {
return counts[slug as keyof CategoryCounts]!;
}
return 0;
};
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2 border-b border-line pb-4">
<div className="flex justify-between items-center">
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
Categories
</h2>
{currentCategory && (
<Link
href="/publications"
className="font-mono text-xs text-ink-mute hover:text-ink hover:underline transition-colors"
>
&larr; Overview
</Link>
)}
</div>
<div className="grid gap-5 sm:gap-6 md:grid-cols-3">
{PUB_CATEGORIES.map((cat) => {
const count = getCount(cat.slug);
const isActive = currentCategory === cat.slug;
return (
<Link
<div
key={cat.slug}
href={`/publications/${cat.slug}`}
className={`px-4 py-2 rounded text-xs font-mono tracking-wider transition-colors ${
className={`flex flex-col justify-between bg-paper p-5 sm:p-6 rounded-lg transition-all ${
isActive
? "bg-cobalt-dark text-white dark:text-ivory font-bold"
: "bg-paper text-ink border border-line hover:border-cobalt-dark"
? "border-2 border-cobalt-dark shadow-md"
: "border border-line shadow-sm hover:border-cobalt-dark/40"
}`}
>
{cat.label}
</Link>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span
className={`font-mono text-xs font-bold uppercase tracking-wider ${
isActive ? "text-cobalt-dark" : "text-ink-mute"
}`}
>
Category
</span>
{isActive && (
<span className="font-mono text-[0.65rem] font-bold px-2 py-0.5 rounded bg-cobalt-dark/10 text-cobalt-dark uppercase">
Current
</span>
)}
</div>
<h3
className={`font-serif text-xl ${
isActive ? "text-cobalt-dark font-normal" : "text-ink font-normal"
}`}
>
{cat.label}
{cat.subtitle && (
<span className="block text-xs font-mono text-ink-mute mt-1 font-normal">
{cat.subtitle}
</span>
)}
</h3>
</div>
<div className="mt-6 pt-4 border-t border-line flex items-baseline justify-between">
<span className="font-display text-3xl font-bold text-ink">
{count}
<span className="text-xs font-mono text-ink-mute ml-1 font-normal">
{cat.slug === "patent" ? "Patents" : "Papers"}
</span>
</span>
{isActive ? (
<span className="inline-flex items-center text-xs font-bold text-cobalt-dark font-mono">
Active View
</span>
) : (
<Link
href={`/publications/${cat.slug}`}
aria-label={`View all ${cat.label} records`}
className="inline-flex items-center text-xs font-bold text-vermillion-dark hover:underline focus:outline-none focus:ring-1 focus:ring-vermillion-dark rounded"
>
View All &rarr;
</Link>
)}
</div>
</div>
);
})}
</div>
+39 -87
View File
@@ -1,100 +1,52 @@
import Link from "next/link";
import type { Metadata } from "next";
import { CategoryNav } from "./_components/CategoryNav";
import { PUB_CATEGORIES } from "../../content/categories";
import { publications, patents } from "../../content/publications";
import { getPublications, getPatents } from "../../lib/api";
export const metadata = {
title: "Publications Overview | ANL",
description: "AI 에이전트 네트워크 연구실(ANL) 연구 실적 개요 및 카테고리별 집계",
export const metadata: Metadata = {
title: "Publications",
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
};
export default function PublicationsIndexPage() {
const totalPubCount = publications.length + patents.length;
export const dynamic = "force-dynamic";
// Sort publication records by publishedAt descending to select top 10 highlights
const top10Highlights = [...publications]
.sort((a, b) => (b.publishedAt || "").localeCompare(a.publishedAt || ""))
.slice(0, 10);
export default async function PublicationsIndexPage() {
const publications = (await getPublications()) ?? [];
const patents = (await getPatents()) ?? [];
const getCount = (slug: string) => {
if (slug === "patent") return patents.length;
return publications.filter((p) => p.category === slug).length;
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,
};
return (
<main className="container-content space-y-8 md:space-y-12 pt-10 md:pt-16 pb-16 md:pb-24">
{/* Header section */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<span className="h-px w-8 bg-cobalt-dark" />
<span className="font-mono text-xs font-bold uppercase tracking-widest text-cobalt-dark">
BIBLIOGRAPHY &amp; INTELLECTUAL PROPERTY
</span>
</div>
<h1 className="headline-ko text-3xl sm:text-4xl text-ink font-bold">
</h1>
<p className="text-sm sm:text-base text-ink-mute max-w-3xl leading-relaxed">
AI (ANL) ({publications.length}) ({patents.length}) {totalPubCount} .
</p>
</section>
// Selected representative publications (curated via database isHighlight flag)
const highlights = publications.filter((p) => p.isHighlight);
{/* Category Navigation */}
<CategoryNav />
return (
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
PUBLICATIONS
</h1>
</section>
{/* Category Summary Cards */}
<section className="grid gap-6 md:grid-cols-3">
{PUB_CATEGORIES.map((cat) => {
const count = getCount(cat.slug);
return (
<div
key={cat.slug}
className="flex flex-col justify-between border border-line bg-paper p-6 rounded-lg transition-colors hover:border-cobalt-dark group"
>
<div className="space-y-3">
<span className="font-mono text-xs text-ink-mute uppercase tracking-wider">
Category
</span>
<h2 className="headline-ko text-xl text-ink font-bold group-hover:text-cobalt-dark transition-colors">
{cat.label}
</h2>
<p className="text-xs text-ink-mute leading-relaxed">
{cat.desc}
</p>
</div>
<div className="mt-6 pt-4 border-t border-line flex items-baseline justify-between">
<span className="font-display text-3xl font-bold text-ink">
{count}
<span className="text-xs font-sans text-ink-mute ml-1 font-normal"></span>
</span>
<Link
href={`/publications/${cat.slug}`}
className="inline-flex items-center text-xs font-bold text-vermillion-dark hover:underline"
>
&rarr;
</Link>
</div>
</div>
);
})}
<section className="pt-6 md:pt-8 border-t border-line">
<CategoryNav counts={counts} />
</section>
{/* Top 10 Highlights Section */}
<section className="space-y-6 pt-6 border-t border-line">
<div className="flex items-center justify-between">
<h2 className="headline-ko text-2xl font-bold text-ink">
(Top 10 Highlights)
</h2>
<span className="font-mono text-xs text-ink-mute">
RECENT HIGHLIGHTS
</span>
</div>
{/* Highlights Section */}
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
Highlights
</h2>
<div className="divide-y divide-line border border-line bg-ivory rounded-lg overflow-hidden">
{top10Highlights.map((pub, idx) => (
<article key={`${pub.category}-${idx}`} className="p-5 hover:bg-paper/50 transition-colors space-y-2">
<div className="divide-y divide-line border border-line bg-paper rounded-lg overflow-hidden shadow-sm">
{highlights.map((pub, idx) => (
<article key={`${pub.category}-${idx}`} className="p-5 hover:bg-ivory/60 transition-colors space-y-2">
<div className="flex flex-wrap items-center gap-2 text-xs font-mono text-ink-mute">
<span className="px-2 py-0.5 rounded bg-paper border border-line text-ink font-bold">
<span className="px-2 py-0.5 rounded bg-ivory border border-line text-ink font-bold">
{pub.publishedAt?.substring(0, 7) || "Recent"}
</span>
<span>·</span>
@@ -102,11 +54,11 @@ export default function PublicationsIndexPage() {
{pub.doi && (
<>
<span>·</span>
<span className="text-vermillion">DOI: {pub.doi}</span>
<span className="text-vermillion-dark">DOI: {pub.doi}</span>
</>
)}
</div>
<h3 className="font-semibold text-base text-ink leading-snug">
<h3 className="font-serif text-base font-normal text-ink leading-snug">
{pub.title}
</h3>
{pub.authors && (
@@ -115,14 +67,14 @@ export default function PublicationsIndexPage() {
</p>
)}
{pub.venue && (
<p className="text-xs italic text-ink-soft">
{pub.venue}
<p className="text-xs italic text-ink-mute">
{pub.venue}{pub.volume ? `, ${pub.volume}` : ""}
</p>
)}
</article>
))}
</div>
</section>
</main>
</div>
);
}
+18 -24
View File
@@ -1,35 +1,29 @@
import React from "react";
import { standardsBodies } from "../../content/standards";
import type { Metadata } from "next";
import { getStandardsBodies } from "../../lib/api";
export default function StandardizationPage() {
const totalDocs = standardsBodies.reduce(
(acc, body) =>
acc + body.projects.reduce((pAcc, proj) => pAcc + proj.documents.length, 0),
0
);
export const metadata: Metadata = {
title: "Standardization",
description: "AI Agent Networking Lab (ANL) international standardization research and contributions",
};
export const dynamic = "force-dynamic";
export default async function StandardizationPage() {
const standardsBodies = (await getStandardsBodies()) ?? [];
return (
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
{/* Header */}
<section className="space-y-3">
<div className="flex items-center gap-3">
<span className="h-px w-8 bg-cobalt-dark"></span>
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
Open Standards &amp; Contributions
</span>
</div>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section>
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
International Standardization
</h1>
<p className="text-ink-mute text-base max-w-2xl">
IEC TC100, ISO/IEC JTC1, ITU-T, TTA / {standardsBodies.length} {totalDocs} .
</p>
</section>
{/* Standards Bodies Section */}
<section className="space-y-12 pt-6 md:pt-10 border-t border-line">
<section className="space-y-8 sm:space-y-10 pt-6 md:pt-8 border-t border-line">
{standardsBodies.map((body, idx) => (
<div key={idx} className="bg-paper p-8 rounded-lg border border-line space-y-6">
<div key={idx} className="bg-paper p-6 sm:p-8 rounded-lg border border-line space-y-6 shadow-sm">
<div className="flex flex-wrap justify-between items-start gap-4 border-b border-line pb-4">
<div>
<div className="flex items-center gap-2">
@@ -103,7 +97,7 @@ export default function StandardizationPage() {
</div>
) : (
<div className="text-xs text-ink-mute font-mono italic">
Project specification in progress
</div>
)}
</div>
+1 -1
View File
@@ -101,7 +101,7 @@ export default function Footer() {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-2.5 py-1 rounded bg-ivory border border-line text-ink-soft hover:text-cobalt-dark hover:border-cobalt/40 transition-colors"
>
<span>College of IT Engineering</span>
<span>CITE</span>
<span className="text-[0.65rem] text-ink-mute"></span>
</a>
</div>
+16 -15
View File
@@ -12,11 +12,11 @@ const NAV_SETTLE_MS = 300; // ms — 라우트 전환 직후 방향 판정 유
// CUSTOMIZATION HOOK: edit nav labels / routes here.
const navItems = [
{ href: "/", ko: "홈", en: "Home", no: "01" },
{ href: "/members", ko: "구성원", en: "Members", no: "02" },
{ href: "/publications", ko: "논문", en: "Publications", no: "03" },
{ href: "/lectures", ko: "강의", en: "Lectures", no: "04" },
{ href: "/standardization", ko: "표준화", en: "Standardization", no: "05" },
{ href: "/", en: "Home", no: "01" },
{ href: "/members", en: "Members", no: "02" },
{ href: "/publications", en: "Publications", no: "03" },
{ href: "/lectures", en: "Lectures", no: "04" },
{ href: "/standardization", en: "Standardization", no: "05" },
];
export default function Header() {
@@ -130,7 +130,7 @@ export default function Header() {
{/* Brand logo */}
<Link
href="/"
className="group flex items-center gap-3 shrink-0"
className="group flex items-center gap-3 min-w-0 desktop:shrink-0"
onClick={() => setOpen(false)}
>
<div className="relative flex items-center justify-center w-9 h-9 sm:w-10 sm:h-10 rounded-xl overflow-hidden shadow-sm ring-1 ring-ink/10 group-hover:ring-cobalt/40 group-hover:shadow-md group-hover:scale-105 transition-all duration-300 shrink-0">
@@ -158,11 +158,11 @@ export default function Header() {
</svg>
</div>
<div className="flex flex-col leading-none">
<div className="flex flex-col leading-none min-w-0">
<span className="headline-ko text-lg text-ink sm:text-xl font-bold tracking-tight group-hover:text-cobalt-dark transition-colors">
ANL
</span>
<span className="kicker mt-1 transition-colors group-hover:text-cobalt-dark">
<span className="kicker mt-1 transition-colors group-hover:text-cobalt-dark truncate">
AI Agent Networking LAB
</span>
</div>
@@ -170,7 +170,7 @@ export default function Header() {
{/* Desktop nav — small caps, wide tracking (visible at >= 1240px) */}
<div className="hidden items-center gap-4 desktop:flex">
<nav className="flex items-center gap-2" aria-label="주 메뉴">
<nav className="flex items-center gap-2" aria-label="Main Navigation">
{navItems.map((item) => {
const active = isActive(item.href);
return (
@@ -206,7 +206,7 @@ export default function Header() {
<button
type="button"
className="inline-flex items-center justify-center border border-ink p-2 text-ink transition hover:bg-ink hover:text-ivory"
aria-label={open ? "메뉴 닫기" : "메뉴 열기"}
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls="mobile-menu"
onClick={() => setOpen((v) => !v)}
@@ -227,7 +227,7 @@ export default function Header() {
<nav
id="mobile-menu"
className="mobile-menu-panel border-t border-ink bg-ivory desktop:hidden"
aria-label="모바일 메뉴"
aria-label="Mobile Navigation"
>
<ul className="container-content flex flex-col py-2">
{navItems.map((item) => {
@@ -252,14 +252,15 @@ export default function Header() {
>
{item.no}
</span>
<span className="headline-ko text-lg">{item.ko}</span>
<span className="font-sans text-base">{item.en}</span>
</span>
<span
className={`text-[0.7rem] font-semibold uppercase tracking-[0.18em] ${
active ? "text-white dark:text-ivory" : "text-ink-mute"
aria-hidden="true"
className={`text-xs font-mono ${
active ? "text-white/80 dark:text-ivory/80" : "text-ink-mute"
}`}
>
{item.en}
</span>
</Link>
</li>
+2
View File
@@ -3,6 +3,7 @@ import { PubCategory } from "./types";
export interface CategoryInfo {
slug: PubCategory;
label: string;
subtitle?: string;
desc: string;
}
@@ -21,6 +22,7 @@ export const PUB_CATEGORIES: CategoryInfo[] = [
{
slug: "patent",
label: "Patent",
subtitle: "(IoT Architecture & Protocols, etc ...)",
desc: "국내외 지식재산권 출원 및 등록 실적",
},
];
-544
View File
@@ -1,544 +0,0 @@
// AUTO-GENERATED by scripts/extract/generate_all.py — DO NOT EDIT BY HAND
import { Member, Alumnus } from "./types";
export const activeMembers: Member[] = [
{
"ko": "고석주",
"en": "Seok-Joo Koh",
"degree": "Professor"
},
{
"ko": "최동규",
"en": "Dong-Kyu Choi",
"degree": "PhD"
},
{
"ko": "남혜빈",
"en": "Hye-Been Nam",
"degree": "PhDCandidate"
},
{
"ko": "임도현",
"en": "Dohyeon Lim",
"degree": "PhDStudent"
},
{
"ko": "최준혁",
"en": "Jun-Hyeok Choi",
"degree": "PhDStudent"
},
{
"ko": "신광선",
"en": "Gwang-Seon Shin",
"degree": "MSCandidate"
},
{
"ko": "이환웅",
"en": "Hwan-Woong Lee",
"degree": "MSStudent"
},
{
"ko": "Hoang Anh Dang",
"en": "Hoang Anh Dang",
"degree": "MSStudent"
},
{
"ko": "김민희",
"en": "Minhee Kim",
"degree": "PhDCandidate",
"affiliation": "School of Computer Science and Engineering"
},
{
"ko": "김근솔",
"en": "Keun-Sol Kim",
"degree": "PhDCandidate",
"affiliation": "Department of Information Security"
},
{
"ko": "민성준",
"en": "Sung-Jun Min",
"degree": "PhDCandidate",
"affiliation": "Department of Information Science"
},
{
"ko": "최진원",
"en": "Jin-Won Choi",
"degree": "PhDStudent",
"affiliation": "Department of Science and Technology"
},
{
"ko": "정기수",
"en": "Ki-Soo Jung",
"degree": "PhDStudent",
"affiliation": "Department of Science and Technology"
},
{
"ko": "이미주",
"en": "Mi-Joo Lee",
"degree": "PhDStudent",
"affiliation": "Department of ICT Convergence"
},
{
"ko": "박유나",
"en": "Yunah Park",
"degree": "MSCandidate",
"affiliation": "Department of Information Security"
},
{
"ko": "박채영",
"en": "Chae-Young Park",
"degree": "MSCandidate",
"affiliation": "Department of Information Science"
}
];
export const alumni: Alumnus[] = [
{
"ko": "김동주",
"en": "Dongju Kim",
"degree": "PhD",
"graduatedAt": "2026-08",
"major": "Computer Science and Engineering",
"currentPosition": "He is now with Daegu Catholic University (대구가톨릭대학교) as Professor"
},
{
"ko": "황정미",
"en": "Jung-Mi Hwang",
"degree": "MS",
"graduatedAt": "2026-08",
"major": "Information Science",
"currentPosition": "She is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "배드로",
"en": "Dro Bae",
"degree": "MS",
"graduatedAt": "2026-02",
"major": "Industrial Engineering",
"currentPosition": "He is now with SK AX"
},
{
"ko": "권세기",
"en": "Se-Gi Kwon",
"degree": "MS",
"graduatedAt": "2026-02",
"major": "Information Security",
"currentPosition": "He is now with J Solution (제이솔루션) as CEO"
},
{
"ko": "조세현",
"en": "Se-Hyun Cho",
"degree": "PhD",
"graduatedAt": "2025-08",
"major": "Information Security",
"currentPosition": "He is now with Cyber Security Center"
},
{
"ko": "김윤성",
"en": "Yun-Seong Kim",
"degree": "MS",
"graduatedAt": "2025-08",
"major": "Data Convergence Computing",
"currentPosition": "He is now with SL (에스엘)"
},
{
"ko": "김수진",
"en": "Su-Jin Kim",
"degree": "MS",
"graduatedAt": "2025-08",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "김상태",
"en": "Sang-Tae Kim",
"degree": "MS",
"graduatedAt": "2025-08",
"major": "Industrial Engineering",
"currentPosition": "He is now with Oh-Sung Electronis (오성전자)"
},
{
"ko": "김규범",
"en": "Gyu-Bum Kim",
"degree": "MS",
"graduatedAt": "2025-08",
"major": "Computer Science and Engineering",
"currentPosition": "He is now with Korea Real Estate Board (한국부동산원)"
},
{
"ko": "정중화",
"en": "Joong-Hwa Jung",
"degree": "PhD",
"graduatedAt": "2025-02",
"currentPosition": "He is now with Lychee AI Coding Academy (리치AI코딩학원) as CEO"
},
{
"ko": "임도현",
"en": "Dohyeon Lim",
"degree": "MS",
"graduatedAt": "2025-02",
"major": "Computer Science and Engineering",
"currentPosition": "He is now with TakenSoft (테이큰소프트)"
},
{
"ko": "김소용",
"en": "So-Yong Kim",
"degree": "PhD",
"graduatedAt": "2024-08",
"currentPosition": "He is now with Catholic University of Leuven (UCLouvain) in Belgium"
},
{
"ko": "김민지",
"en": "Min-Ji Kim",
"degree": "MS",
"graduatedAt": "2024-02",
"currentPosition": "She is now with LG Electronics"
},
{
"ko": "안은지",
"en": "Eun-Ji Ahn",
"degree": "MS",
"graduatedAt": "2024-02",
"currentPosition": "S is now with National Security Research Institute (국가보안연구소)"
},
{
"ko": "오동규",
"en": "Dong-Kyu Oh",
"degree": "MS",
"graduatedAt": "2024-02",
"major": "Industrial Engineering",
"currentPosition": "He is now with Human-Plus (휴먼플러스)"
},
{
"ko": "김재성",
"en": "Jae-Seong Kim",
"degree": "MS",
"graduatedAt": "2023-08",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "최동규",
"en": "Dong-Kyu Choi",
"degree": "PhD",
"graduatedAt": "2023-02",
"currentPosition": "He is now with ISL in KNU"
},
{
"ko": "최진원",
"en": "Jin-Won Choi",
"degree": "MS",
"graduatedAt": "2023-02",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "김지은",
"en": "Ji-Eun Kim",
"degree": "MS",
"graduatedAt": "2023-02",
"major": "Information Science",
"currentPosition": "She is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "김철민",
"en": "Cheol-Min Kim",
"degree": "PhD",
"graduatedAt": "2022-08",
"currentPosition": "He is now with KETI (한국전자기술연구원)"
},
{
"ko": "김경식",
"en": "Kyung-Sik Kim",
"degree": "MS",
"graduatedAt": "2022-02",
"currentPosition": "He is now with Hyundai Autoever (현대오토에버)"
},
{
"ko": "김근수",
"en": "Keun-Soo Kim",
"degree": "MS",
"graduatedAt": "2022-02",
"currentPosition": "He is now with Shinhan Bank (신한은행)"
},
{
"ko": "정성우",
"en": "Seong-Woo Jeong",
"degree": "MS",
"graduatedAt": "2022-02",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "최민철",
"en": "Min-Cheol Choi",
"degree": "MS",
"graduatedAt": "2022-02",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "장준희",
"en": "Jun-Hee Jang",
"degree": "MS",
"graduatedAt": "2021-08",
"major": "Computer Science and Engineering",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "Muhammad Hafidh Firmansyah",
"en": "Muhammad Hafidh Firmansyah",
"degree": "MS",
"graduatedAt": "2021-08",
"currentPosition": "He is now with State Polytechnic University of Jember in Indonesia"
},
{
"ko": "장강민",
"en": "Kang-Min Jang",
"degree": "MS",
"graduatedAt": "2021-02",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "이홍근",
"en": "Hong-Keun Lee",
"degree": "MS",
"graduatedAt": "2021-02",
"major": "Information Science",
"currentPosition": "He is now with NIA (한국지능정보사회진흥원)"
},
{
"ko": "남혜빈",
"en": "Hyebeen Nam",
"degree": "MS",
"graduatedAt": "2021-02",
"currentPosition": "She is now with ISL in KNU"
},
{
"ko": "최낙중",
"en": "Nak-Jung Choi",
"degree": "PhD",
"graduatedAt": "2020-02",
"currentPosition": "He is now with Agency for Defense Development (국방과학연구소)"
},
{
"ko": "정민우",
"en": "Min-Woo Jung",
"degree": "MS",
"graduatedAt": "2019-02",
"currentPosition": "He is now with LG Electronics (LG전자)"
},
{
"ko": "강형우",
"en": "Hyung-Woo Kang",
"degree": "PhD",
"graduatedAt": "2018-08",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "최예찬",
"en": "Ye-Chan Choi",
"degree": "MS",
"graduatedAt": "2018-02",
"currentPosition": "He is now with LG-CNS"
},
{
"ko": "최상일",
"en": "Sang-Il Choi",
"degree": "PhD",
"graduatedAt": "2017-02",
"currentPosition": "He is now with Daegu Catholic University (대구가톨릭대학교) as a professor"
},
{
"ko": "박진호",
"en": "Jin-Ho Park",
"degree": "MS",
"graduatedAt": "2016-08",
"currentPosition": "He is now with BiThumb (빗썸코리아)"
},
{
"ko": "석성윤",
"en": "Sung-Yoon Seok",
"degree": "MS",
"graduatedAt": "2016-08",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김진규",
"en": "Jin-Kyu Kim",
"degree": "MS",
"graduatedAt": "2016-08",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김재철",
"en": "Jae-Cheol Kim",
"degree": "MS",
"graduatedAt": "2016-08",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김지인",
"en": "Ji-In Kim",
"degree": "PhD",
"graduatedAt": "2016-02",
"currentPosition": "He is now with FAM Tech. (팸텍) as CEO"
},
{
"ko": "김우주",
"en": "Woo-Ju Kim",
"degree": "MS",
"graduatedAt": "2016-02",
"currentPosition": "He is now with ZCube (지큐브)"
},
{
"ko": "박정섭",
"en": "Jung-Sub Park",
"degree": "MS",
"graduatedAt": "2015-08",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "최종명",
"en": "Jong-Myoung Choi",
"degree": "MS",
"graduatedAt": "2015-02",
"currentPosition": "He is now with NP Communications"
},
{
"ko": "이종관",
"en": "Jong-Kwan Lee",
"degree": "MS",
"graduatedAt": "2014-02",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Hanwha Ocean (한화오션)"
},
{
"ko": "이상헌",
"en": "Sang-Hun Lee",
"degree": "MS",
"graduatedAt": "2014-02",
"currentPosition": "He is now with Inno Wireless"
},
{
"ko": "민경욱",
"en": "Kyeong-Wook Min",
"degree": "MS",
"graduatedAt": "2013-02",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김상헌",
"en": "Sang-Heon Kim",
"degree": "MS",
"graduatedAt": "2013-02",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "Moneeb Gohar",
"en": "Moneeb Gohar",
"degree": "PhD",
"graduatedAt": "2012-08",
"currentPosition": "He is now with Department of Computer Science, Bahria University (at Islamabad) in Pakistan"
},
{
"ko": "박재완",
"en": "Jae-Wan Park",
"degree": "MS",
"graduatedAt": "2012-02",
"currentPosition": "He is now with Ministry of Justice (Immigration Information Center, 법무부)"
},
{
"ko": "이재경",
"en": "Jae-Kyoung Lee",
"degree": "MS",
"graduatedAt": "2012-02",
"currentPosition": "She is now with Korea Transportation Satefy Authority (한국교통안전공단)"
},
{
"ko": "하원기",
"en": "Won-Ki Ha",
"degree": "MS",
"graduatedAt": "2012-02",
"major": "Samsung Electronics Program",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김근희",
"en": "Keun-Hee Kim",
"degree": "MS",
"graduatedAt": "2012-02",
"major": "Samsung Electronics Program",
"currentPosition": "She is now with Samsung Electronics (삼성전자)"
},
{
"ko": "권순홍",
"en": "Soon-Hong Kwon",
"degree": "MS",
"graduatedAt": "2010-02",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "김동필",
"en": "Dong-Phil Kim",
"degree": "PhD",
"graduatedAt": "2009-02",
"currentPosition": "He is now with National Security Research Institute (국가보안연구소)"
},
{
"ko": "최린",
"en": "Lin Cui",
"degree": "PhD",
"graduatedAt": "2009-02",
"currentPosition": "He is now with Tianjin University of Technology and Education in China"
},
{
"ko": "이동화",
"en": "Dong-Hwa Lee",
"degree": "MS",
"graduatedAt": "2009-02",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
},
{
"ko": "박재성",
"en": "Jae-Sung Park",
"degree": "MS",
"graduatedAt": "2008-08",
"currentPosition": "He is now with Gom&Company (곰앤컴퍼니)"
},
{
"ko": "주수경",
"en": "Su-Kyoung Ju",
"degree": "MS",
"graduatedAt": "2008-02",
"currentPosition": "She is now with Youngjin Univ. (영진전문대학교)"
},
{
"ko": "윤성식",
"en": "Sung-Shik Yoon",
"degree": "MS",
"graduatedAt": "2007-08",
"currentPosition": "He is now with Jo-Il Textile Processing Company (조일가공) as CEO"
},
{
"ko": "김상태",
"en": "Sang-Tae Kim",
"degree": "MS",
"graduatedAt": "2007-02",
"currentPosition": "He is now with LG Electronics (LG전자)"
},
{
"ko": "하종식",
"en": "Jong-Shik Ha",
"degree": "MS",
"graduatedAt": "2007-02",
"currentPosition": "He is now with Samsung Electronics (삼성전자)"
}
];
+1
View File
@@ -31,6 +31,7 @@ export interface Publication {
publishedAt: string; // ISO 8601 YYYY-MM or YYYY-MM-DD
kci?: boolean;
doi?: string;
isHighlight?: boolean;
}
export interface Patent {
+318
View File
@@ -0,0 +1,318 @@
import type {
Member,
Alumnus,
Publication,
Patent,
Semester,
StandardsBody,
ResearchProject,
PubCategory,
DocStatus,
} from "../content/types";
const API_BASE_URL = process.env.API_BASE_URL ?? "http://localhost:8080/api/v1";
const FETCH_TIMEOUT_MS = 3000;
interface APIResponse<T> {
data: T;
}
// Raw backend response schemas (snake_case)
interface BackendResearchProject {
id: number;
slug: string;
title: string;
abstract: string;
keywords: string[];
organization?: string | null;
standards_org?: string | null;
period?: string | null;
funder?: string | null;
}
interface BackendResearchArea {
id: number;
name_en: string;
name_kr?: string | null;
display_order: number;
}
interface BackendStatsSummary {
intl_publications: number;
standardization_docs: number;
patents: number;
}
interface BackendMember {
id: number;
name_ko: string;
name_en: string;
degree: string;
affiliation?: string | null;
email?: string | null;
is_advisor: boolean;
display_order: number;
}
interface BackendAlumnus {
id: number;
name_ko: string;
name_en: string;
degree: string;
graduated_at: string;
major?: string | null;
current_position?: string | null;
}
interface BackendPublication {
id: number;
category: string;
title: string;
authors?: string | null;
venue: string;
volume?: string | null;
published_at: string;
kci: boolean;
doi?: string | null;
is_highlight: boolean;
}
interface BackendPatent {
id: number;
title: string;
inventors: string;
application_no: string;
application_at: string;
registration_no?: string | null;
registration_at?: string | null;
country: string;
published_at: string;
}
interface BackendCourse {
id: number;
semester_id: number;
code: string;
name_en: string;
name_kr?: string | null;
note?: string | null;
display_order: number;
}
interface BackendSemester {
id: number;
year: number;
term: string;
courses: BackendCourse[];
}
interface BackendStandardDoc {
id: number;
body_id: number;
wg: string;
project_name: string;
title: string;
doc_ref: string;
status: string;
published_at?: string | null;
}
interface BackendStandardProject {
wg: string;
name: string;
documents: BackendStandardDoc[];
}
interface BackendStandardsBody {
id: number;
org: string;
full_name: string;
scope: string;
period: string;
role?: string | null;
display_order: number;
projects: BackendStandardProject[];
}
/**
* Generic fetch helper with bounded timeout and safe JSON handling.
* Returns data payload or null on any failure (network, timeout, non-2xx, parse error).
*/
async function apiGet<T>(path: string): Promise<T | null> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
const res = await fetch(`${API_BASE_URL}${path}`, {
signal: controller.signal,
cache: "no-store",
});
clearTimeout(timeout);
if (!res.ok) {
return null;
}
const json = (await res.json()) as APIResponse<T>;
return json?.data ?? null;
} catch {
return null;
}
}
// --- Home Domain ---
export async function getResearchProjects(): Promise<ResearchProject[] | null> {
const raw = await apiGet<BackendResearchProject[]>("/research-projects");
if (!raw) return null;
return raw.map((p) => ({
slug: p.slug,
title: p.title,
abstract: p.abstract,
keywords: p.keywords,
standardsTrack: p.organization ?? undefined,
standardsOrg: p.standards_org ?? undefined,
period: p.period ?? undefined,
funder: p.funder ?? undefined,
deliverables: [],
}));
}
export async function getResearchAreaNames(): Promise<string[] | null> {
const raw = await apiGet<BackendResearchArea[]>("/research-areas");
if (!raw) return null;
return raw.map((a) => a.name_en);
}
export async function getStatsSummary(): Promise<{
intlPubsCount: number;
stdDocsCount: number;
patentCount: number;
} | null> {
const raw = await apiGet<BackendStatsSummary>("/stats/summary");
if (!raw) return null;
return {
intlPubsCount: raw.intl_publications,
stdDocsCount: raw.standardization_docs,
patentCount: raw.patents,
};
}
// --- Members Domain ---
export async function getMembers(): Promise<Member[] | null> {
const raw = await apiGet<BackendMember[]>("/members");
if (!raw) return null;
return raw.map((m) => ({
ko: m.name_ko,
en: m.name_en,
degree: m.degree as Member["degree"],
affiliation: m.affiliation ?? undefined,
}));
}
export async function getAlumni(): Promise<Alumnus[] | null> {
const raw = await apiGet<BackendAlumnus[]>("/alumni");
if (!raw) return null;
return raw.map((a) => ({
ko: a.name_ko,
en: a.name_en,
degree: a.degree as Alumnus["degree"],
graduatedAt: a.graduated_at,
major: a.major ?? undefined,
currentPosition: a.current_position ?? undefined,
}));
}
// --- Publications & Patents Domain ---
export async function getPublications(
category?: "intl-journal-conf" | "domestic-journal-conf"
): Promise<Publication[] | null> {
const raw = await apiGet<BackendPublication[]>(
category ? `/publications?category=${category}` : "/publications"
);
if (!raw) return null;
return raw.map((p) => ({
category: p.category as PubCategory,
title: p.title,
authors: p.authors ?? undefined,
venue: p.venue,
volume: p.volume ?? undefined,
publishedAt: p.published_at,
kci: p.kci,
doi: p.doi ?? undefined,
isHighlight: p.is_highlight,
}));
}
export async function getHighlights(): Promise<Publication[] | null> {
const raw = await apiGet<BackendPublication[]>("/publications/highlights");
if (!raw) return null;
return raw.map((p) => ({
category: p.category as PubCategory,
title: p.title,
authors: p.authors ?? undefined,
venue: p.venue,
volume: p.volume ?? undefined,
publishedAt: p.published_at,
kci: p.kci,
doi: p.doi ?? undefined,
isHighlight: p.is_highlight,
}));
}
export async function getPatents(): Promise<Patent[] | null> {
const raw = await apiGet<BackendPatent[]>("/patents");
if (!raw) return null;
return raw.map((p) => ({
category: "patent",
title: p.title,
inventors: p.inventors,
applicationNo: p.application_no,
applicationAt: p.application_at,
registrationNo: p.registration_no ?? undefined,
registrationAt: p.registration_at ?? undefined,
country: p.country,
publishedAt: p.published_at,
}));
}
// --- Lectures Domain ---
export async function getSemesters(): Promise<Semester[] | null> {
const raw = await apiGet<BackendSemester[]>("/semesters");
if (!raw) return null;
return raw.map((s) => ({
term: s.term as Semester["term"],
year: s.year,
courses: s.courses.map((c) => ({
code: c.code,
en: c.name_en,
note: c.note ?? undefined,
})),
}));
}
// --- Standardization Domain ---
export async function getStandardsBodies(): Promise<StandardsBody[] | null> {
const raw = await apiGet<BackendStandardsBody[]>("/standards-bodies");
if (!raw) return null;
return raw.map((b) => ({
org: b.org,
full: b.full_name,
scope: b.scope as StandardsBody["scope"],
period: b.period,
role: b.role ?? undefined,
projects: b.projects.map((p) => ({
wg: p.wg,
name: p.name,
documents: p.documents.map((d) => ({
title: d.title,
ref: d.doc_ref,
status: d.status as DocStatus,
publishedAt: d.published_at ?? undefined,
})),
})),
}));
}
@@ -1,11 +1,31 @@
#!/usr/bin/env python3
"""
Data Verification Gate (AC-07 / AC-08 / AC-10 / AC-11 / AC-30 / Live Database & API Pipeline Integrity)
Verifies:
1. Full data extraction from legacy HTML sources (16 members, 60 alumni, 68 intl, 80 dom, 75 pat, 45 sem, 6 bodies, 44 docs, 3 projects).
2. SQLite Backend Database (backend/anl.db) tables & row counts matching baseline.
3. Live Go REST API endpoints (http://localhost:8080/api/v1) data losslessness & schema consistency if server is active.
4. ISO 8601 strict date formatting across all entities.
5. Publication volume cross-check and keyword substring assertions.
"""
import sys
import os
import re
import sqlite3
import json
import urllib.request
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(BASE_DIR, "refer_landing_page", "scripts", "extract"))
ROOT_PROJECT_DIR = os.path.dirname(BASE_DIR)
DB_PATH = os.path.join(ROOT_PROJECT_DIR, "backend", "anl.db")
if not os.path.exists(DB_PATH):
DB_PATH = os.path.join(BASE_DIR, "backend", "anl.db")
API_BASE_URL = os.environ.get("API_BASE_URL", "http://localhost:8080/api/v1")
MONTH_MAP = {
"january": "01", "jan": "01", "february": "02", "feb": "02", "march": "03", "mar": "03",
"april": "04", "apr": "04", "may": "05", "june": "06", "jun": "06", "july": "07", "jul": "07",
@@ -25,42 +45,22 @@ def verify():
total_pubs = len(intl_p) + len(dom_p) + len(pat_p)
std_docs_count = sum(len(p["documents"]) for b in bodies for p in b["projects"])
print("=== DATA VERIFICATION GATE (AC-07 / AC-08 / AC-10 / AC-11 / AC-30 / VOLUME CROSS-CHECK) ===")
print(f"Active Members: {len(active_m)} (Expected: 16)")
print(f"Alumni: {len(alumni_m)} (Expected: 60)")
print(f"International Papers: {len(intl_p)} (Expected: 68)")
print(f"Domestic Papers: {len(dom_p)} (Expected: 80)")
print(f"Patents: {len(pat_p)} (Expected: 75)")
print(f"Total Publications: {total_pubs} (Expected: 223)")
print(f"Lectures Semesters: {len(semesters)} (Expected: 45)")
print(f"Standard Bodies: {len(bodies)} (Expected: 6)")
print(f"Standard Documents: {std_docs_count} (Expected: 44)")
print(f"Research Projects: {len(projects)} (Expected: 3)")
print("=== DATA VERIFICATION GATE (AC-07 / AC-08 / AC-10 / AC-11 / AC-30 / LIVE PIPELINE INTEGRITY) ===")
print(f"[HTML Source Extraction Baseline]")
print(f" Active Members: {len(active_m)} (Expected: 16)")
print(f" Alumni: {len(alumni_m)} (Expected: 60)")
print(f" International Papers: {len(intl_p)} (Expected: 68)")
print(f" Domestic Papers: {len(dom_p)} (Expected: 80)")
print(f" Patents: {len(pat_p)} (Expected: 75)")
print(f" Total Publications: {total_pubs} (Expected: 223)")
print(f" Lectures Semesters: {len(semesters)} (Expected: 45)")
print(f" Standard Bodies: {len(bodies)} (Expected: 6)")
print(f" Standard Documents: {std_docs_count} (Expected: 44)")
print(f" Research Projects: {len(projects)} (Expected: 3)")
errors = []
if len(projects) != 3:
errors.append(f"Research Projects count mismatch: got {len(projects)}, expected 3")
# AC-30: Keyword Substring Verification & AC-32 Deliverable WG Attribution
for proj in projects:
abstract = proj.get("abstract", "")
for kw in proj.get("keywords", []):
if kw not in abstract:
errors.append(f"AC-30 Keyword Violation in project '{proj.get('slug')}': keyword '{kw}' not in abstract")
if proj["slug"] == "ccis":
for d in proj.get("deliverables", []):
if "63246" not in d["ref"]:
errors.append(f"AC-32 Violation: CCIS project deliverable '{d['ref']}' does not belong to CCIS WG (IEC 63246)")
# N1: Title artifact check (No trailing quotes or commas in publication titles)
for p in intl_p + dom_p + pat_p:
t = p.get("title", "")
if re.search(r'["“”]|,$', t):
errors.append(f"N1 Title Artifact Violation in publication '{p.get('id')}': title '{t}' contains quotes or trailing comma")
# 1. Count checks
# 1. Source Extraction Integrity Checks
if len(active_m) != 16:
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
if len(alumni_m) != 60:
@@ -79,28 +79,175 @@ def verify():
errors.append(f"Standard Bodies count mismatch: got {len(bodies)}, expected 6")
if std_docs_count != 44:
errors.append(f"Standard Documents count mismatch: got {std_docs_count}, expected 44")
if len(projects) != 3:
errors.append(f"Research Projects count mismatch: got {len(projects)}, expected 3")
# 2. File checks (BOM, Strict ISO 8601, and Semantic Date Ranges)
# 2. SQLite Backend Database Verification
if os.path.exists(DB_PATH):
print(f"\n[SQLite Database Verification ({DB_PATH})]")
try:
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
def get_count(table, where=""):
query = f"SELECT count(*) FROM {table} {where}"
cur.execute(query)
return cur.fetchone()[0]
db_counts = {
"members": get_count("members"),
"alumni": get_count("alumni"),
"intl_pubs": get_count("publications", "WHERE category='intl-journal-conf'"),
"dom_pubs": get_count("publications", "WHERE category='domestic-journal-conf'"),
"patents": get_count("patents"),
"semesters": get_count("semesters"),
"standards_bodies": get_count("standards_bodies"),
"standard_documents": get_count("standard_documents"),
"research_projects": get_count("research_projects"),
}
for k, v in db_counts.items():
print(f" DB {k}: {v}")
if db_counts["members"] != 16:
errors.append(f"SQLite members count: {db_counts['members']}, expected 16")
if db_counts["alumni"] != 60:
errors.append(f"SQLite alumni count: {db_counts['alumni']}, expected 60")
if db_counts["intl_pubs"] != 68:
errors.append(f"SQLite intl publications count: {db_counts['intl_pubs']}, expected 68")
if db_counts["dom_pubs"] != 80:
errors.append(f"SQLite dom publications count: {db_counts['dom_pubs']}, expected 80")
if db_counts["patents"] != 75:
errors.append(f"SQLite patents count: {db_counts['patents']}, expected 75")
if db_counts["semesters"] != 45:
errors.append(f"SQLite semesters count: {db_counts['semesters']}, expected 45")
if db_counts["standards_bodies"] != 6:
errors.append(f"SQLite standards bodies count: {db_counts['standards_bodies']}, expected 6")
if db_counts["standard_documents"] != 44:
errors.append(f"SQLite standard documents count: {db_counts['standard_documents']}, expected 44")
if db_counts["research_projects"] != 3:
errors.append(f"SQLite research projects count: {db_counts['research_projects']}, expected 3")
conn.close()
print(" ✅ SQLite Database records match expected baseline exactly")
except Exception as e:
errors.append(f"SQLite DB verification failed: {e}")
else:
print(f"\n⚠️ SQLite database not found at {DB_PATH}, skipping direct DB query")
# 3. Live Go REST API Endpoint Verification (if backend is active)
is_api_live = False
try:
req = urllib.request.Request(f"{API_BASE_URL}/health", headers={"User-Agent": "DataVerifier"})
with urllib.request.urlopen(req, timeout=1) as resp:
if resp.status == 200:
is_api_live = True
except Exception:
is_api_live = False
if is_api_live:
print(f"\n[Live Go REST API Verification ({API_BASE_URL})]")
try:
def fetch_json(endpoint):
req = urllib.request.Request(f"{API_BASE_URL}{endpoint}", headers={"User-Agent": "DataVerifier"})
with urllib.request.urlopen(req, timeout=3) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data.get("data", [])
stats = fetch_json("/stats/summary")
api_members = fetch_json("/members")
api_alumni = fetch_json("/alumni")
api_intl = fetch_json("/publications?category=intl-journal-conf")
api_dom = fetch_json("/publications?category=domestic-journal-conf")
api_patents = fetch_json("/patents")
api_semesters = fetch_json("/semesters")
api_standards = fetch_json("/standards-bodies")
api_projects = fetch_json("/research-projects")
api_std_docs_count = sum(len(p.get("documents", [])) for b in api_standards for p in b.get("projects", []))
print(f" API stats summary: {stats}")
print(f" API members count: {len(api_members)}")
print(f" API alumni count: {len(api_alumni)}")
print(f" API publications (intl): {len(api_intl)}, (dom): {len(api_dom)}")
print(f" API patents count: {len(api_patents)}")
print(f" API semesters count: {len(api_semesters)}")
print(f" API standards bodies: {len(api_standards)} (docs: {api_std_docs_count})")
print(f" API projects count: {len(api_projects)}")
if stats.get("intl_publications") != 68:
errors.append(f"API stats intl_publications mismatch: {stats.get('intl_publications')}, expected 68")
if stats.get("standardization_docs") != 44:
errors.append(f"API stats standardization_docs mismatch: {stats.get('standardization_docs')}, expected 44")
if stats.get("patents") != 75:
errors.append(f"API stats patents mismatch: {stats.get('patents')}, expected 75")
if len(api_members) != 16:
errors.append(f"API members count: {len(api_members)}, expected 16")
if len(api_alumni) != 60:
errors.append(f"API alumni count: {len(api_alumni)}, expected 60")
if len(api_intl) != 68:
errors.append(f"API intl publications count: {len(api_intl)}, expected 68")
if len(api_dom) != 80:
errors.append(f"API dom publications count: {len(api_dom)}, expected 80")
if len(api_patents) != 75:
errors.append(f"API patents count: {len(api_patents)}, expected 75")
if len(api_semesters) != 45:
errors.append(f"API semesters count: {len(api_semesters)}, expected 45")
if len(api_standards) != 6:
errors.append(f"API standards bodies count: {len(api_standards)}, expected 6")
if api_std_docs_count != 44:
errors.append(f"API standard documents count: {api_std_docs_count}, expected 44")
if len(api_projects) != 3:
errors.append(f"API research projects count: {len(api_projects)}, expected 3")
print(" ✅ Live REST API endpoints serve 100% losslessly verified data")
except Exception as e:
errors.append(f"Live API verification error: {e}")
else:
print(f"\n️ Live Go API server not responding at {API_BASE_URL} (tested offline DB mode)")
# 4. AC-30 Keyword Substring Verification & AC-32 Deliverable WG Attribution
for proj in projects:
abstract = proj.get("abstract", "")
for kw in proj.get("keywords", []):
if kw not in abstract:
errors.append(f"AC-30 Keyword Violation in project '{proj.get('slug')}': keyword '{kw}' not in abstract")
if proj["slug"] == "ccis":
for d in proj.get("deliverables", []):
if "63246" not in d["ref"]:
errors.append(f"AC-32 Violation: CCIS project deliverable '{d['ref']}' does not belong to CCIS WG (IEC 63246)")
# 5. Title artifact check (No trailing quotes or commas in publication titles)
for p in intl_p + dom_p + pat_p:
t = p.get("title", "")
if re.search(r'["“”]|,$', t):
errors.append(f"N1 Title Artifact Violation in publication '{p.get('id')}': title '{t}' contains quotes or trailing comma")
# 6. Strict ISO 8601 Date and BOM Verification
content_dir = os.path.join(BASE_DIR, "content")
date_regex = re.compile(r'^(19|20)\d{2}(-(0[1-9]|1[0-2]))?(-(0[1-9]|[12]\d|3[01]))?$')
for fname in ["members.ts", "publications.ts", "lectures.ts", "standards.ts"]:
fpath = os.path.join(content_dir, fname)
if not os.path.exists(fpath):
errors.append(f"Missing content file: {fname}")
continue
with open(fpath, "r", encoding="utf-8") as f:
text = f.read()
for fname in os.listdir(content_dir) if os.path.exists(content_dir) else []:
if fname.endswith(".ts"):
fpath = os.path.join(content_dir, fname)
with open(fpath, "r", encoding="utf-8") as f:
text = f.read()
if text.startswith('\ufeff'):
errors.append(f"BOM (U+FEFF) found in {fname}")
if text.startswith('\ufeff'):
errors.append(f"BOM (U+FEFF) found in {fname}")
for p in intl_p + dom_p + pat_p:
for field in ["publishedAt", "applicationAt", "registrationAt"]:
val = p.get(field)
if val and not date_regex.match(val):
errors.append(f"Invalid ISO 8601 date in publication '{p.get('title')}': {field}='{val}'")
dates = re.findall(r'"(publishedAt|applicationAt|registrationAt|graduatedAt)": "([^"]+)"', text)
for k, d in dates:
if not date_regex.match(d):
errors.append(f"Invalid ISO 8601 or out-of-bounds date in {fname}: {k}='{d}'")
for m in alumni_m:
val = m.get("graduatedAt")
if val and not date_regex.match(val):
errors.append(f"Invalid ISO 8601 date in alumnus '{m.get('ko')}': graduatedAt='{val}'")
# 3. Volume Cross-Check for Publications (Ensures volume date candidates match publishedAt)
# 7. Volume Cross-Check for Publications
MON_PAT = r'(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
for pub in intl_p + dom_p:
+179 -89
View File
@@ -1,44 +1,64 @@
#!/usr/bin/env python3
"""
E2E Test Suite Runner for ANL Homepage Development
E2E Test Suite Runner for ANL Homepage Development (v2.0 Dynamic Fetching Architecture)
Verifies:
0. Clean step (npm run clean -> removes .next and tsconfig.tsbuildinfo)
1. ESLint (npm run lint)
2. Next.js Static SSG Build (npm run build) with 0 dynamic routes
2. Next.js Dynamic/Static Hybrid Build (npm run build) with exact dynamic/static route allocation (AC-17)
3. TypeScript type check (npx tsc --noEmit)
4. Existence and content of static SSG routes HTML/body files
5. Data count losslessness & integrity gate (verify_counts.py)
4. Live Server Route Verification (Live HTTP status 200 & content)
5. Data count losslessness & fallback dataset integrity (verify_counts.py)
6. Light/Dark mode theme tokens and CSS attributes in source and build assets
7. DOM & HTML Content Assertions (Unittest)
7. Live DOM & HTML Content Assertions (Unittest)
8. Layout Architecture & No-Escape Gate (AC-25 / AC-26 / AC-40 / AC-41 / R-8.17)
9. Color Token Alpha Contract & Header Style Gate (AC-35 / AC-36 / AC-37)
10. WCAG 2.1 AA Color Contrast & Whitelist Gate (AC-43 / AC-44 / AC-45 / AC-47 / AC-48)
"""
import os
import sys
import time
import subprocess
import re
import urllib.request
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
REFER_DIR = os.path.join(ROOT_DIR, "refer_landing_page")
# Expected Static SSG Routes HTML/Body output paths relative to REFER_DIR
EXPECTED_SSG_ROUTES = [
os.path.join(".next", "server", "app", "index.html"),
os.path.join(".next", "server", "app", "members.html"),
os.path.join(".next", "server", "app", "publications.html"),
os.path.join(".next", "server", "app", "publications", "intl-journal-conf.html"),
os.path.join(".next", "server", "app", "publications", "domestic-journal-conf.html"),
os.path.join(".next", "server", "app", "publications", "patent.html"),
os.path.join(".next", "server", "app", "lectures.html"),
os.path.join(".next", "server", "app", "standardization.html"),
os.path.join(".next", "server", "app", "_not-found.html"),
os.path.join(".next", "server", "app", "robots.txt.body"),
os.path.join(".next", "server", "app", "sitemap.xml.body"),
os.path.join(".next", "server", "pages", "404.html"),
EXPECTED_DYNAMIC_ROUTES = [
"/",
"/members",
"/publications",
"/publications/[category]",
"/lectures",
"/standardization",
]
def run_cmd(cmd, cwd=REFER_DIR):
EXPECTED_STATIC_ROUTES = [
"/_not-found",
"/icon.svg",
"/robots.txt",
"/sitemap.xml",
]
LIVE_ROUTES = [
"/",
"/members",
"/publications",
"/publications/intl-journal-conf",
"/publications/domestic-journal-conf",
"/publications/patent",
"/lectures",
"/standardization",
"/_not-found",
]
def run_cmd(cmd, cwd=REFER_DIR, env=None):
print(f"Running: {' '.join(cmd)} (cwd: {cwd})")
res = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
run_env = os.environ.copy()
if env:
run_env.update(env)
res = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=run_env)
return res
def test_clean():
@@ -60,22 +80,44 @@ def test_lint():
return True
def test_build():
print("\n--- Gate 2: Next.js Static SSG Build Gate ---")
print("\n--- Gate 2: Next.js Dynamic/Static Hybrid Build Gate (AC-17) ---")
res = run_cmd(["npm", "run", "build"])
if res.returncode != 0:
print(f" SSG Build failed:\n{res.stdout}\n{res.stderr}")
print(f"❌ Build failed:\n{res.stdout}\n{res.stderr}")
return False
# Check stdout/stderr for dynamic routes (f Dynamic)
if "ƒ" in res.stdout or "Dynamic" in res.stdout:
# Search specifically for dynamic routes output line
lines = res.stdout.splitlines()
dynamic_lines = [l for l in lines if "ƒ" in l or "Dynamic" in l]
if dynamic_lines:
print(f"❌ Found dynamic route(s) in build report: {dynamic_lines}")
# Parse exact Next.js route table output lines (e.g. "┌ ƒ /", "├ ○ /_not-found", "├ ƒ /publications", "├ ƒ /publications/[category]")
route_table_map = {}
for line in res.stdout.splitlines():
m = re.match(r'^[┌├└]\s+([ƒ○●])\s+(\S+)', line.strip())
if m:
mode, route = m.group(1), m.group(2)
route_table_map[route] = mode
print(f"Parsed route modes from build table ({len(route_table_map)} routes): {route_table_map}")
# Check exact match for each dynamic route
for d_route in EXPECTED_DYNAMIC_ROUTES:
mode = route_table_map.get(d_route)
if mode != "ƒ":
print(f"❌ Gate 2 Violation: Expected dynamic route '{d_route}' has mode '{mode}', expected 'ƒ'")
return False
print("✅ Gate 2 PASSED: Static SSG Build successful (0 dynamic routes)")
# Check exact match for each static route
for s_route in EXPECTED_STATIC_ROUTES:
mode = route_table_map.get(s_route)
if mode not in ("", ""):
print(f"❌ Gate 2 Violation: Expected static route '{s_route}' has mode '{mode}', expected '' or ''")
return False
# Check that no other unexpected routes exist in table
all_expected = set(EXPECTED_DYNAMIC_ROUTES + EXPECTED_STATIC_ROUTES)
found_routes = set(route_table_map.keys())
if all_expected != found_routes:
print(f"❌ Gate 2 Violation: Route set mismatch. Missing: {all_expected - found_routes}, Extra: {found_routes - all_expected}")
return False
print("✅ Gate 2 PASSED: Hybrid dynamic/static route allocation verified (6 dynamic routes, 4 static routes with exact table matching)")
return True
def test_tsc():
@@ -87,35 +129,81 @@ def test_tsc():
print("✅ Gate 3 PASSED: TypeScript type check clean (exit code 0)")
return True
def test_ssg_routes():
print("\n--- Gate 4: Static SSG HTML Route Verification ---")
missing = []
for rel_path in EXPECTED_SSG_ROUTES:
full_path = os.path.join(REFER_DIR, rel_path)
if not os.path.exists(full_path):
missing.append(rel_path)
else:
size = os.path.getsize(full_path)
print(f" [OK] {rel_path} ({size} bytes)")
import socket
if missing:
print(f"❌ Missing {len(missing)} SSG routes:")
for m in missing:
print(f" - {m}")
return False
def get_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
print(f"✅ Gate 4 PASSED: All {len(EXPECTED_SSG_ROUTES)} static SSG routes exist and are non-empty")
def start_next_server(port=3000):
print(f"\nStarting live Next.js server on port {port}...")
server_env = os.environ.copy()
server_env["PORT"] = str(port)
proc = subprocess.Popen(
["npm", "run", "start"],
cwd=REFER_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=server_env,
)
ready = False
for _ in range(30):
try:
with urllib.request.urlopen(f"http://localhost:{port}/", timeout=1) as resp:
if resp.status == 200:
ready = True
break
except Exception:
time.sleep(0.5)
if not ready:
proc.terminate()
raise RuntimeError(f"Next.js server failed to become ready on port {port}")
print(f"Live Next.js server is ready at http://localhost:{port}")
return proc
def fetch_live_routes(base_url="http://localhost:3000"):
bodies = {}
for route in LIVE_ROUTES:
url = f"{base_url}{route}"
req = urllib.request.Request(url, headers={"User-Agent": "E2ETestRunner"})
try:
with urllib.request.urlopen(req, timeout=5) as resp:
content = resp.read().decode("utf-8")
bodies[route] = content
except urllib.error.HTTPError as e:
# Handle 404 for _not-found
if e.code == 404 and "_not-found" in route:
content = e.read().decode("utf-8")
bodies[route] = content
else:
raise e
return bodies
def test_live_routes(bodies):
print("\n--- Gate 4: Live Server Route Verification ---")
for route, body in bodies.items():
if len(body) == 0:
print(f"❌ Empty response body for route '{route}'")
return False
print(f" [OK] {route} ({len(body)} bytes)")
print(f"✅ Gate 4 PASSED: All {len(bodies)} live routes served non-empty responses")
return True
def test_data_counts():
print("\n--- Gate 5: Data Losslessness Integrity Gate ---")
print("\n--- Gate 5: Fallback Dataset Integrity Gate ---")
script_path = os.path.join(REFER_DIR, "scripts", "extract", "verify_counts.py")
res = run_cmd([sys.executable, script_path], cwd=ROOT_DIR)
if res.returncode != 0:
print(f"❌ Data count verification failed:\n{res.stdout}\n{res.stderr}")
return False
print(res.stdout.strip())
print("✅ Gate 5 PASSED: Data count integrity & volume losslessness verified")
print("✅ Gate 5 PASSED: Data count integrity & fallback dataset volume verified")
return True
def test_theme_tokens():
@@ -148,53 +236,43 @@ def test_theme_tokens():
print("✅ Gate 6 PASSED: Light/Dark theme CSS variables and Tailwind tokens present")
return True
def test_unittest_suite():
def test_unittest_suite(port=3000):
print("\n--- Gate 7: DOM & HTML Content Assertions (Unittest) ---")
test_path = os.path.join(REFER_DIR, "tests", "e2e_test_suite.py")
res = run_cmd([sys.executable, "-m", "unittest", test_path], cwd=ROOT_DIR)
res = run_cmd([sys.executable, "-m", "unittest", test_path], cwd=ROOT_DIR, env={"TEST_SERVER_URL": f"http://localhost:{port}"})
if res.returncode != 0:
print(f"❌ Unittest suite failed:\n{res.stdout}\n{res.stderr}")
return False
print("✅ Gate 7 PASSED: All 7 DOM & static HTML content assertion tests passed")
print("✅ Gate 7 PASSED: All 10 DOM & live HTML content assertion tests passed")
return True
def test_layout_architecture():
def test_layout_architecture(bodies):
print("\n--- Gate 8: Layout Architecture & No-Escape Gate (AC-25 / AC-26 / AC-40 / AC-41 / R-8.17) ---")
# 1) Check HTML SSG pages for container-content inside main across ALL SSG routes (C2, AC-25)
routes_to_check = EXPECTED_SSG_ROUTES
# 1) Check live HTML responses for container-content inside main across all routes (C2, AC-25)
main_hashes = {}
for rel_p in routes_to_check:
if not rel_p.endswith(".html"):
for route, html in bodies.items():
if "_not-found" in route or "404" in route:
continue
full_p = os.path.join(REFER_DIR, rel_p)
if not os.path.exists(full_p):
print(f"❌ Missing SSG route HTML: {rel_p}")
return False
with open(full_p, "r", encoding="utf-8") as f:
html = f.read()
# Verify <main> tag exists and does NOT contain max-w- or px-
main_match = re.search(r'<main(?:\s+[^>]*)?>(.*?)</main>', html, flags=re.S)
if not main_match:
print(f"❌ <main> tag not found in {rel_p}")
print(f"❌ <main> tag not found in {route}")
return False
main_inner = main_match.group(1)
# C1: Verify child wrapper inside <main> has container-content
if "container-content" not in main_inner and "_not-found" not in rel_p and "404" not in rel_p:
print(f"❌ container-content missing inside <main> in {rel_p}")
if "container-content" not in main_inner:
print(f"❌ container-content missing inside <main> in {route}")
return False
# AC-41: Check for duplicate <main> normalized content hashes (except 404 pages)
if "404" not in rel_p and "_not-found" not in rel_p:
norm_content = re.sub(r'\s+', '', main_inner)
if norm_content in main_hashes:
print(f"❌ AC-41 Violation: Duplicate <main> content found between {rel_p} and {main_hashes[norm_content]}")
return False
main_hashes[norm_content] = rel_p
# AC-41: Check for duplicate <main> normalized content hashes
norm_content = re.sub(r'\s+', '', main_inner)
if norm_content in main_hashes:
print(f"❌ AC-41 Violation: Duplicate <main> content found between {route} and {main_hashes[norm_content]}")
return False
main_hashes[norm_content] = route
# 2) Check source for forbidden escaped arbitrary classes (AC-26)
forbidden_terms = ["w-screen", "-mx-[50vw]", "min-[1240px]:"]
@@ -276,21 +354,20 @@ def test_color_contrast_gate():
def parse_tokens(block):
res = {}
for m in re.finditer(r'--([a-z-]+)-rgb:\s*(\d+)\s+(\d+)\s+(\d+);', block):
for m in re.finditer(r'--([a-z-]+-?d?a?r?k?)-rgb:\s*(\d+)\s+(\d+)\s+(\d+);', block):
res[m.group(1)] = (int(m.group(2)), int(m.group(3)), int(m.group(4)))
return res
light_tokens = parse_tokens(root_match.group(1))
dark_tokens = parse_tokens(dark_match.group(1))
# AC-43: Verify contrast thresholds for cobalt-dark and vermillion-dark on ivory and paper
cd_light = light_tokens.get("cobalt-dark")
vd_light = light_tokens.get("vermillion-dark")
ivory_light = light_tokens.get("ivory")
paper_light = light_tokens.get("paper")
if not all([cd_light, vd_light, ivory_light, paper_light]):
print("❌ AC-43 Failed: Missing required light RGB tokens")
print(f"❌ AC-43 Failed: Missing required light RGB tokens (found: {list(light_tokens.keys())})")
return False
ratio_cd_ivory = contrast_ratio(cd_light, ivory_light)
@@ -323,9 +400,6 @@ def test_color_contrast_gate():
print(f"❌ AC-45 Violation: Light/Dark vermillion-dark inversion contrast too low ({inversion_ratio_vd:.2f} < 2.0)")
return False
# AC-44: Verify text-cobalt or text-vermillion (DEFAULT) is not combined with small text sizes
# AC-47: Verify text-white utility usage <= 8 instances
# C11-9 (AC-48): Verify zero dark: modifier usage on accent colors (R-8.29)
white_count = 0
small_size_pattern = re.compile(r'text-(xs|sm|\[0\.65rem\]|\[0\.78rem\])')
default_color_pattern = re.compile(r'text-(cobalt|vermillion)(?![a-z-])')
@@ -339,18 +413,14 @@ def test_color_contrast_gate():
with open(fpath, "r", encoding="utf-8") as f:
content = f.read()
# AC-47 count
white_count += content.count("text-white")
# AC-44 & C11-9 check line by line
for line_idx, line in enumerate(content.splitlines(), start=1):
# C11-9: Zero dark: modifier on accent colors (allow dark:text-ivory / dark:group-open:text-ivory exceptions)
if dark_accent_pattern.search(line):
print(f"❌ C11-9 (AC-48) Violation: Forbidden dark: modifier on accent color at {fpath}:{line_idx}\n {line.strip()}")
return False
if small_size_pattern.search(line) and default_color_pattern.search(line):
# Ignore allowed exceptions (e.g. HeroComposition, Pillar bullets, or explicitly exempted titles)
if "Pillar #" in line or "w-1.5 h-1.5" in line or "text-3xl" in line:
continue
print(f"❌ AC-44 Violation: Small text combined with DEFAULT accent color at {fpath}:{line_idx}\n {line.strip()}")
@@ -365,7 +435,7 @@ def test_color_contrast_gate():
def main():
print("=" * 70)
print(" ANL HOMEPAGE E2E TEST SUITE RUNNER — TIERS 1-4")
print(" ANL HOMEPAGE E2E TEST SUITE RUNNER — TIERS 1-4 (v2.0 DYNAMIC)")
print("=" * 70)
success = True
@@ -373,14 +443,34 @@ def main():
success &= test_lint()
success &= test_build()
success &= test_tsc()
success &= test_ssg_routes()
success &= test_data_counts()
success &= test_theme_tokens()
success &= test_unittest_suite()
success &= test_layout_architecture()
success &= test_token_contracts()
success &= test_color_contrast_gate()
if not success:
print("\n❌ Build or static gates failed before live server execution.")
sys.exit(1)
# Start live Next.js server once and run Gate 4, Gate 7, Gate 8
port = get_free_port()
server_proc = None
try:
server_proc = start_next_server(port=port)
bodies = fetch_live_routes(f"http://localhost:{port}")
success &= test_live_routes(bodies)
success &= test_unittest_suite(port=port)
success &= test_layout_architecture(bodies)
finally:
if server_proc:
print("\nShutting down live Next.js server...")
server_proc.terminate()
try:
server_proc.wait(timeout=5)
except Exception:
server_proc.kill()
print("\n" + "-" * 70)
if success:
print("ALL E2E TEST SUITES PASSED CLEAN (Exit Code 0).")
+136 -24
View File
@@ -1,35 +1,94 @@
#!/usr/bin/env python3
"""
Python Unittest Suite for ANL Homepage E2E Verification
Parses prerendered SSG HTML files to assert DOM structure, text content, and theme attributes.
Queries live running Next.js server to assert DOM structure, text content, and theme attributes.
"""
import unittest
import os
import re
import time
import subprocess
import urllib.request
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SERVER_APP_DIR = os.path.join(BASE_DIR, ".next", "server", "app")
if not os.path.exists(SERVER_APP_DIR):
for candidate in [
os.path.join(os.getcwd(), "refer_landing_page", ".next", "server", "app"),
os.path.join(os.getcwd(), "website", ".next", "server", "app"),
os.path.join(os.getcwd(), ".next", "server", "app"),
]:
if os.path.exists(candidate):
SERVER_APP_DIR = candidate
break
TEST_SERVER_URL = os.environ.get("TEST_SERVER_URL", "http://localhost:3000")
class TestANLHomepageE2E(unittest.TestCase):
_server_proc = None
_html_cache = {}
@classmethod
def setUpClass(cls):
# Check if server is already responding at TEST_SERVER_URL
is_ready = False
try:
with urllib.request.urlopen(f"{TEST_SERVER_URL}/", timeout=1) as resp:
if resp.status == 200:
is_ready = True
except Exception:
is_ready = False
if not is_ready:
# Spawn local next start server
cls._server_proc = subprocess.Popen(
["npm", "run", "start"],
cwd=BASE_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
for _ in range(30):
try:
with urllib.request.urlopen(f"{TEST_SERVER_URL}/", timeout=1) as resp:
if resp.status == 200:
is_ready = True
break
except Exception:
time.sleep(0.5)
if not is_ready:
cls._server_proc.terminate()
raise RuntimeError(f"Next.js server failed to become ready at {TEST_SERVER_URL}")
@classmethod
def tearDownClass(cls):
if cls._server_proc:
cls._server_proc.terminate()
try:
cls._server_proc.wait(timeout=5)
except Exception:
cls._server_proc.kill()
def get_route_html(self, route):
if route in self._html_cache:
return self._html_cache[route]
url = f"{TEST_SERVER_URL}{route}"
req = urllib.request.Request(url, headers={"User-Agent": "E2ETestRunner"})
with urllib.request.urlopen(req, timeout=10) as resp:
html = resp.read().decode("utf-8")
self._html_cache[route] = html
return html
def read_html(self, relative_path):
full_path = os.path.join(SERVER_APP_DIR, relative_path)
self.assertTrue(os.path.exists(full_path), f"File {relative_path} does not exist")
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
route_map = {
"index.html": "/",
"members.html": "/members",
"publications.html": "/publications",
os.path.join("publications", "intl-journal-conf.html"): "/publications/intl-journal-conf",
"publications/intl-journal-conf.html": "/publications/intl-journal-conf",
os.path.join("publications", "domestic-journal-conf.html"): "/publications/domestic-journal-conf",
"publications/domestic-journal-conf.html": "/publications/domestic-journal-conf",
os.path.join("publications", "patent.html"): "/publications/patent",
"publications/patent.html": "/publications/patent",
"lectures.html": "/lectures",
"standardization.html": "/standardization",
}
route = route_map.get(relative_path, f"/{relative_path}")
return self.get_route_html(route)
def test_01_home_page_structure(self):
"""TC-T1-F3: Verify Home page SSG HTML content and semantic structures."""
"""TC-T1-F3: Verify Home page HTML content and semantic structures."""
html = self.read_html("index.html")
# 1. Brand & Header titles
@@ -54,15 +113,19 @@ class TestANLHomepageE2E(unittest.TestCase):
html = self.read_html("members.html")
# Professor profile
self.assertIn("고석주", html)
self.assertIn("Prof. Seok-Joo Koh", html)
self.assertIn("Seok-Joo Koh", html)
self.assertIn("sjkoh@knu.ac.kr", html)
# Members content sanity checks
self.assertIn("Members", html)
# Sections & Degree formatting checks
self.assertIn("LAB MEMBERS", html)
self.assertIn("Active Researchers", html)
self.assertIn("Graduates", html)
self.assertIn("Ph. D. Candidate", html)
self.assertIn("Project Editor", html)
def test_03_publications_category_routes(self):
"""TC-T1-F5: Verify Publications categories SSG static pages."""
"""TC-T1-F5: Verify Publications categories live dynamic pages."""
cats = [
("intl-journal-conf.html", "International Journal"),
("domestic-journal-conf.html", "Domestic Journal"),
@@ -87,8 +150,6 @@ class TestANLHomepageE2E(unittest.TestCase):
def test_06_theme_css_variables(self):
"""TC-T1-F2: Verify globals.css and built assets contain theme tokens."""
globals_path = os.path.join(BASE_DIR, "app", "globals.css")
if not os.path.exists(globals_path):
globals_path = os.path.join(os.getcwd(), "refer_landing_page", "app", "globals.css")
with open(globals_path, "r", encoding="utf-8") as f:
css = f.read()
@@ -96,7 +157,7 @@ class TestANLHomepageE2E(unittest.TestCase):
self.assertIn(token, css, f"Missing CSS variable {token} in globals.css")
def test_07_research_projects_pageview(self):
"""TC-T1-F7: Verify Research Project PageView content in Home static SSG HTML (AC-27 / AC-31)."""
"""TC-T1-F7: Verify Research Project PageView content in Home HTML."""
html = self.read_html("index.html")
# 1. Project PageView Title & Projects
@@ -114,8 +175,59 @@ class TestANLHomepageE2E(unittest.TestCase):
self.assertIn("IEC TC100: Multimedia Systems and Equipment", html)
self.assertIn("ISO/IEC JTC1/SC6: Reliable Multicasting", html)
# 4. Assert zero placeholder Funder labels (AC-31)
# 4. Assert zero placeholder Funder labels
self.assertNotIn("Funder:", html)
def test_08_publications_highlights_and_category_nav(self):
"""TC-T1-F8: Verify 5 representative highlights and CategoryNav active border in Publications."""
html = self.read_html("publications.html")
# 1. Verify 5 designated representative publications
expected_highlights = [
"Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G",
"Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G",
"mQUIC: Use of QUIC for Handover Support with Connection Migration in Wireless/Mobile Networks",
"Use of QUIC for CoAP Transport in IoT Networks",
"Use of QUIC for Mobile-Oriented Future Internet (Q-MOFI)",
]
for title in expected_highlights:
self.assertIn(title, html, f"Missing representative highlight publication: '{title}'")
# 2. Verify Highlights heading & section
self.assertIn("Highlights", html)
self.assertIn("Categories", html)
self.assertIn("(IoT Architecture &amp; Protocols, etc ...)", html)
# 3. Verify category detail active card border & label
patent_html = self.read_html(os.path.join("publications", "patent.html"))
self.assertIn("border-2 border-cobalt-dark", patent_html)
self.assertIn("Active View ●", patent_html)
self.assertIn("Patent", patent_html)
def test_09_footer_cite_and_brand_links(self):
"""TC-T1-F9: Verify Footer CITE abbreviation and brand/contact elements."""
html = self.read_html("index.html")
# 1. Institution link label CITE
self.assertIn("<span>CITE</span>", html)
self.assertIn("<span>KNU</span>", html)
self.assertIn("<span>CSE</span>", html)
# 2. Advisor contact info
self.assertIn("Prof. Seok-Joo Koh", html)
self.assertIn("sjkoh@knu.ac.kr", html)
def test_10_subpage_layout_and_english_headers(self):
"""TC-T1-F10: Verify English page headers across all subpages."""
subpages = [
("members.html", "LAB MEMBERS"),
("publications.html", "PUBLICATIONS"),
("lectures.html", "LECTURES"),
("standardization.html", "International Standardization"),
]
for rel_path, expected_h1 in subpages:
html = self.read_html(rel_path)
self.assertIn(expected_h1, html, f"Subpage {rel_path} missing H1 '{expected_h1}'")
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -7,10 +7,10 @@
# - .mam.env present --force → overwrite .mam.env from .mam.env.example (backs up to .mam.env.bak).
# - --migrate-legacy → explicitly rename existing .env to .mam.env if absent.
#
# Paths are resolved relative to this script (repo root = parent of scripts/),
# Paths are resolved relative to this script (repo root = parent of deploy/),
# so it works regardless of the caller's cwd.
#
# Usage: scripts/generate-env.sh [--force] [--migrate-legacy] [-h|--help]
# Usage: deploy/generate-env.sh [--force] [--migrate-legacy] [-h|--help]
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"