Compare commits
8
Commits
a2b626b192
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadd14feb1 | ||
|
|
f96f687dd1 | ||
|
|
5041ab1ea8 | ||
|
|
9cb6258d3d | ||
|
|
fb6881070c | ||
|
|
75bf334fec | ||
|
|
f34e9bad57 | ||
|
|
b1640cdac4 |
@@ -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)
|
||||
# ==============================================================================
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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*
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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! ===")
|
||||
}
|
||||
@@ -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! ===")
|
||||
}
|
||||
@@ -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
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Executable
+175
@@ -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()
|
||||
@@ -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[] = [
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
+1156
-1421
File diff suppressed because it is too large
Load Diff
@@ -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[] = [
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
]
|
||||
@@ -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
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
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.",
|
||||
@@ -12,7 +20,10 @@ const DEGREE_LABEL: Record<string, string> = {
|
||||
MS: "M. S.",
|
||||
};
|
||||
|
||||
export default function MembersPage() {
|
||||
export default async function MembersPage() {
|
||||
const activeMembers = (await getMembers()) ?? [];
|
||||
const alumni = (await getAlumni()) ?? [];
|
||||
|
||||
const phdStudents = activeMembers.filter(
|
||||
(m) =>
|
||||
m.degree === "PhD" ||
|
||||
@@ -24,16 +35,16 @@ export default function MembersPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container-content py-8 sm:py-12 space-y-12 sm:space-y-16">
|
||||
<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">
|
||||
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||
LAB MEMBERS
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
{/* Professor Section */}
|
||||
<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="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">
|
||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
||||
@@ -77,10 +88,10 @@ 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>
|
||||
<h2 className="font-serif text-3xl font-normal text-cobalt-dark">
|
||||
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||
Active Researchers
|
||||
</h2>
|
||||
</div>
|
||||
@@ -148,10 +159,10 @@ 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>
|
||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
||||
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||
Graduates
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -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 & 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 & 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"
|
||||
>
|
||||
← 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 →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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 & 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"
|
||||
>
|
||||
전체 보기 →
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "국내외 지식재산권 출원 및 등록 실적",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 (삼성전자)"
|
||||
}
|
||||
];
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).")
|
||||
|
||||
@@ -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
|
||||
@@ -66,7 +125,7 @@ class TestANLHomepageE2E(unittest.TestCase):
|
||||
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"),
|
||||
@@ -91,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()
|
||||
|
||||
@@ -100,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
|
||||
@@ -118,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 & 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()
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user