Compare commits
16
Commits
a838af4b94
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadd14feb1 | ||
|
|
f96f687dd1 | ||
|
|
5041ab1ea8 | ||
|
|
9cb6258d3d | ||
|
|
fb6881070c | ||
|
|
75bf334fec | ||
|
|
f34e9bad57 | ||
|
|
b1640cdac4 | ||
|
|
a2b626b192 | ||
|
|
44c21ab59e | ||
|
|
8eabd7a2db | ||
|
|
b8f1a000fd | ||
|
|
184ff8263c | ||
|
|
1b84803582 | ||
|
|
73c4f8a8cb | ||
|
|
1470806e06 |
+11
@@ -7,3 +7,14 @@
|
|||||||
|
|
||||||
# Binary zip archives
|
# Binary zip archives
|
||||||
*.zip
|
*.zip
|
||||||
|
# >>> MAM managed block (managed by install.sh — do not edit) >>>
|
||||||
|
/.venv/
|
||||||
|
/.mam/
|
||||||
|
/.mam_deploy/
|
||||||
|
/.mam.env
|
||||||
|
/.mam.env.*
|
||||||
|
!/.mam.env.example
|
||||||
|
/.cache/multi-agent-mux-monitor/
|
||||||
|
/.mam-skill-backup.*/
|
||||||
|
CURRENT_JOB.md
|
||||||
|
# <<< MAM managed block <<<
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# .mam.env.example — committable template for the multi-agent-mux-* skills
|
||||||
|
#
|
||||||
|
# This file is tracked in git and contains NO secrets. To get a working local
|
||||||
|
# config, copy it to `.mam.env` (which is git-ignored) and edit as needed:
|
||||||
|
#
|
||||||
|
# scripts/generate-env.sh # creates .mam.env from this template if absent
|
||||||
|
# # or manually: cp .mam.env.example .mam.env
|
||||||
|
#
|
||||||
|
# Every variable below is OPTIONAL. The skills already resolve sane defaults
|
||||||
|
# (shown after each `#default:` line), so an unset/commented variable just keeps
|
||||||
|
# the built-in behaviour. Uncomment + edit only the ones you want to override.
|
||||||
|
#
|
||||||
|
# SECURITY: never put real secrets in this template. Secret-bearing vars use a
|
||||||
|
# `replace_me` placeholder — fill them in only in your local `.mam.env`.
|
||||||
|
# SECURITY NOTE:
|
||||||
|
# The default MQTT broker (broker.hivemq.com) is public and unencrypted. Job event
|
||||||
|
# payloads (including task details) sent to the default broker can be read by anyone.
|
||||||
|
# For production or private workloads, configure a private broker with TLS and Auth below.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Workspace / runtime paths
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
# Single source of truth for the agent session registry YAML.
|
||||||
|
#default: <workspace>/.mam/agent-sessions.yaml
|
||||||
|
# AGENT_SESSIONS_YAML=/path/to/workspace/.mam/agent-sessions.yaml
|
||||||
|
|
||||||
|
# Root directory that holds Claude Code per-project conversation logs (*.jsonl).
|
||||||
|
#default: $HOME/.claude/projects
|
||||||
|
# CLAUDE_PROJECT_DIR=$HOME/.claude/projects
|
||||||
|
|
||||||
|
# Directory scanned for per-session launcher wrappers (~/.local/bin/<session>).
|
||||||
|
#default: $HOME/.local/bin
|
||||||
|
# LOCAL_BIN=$HOME/.local/bin
|
||||||
|
|
||||||
|
# Isolated Herdr session socket name (`herdr --session <name>`).
|
||||||
|
#default: mam-<workspace-slug>
|
||||||
|
# HERDR_SESSION_NAME=mam-multi-agent-mux
|
||||||
|
|
||||||
|
# Legacy Herdr server identifier (DEPRECATED: use HERDR_SESSION_NAME instead).
|
||||||
|
# Written into .mam.env by install.sh as an active default.
|
||||||
|
#default: multi-agent-mux
|
||||||
|
# HERDR_SERVER_NAME=multi-agent-mux
|
||||||
|
|
||||||
|
# Path to the Herdr Unix domain socket.
|
||||||
|
#default: $HOME/.config/herdr/herdr.sock
|
||||||
|
# HERDR_SOCKET_PATH=$HOME/.config/herdr/herdr.sock
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# delegate-job / MQTT broker
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
# MQTT broker host the delegate-job publisher/subscriber connects to.
|
||||||
|
#default: broker.hivemq.com
|
||||||
|
# MQTT_BROKER=broker.hivemq.com
|
||||||
|
|
||||||
|
# MQTT broker port. Written into .mam.env by install.sh as an active default.
|
||||||
|
#default: 1883
|
||||||
|
# MQTT_PORT=1883
|
||||||
|
|
||||||
|
# MQTT reconnect retry interval (seconds).
|
||||||
|
#default: 2
|
||||||
|
# MQTT_RETRY_INTERVAL=2
|
||||||
|
|
||||||
|
# MQTT maximum reconnect retries.
|
||||||
|
#default: 5
|
||||||
|
# MQTT_MAX_RETRIES=5
|
||||||
|
|
||||||
|
# Broker auth username. Leave unset for anonymous brokers.
|
||||||
|
#default: (unset → anonymous)
|
||||||
|
# MQTT_USERNAME=replace_me
|
||||||
|
|
||||||
|
# Broker auth password. SECRET — fill in only in your local .mam.env, never commit.
|
||||||
|
#default: (unset → anonymous)
|
||||||
|
# MQTT_PASSWORD=replace_me
|
||||||
|
|
||||||
|
# Prefix for generated MQTT client ids (publisher/subscriber/monitor).
|
||||||
|
#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
|
||||||
|
|
||||||
|
# Retention period (days) for delegate-job event logs.
|
||||||
|
#default: 7
|
||||||
|
# MAM_EVENT_RETENTION_DAYS=7
|
||||||
|
|
||||||
|
# Garbage collection threshold (minutes) for old message buffer logs.
|
||||||
|
#default: 60
|
||||||
|
# MAM_BUFFER_GC_MINUTES=60
|
||||||
|
|
||||||
|
# Marker string used by loop delegation guard (O-3).
|
||||||
|
#default: (built-in guard marker)
|
||||||
|
# MAM_LOOP_GUARD_MARKER=mam_loop_active
|
||||||
|
|
||||||
|
# Path to a CA bundle for TLS broker verification (set MQTT_TLS=1 to use TLS).
|
||||||
|
#default: (unset → no custom CA)
|
||||||
|
# MQTT_CA_CERTS=/path/to/ca.crt
|
||||||
|
|
||||||
|
# Client certificate for mutual-TLS brokers.
|
||||||
|
#default: (unset → no client cert)
|
||||||
|
# MQTT_CERTFILE=/path/to/client.crt
|
||||||
|
|
||||||
|
# Client private key for mutual-TLS brokers. SECRET — keep the key file private.
|
||||||
|
#default: (unset → no client key)
|
||||||
|
# MQTT_KEYFILE=/path/to/client.key
|
||||||
|
|
||||||
|
# Directory for delegate-job audit logs (sits beside .mam/jobs/).
|
||||||
|
#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)
|
||||||
|
# ==============================================================================
|
||||||
|
# Note: These variables are read from the execution environment by deployment scripts.
|
||||||
|
# Since deploy/install.sh runs before .mam.env exists, you must pass them via export
|
||||||
|
# or prepended variables (e.g. MAM_REPO_URL=... bash deploy/install.sh).
|
||||||
|
# If you run a private mirror, we strongly recommend configuring all three variables.
|
||||||
|
|
||||||
|
# Distribution repository URL (cloned during recovery steps).
|
||||||
|
#default: https://git.godopu.com/tmpl/multi-agent-mux.git
|
||||||
|
# MAM_REPO_URL=https://git.godopu.com/tmpl/multi-agent-mux.git
|
||||||
|
|
||||||
|
# Distribution archive download URL (used for bootstrap extraction).
|
||||||
|
#default: https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz
|
||||||
|
# MAM_ARCHIVE_URL=https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz
|
||||||
|
|
||||||
|
# Distribution update/installer script URL.
|
||||||
|
#default: https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh
|
||||||
|
# MAM_INSTALLER_URL=https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh
|
||||||
@@ -68,3 +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.
|
**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.
|
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.
|
||||||
|
|
||||||
|
## 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` |
|
| **문서 ID** | `REQ-ANL-WEB-001` |
|
||||||
| **버전** | `1.14` |
|
| **버전** | `2.0` |
|
||||||
| **작성일** | 2026-07-30 |
|
| **작성일** | 2026-08-24 |
|
||||||
| **작성자** | claude (`canary-projects-landing-page-creator-claude`) · Role: planner |
|
| **작성자** | agy (`herdr:creator-agy-01`) / claude (`herdr:planner-reviewer-claude-01`) · Role: worker / 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 심사 전에는 집행하지 않는다** |
|
| **상태** | 승인 (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), 지도교수 |
|
| **대상 독자** | 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) |
|
| **선행 문서** | [`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 애플리케이션) 전체 |
|
| **적용 범위** | `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-01 | 콘텐츠 데이터는 페이지 컴포넌트에서 분리해 **`content/` 디렉터리의 타입 지정 모듈**로 이관한다 (`content/members.ts`, `content/publications.ts`, `content/lectures.ts`, `content/standards.ts`) | MUST |
|
||||||
| DM-02 | 각 모듈은 명시적 TypeScript 인터페이스를 export 한다. `any` 사용 금지 | 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 |
|
| DM-04 | 각 페이지 상단의 `// CUSTOMIZATION HOOK` 주석 규약(`DESIGN.md` "콘텐츠 우선" 원칙)은 `content/` 모듈로 이전하되 **규약 자체는 유지**한다 | MUST |
|
||||||
|
|
||||||
### 7.2 스키마 정의 (계약)
|
### 7.2 스키마 정의 (계약)
|
||||||
@@ -648,7 +648,7 @@ Job `343b5808` 검증에서 본문 좌우 여백·최대 폭 미적용(B1)이,
|
|||||||
**품질 게이트**
|
**품질 게이트**
|
||||||
- [ ] AC-15 `npm run lint` 통과 (경고 0)
|
- [ ] AC-15 `npm run lint` 통과 (경고 0)
|
||||||
- [ ] AC-16 `npx tsc --noEmit` 통과
|
- [ ] 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-18 First Load JS 증가분 ≤ 30 kB
|
||||||
- [ ] AC-19 4종 뷰포트에서 가로 스크롤 없음
|
- [ ] AC-19 4종 뷰포트에서 가로 스크롤 없음
|
||||||
- [ ] AC-20 신규 Tailwind 반응형 유틸리티가 빌드 CSS에 실제 생성됨을 확인 (JIT 누락 시 빌드는 성공하나 조판이 무너짐)
|
- [ ] 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.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.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` |
|
| 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,98 @@
|
|||||||
|
`body > main > div > section.space-y-3 > div > span.font-mono.text-xs.tracking-widest.uppercase.text-cobalt-dark.font-bold`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-3 > h1`
|
||||||
|
- 연구실 구성원 -> "LAB MEMBERS"
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-3 > p`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(2) > div.font-mono.text-xs.text-cobalt-dark.uppercase.tracking-widest.font-bold`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(2) > div.font-mono.text-xs.text-cobalt-dark.uppercase.tracking-widest.font-bold`
|
||||||
|
- "고석주 교수" -> "Prof. Seok-Joo Koh"로 변경
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(2) > div.bg-paper.p-8.rounded-lg.border.border-line.grid.grid-cols-1.lg\:grid-cols-12.gap-8.items-center > div.lg\:col-span-8.space-y-4 > div.flex.items-baseline.gap-3 > span`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(2) > div.bg-paper.p-8.rounded-lg.border.border-line.grid.grid-cols-1.lg\:grid-cols-12.gap-8.items-center > div.lg\:col-span-8.space-y-4 > p`
|
||||||
|
|
||||||
|
- 다음 세부 커리어를 기반으로 설명 작성 (영어로) 및 details 링크 연결.
|
||||||
|
- details 링크 클릭 시 modal dialog를 통해 아래 세부 내용 보여주기
|
||||||
|
|
||||||
|
```
|
||||||
|
# Education
|
||||||
|
- KAIST, Management Science, B. S., 1992. 2.
|
||||||
|
- KAIST, Management Science, M. S., 1994. 2.
|
||||||
|
- KAIST, Industrial Engineering, Ph. D., 1998. 8.
|
||||||
|
|
||||||
|
# Career
|
||||||
|
|
||||||
|
2024. 3 - Present
|
||||||
|
Dean
|
||||||
|
College of IT Engineering (CITE)
|
||||||
|
Kyungpook National University (KNU)
|
||||||
|
|
||||||
|
2015. 12 - Present
|
||||||
|
Director
|
||||||
|
Institute of Software Education (SWEDU)
|
||||||
|
Kyungpook National University (KNU)
|
||||||
|
|
||||||
|
2004. 3 - Present
|
||||||
|
Professor
|
||||||
|
School of Computer Science and Engineering (CSE)
|
||||||
|
College of IT Engineering (CITE)
|
||||||
|
Kyungpook National University (KNU)
|
||||||
|
|
||||||
|
1998. 8 - 2004. 2
|
||||||
|
Senior Researcher
|
||||||
|
Protocol Engineering Center (PEC)
|
||||||
|
Electronics Telecommunications Research Institute (ETRI)
|
||||||
|
|
||||||
|
# International Standardization
|
||||||
|
- ISO/IEC JTC1 SC6/WG7: Reliable/Mobile Multicasting (1999 ~ 2019): Project Editor
|
||||||
|
- ISO/IEC JTC1 SC41/WG5: Internet-of-Things (IoT) Applications (2020 ~ Present): Project Editor
|
||||||
|
- ITU-T SG13: Mobility Management (1999 ~ 2015): Project Editor
|
||||||
|
- ITU-T SG20: VLC-based IoT (2018 ~ 2020): Project Editor
|
||||||
|
- IEC TC100: Multimedia Systems and Equipment (2018 ~ Present): Project Editor, TA15 TAM, WG11 Convenor
|
||||||
|
|
||||||
|
# Membership on Academic Society
|
||||||
|
|
||||||
|
- KIPS (Korea Information Processing Society)
|
||||||
|
- KICS (Korean Institute of Communications and Information Sciences)
|
||||||
|
- KIISE (Korean Institute of Information Scientists and Engineers)
|
||||||
|
- IEEE (Institute of Electrical and Electronics Engineers)
|
||||||
|
```
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.flex.justify-between.items-end > div > div`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.flex.justify-between.items-end > div > h2`
|
||||||
|
- "재학 연구원" -> Active Researchers
|
||||||
|
- 글자색 변경: primary color
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div:nth-child(2)`
|
||||||
|
- PhD -> Ph. D
|
||||||
|
- PhDCandidate -> Ph. D. Candidate
|
||||||
|
- PhDStudent -> Ph. D. Student
|
||||||
|
|
||||||
|
`body > main > div > section.space-y-8.pt-6.md\:pt-10.border-t.border-line > div.space-y-4.pt-4`
|
||||||
|
- MSCandidate -> M. S. Candidate
|
||||||
|
- MSStudent -> M. S. Student
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(4) > div > div > div`
|
||||||
|
- 삭제
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(4) > div > div > h2`
|
||||||
|
- 졸업생 목록 -> "Graduates"
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(4) > details > summary > div > span`
|
||||||
|
- 영어로 작성
|
||||||
|
`body > main > div > section:nth-child(4) > details > summary > div > p`
|
||||||
|
- 영어로 작성
|
||||||
|
|
||||||
|
`body > main > div > section:nth-child(4) > details > summary > span > span.group-open\:hidden`
|
||||||
|
- 전체 보기 -> Open
|
||||||
|
|
||||||
|
- 삭제된 component들의 크기를 반영하여 화면 비율 조정
|
||||||
@@ -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",
|
"term": "Spring",
|
||||||
"year": 2026,
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"standardsTrack": "National Research Foundation of Korea (NRF)",
|
||||||
|
"standardsOrg": "NRF",
|
||||||
|
"period": "2026 ~ Present",
|
||||||
|
"deliverables": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ccis",
|
||||||
|
"title": "MCM",
|
||||||
|
"abstract": "Multilateral and Collaborative Metaverse (MCM) refers to metaverse systems in which two or more participants simultaneously interact within a shared virtual space for a common collaborative purpose. Unlike single-user or purely competitive environments, an MCM service is defined by coordinated participation: its intended outcome cannot be achieved by a single user acting alone. The project defines general requirements, service classification, and a gap analysis against existing standards.",
|
||||||
|
"keywords": [
|
||||||
|
"MCM",
|
||||||
|
"metaverse",
|
||||||
|
"shared virtual space",
|
||||||
|
"coordinated participation",
|
||||||
|
"service classification"
|
||||||
|
],
|
||||||
|
"standardsTrack": "IEC/TC 100/WG 12 supported by KEIT",
|
||||||
|
"standardsOrg": "IEC TC100",
|
||||||
|
"period": "2026 ~ Present",
|
||||||
|
"deliverables": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "vlc-iot",
|
||||||
|
"title": "MVQM",
|
||||||
|
"abstract": "Management of Visual Quality in Metaverse Systems (MVQM) is a conceptual and functional approach for managing visual quality by dynamically adapting visual data delivery in response to user context, device capability, and network conditions. It emphasizes user-centric, context-aware quality management over static system-centric optimization, covering hybrid visual assets, level of detail, and area of interest adaptation.",
|
||||||
|
"keywords": [
|
||||||
|
"MVQM",
|
||||||
|
"visual quality",
|
||||||
|
"hybrid visual assets",
|
||||||
|
"level of detail",
|
||||||
|
"area of interest"
|
||||||
|
],
|
||||||
|
"standardsTrack": "IEC/TC 100/WG 12 supported by KEIT",
|
||||||
|
"standardsOrg": "IEC TC100",
|
||||||
|
"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",
|
"org": "IEC TC100",
|
||||||
"full": "Multimedia Systems and Equipment",
|
"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,87 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export function DirectionsBlock() {
|
|
||||||
return (
|
|
||||||
<section className="mt-20 pt-16 border-t border-line" id="directions">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<span className="h-px w-8 bg-cobalt-dark"></span>
|
|
||||||
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
|
|
||||||
Location & Contact
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
|
||||||
{/* Left info column */}
|
|
||||||
<div className="lg:col-span-7 space-y-6">
|
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
|
||||||
오시는 길 & 연구실 소재지
|
|
||||||
</h2>
|
|
||||||
<p className="text-ink-mute text-base leading-relaxed">
|
|
||||||
경북대학교 AI 에이전트 네트워크 연구실 (ANL)은 IT대학 5호관에 위치하고 있으며, 차세대 차세대 에이전트 오케스트레이션 및 고성능 네트워크 백본 연구를 수행합니다.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<address className="not-italic space-y-4 pt-2 text-sm font-sans text-ink">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<span className="font-mono text-xs uppercase tracking-wider text-ink-mute w-20 pt-0.5">
|
|
||||||
소재지
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">
|
|
||||||
경북대학교 IT대학 5호관(415동) 527호
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-ink-mute mt-0.5">
|
|
||||||
School of Computer Science and Engineering, Kyungpook National University
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="font-mono text-xs uppercase tracking-wider text-ink-mute w-20">
|
|
||||||
지도교수
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
고석주 교수 (<a href="mailto:sjkoh@knu.ac.kr" className="text-vermillion hover:underline">sjkoh@knu.ac.kr</a>)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<span className="font-mono text-xs uppercase tracking-wider text-ink-mute w-20 pt-0.5">
|
|
||||||
관련 기관
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs font-mono text-cobalt-dark">
|
|
||||||
<a href="http://www.knu.ac.kr/" target="_blank" rel="noreferrer" className="hover:underline">
|
|
||||||
경북대학교 (KNU) ↗
|
|
||||||
</a>
|
|
||||||
<a href="http://cse.knu.ac.kr/" target="_blank" rel="noreferrer" className="hover:underline">
|
|
||||||
컴퓨터학부 ↗
|
|
||||||
</a>
|
|
||||||
<a href="http://it.knu.ac.kr/" target="_blank" rel="noreferrer" className="hover:underline">
|
|
||||||
IT대학 ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</address>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right card column */}
|
|
||||||
<div className="lg:col-span-5 bg-paper p-6 rounded-lg border border-line flex flex-col justify-between">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider">
|
|
||||||
Research Environment
|
|
||||||
</div>
|
|
||||||
<h3 className="font-serif text-xl font-normal text-ink">
|
|
||||||
AI Agent Networking Lab
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-ink-mute leading-relaxed">
|
|
||||||
QUIC/gRPC 스트리밍 통신 인프라와 컨테이너 격리 프로세스 격리 환경을 포함한 멀티 에이전트 실험실 공간을 운용합니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 pt-4 border-t border-line flex justify-between items-center text-xs font-mono text-ink-mute">
|
|
||||||
<span>IT-5 Building #527</span>
|
|
||||||
<span className="text-vermillion font-bold">ANL Laboratory</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { researchProjects } from "../../content/projects";
|
import type { ResearchProject } from "../../content/types";
|
||||||
import ProjectPageViewControls from "./ProjectPageViewControls";
|
import ProjectPageViewControls from "./ProjectPageViewControls";
|
||||||
|
|
||||||
export function ProjectPageView() {
|
interface ProjectPageViewProps {
|
||||||
const slugs = researchProjects.map((p) => p.slug);
|
projects?: ResearchProject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectPageView({ projects: projectsProp }: ProjectPageViewProps = {}) {
|
||||||
|
const projects = projectsProp ?? [];
|
||||||
|
const slugs = projects.map((p) => p.slug);
|
||||||
const trackId = "research-projects-track";
|
const trackId = "research-projects-track";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,11 +22,11 @@ export function ProjectPageView() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||||
주요 연구 프로젝트 (Research Projects)
|
Research Projects
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
총 {researchProjects.length}개 핵심 국제/국내 표준화 연구 과제
|
{projects.length} core international standardization research projects
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,13 +39,13 @@ export function ProjectPageView() {
|
|||||||
aria-label="연구 프로젝트 목록"
|
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"
|
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
|
<article
|
||||||
key={proj.slug}
|
key={proj.slug}
|
||||||
id={`project-${proj.slug}`}
|
id={`project-${proj.slug}`}
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
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]"
|
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">
|
<div className="space-y-4">
|
||||||
@@ -76,7 +81,7 @@ export function ProjectPageView() {
|
|||||||
|
|
||||||
{/* Metadata Footer: Standards Track & Deliverables */}
|
{/* Metadata Footer: Standards Track & Deliverables */}
|
||||||
<div className="space-y-3 pt-3 border-t border-line/60 text-xs font-mono">
|
<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 ? (
|
{proj.funder ? (
|
||||||
<div className="flex items-baseline justify-between gap-2">
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<span className="text-ink-mute">Funder:</span>
|
<span className="text-ink-mute">Funder:</span>
|
||||||
@@ -84,7 +89,7 @@ export function ProjectPageView() {
|
|||||||
</div>
|
</div>
|
||||||
) : proj.standardsTrack ? (
|
) : proj.standardsTrack ? (
|
||||||
<div className="flex items-baseline justify-between gap-2">
|
<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>
|
<span className="text-cobalt-dark font-bold">{proj.standardsTrack}</span>
|
||||||
</div>
|
</div>
|
||||||
) : proj.standardsOrg ? (
|
) : proj.standardsOrg ? (
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -41,10 +41,10 @@
|
|||||||
|
|
||||||
--ink-rgb: 10 10 10;
|
--ink-rgb: 10 10 10;
|
||||||
--ink-soft-rgb: 38 36 32;
|
--ink-soft-rgb: 38 36 32;
|
||||||
--ink-mute-rgb: 107 101 91;
|
--ink-mute-rgb: 107 114 128;
|
||||||
--ivory-rgb: 245 241 232;
|
--ivory-rgb: 255 255 255;
|
||||||
--paper-rgb: 236 230 214;
|
--paper-rgb: 248 249 250;
|
||||||
--line-rgb: 216 209 192;
|
--line-rgb: 229 231 235;
|
||||||
--vermillion-rgb: 217 52 43;
|
--vermillion-rgb: 217 52 43;
|
||||||
--vermillion-dark-rgb: 168 35 28;
|
--vermillion-dark-rgb: 168 35 28;
|
||||||
--cobalt-rgb: 78 119 255;
|
--cobalt-rgb: 78 119 255;
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
--cobalt: rgb(var(--cobalt-rgb));
|
--cobalt: rgb(var(--cobalt-rgb));
|
||||||
--cobalt-dark: rgb(var(--cobalt-dark-rgb));
|
--cobalt-dark: rgb(var(--cobalt-dark-rgb));
|
||||||
|
|
||||||
--grain-color: rgba(10, 10, 10, 0.022);
|
--grain-color: rgba(10, 10, 10, 0.015);
|
||||||
|
|
||||||
/* CUSTOMIZATION HOOK — MOTION: global reveal timing */
|
/* CUSTOMIZATION HOOK — MOTION: global reveal timing */
|
||||||
--reveal-duration: 600ms;
|
--reveal-duration: 600ms;
|
||||||
@@ -172,6 +172,14 @@ body {
|
|||||||
.draw-underline:hover {
|
.draw-underline:hover {
|
||||||
background-size: 100% 2px;
|
background-size: 100% 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile menu scroll container for low-height / landscape viewports (F4) */
|
||||||
|
.mobile-menu-panel {
|
||||||
|
max-height: calc(100vh - 6rem); /* Fallback: browsers without dvh support */
|
||||||
|
max-height: calc(100dvh - 6rem); /* Modern: dynamic viewport height */
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain; /* Prevent chaining scroll to main page */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================================================================
|
/* ==================================================================
|
||||||
@@ -217,6 +225,11 @@ body {
|
|||||||
[class*="animate-"] {
|
[class*="animate-"] {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
/* 스마트 스티키: 모션 줄임 사용자에게는 항상 펼친 상태 유지 */
|
||||||
|
header[data-smart-sticky] {
|
||||||
|
transform: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="cobaltGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4E77FF"/>
|
||||||
|
<stop offset="100%" stop-color="#2B4CDE"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="32" height="32" rx="7" fill="url(#cobaltGrad)"/>
|
||||||
|
<!-- Network Agent Nodes & Graph -->
|
||||||
|
<circle cx="16" cy="9" r="2.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="9" cy="22" r="2.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="23" cy="22" r="2.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="16" cy="17" r="2" fill="#FFD666"/>
|
||||||
|
|
||||||
|
<path d="M16 9 L9 22 M16 9 L23 22 M9 22 L23 22 M16 9 L16 17 M9 22 L16 17 M23 22 L16 17"
|
||||||
|
stroke="#FFFFFF" stroke-width="1.2" stroke-opacity="0.8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 787 B |
@@ -34,7 +34,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="ko">
|
<html lang="ko" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
{/* No-JS fallback for the .reveal scroll-reveal: without this, every
|
{/* No-JS fallback for the .reveal scroll-reveal: without this, every
|
||||||
|
|||||||
@@ -1,61 +1,63 @@
|
|||||||
import React from "react";
|
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 recentSemesters = semesters.slice(0, 3);
|
||||||
const olderSemesters = semesters.slice(3);
|
const olderSemesters = semesters.slice(3);
|
||||||
|
|
||||||
|
const oldestSem = olderSemesters[olderSemesters.length - 1];
|
||||||
|
const newestOlderSem = olderSemesters[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Header */}
|
{/* Page Header */}
|
||||||
<section className="space-y-3">
|
<section>
|
||||||
<div className="flex items-center gap-3">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
<span className="h-px w-8 bg-cobalt-dark"></span>
|
LECTURES
|
||||||
<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">
|
|
||||||
강의 이력
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-ink-mute text-base max-w-2xl">
|
|
||||||
2016년 가을학기부터 2026년 봄학기까지 개설된 네트워크 및 소프트웨어 분야 약 20개 학기의 학부/대학원 강의 목록입니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Recent 3 Semesters */}
|
{/* Recent 3 Semesters */}
|
||||||
<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 className="flex justify-between items-center">
|
||||||
<div>
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||||
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
|
Recent Courses
|
||||||
Recent Courses ({recentSemesters.length} Semesters)
|
|
||||||
</div>
|
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
|
|
||||||
최근 개설 강의
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
<span className="font-mono text-xs text-ink-mute hidden sm:inline">
|
||||||
|
Latest 3 Semesters
|
||||||
|
</span>
|
||||||
</div>
|
</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) => (
|
{recentSemesters.map((sem, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
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="space-y-3">
|
||||||
<div className="flex justify-between items-center border-b border-line pb-2">
|
<div className="flex justify-between items-center border-b border-line pb-2.5">
|
||||||
<span className="font-serif text-xl font-normal text-ink">
|
<span className="font-serif text-lg sm:text-xl font-normal text-ink">
|
||||||
{sem.term} {sem.year}
|
{sem.term} {sem.year}
|
||||||
</span>
|
</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
|
Latest
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="space-y-2 pt-1">
|
<ul className="space-y-2.5 pt-1">
|
||||||
{sem.courses.map((c, cIdx) => (
|
{sem.courses.map((c, cIdx) => (
|
||||||
<li key={cIdx} className="space-y-0.5">
|
<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}
|
{c.en}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute">
|
<div className="font-mono text-xs text-ink-mute">
|
||||||
@@ -66,8 +68,9 @@ export default function LecturesPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-3 border-t border-line/60 font-mono text-xs text-ink-mute">
|
<div className="pt-3 border-t border-line/60 font-mono text-xs text-ink-mute flex justify-between items-center">
|
||||||
Course Count: {sem.courses.length}
|
<span>Total Offerings</span>
|
||||||
|
<span className="font-semibold text-ink">{sem.courses.length} Courses</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -75,30 +78,30 @@ export default function LecturesPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Older Semesters (Native <details>/<summary> progressive disclosure) */}
|
{/* Older Semesters (Native <details>/<summary> progressive disclosure) */}
|
||||||
<section className="space-y-6 pt-6 border-t border-line">
|
<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-6 transition-all duration-200">
|
<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">
|
<summary className="flex justify-between items-center cursor-pointer list-none select-none">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="font-serif text-xl font-normal text-ink">
|
<span className="font-serif text-lg sm:text-xl font-normal text-ink">
|
||||||
이전 전체 강의 이력 ({olderSemesters.length}개 학기) 열람하기
|
Browse all semesters
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<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>
|
</p>
|
||||||
</div>
|
</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="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">전체 보기 ▾</span>
|
<span className="group-open:hidden">Open ▾</span>
|
||||||
<span className="hidden group-open:inline">접기 ▴</span>
|
<span className="hidden group-open:inline">Close ▴</span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</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) => (
|
{olderSemesters.map((sem, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
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>
|
<span>
|
||||||
{sem.term} {sem.year}
|
{sem.term} {sem.year}
|
||||||
</span>
|
</span>
|
||||||
@@ -110,7 +113,7 @@ export default function LecturesPage() {
|
|||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{sem.courses.map((c, cIdx) => (
|
{sem.courses.map((c, cIdx) => (
|
||||||
<li key={cIdx} className="space-y-0.5">
|
<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}
|
{c.en}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-[0.7rem] text-ink-mute">
|
<div className="font-mono text-[0.7rem] text-ink-mute">
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
|
// CUSTOMIZATION HOOK: Detailed career & background data for Prof. Seok-Joo Koh
|
||||||
|
const EDUCATION = [
|
||||||
|
"KAIST, Management Science, B. S., 1992. 2.",
|
||||||
|
"KAIST, Management Science, M. S., 1994. 2.",
|
||||||
|
"KAIST, Industrial Engineering, Ph. D., 1998. 8.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const CAREER = [
|
||||||
|
{
|
||||||
|
period: "2024. 3 - Present",
|
||||||
|
role: "Dean",
|
||||||
|
org: "College of IT Engineering (CITE)",
|
||||||
|
inst: "Kyungpook National University (KNU)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2015. 12 - Present",
|
||||||
|
role: "Director",
|
||||||
|
org: "Institute of Software Education (SWEDU)",
|
||||||
|
inst: "Kyungpook National University (KNU)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2004. 3 - Present",
|
||||||
|
role: "Professor",
|
||||||
|
org: "School of Computer Science and Engineering (CSE), College of IT Engineering (CITE)",
|
||||||
|
inst: "Kyungpook National University (KNU)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "1998. 8 - 2004. 2",
|
||||||
|
role: "Senior Researcher",
|
||||||
|
org: "Protocol Engineering Center (PEC)",
|
||||||
|
inst: "Electronics Telecommunications Research Institute (ETRI)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const STANDARDIZATION = [
|
||||||
|
"ISO/IEC JTC1 SC6/WG7: Reliable/Mobile Multicasting (1999 ~ 2019): Project Editor",
|
||||||
|
"ISO/IEC JTC1 SC41/WG5: Internet-of-Things (IoT) Applications (2020 ~ Present): Project Editor",
|
||||||
|
"ITU-T SG13: Mobility Management (1999 ~ 2015): Project Editor",
|
||||||
|
"ITU-T SG20: VLC-based IoT (2018 ~ 2020): Project Editor",
|
||||||
|
"IEC TC100: Multimedia Systems and Equipment (2018 ~ Present): Project Editor, TA15 TAM, WG11 Convenor",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MEMBERSHIPS = [
|
||||||
|
"KIPS (Korea Information Processing Society)",
|
||||||
|
"KICS (Korean Institute of Communications and Information Sciences)",
|
||||||
|
"KIISE (Korean Institute of Information Scientists and Engineers)",
|
||||||
|
"IEEE (Institute of Electrical and Electronics Engineers)",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProfessorDetailsDialog() {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogRef.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
||||||
|
if (e.target === dialogRef.current) {
|
||||||
|
dialogRef.current?.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="inline-flex items-center gap-1 font-mono text-xs font-semibold text-cobalt-dark hover:underline focus:outline-none focus:ring-1 focus:ring-cobalt-dark rounded"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<span>Details →</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
aria-labelledby="prof-dialog-title"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
className="bg-paper text-ink border border-line rounded-lg p-0 max-w-2xl w-[92vw] max-h-[85vh] supports-[height:100dvh]:max-h-[85dvh] backdrop:bg-ink/50 backdrop:backdrop-blur-sm open:flex open:flex-col shadow-2xl m-auto"
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-line p-6 bg-paper sticky top-0 z-10">
|
||||||
|
<div>
|
||||||
|
<span className="font-mono text-xs text-cobalt-dark uppercase tracking-wider font-bold">
|
||||||
|
Curriculum Vitae
|
||||||
|
</span>
|
||||||
|
<h2
|
||||||
|
id="prof-dialog-title"
|
||||||
|
className="font-serif text-2xl font-normal text-ink mt-0.5"
|
||||||
|
>
|
||||||
|
Prof. Seok-Joo Koh
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close dialog"
|
||||||
|
className="p-1.5 rounded border border-line hover:bg-ivory text-ink-mute hover:text-ink transition-colors font-mono text-xs"
|
||||||
|
>
|
||||||
|
✕ Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Scrollable Content */}
|
||||||
|
<div className="p-6 sm:p-8 space-y-8 overflow-y-auto overscroll-contain text-sm">
|
||||||
|
{/* Education */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
|
||||||
|
Education
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2 text-ink/90 font-sans">
|
||||||
|
{EDUCATION.map((edu, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2.5">
|
||||||
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
|
||||||
|
<span>{edu}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Career */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
|
||||||
|
Career
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{CAREER.map((car, idx) => (
|
||||||
|
<div key={idx} className="border-l-2 border-line pl-3 space-y-0.5">
|
||||||
|
<div className="font-mono text-xs text-ink-mute">{car.period}</div>
|
||||||
|
<div className="font-medium text-ink">
|
||||||
|
{car.role} · <span className="text-ink-mute font-normal">{car.org}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-ink-soft">{car.inst}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* International Standardization */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
|
||||||
|
International Standardization
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2.5 text-ink/90 font-sans">
|
||||||
|
{STANDARDIZATION.map((std, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2.5 leading-relaxed">
|
||||||
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
|
||||||
|
<span>{std}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Membership on Academic Society */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h3 className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark border-b border-line pb-1.5">
|
||||||
|
Membership on Academic Society
|
||||||
|
</h3>
|
||||||
|
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-ink/90 font-sans">
|
||||||
|
{MEMBERSHIPS.map((mem, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2">
|
||||||
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
|
||||||
|
<span>{mem}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,74 +1,84 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { activeMembers, alumni } from "../../content/members";
|
import type { Metadata } from "next";
|
||||||
|
import { getMembers, getAlumni } from "../../lib/api";
|
||||||
|
import { ProfessorDetailsDialog } from "./_components/ProfessorDetailsDialog";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Members",
|
||||||
|
description: "AI Agent Networking Lab (ANL) professor, active researchers, and alumni directory",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const DEGREE_LABEL: Record<string, string> = {
|
||||||
|
Professor: "Professor",
|
||||||
|
PhD: "Ph. D.",
|
||||||
|
PhDCandidate: "Ph. D. Candidate",
|
||||||
|
PhDStudent: "Ph. D. Student",
|
||||||
|
MSCandidate: "M. S. Candidate",
|
||||||
|
MSStudent: "M. S. Student",
|
||||||
|
MS: "M. S.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function MembersPage() {
|
||||||
|
const activeMembers = (await getMembers()) ?? [];
|
||||||
|
const alumni = (await getAlumni()) ?? [];
|
||||||
|
|
||||||
export default function MembersPage() {
|
|
||||||
const professor = activeMembers.find((m) => m.degree === "Professor");
|
|
||||||
const phdStudents = activeMembers.filter(
|
const phdStudents = activeMembers.filter(
|
||||||
(m) => m.degree === "PhD" || m.degree === "PhDCandidate" || m.degree === "PhDStudent"
|
(m) =>
|
||||||
|
m.degree === "PhD" ||
|
||||||
|
m.degree === "PhDCandidate" ||
|
||||||
|
m.degree === "PhDStudent"
|
||||||
);
|
);
|
||||||
const msStudents = activeMembers.filter(
|
const msStudents = activeMembers.filter(
|
||||||
(m) => m.degree === "MSCandidate" || m.degree === "MSStudent"
|
(m) => m.degree === "MSCandidate" || m.degree === "MSStudent"
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Header */}
|
{/* Page Header */}
|
||||||
<section className="space-y-3">
|
<section>
|
||||||
<div className="flex items-center gap-3">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
<span className="h-px w-8 bg-cobalt-dark"></span>
|
LAB MEMBERS
|
||||||
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
|
|
||||||
People & Researchers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
|
|
||||||
연구실 구성원
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-ink-mute text-base max-w-2xl">
|
|
||||||
지도교수와 재학 연구원 16명, 그리고 IT 산업체 및 학계에서 활약하고 있는 60명의 졸업생 인벤토리입니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Professor 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="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
|
|
||||||
Faculty
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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="bg-paper p-8 rounded-lg border border-line grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
||||||
<div className="lg:col-span-8 space-y-4">
|
<div className="lg:col-span-8 space-y-4">
|
||||||
<div className="flex items-baseline gap-3">
|
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
<h2 className="font-serif text-3xl font-normal text-ink">
|
||||||
{professor?.ko || "고석주"} 교수
|
Prof. Seok-Joo Koh
|
||||||
</h2>
|
</h2>
|
||||||
<span className="font-mono text-sm text-ink-mute">
|
<div className="text-sm text-ink-mute leading-relaxed space-y-2">
|
||||||
{professor?.en || "Seok-Joo Koh"}, Professor
|
<p>
|
||||||
</span>
|
Professor at the School of Computer Science and Engineering, Kyungpook National University, and Dean of the College of IT Engineering. He directs the AI Agent Networking Laboratory (ANL), leading research on QUIC/gRPC transport, A2A/ACP protocols, MCP & SKILL architectures, and multi-agent orchestration, and serves as Project Editor in ISO/IEC JTC1, ITU-T, and IEC TC100.
|
||||||
</div>
|
|
||||||
<p className="text-sm text-ink-mute leading-relaxed">
|
|
||||||
경북대학교 컴퓨터학부 교수 · AI 에이전트 네트워크 연구실 (ANL) 지도교수.
|
|
||||||
QUIC/gRPC 통신, A2A/ACP 프로토콜, MCP & SKILL 아키텍처 및 에이전트 오케스트레이션 연구를 총괄하고 있습니다.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div>
|
||||||
|
<ProfessorDetailsDialog />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="pt-4 border-t border-line/60 flex flex-wrap gap-4 text-xs font-mono">
|
<div className="pt-4 border-t border-line/60 flex flex-wrap gap-4 text-xs font-mono">
|
||||||
<a
|
<a
|
||||||
href="mailto:sjkoh@knu.ac.kr"
|
href="mailto:sjkoh@knu.ac.kr"
|
||||||
className="text-cobalt-dark hover:underline"
|
className="text-cobalt-dark hover:underline font-bold"
|
||||||
>
|
>
|
||||||
sjkoh@knu.ac.kr
|
sjkoh@knu.ac.kr
|
||||||
</a>
|
</a>
|
||||||
<span className="text-ink-mute">•</span>
|
<span className="text-ink-mute">|</span>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/sjkoh12"
|
href="https://github.com/sjkoh12"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-cobalt-dark hover:underline"
|
className="text-ink hover:text-cobalt-dark hover:underline"
|
||||||
>
|
>
|
||||||
GitHub @sjkoh12 ↗
|
GitHub Profile ↗
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-4 bg-ivory p-6 rounded border border-line text-xs space-y-2 font-mono">
|
<div className="lg:col-span-4 bg-ivory p-6 rounded border border-line text-xs space-y-2 font-mono">
|
||||||
<div className="text-ink font-bold border-b border-line pb-1">
|
<div className="text-ink font-bold border-b border-line pb-1">
|
||||||
Affiliation & Office
|
Affiliation & Office
|
||||||
</div>
|
</div>
|
||||||
<p className="text-ink-mute">Kyungpook National University</p>
|
<p className="text-ink-mute">Kyungpook National University</p>
|
||||||
<p className="text-ink-mute">IT Building #5 Room 527</p>
|
<p className="text-ink-mute">IT Building #5 Room 527</p>
|
||||||
@@ -78,14 +88,11 @@ export default function MembersPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Active Researchers 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 className="flex justify-between items-end">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||||
Active Researchers ({activeMembers.length} Members)
|
Active Researchers
|
||||||
</div>
|
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
|
|
||||||
재학 연구원
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +113,7 @@ export default function MembersPage() {
|
|||||||
{mem.ko}
|
{mem.ko}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-xs text-cobalt-dark">
|
<span className="font-mono text-xs text-cobalt-dark">
|
||||||
{mem.degree}
|
{DEGREE_LABEL[mem.degree] || mem.degree}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
|
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
|
||||||
@@ -136,7 +143,7 @@ export default function MembersPage() {
|
|||||||
{mem.ko}
|
{mem.ko}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-xs text-cobalt-dark">
|
<span className="font-mono text-xs text-cobalt-dark">
|
||||||
{mem.degree}
|
{DEGREE_LABEL[mem.degree] || mem.degree}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
|
<div className="font-mono text-xs text-ink-mute">{mem.en}</div>
|
||||||
@@ -152,14 +159,11 @@ export default function MembersPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Alumni Section (Native <details>/<summary> progressive disclosure) */}
|
{/* 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 className="flex justify-between items-end">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-widest font-bold">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||||
Alumni Inventory ({alumni.length} Graduates)
|
Graduates
|
||||||
</div>
|
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
|
|
||||||
졸업생 목록
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,15 +172,15 @@ export default function MembersPage() {
|
|||||||
<summary className="flex justify-between items-center cursor-pointer list-none select-none">
|
<summary className="flex justify-between items-center cursor-pointer list-none select-none">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="font-serif text-xl font-normal text-ink">
|
<span className="font-serif text-xl font-normal text-ink">
|
||||||
전체 졸업생 {alumni.length}명 열람하기
|
Browse all graduates
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
2007년부터 2026년까지 수여된 박사(Ph.D.) 및 석사(M.S.) 졸업생 이력
|
Ph. D. and M. S. graduates conferred from 2007 to 2026
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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="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="group-open:hidden">Open ▾</span>
|
||||||
<span className="hidden group-open:inline">접기 ▴</span>
|
<span className="hidden group-open:inline">Close ▴</span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
@@ -191,7 +195,7 @@ export default function MembersPage() {
|
|||||||
{alum.ko} ({alum.en})
|
{alum.ko} ({alum.en})
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-xs text-ink-mute">
|
<span className="font-mono text-xs text-ink-mute">
|
||||||
{alum.degree}
|
{DEGREE_LABEL[alum.degree] || alum.degree}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute flex justify-between">
|
<div className="font-mono text-xs text-ink-mute flex justify-between">
|
||||||
|
|||||||
+103
-124
@@ -1,62 +1,28 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import HeroComposition from "./_components/HeroComposition";
|
import HeroComposition from "./_components/HeroComposition";
|
||||||
import { DirectionsBlock } from "./_components/DirectionsBlock";
|
|
||||||
import { ProjectPageView } from "./_components/ProjectPageView";
|
import { ProjectPageView } from "./_components/ProjectPageView";
|
||||||
import { publications, patents } from "../content/publications";
|
import { getStatsSummary, getResearchAreaNames, getResearchProjects } from "../lib/api";
|
||||||
|
|
||||||
const PILLARS = [
|
export const dynamic = "force-dynamic";
|
||||||
{
|
|
||||||
no: "01",
|
const INTERNATIONAL_SDOS = [
|
||||||
en: "Extensibility & Protocol",
|
"IEC TC100: Multimedia Systems and Equipment",
|
||||||
ko: "AI Agent 기능 확장 프로토콜",
|
"ISO/IEC JTC1/SC41: Internet-of-Things (IoT) Applications",
|
||||||
theme: "vermillion" as const,
|
"ITU-T SG20: IoT Services",
|
||||||
topics: [
|
"ITU-T SG13: Mobility Management",
|
||||||
"Model Context Protocol (MCP) 서버/클라이언트 아키텍처",
|
"ISO/IEC JTC1/SC6: Reliable Multicasting",
|
||||||
"SKILL 도구 캡슐화 및 재사용 패키징",
|
|
||||||
"CLI 에이전트 인터페이스 제어 및 웹 가동 제어",
|
|
||||||
],
|
|
||||||
desc: "에이전트가 외부 도구 및 시스템과 표준화된 방식으로 연동할 수 있도록 MCP와 SKILL 패키징 기술을 개발합니다.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: "02",
|
|
||||||
en: "Multi-Agent Systems",
|
|
||||||
ko: "멀티 에이전트 협업 및 오케스트레이션",
|
|
||||||
theme: "cobalt" as const,
|
|
||||||
topics: [
|
|
||||||
"HERDR 백그라운드 프로세스 컨테이너 격리 제어",
|
|
||||||
"crewAI 기반 역할 분담 및 태스크 위임 체계",
|
|
||||||
"LangGraph 순환 상태 그래프 기반 워크플로우 제어",
|
|
||||||
],
|
|
||||||
desc: "다중 에이전트 환경에서 독립적인 역할 분담과 선순환 오케스트레이션을 보장하는 자율 협업 플랫폼을 연구합니다.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: "03",
|
|
||||||
en: "Interoperability Standards",
|
|
||||||
ko: "에이전트 상호운용성 개방형 표준",
|
|
||||||
theme: "vermillion" as const,
|
|
||||||
topics: [
|
|
||||||
"Agent-to-Agent (A2A) 프로토콜 규격 정의 및 검증",
|
|
||||||
"Agent Communication Protocol (ACP) 메시지 교환 규격",
|
|
||||||
"Agent Card (/.well-known/agent-card.json) 메타데이터 검증",
|
|
||||||
],
|
|
||||||
desc: "서로 다른 프레임워크 기반의 에이전트 간의 상호운용성을 보장하는 국제 표준 개방형 통신 프로토콜을 구현합니다.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: "04",
|
|
||||||
en: "High-Performance Backbone",
|
|
||||||
ko: "AIoT & 고성능 통신 백본",
|
|
||||||
theme: "cobalt" as const,
|
|
||||||
topics: [
|
|
||||||
"gRPC / QUIC 양방향 스트리밍 에이전트 백본 통신",
|
|
||||||
"Protocol Buffers (Protobuf) 직렬화 통신 최적화",
|
|
||||||
"MQTT / CoAP 엣지 메시 통신망 통합",
|
|
||||||
],
|
|
||||||
desc: "초저지연 양방향 스트리밍 통신 인프라를 바탕으로 고성능 에이전트 백본 네트워크를 설계하고 실증합니다.",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default async function HomePage() {
|
||||||
const pubCount = publications.length + patents.length;
|
const stats = (await getStatsSummary()) ?? {
|
||||||
|
intlPubsCount: 0,
|
||||||
|
patentCount: 0,
|
||||||
|
stdDocsCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const researchAreas = (await getResearchAreaNames()) ?? [];
|
||||||
|
const researchProjects = (await getResearchProjects()) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content space-y-16 md:space-y-24 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content space-y-16 md:space-y-24 pt-10 md:pt-16 pb-16 md:pb-24">
|
||||||
@@ -75,120 +41,95 @@ export default function HomePage() {
|
|||||||
<span className="italic font-serif text-cobalt-dark">Orchestration</span>
|
<span className="italic font-serif text-cobalt-dark">Orchestration</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-ink-mute text-base sm:text-lg leading-relaxed max-w-2xl">
|
<p className="text-ink-mute text-base sm:text-lg leading-relaxed max-w-2xl">
|
||||||
AI 에이전트 네트워크 연구실(ANL)은 멀티 AI 에이전트 오케스트레이션을 위해 서로 소통하고 협력하는 에이전트 네트워킹 기술과 표준 프레임워크 기술을 연구합니다.
|
The AI Agent Networking Laboratory (ANL) studies agent networking technologies and standard frameworks that enable AI agents to communicate and collaborate with one another for multi-agent orchestration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Metrics Row (Derived from data array, AC-13) */}
|
{/* Metrics Row (Derived from real content data) */}
|
||||||
<div className="pt-6 border-t border-line grid grid-cols-3 gap-4">
|
<div className="pt-6 border-t border-line grid grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||||
{PILLARS.length}
|
{stats.intlPubsCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1">
|
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||||
Research Areas
|
International Journals & Conference
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||||
{Math.floor((publications.length + patents.length) / 10) * 10}+
|
{stats.stdDocsCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1">
|
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||||
Publications
|
Standardization Contributions
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-serif text-3xl text-ink font-normal">
|
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||||
2026
|
{stats.patentCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1">
|
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||||
Issue Static
|
Patent<br />
|
||||||
|
<span className="text-[0.68rem] tracking-normal font-sans normal-case text-ink-mute">
|
||||||
|
(IoT Architecture & Protocols)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-5 border border-line bg-paper p-4 rounded-lg">
|
<div className="w-full max-w-[500px] mx-auto my-[30px] lg:my-0 lg:max-w-none lg:mx-0 lg:col-span-5 border border-line bg-paper p-4 rounded-lg">
|
||||||
<HeroComposition />
|
<HeroComposition />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Research Project PageView Component */}
|
{/* Research Project PageView Component */}
|
||||||
<ProjectPageView />
|
<ProjectPageView projects={researchProjects} />
|
||||||
|
|
||||||
{/* Core Research Areas Grid */}
|
{/* 14 Research Areas Grid */}
|
||||||
<section className="space-y-8 pt-20 md:pt-28 border-t border-line">
|
<section className="space-y-8 pt-16 md:pt-20 border-t border-line">
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 border-b border-line pb-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-xs text-cobalt-dark uppercase tracking-widest font-bold">
|
<div className="flex items-center gap-3 mb-1">
|
||||||
Research Areas
|
<span className="h-px w-8 bg-cobalt-dark"></span>
|
||||||
|
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
|
||||||
|
Core Domains
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink mt-1">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||||
핵심 연구 분야
|
Research Areas
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-ink-mute">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
Magazine Layout 01–04
|
{researchAreas.length} fundamental networking protocols and AI system domains
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{PILLARS.map((pillar) => {
|
{researchAreas.map((area, idx) => {
|
||||||
const isVermillion = pillar.theme === "vermillion";
|
const no = String(idx + 1).padStart(2, "0");
|
||||||
|
const isOdd = (idx + 1) % 2 !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pillar.no}
|
key={no}
|
||||||
className="bg-paper p-6 rounded-lg border border-line flex flex-col justify-between hover:border-ink transition-colors duration-200"
|
className="bg-paper p-6 rounded-lg border border-line flex flex-col justify-between hover:border-ink transition-colors duration-200 h-full"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span
|
<span
|
||||||
className={`font-mono text-sm font-bold ${
|
className={`font-mono text-xs font-bold ${
|
||||||
isVermillion ? "text-vermillion" : "text-cobalt-dark"
|
isOdd ? "text-vermillion-dark" : "text-cobalt-dark"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
[{pillar.no}]
|
{no}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-xs text-ink-mute uppercase">
|
<span className="font-mono text-[0.68rem] text-ink-mute uppercase tracking-wider">
|
||||||
{pillar.en}
|
Topic #{no}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 className="font-serif text-base font-normal text-ink leading-snug">
|
||||||
<h3 className="font-serif text-xl font-normal text-ink">
|
{area}
|
||||||
{pillar.ko}
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-xs text-ink-mute leading-relaxed">
|
|
||||||
{pillar.desc}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul className="space-y-2 pt-2">
|
|
||||||
{pillar.topics.map((topic, tIdx) => (
|
|
||||||
<li
|
|
||||||
key={tIdx}
|
|
||||||
className="text-xs text-ink flex items-start gap-2"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block w-1.5 h-1.5 rounded-full mt-1 ${
|
|
||||||
isVermillion ? "bg-vermillion" : "bg-cobalt"
|
|
||||||
}`}
|
|
||||||
></span>
|
|
||||||
<span>{topic}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-line/60 flex justify-between items-center text-xs font-mono text-ink-mute">
|
|
||||||
<span>ANL Core Spec</span>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
isVermillion ? "text-vermillion" : "text-cobalt-dark"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Pillar #{pillar.no}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -196,8 +137,46 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Directions Block */}
|
{/* International Standardizations Section */}
|
||||||
<DirectionsBlock />
|
<section className="space-y-6 pt-16 md:pt-20 border-t border-line">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 border-b border-line pb-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-1">
|
||||||
|
<span className="h-px w-8 bg-cobalt-dark"></span>
|
||||||
|
<span className="font-mono text-xs tracking-widest uppercase text-cobalt-dark font-bold">
|
||||||
|
Global Engagement
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||||
|
International Standardizations
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-paper p-6 sm:p-8 rounded-lg border border-line space-y-6">
|
||||||
|
<p className="text-ink/80 text-sm sm:text-base leading-relaxed max-w-3xl">
|
||||||
|
Along with the research activities, we are also in pursuit of the International Standardization, from the market deployment perspective, in the following SDOs (Standard-Defining Organizations):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2">
|
||||||
|
{INTERNATIONAL_SDOS.map((sdo, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2.5 text-sm text-ink font-sans">
|
||||||
|
<span className="inline-block w-1.5 h-1.5 rounded-full bg-cobalt-dark mt-2 shrink-0"></span>
|
||||||
|
<span>{sdo}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-line/60">
|
||||||
|
<Link
|
||||||
|
href="/standardization"
|
||||||
|
className="inline-flex items-center gap-2 font-mono text-xs font-semibold uppercase tracking-wider text-cobalt-dark hover:text-ink transition-colors draw-underline"
|
||||||
|
>
|
||||||
|
<span>Please refer to here for details →</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { CategoryNav } from "../_components/CategoryNav";
|
import { CategoryNav } from "../_components/CategoryNav";
|
||||||
import { publications, patents } from "../../../content/publications";
|
import { getPublications, getPatents } from "../../../lib/api";
|
||||||
import { PUB_CATEGORIES } from "../../../content/categories";
|
import { PUB_CATEGORIES } from "../../../content/categories";
|
||||||
import { PubCategory } from "../../../content/types";
|
import { PubCategory } from "../../../content/types";
|
||||||
|
|
||||||
@@ -11,14 +12,20 @@ interface PageProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSG static pre-rendering (● SSG) for Next.js 14
|
export function generateMetadata({ params }: PageProps): Metadata {
|
||||||
export function generateStaticParams() {
|
const currentCat = PUB_CATEGORIES.find((c) => c.slug === params.category);
|
||||||
return PUB_CATEGORIES.map((cat) => ({
|
if (!currentCat) {
|
||||||
category: cat.slug,
|
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 { category } = params;
|
||||||
const currentCat = PUB_CATEGORIES.find((c) => c.slug === category);
|
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 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[] = [];
|
let records: any[] = [];
|
||||||
if (slug === "patent") {
|
if (slug === "patent") {
|
||||||
records = patents.map((p) => ({
|
records = patents.map((p) => ({
|
||||||
@@ -55,44 +71,39 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content space-y-8 md:space-y-12 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Header */}
|
{/* Page Header */}
|
||||||
<section className="space-y-3">
|
<section className="space-y-1">
|
||||||
<div className="flex items-center gap-3">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
<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">
|
|
||||||
{currentCat.label}
|
{currentCat.label}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-ink-mute text-base max-w-2xl">
|
{currentCat.subtitle && (
|
||||||
{currentCat.desc} — 총 {records.length}건의 실데이터 카탈로그입니다.
|
<p className="font-mono text-sm text-ink-mute">
|
||||||
|
{currentCat.subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Category Nav */}
|
{/* Category Nav Cards */}
|
||||||
<CategoryNav currentCategory={slug} />
|
<section className="pt-6 md:pt-8 border-t border-line">
|
||||||
|
<CategoryNav currentCategory={slug} counts={counts} />
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Record List 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">
|
<div className="flex justify-between items-center border-b border-line pb-3">
|
||||||
<span className="font-mono text-xs text-ink-mute uppercase">
|
<span className="font-mono text-xs text-ink-mute uppercase">
|
||||||
Showing {records.length} Pre-rendered Records
|
{records.length} Records
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs text-vermillion-dark font-bold">
|
|
||||||
SSG ● Static HTML
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{records.length === 0 ? (
|
{records.length === 0 ? (
|
||||||
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
|
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
|
||||||
<p className="font-serif text-lg text-ink">
|
<p className="font-serif text-lg text-ink">
|
||||||
현재 등록된 실적이 0건입니다.
|
No records found in this category.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
(D-01 카테고리 정류 확정 후 후속 배치가 수행됩니다)
|
Records will be updated periodically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -116,13 +127,13 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
|||||||
|
|
||||||
{rec.isPatent ? (
|
{rec.isPatent ? (
|
||||||
<div className="space-y-1 text-xs text-ink-mute font-mono">
|
<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>
|
<p>
|
||||||
출원번호: {rec.appNo} ({rec.country}, {rec.appDate})
|
App No: {rec.appNo} ({rec.country}, {rec.appDate})
|
||||||
</p>
|
</p>
|
||||||
{rec.regNo && (
|
{rec.regNo && (
|
||||||
<p className="text-cobalt-dark font-bold">
|
<p className="text-cobalt-dark font-bold">
|
||||||
등록번호: {rec.regNo} ({rec.regDate})
|
Reg No: {rec.regNo} ({rec.regDate})
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +144,7 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
|||||||
<span>{rec.volume}</span>
|
<span>{rec.volume}</span>
|
||||||
{rec.kci && (
|
{rec.kci && (
|
||||||
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
|
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
|
||||||
KCI 등재
|
KCI Indexed
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,28 +3,108 @@ import Link from "next/link";
|
|||||||
import { PUB_CATEGORIES } from "../../../content/categories";
|
import { PUB_CATEGORIES } from "../../../content/categories";
|
||||||
import { PubCategory } from "../../../content/types";
|
import { PubCategory } from "../../../content/types";
|
||||||
|
|
||||||
interface CategoryNavProps {
|
export interface CategoryCounts {
|
||||||
currentCategory?: PubCategory;
|
"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 (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
||||||
{PUB_CATEGORIES.map((cat) => {
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||||
const isActive = currentCategory === cat.slug;
|
Categories
|
||||||
return (
|
</h2>
|
||||||
|
{currentCategory && (
|
||||||
<Link
|
<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 (
|
||||||
|
<div
|
||||||
key={cat.slug}
|
key={cat.slug}
|
||||||
href={`/publications/${cat.slug}`}
|
className={`flex flex-col justify-between bg-paper p-5 sm:p-6 rounded-lg transition-all ${
|
||||||
className={`px-4 py-2 rounded text-xs font-mono tracking-wider transition-colors ${
|
|
||||||
isActive
|
isActive
|
||||||
? "bg-cobalt-dark text-white dark:text-ivory font-bold"
|
? "border-2 border-cobalt-dark shadow-md"
|
||||||
: "bg-paper text-ink border border-line hover:border-cobalt-dark"
|
: "border border-line shadow-sm hover:border-cobalt-dark/40"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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.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>
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,100 +1,52 @@
|
|||||||
import Link from "next/link";
|
import type { Metadata } from "next";
|
||||||
import { CategoryNav } from "./_components/CategoryNav";
|
import { CategoryNav } from "./_components/CategoryNav";
|
||||||
import { PUB_CATEGORIES } from "../../content/categories";
|
import { getPublications, getPatents } from "../../lib/api";
|
||||||
import { publications, patents } from "../../content/publications";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Publications Overview | ANL",
|
title: "Publications",
|
||||||
description: "AI 에이전트 네트워크 연구실(ANL) 연구 실적 개요 및 카테고리별 집계",
|
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PublicationsIndexPage() {
|
export const dynamic = "force-dynamic";
|
||||||
const totalPubCount = publications.length + patents.length;
|
|
||||||
|
|
||||||
// Sort publication records by publishedAt descending to select top 10 highlights
|
export default async function PublicationsIndexPage() {
|
||||||
const top10Highlights = [...publications]
|
const publications = (await getPublications()) ?? [];
|
||||||
.sort((a, b) => (b.publishedAt || "").localeCompare(a.publishedAt || ""))
|
const patents = (await getPatents()) ?? [];
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
const getCount = (slug: string) => {
|
const counts = {
|
||||||
if (slug === "patent") return patents.length;
|
"intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length,
|
||||||
return publications.filter((p) => p.category === slug).length;
|
"domestic-journal-conf": publications.filter((p) => p.category === "domestic-journal-conf").length,
|
||||||
|
patent: patents.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Selected representative publications (curated via database isHighlight flag)
|
||||||
|
const highlights = publications.filter((p) => p.isHighlight);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container-content space-y-8 md:space-y-12 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Header section */}
|
{/* Page Header */}
|
||||||
<section className="space-y-4">
|
<section>
|
||||||
<div className="flex items-center gap-3">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
<span className="h-px w-8 bg-cobalt-dark" />
|
PUBLICATIONS
|
||||||
<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>
|
</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>
|
</section>
|
||||||
|
|
||||||
{/* Category Navigation */}
|
|
||||||
<CategoryNav />
|
|
||||||
|
|
||||||
{/* Category Summary Cards */}
|
{/* Category Summary Cards */}
|
||||||
<section className="grid gap-6 md:grid-cols-3">
|
<section className="pt-6 md:pt-8 border-t border-line">
|
||||||
{PUB_CATEGORIES.map((cat) => {
|
<CategoryNav counts={counts} />
|
||||||
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>
|
</section>
|
||||||
|
|
||||||
{/* Top 10 Highlights Section */}
|
{/* Highlights Section */}
|
||||||
<section className="space-y-6 pt-6 border-t border-line">
|
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
|
||||||
<div className="flex items-center justify-between">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||||
<h2 className="headline-ko text-2xl font-bold text-ink">
|
Highlights
|
||||||
최신 대표 실적 (Top 10 Highlights)
|
|
||||||
</h2>
|
</h2>
|
||||||
<span className="font-mono text-xs text-ink-mute">
|
|
||||||
RECENT HIGHLIGHTS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-line border border-line bg-ivory rounded-lg overflow-hidden">
|
<div className="divide-y divide-line border border-line bg-paper rounded-lg overflow-hidden shadow-sm">
|
||||||
{top10Highlights.map((pub, idx) => (
|
{highlights.map((pub, idx) => (
|
||||||
<article key={`${pub.category}-${idx}`} className="p-5 hover:bg-paper/50 transition-colors space-y-2">
|
<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">
|
<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"}
|
{pub.publishedAt?.substring(0, 7) || "Recent"}
|
||||||
</span>
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
@@ -102,11 +54,11 @@ export default function PublicationsIndexPage() {
|
|||||||
{pub.doi && (
|
{pub.doi && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span className="text-vermillion">DOI: {pub.doi}</span>
|
<span className="text-vermillion-dark">DOI: {pub.doi}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}
|
{pub.title}
|
||||||
</h3>
|
</h3>
|
||||||
{pub.authors && (
|
{pub.authors && (
|
||||||
@@ -115,14 +67,14 @@ export default function PublicationsIndexPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{pub.venue && (
|
{pub.venue && (
|
||||||
<p className="text-xs italic text-ink-soft">
|
<p className="text-xs italic text-ink-mute">
|
||||||
{pub.venue}
|
{pub.venue}{pub.volume ? `, ${pub.volume}` : ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { standardsBodies } from "../../content/standards";
|
import type { Metadata } from "next";
|
||||||
|
import { getStandardsBodies } from "../../lib/api";
|
||||||
|
|
||||||
export default function StandardizationPage() {
|
export const metadata: Metadata = {
|
||||||
const totalDocs = standardsBodies.reduce(
|
title: "Standardization",
|
||||||
(acc, body) =>
|
description: "AI Agent Networking Lab (ANL) international standardization research and contributions",
|
||||||
acc + body.projects.reduce((pAcc, proj) => pAcc + proj.documents.length, 0),
|
};
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function StandardizationPage() {
|
||||||
|
const standardsBodies = (await getStandardsBodies()) ?? [];
|
||||||
return (
|
return (
|
||||||
<div className="container-content space-y-12 md:space-y-16 pt-10 md:pt-16 pb-16 md:pb-24">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Header */}
|
{/* Page Header */}
|
||||||
<section className="space-y-3">
|
<section>
|
||||||
<div className="flex items-center gap-3">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
<span className="h-px w-8 bg-cobalt-dark"></span>
|
International Standardization
|
||||||
<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">
|
|
||||||
표준화 연구 및 기여
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-ink-mute text-base max-w-2xl">
|
|
||||||
IEC TC100, ISO/IEC JTC1, ITU-T, TTA 등 국제/국내 표준화 기구를 통해 제정 및 진행 중인 {standardsBodies.length}개 기구 {totalDocs}건의 표준 규격 문서 계층 인벤토리입니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Standards Bodies 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) => (
|
{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 className="flex flex-wrap justify-between items-start gap-4 border-b border-line pb-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -103,7 +97,7 @@ export default function StandardizationPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-ink-mute font-mono italic">
|
<div className="text-xs text-ink-mute font-mono italic">
|
||||||
진행 중인 주 프로젝트 규격서
|
Project specification in progress
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import Marquee from "@/components/Marquee";
|
|||||||
|
|
||||||
// CUSTOMIZATION HOOK: replace address / email placeholders & colophon below.
|
// CUSTOMIZATION HOOK: replace address / email placeholders & colophon below.
|
||||||
const links = [
|
const links = [
|
||||||
{ href: "/", ko: "홈", en: "Home" },
|
{ href: "/", en: "Home" },
|
||||||
{ href: "/members", ko: "구성원", en: "Members" },
|
{ href: "/members", en: "Members" },
|
||||||
{ href: "/publications", ko: "논문", en: "Publications" },
|
{ href: "/publications", en: "Publications" },
|
||||||
{ href: "/lectures", ko: "강의", en: "Lectures" },
|
{ href: "/lectures", en: "Lectures" },
|
||||||
{ href: "/standardization", ko: "표준화", en: "Standardization" },
|
{ href: "/standardization", en: "Standardization" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
@@ -25,53 +25,136 @@ export default function Footer() {
|
|||||||
className="border-b border-line py-4 font-display text-lg tracking-wide text-ink/90"
|
className="border-b border-line py-4 font-display text-lg tracking-wide text-ink/90"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container-content grid gap-12 py-16 lg:grid-cols-12">
|
<div className="container-content grid gap-10 py-16 lg:grid-cols-12">
|
||||||
{/* Masthead title */}
|
{/* Brand Block */}
|
||||||
<div className="lg:col-span-5">
|
<div className="lg:col-span-5 space-y-4">
|
||||||
<p className="kicker text-ink-mute">Colophon</p>
|
<Link
|
||||||
<h3 className="headline-ko mt-3 text-2xl leading-snug text-ink">
|
href="/"
|
||||||
AI 에이전트 네트워크 연구실
|
className="group inline-flex items-center gap-3.5 shrink-0"
|
||||||
</h3>
|
>
|
||||||
<p className="font-display mt-1 text-xl italic text-ink-soft">
|
<div className="relative flex items-center justify-center w-10 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">
|
||||||
AI Agent Networking Lab (ANL)
|
<svg viewBox="0 0 32 32" className="w-full h-full" aria-hidden="true">
|
||||||
</p>
|
<defs>
|
||||||
<p className="mt-5 max-w-sm text-sm leading-relaxed text-ink-mute">
|
<linearGradient id="footerLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
경북대학교 컴퓨터학부<br />
|
<stop offset="0%" stopColor="#4E77FF" />
|
||||||
Kyungpook National University,<br />
|
<stop offset="100%" stopColor="#2B4CDE" />
|
||||||
School of Computer Science and Engineering
|
</linearGradient>
|
||||||
|
<radialGradient id="footerNodeGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor="#FFD666" stopOpacity="1" />
|
||||||
|
<stop offset="100%" stopColor="#FF9C1A" stopOpacity="0.8" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="32" height="32" rx="7" fill="url(#footerLogoGrad)" />
|
||||||
|
{/* Network Graph Edges */}
|
||||||
|
<path
|
||||||
|
d="M16 9 L9 22 M16 9 L23 22 M9 22 L23 22 M16 9 L16 17 M9 22 L16 17 M23 22 L16 17"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
strokeOpacity="0.85"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
{/* Network Agent Outer Nodes */}
|
||||||
|
<circle cx="16" cy="9" r="2.5" fill="#FFFFFF" />
|
||||||
|
<circle cx="9" cy="22" r="2.5" fill="#FFFFFF" />
|
||||||
|
<circle cx="23" cy="22" r="2.5" fill="#FFFFFF" />
|
||||||
|
{/* Central Coordinating Hub Node */}
|
||||||
|
<circle cx="16" cy="17" r="2.2" fill="url(#footerNodeGlow)" className="animate-pulse" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col leading-none">
|
||||||
|
<span className="headline-ko text-xl font-bold tracking-tight text-ink group-hover:text-cobalt-dark transition-colors">
|
||||||
|
ANL
|
||||||
|
</span>
|
||||||
|
<span className="kicker mt-1 transition-colors group-hover:text-cobalt-dark">
|
||||||
|
AI Agent Networking LAB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<p className="text-xs sm:text-sm text-ink-mute leading-relaxed max-w-sm">
|
||||||
|
Research on agent networking technologies and standard framework technologies for multi-AI agent orchestration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="pt-2 flex flex-wrap gap-2 text-xs font-mono">
|
||||||
|
<a
|
||||||
|
href="https://www.knu.ac.kr/"
|
||||||
|
target="_blank"
|
||||||
|
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>KNU</span>
|
||||||
|
<span className="text-[0.65rem] text-ink-mute">↗</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://cse.knu.ac.kr/"
|
||||||
|
target="_blank"
|
||||||
|
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>CSE</span>
|
||||||
|
<span className="text-[0.65rem] text-ink-mute">↗</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://it.knu.ac.kr/"
|
||||||
|
target="_blank"
|
||||||
|
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>CITE</span>
|
||||||
|
<span className="text-[0.65rem] text-ink-mute">↗</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact */}
|
{/* Contact */}
|
||||||
<div className="lg:col-span-4">
|
<div className="lg:col-span-4 space-y-3">
|
||||||
<p className="kicker text-ink-mute">Contact</p>
|
<div className="flex items-center gap-2">
|
||||||
<address className="mt-3 not-italic text-sm leading-relaxed text-ink-soft">
|
<span className="h-px w-6 bg-cobalt-dark"></span>
|
||||||
(41566) 대구광역시 북구 대학로 80<br />
|
<p className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark">
|
||||||
경북대학교 IT대학 5호관(415동) 527호<br />
|
Contact
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<address className="not-italic text-sm leading-relaxed text-ink-soft space-y-2">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-ink">Prof. Seok-Joo Koh</div>
|
||||||
<a
|
<a
|
||||||
href="mailto:sjkoh@knu.ac.kr"
|
href="mailto:sjkoh@knu.ac.kr"
|
||||||
className="draw-underline mt-2 inline-block text-ink hover:text-vermillion-dark font-medium"
|
className="inline-block text-xs font-mono text-cobalt-dark hover:underline mt-0.5"
|
||||||
>
|
>
|
||||||
sjkoh@knu.ac.kr
|
sjkoh@knu.ac.kr
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-ink-mute leading-normal pt-1.5 border-t border-line/50">
|
||||||
|
Room 527, Bldg. 415 (IT-5), College of IT Engineering<br />
|
||||||
|
Kyungpook National University<br />
|
||||||
|
80 Daehak-ro, Buk-gu, Daegu 41566, Republic of Korea
|
||||||
|
</div>
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Index */}
|
{/* Navigation Index */}
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3 space-y-3">
|
||||||
<p className="kicker text-ink-mute">Index</p>
|
<div className="flex items-center gap-2">
|
||||||
<ul className="mt-3 space-y-2">
|
<span className="h-px w-6 bg-cobalt-dark"></span>
|
||||||
|
<p className="font-mono text-xs font-bold uppercase tracking-wider text-cobalt-dark">
|
||||||
|
Index
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
{links.map((l, i) => (
|
{links.map((l, i) => (
|
||||||
<li key={l.href} className="flex items-baseline gap-3">
|
<li key={l.href}>
|
||||||
<span className="font-display text-xs text-ink-mute">
|
|
||||||
{String(i + 1).padStart(2, "0")}
|
|
||||||
</span>
|
|
||||||
<Link
|
<Link
|
||||||
href={l.href}
|
href={l.href}
|
||||||
className="text-sm text-ink-soft transition hover:text-vermillion-dark"
|
className="group flex items-center justify-between text-xs font-mono text-ink-soft hover:text-cobalt-dark transition-colors py-1 border-b border-line/40 hover:border-cobalt/40"
|
||||||
>
|
>
|
||||||
{l.ko}{" "}
|
<span className="font-sans text-sm text-ink-soft group-hover:text-cobalt-dark">
|
||||||
<span className="text-xs text-ink-mute">{l.en}</span>
|
{l.en}
|
||||||
|
</span>
|
||||||
|
<span className="text-[0.7rem] text-ink-mute group-hover:text-cobalt-dark">
|
||||||
|
{String(i + 1).padStart(2, "0")} →
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -82,9 +165,8 @@ export default function Footer() {
|
|||||||
<div className="border-t border-line">
|
<div className="border-t border-line">
|
||||||
<div className="container-content flex flex-col gap-2 py-5 sm:flex-row sm:items-center sm:justify-between">
|
<div className="container-content flex flex-col gap-2 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<p className="text-xs text-ink-mute">
|
<p className="text-xs text-ink-mute">
|
||||||
© {new Date().getFullYear()} AI 에이전트 네트워크 연구실 (ANL) · 경북대학교 컴퓨터학부. All rights reserved.
|
© {new Date().getFullYear()} AI Agent Networking Lab (ANL) · School of Computer Science and Engineering, KNU. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="kicker text-ink-mute">Set in Fraunces · Noto Serif KR · Pretendard</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -2,22 +2,103 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import ThemeToggle from "./ThemeToggle";
|
import ThemeToggle from "./ThemeToggle";
|
||||||
|
|
||||||
|
// CUSTOMIZATION HOOK — MOTION: 스마트 스티키 튜닝
|
||||||
|
const SCROLL_DELTA = 6; // px — 방향 전환 인식 최소 이동량(지터 방지)
|
||||||
|
const HIDE_AFTER = 140; // px — 이 지점 이전에는 숨기지 않음(실측 데스크톱 헤더 높이 122.8px + 여유)
|
||||||
|
const NAV_SETTLE_MS = 300; // ms — 라우트 전환 직후 방향 판정 유예 구간
|
||||||
|
|
||||||
// CUSTOMIZATION HOOK: edit nav labels / routes here.
|
// CUSTOMIZATION HOOK: edit nav labels / routes here.
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", ko: "홈", en: "Home", no: "01" },
|
{ href: "/", en: "Home", no: "01" },
|
||||||
{ href: "/members", ko: "구성원", en: "Members", no: "02" },
|
{ href: "/members", en: "Members", no: "02" },
|
||||||
{ href: "/publications", ko: "논문", en: "Publications", no: "03" },
|
{ href: "/publications", en: "Publications", no: "03" },
|
||||||
{ href: "/lectures", ko: "강의", en: "Lectures", no: "04" },
|
{ href: "/lectures", en: "Lectures", no: "04" },
|
||||||
{ href: "/standardization", ko: "표준화", en: "Standardization", no: "05" },
|
{ href: "/standardization", en: "Standardization", no: "05" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [hidden, setHidden] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const lastY = useRef(0);
|
||||||
|
const ticking = useRef(false);
|
||||||
|
const navSettling = useRef(false);
|
||||||
|
const openRef = useRef(open);
|
||||||
|
openRef.current = open;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// (A) 모션 줄임 사용자는 자동 숨김 자체를 비활성화 — DESIGN.md 접근성 원칙
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
lastY.current = window.scrollY;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
ticking.current = false;
|
||||||
|
const y = window.scrollY;
|
||||||
|
|
||||||
|
// (B) 최상단 + iOS 러버밴드(음수) 구간 → 항상 표시
|
||||||
|
if (y <= 0) {
|
||||||
|
setHidden(false);
|
||||||
|
lastY.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (F-2) 라우트 전환 정착 구간: 전환이 유발한 스크롤 점프는 방향 판정에서 제외.
|
||||||
|
// 기준점만 따라가고 hidden은 건드리지 않는다.
|
||||||
|
if (navSettling.current) {
|
||||||
|
lastY.current = y;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (C) 모바일 메뉴가 열려 있으면 숨기지 않되, lastY는 계속 추적
|
||||||
|
// (추적하지 않으면 메뉴를 닫는 순간 헤더가 튀며 사라짐)
|
||||||
|
if (openRef.current) {
|
||||||
|
setHidden(false);
|
||||||
|
lastY.current = y;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = y - lastY.current;
|
||||||
|
|
||||||
|
// (D) 미세 이동 무시. 이때 lastY를 갱신하지 않고 보존
|
||||||
|
if (Math.abs(delta) < SCROLL_DELTA) return;
|
||||||
|
|
||||||
|
if (delta > 0 && y > HIDE_AFTER) {
|
||||||
|
setHidden(true); // 아래로 → 숨김
|
||||||
|
} else if (delta < 0) {
|
||||||
|
setHidden(false); // 위로 → 복귀
|
||||||
|
}
|
||||||
|
|
||||||
|
lastY.current = y;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
if (ticking.current) return;
|
||||||
|
ticking.current = true;
|
||||||
|
requestAnimationFrame(update); // 프레임당 1회로 제한
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// (F) 라우트 전환 대응 — persistent layout이라 hidden이 페이지를 넘어 생존한다.
|
||||||
|
// 스크롤 이벤트 발화 여부와 무관하게 표시 상태를 보장한다.
|
||||||
|
useEffect(() => {
|
||||||
|
setHidden(false);
|
||||||
|
lastY.current = window.scrollY; // 현재 실제 위치로 동기화
|
||||||
|
setOpen(false); // 뒤로/앞으로 이동 시 모바일 메뉴 잔존 방지
|
||||||
|
navSettling.current = true;
|
||||||
|
const id = window.setTimeout(() => {
|
||||||
|
navSettling.current = false;
|
||||||
|
}, NAV_SETTLE_MS);
|
||||||
|
return () => window.clearTimeout(id);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
const isActive = (href: string) =>
|
const isActive = (href: string) =>
|
||||||
pathname
|
pathname
|
||||||
? href === "/"
|
? href === "/"
|
||||||
@@ -26,7 +107,13 @@ export default function Header() {
|
|||||||
: false;
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-ink bg-ivory/85 backdrop-blur">
|
<header
|
||||||
|
data-smart-sticky
|
||||||
|
onFocusCapture={() => setHidden(false)}
|
||||||
|
className={`flow-root sticky top-0 z-50 border-b border-ink bg-ivory/85 backdrop-blur transition-transform duration-300 ease-out ${
|
||||||
|
hidden ? "-translate-y-full" : "translate-y-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{/* Journal issue strip — magazine masthead feel. */}
|
{/* Journal issue strip — magazine masthead feel. */}
|
||||||
<div className="hidden border-b border-line desktop:block">
|
<div className="hidden border-b border-line desktop:block">
|
||||||
<div className="container-content flex items-center justify-between py-1.5">
|
<div className="container-content flex items-center justify-between py-1.5">
|
||||||
@@ -35,28 +122,55 @@ export default function Header() {
|
|||||||
<span className="kicker text-ink-mute">
|
<span className="kicker text-ink-mute">
|
||||||
Kyungpook National University · School of Computer Science & Engineering
|
Kyungpook National University · School of Computer Science & Engineering
|
||||||
</span>
|
</span>
|
||||||
<span className="kicker">대구 · Daegu</span>
|
<span className="kicker">Daegu / South Korea</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-content flex h-[4.25rem] items-center justify-between gap-4">
|
<div className="container-content flex h-[4.25rem] items-center justify-between gap-4 my-2 sm:my-3">
|
||||||
{/* Brand logo */}
|
{/* Brand logo */}
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="group flex flex-col leading-none shrink-0"
|
className="group flex items-center gap-3 min-w-0 desktop:shrink-0"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<span className="headline-ko text-lg text-ink sm:text-xl">
|
<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">
|
||||||
AI 에이전트 네트워크 연구실
|
<svg viewBox="0 0 32 32" className="w-full h-full" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="headerLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#4E77FF"/>
|
||||||
|
<stop offset="100%" stopColor="#2B4CDE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="nodeGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor="#FFD666" stopOpacity="1"/>
|
||||||
|
<stop offset="100%" stopColor="#FF9C1A" stopOpacity="0.8"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="32" height="32" rx="7" fill="url(#headerLogoGrad)"/>
|
||||||
|
{/* Network Graph Edges */}
|
||||||
|
<path d="M16 9 L9 22 M16 9 L23 22 M9 22 L23 22 M16 9 L16 17 M9 22 L16 17 M23 22 L16 17"
|
||||||
|
stroke="#FFFFFF" strokeWidth="1.2" strokeOpacity="0.85" strokeLinecap="round"/>
|
||||||
|
{/* Network Agent Outer Nodes */}
|
||||||
|
<circle cx="16" cy="9" r="2.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="9" cy="22" r="2.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="23" cy="22" r="2.5" fill="#FFFFFF"/>
|
||||||
|
{/* Central Coordinating Hub Node */}
|
||||||
|
<circle cx="16" cy="17" r="2.2" fill="url(#nodeGlow)" className="animate-pulse"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
<span className="kicker mt-1 group-hover:text-vermillion">
|
<span className="kicker mt-1 transition-colors group-hover:text-cobalt-dark truncate">
|
||||||
AI Agent Networking Lab (ANL)
|
AI Agent Networking LAB
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop nav — small caps, wide tracking (visible at >= 1240px) */}
|
{/* Desktop nav — small caps, wide tracking (visible at >= 1240px) */}
|
||||||
<div className="hidden items-center gap-4 desktop:flex">
|
<div className="hidden items-center gap-4 desktop:flex">
|
||||||
<nav className="flex items-center gap-2" aria-label="주 메뉴">
|
<nav className="flex items-center gap-2" aria-label="Main Navigation">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const active = isActive(item.href);
|
const active = isActive(item.href);
|
||||||
return (
|
return (
|
||||||
@@ -92,7 +206,7 @@ export default function Header() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center justify-center border border-ink p-2 text-ink transition hover:bg-ink hover:text-ivory"
|
className="inline-flex items-center justify-center border border-ink p-2 text-ink transition hover:bg-ink hover:text-ivory"
|
||||||
aria-label={open ? "메뉴 닫기" : "메뉴 열기"}
|
aria-label={open ? "Close menu" : "Open menu"}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
@@ -112,8 +226,8 @@ export default function Header() {
|
|||||||
{open && (
|
{open && (
|
||||||
<nav
|
<nav
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
className="border-t border-ink bg-ivory desktop:hidden"
|
className="mobile-menu-panel border-t border-ink bg-ivory desktop:hidden"
|
||||||
aria-label="모바일 메뉴"
|
aria-label="Mobile Navigation"
|
||||||
>
|
>
|
||||||
<ul className="container-content flex flex-col py-2">
|
<ul className="container-content flex flex-col py-2">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
@@ -138,14 +252,15 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
{item.no}
|
{item.no}
|
||||||
</span>
|
</span>
|
||||||
<span className="headline-ko text-lg">{item.ko}</span>
|
<span className="font-sans text-base">{item.en}</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[0.7rem] font-semibold uppercase tracking-[0.18em] ${
|
aria-hidden="true"
|
||||||
active ? "text-white dark:text-ivory" : "text-ink-mute"
|
className={`text-xs font-mono ${
|
||||||
|
active ? "text-white/80 dark:text-ivory/80" : "text-ink-mute"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.en}
|
→
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PubCategory } from "./types";
|
|||||||
export interface CategoryInfo {
|
export interface CategoryInfo {
|
||||||
slug: PubCategory;
|
slug: PubCategory;
|
||||||
label: string;
|
label: string;
|
||||||
|
subtitle?: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export const PUB_CATEGORIES: CategoryInfo[] = [
|
|||||||
{
|
{
|
||||||
slug: "patent",
|
slug: "patent",
|
||||||
label: "Patent",
|
label: "Patent",
|
||||||
|
subtitle: "(IoT Architecture & Protocols, etc ...)",
|
||||||
desc: "국내외 지식재산권 출원 및 등록 실적",
|
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 (삼성전자)"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// 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": "IoT Standards",
|
|
||||||
"abstract": "Internet of Things covers a huge range of industries and use cases that scale from a single constrained device up to massive cross-platform deployments of embedded technologies and cloud systems connecting in real-time.",
|
|
||||||
"keywords": [
|
|
||||||
"Internet of Things",
|
|
||||||
"embedded technologies",
|
|
||||||
"cloud systems",
|
|
||||||
"real-time"
|
|
||||||
],
|
|
||||||
"standardsOrg": "ISO/IEC JTC1/SC41",
|
|
||||||
"period": "2020 ~ Present",
|
|
||||||
"deliverables": [
|
|
||||||
{
|
|
||||||
"title": "Part 1: Framework",
|
|
||||||
"ref": "ISO/IEC TR 30189-1 (March 2025)",
|
|
||||||
"status": "TR"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Part 2: Use Cases",
|
|
||||||
"ref": "ISO/IEC CDTR 30189-2 (In Progress)",
|
|
||||||
"status": "InProgress"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ccis",
|
|
||||||
"title": "CCIS",
|
|
||||||
"abstract": "In-Vehicle Infotainment (IVI) refers to vehicle systems that combine entertainment and information delivery to drivers and passengers. Configurable Car Infotainment Service (CCIS) is a service that helps users to more easily manage and control In-Vehicle infotainment devices and content.",
|
|
||||||
"keywords": [
|
|
||||||
"In-Vehicle Infotainment",
|
|
||||||
"CCIS",
|
|
||||||
"infotainment devices"
|
|
||||||
],
|
|
||||||
"standardsTrack": "IEC/TC 100/TA 17",
|
|
||||||
"standardsOrg": "IEC TC100",
|
|
||||||
"period": "2018 ~ Present",
|
|
||||||
"deliverables": [
|
|
||||||
{
|
|
||||||
"title": "Configurable Car Infotainment Services (CCIS) - Part 1: General",
|
|
||||||
"ref": "IEC 63246-1 (August 2021)",
|
|
||||||
"status": "IS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Configurable Car Infotainment Services (CCIS) - Part 2: Requirements",
|
|
||||||
"ref": "IEC 63246-2 (January 2022)",
|
|
||||||
"status": "IS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Configurable Car Infotainment Services (CCIS) - Part 3: Framework",
|
|
||||||
"ref": "IEC 63246-3 (January 2022)",
|
|
||||||
"status": "IS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Configurable Car Infotainment Services (CCIS) - Part 4: Protocol",
|
|
||||||
"ref": "IEC TR 63246-4 (December 2022)",
|
|
||||||
"status": "TR"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "vlc-iot",
|
|
||||||
"title": "VLC-IoT",
|
|
||||||
"abstract": "Visible-light communication (VLC) transmits data by intensity modulating optical sources, such as light-emitting diodes (LEDs) and laser diodes (LDs), faster than the persistence of the human eye. The goal of IoT-VLC is to design a framework of IoT services based on VLC.",
|
|
||||||
"keywords": [
|
|
||||||
"Visible-light communication",
|
|
||||||
"VLC",
|
|
||||||
"light-emitting diodes",
|
|
||||||
"laser diodes",
|
|
||||||
"IoT services"
|
|
||||||
],
|
|
||||||
"standardsTrack": "ITU-T SG20",
|
|
||||||
"standardsOrg": "ITU-T SG20",
|
|
||||||
"period": "2018 ~ 2020",
|
|
||||||
"deliverables": [
|
|
||||||
{
|
|
||||||
"title": "Framework of IoT Services based on Visible Light Communications (VLC)",
|
|
||||||
"ref": "ITU-T Recommendation Y.4465 (January 2020)",
|
|
||||||
"status": "IS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Functional Architecture for IoT Services based on VLC",
|
|
||||||
"ref": "ITU-T Recommendation Y.4474 (August 2020)",
|
|
||||||
"status": "IS"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -31,6 +31,7 @@ export interface Publication {
|
|||||||
publishedAt: string; // ISO 8601 YYYY-MM or YYYY-MM-DD
|
publishedAt: string; // ISO 8601 YYYY-MM or YYYY-MM-DD
|
||||||
kci?: boolean;
|
kci?: boolean;
|
||||||
doi?: string;
|
doi?: string;
|
||||||
|
isHighlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Patent {
|
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,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -455,72 +455,36 @@ def parse_standards():
|
|||||||
return bodies
|
return bodies
|
||||||
|
|
||||||
def parse_projects():
|
def parse_projects():
|
||||||
bodies = parse_standards()
|
|
||||||
body_map = {b["org"]: b for b in bodies}
|
|
||||||
|
|
||||||
# Extract deliverables for CCIS (IEC TC100 - Configurable Car Infotainment Services WG)
|
|
||||||
ccis_docs = []
|
|
||||||
if "IEC TC100" in body_map:
|
|
||||||
for p in body_map["IEC TC100"]["projects"]:
|
|
||||||
if "Configurable Car Infotainment Services" in p["name"]:
|
|
||||||
for d in p["documents"]:
|
|
||||||
ccis_docs.append({
|
|
||||||
"title": d["title"],
|
|
||||||
"ref": d["ref"],
|
|
||||||
"status": d["status"]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Extract deliverables for VLC-IoT (ITU-T SG20)
|
|
||||||
vlc_docs = []
|
|
||||||
if "ITU-T SG20" in body_map:
|
|
||||||
for p in body_map["ITU-T SG20"]["projects"]:
|
|
||||||
for d in p["documents"]:
|
|
||||||
vlc_docs.append({
|
|
||||||
"title": d["title"],
|
|
||||||
"ref": d["ref"],
|
|
||||||
"status": d["status"]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Extract deliverables for IoT Standards (ISO/IEC JTC1/SC41)
|
|
||||||
iot_docs = []
|
|
||||||
if "ISO/IEC JTC1/SC41" in body_map:
|
|
||||||
for p in body_map["ISO/IEC JTC1/SC41"]["projects"]:
|
|
||||||
for d in p["documents"]:
|
|
||||||
iot_docs.append({
|
|
||||||
"title": d["title"],
|
|
||||||
"ref": d["ref"],
|
|
||||||
"status": d["status"]
|
|
||||||
})
|
|
||||||
|
|
||||||
projects = [
|
projects = [
|
||||||
{
|
{
|
||||||
"slug": "iot-standards",
|
"slug": "iot-standards",
|
||||||
"title": "IoT Standards",
|
"title": "AIoT & AI-RAN",
|
||||||
"abstract": "Internet of Things covers a huge range of industries and use cases that scale from a single constrained device up to massive cross-platform deployments of embedded technologies and cloud systems connecting in real-time.",
|
"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": ["Internet of Things", "embedded technologies", "cloud systems", "real-time"],
|
"keywords": ["AIoT", "agent-based IoT", "AI-RAN", "constrained edge protocols", "distributed autonomous control"],
|
||||||
"standardsOrg": "ISO/IEC JTC1/SC41",
|
"standardsTrack": "National Research Foundation of Korea (NRF)",
|
||||||
"period": body_map.get("ISO/IEC JTC1/SC41", {}).get("period", "2020 ~ Present"),
|
"standardsOrg": "NRF",
|
||||||
"deliverables": iot_docs,
|
"period": "2026 ~ Present",
|
||||||
|
"deliverables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "ccis",
|
"slug": "ccis",
|
||||||
"title": "CCIS",
|
"title": "MCM",
|
||||||
"abstract": "In-Vehicle Infotainment (IVI) refers to vehicle systems that combine entertainment and information delivery to drivers and passengers. Configurable Car Infotainment Service (CCIS) is a service that helps users to more easily manage and control In-Vehicle infotainment devices and content.",
|
"abstract": "Multilateral and Collaborative Metaverse (MCM) refers to metaverse systems in which two or more participants simultaneously interact within a shared virtual space for a common collaborative purpose. Unlike single-user or purely competitive environments, an MCM service is defined by coordinated participation: its intended outcome cannot be achieved by a single user acting alone. The project defines general requirements, service classification, and a gap analysis against existing standards.",
|
||||||
"keywords": ["In-Vehicle Infotainment", "CCIS", "infotainment devices"],
|
"keywords": ["MCM", "metaverse", "shared virtual space", "coordinated participation", "service classification"],
|
||||||
"standardsTrack": "IEC/TC 100/TA 17",
|
"standardsTrack": "IEC/TC 100/WG 12 supported by KEIT",
|
||||||
"standardsOrg": "IEC TC100",
|
"standardsOrg": "IEC TC100",
|
||||||
"period": body_map.get("IEC TC100", {}).get("period", "2018 ~ Present"),
|
"period": "2026 ~ Present",
|
||||||
"deliverables": ccis_docs,
|
"deliverables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "vlc-iot",
|
"slug": "vlc-iot",
|
||||||
"title": "VLC-IoT",
|
"title": "MVQM",
|
||||||
"abstract": "Visible-light communication (VLC) transmits data by intensity modulating optical sources, such as light-emitting diodes (LEDs) and laser diodes (LDs), faster than the persistence of the human eye. The goal of IoT-VLC is to design a framework of IoT services based on VLC.",
|
"abstract": "Management of Visual Quality in Metaverse Systems (MVQM) is a conceptual and functional approach for managing visual quality by dynamically adapting visual data delivery in response to user context, device capability, and network conditions. It emphasizes user-centric, context-aware quality management over static system-centric optimization, covering hybrid visual assets, level of detail, and area of interest adaptation.",
|
||||||
"keywords": ["Visible-light communication", "VLC", "light-emitting diodes", "laser diodes", "IoT services"],
|
"keywords": ["MVQM", "visual quality", "hybrid visual assets", "level of detail", "area of interest"],
|
||||||
"standardsTrack": "ITU-T SG20",
|
"standardsTrack": "IEC/TC 100/WG 12 supported by KEIT",
|
||||||
"standardsOrg": "ITU-T SG20",
|
"standardsOrg": "IEC TC100",
|
||||||
"period": body_map.get("ITU-T SG20", {}).get("period", "2018 ~ 2020"),
|
"period": "2026 ~ Present",
|
||||||
"deliverables": vlc_docs,
|
"deliverables": [],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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"))
|
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 = {
|
MONTH_MAP = {
|
||||||
"january": "01", "jan": "01", "february": "02", "feb": "02", "march": "03", "mar": "03",
|
"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",
|
"april": "04", "apr": "04", "may": "05", "june": "06", "jun": "06", "july": "07", "jul": "07",
|
||||||
@@ -25,7 +45,8 @@ def verify():
|
|||||||
total_pubs = len(intl_p) + len(dom_p) + len(pat_p)
|
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"])
|
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("=== 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" Active Members: {len(active_m)} (Expected: 16)")
|
||||||
print(f" Alumni: {len(alumni_m)} (Expected: 60)")
|
print(f" Alumni: {len(alumni_m)} (Expected: 60)")
|
||||||
print(f" International Papers: {len(intl_p)} (Expected: 68)")
|
print(f" International Papers: {len(intl_p)} (Expected: 68)")
|
||||||
@@ -39,28 +60,7 @@ def verify():
|
|||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
if len(projects) != 3:
|
# 1. Source Extraction Integrity Checks
|
||||||
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
|
|
||||||
if len(active_m) != 16:
|
if len(active_m) != 16:
|
||||||
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
|
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
|
||||||
if len(alumni_m) != 60:
|
if len(alumni_m) != 60:
|
||||||
@@ -79,28 +79,175 @@ def verify():
|
|||||||
errors.append(f"Standard Bodies count mismatch: got {len(bodies)}, expected 6")
|
errors.append(f"Standard Bodies count mismatch: got {len(bodies)}, expected 6")
|
||||||
if std_docs_count != 44:
|
if std_docs_count != 44:
|
||||||
errors.append(f"Standard Documents count mismatch: got {std_docs_count}, expected 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")
|
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]))?$')
|
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"]:
|
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)
|
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:
|
with open(fpath, "r", encoding="utf-8") as f:
|
||||||
text = f.read()
|
text = f.read()
|
||||||
|
|
||||||
if text.startswith('\ufeff'):
|
if text.startswith('\ufeff'):
|
||||||
errors.append(f"BOM (U+FEFF) found in {fname}")
|
errors.append(f"BOM (U+FEFF) found in {fname}")
|
||||||
|
|
||||||
dates = re.findall(r'"(publishedAt|applicationAt|registrationAt|graduatedAt)": "([^"]+)"', text)
|
for p in intl_p + dom_p + pat_p:
|
||||||
for k, d in dates:
|
for field in ["publishedAt", "applicationAt", "registrationAt"]:
|
||||||
if not date_regex.match(d):
|
val = p.get(field)
|
||||||
errors.append(f"Invalid ISO 8601 or out-of-bounds date in {fname}: {k}='{d}'")
|
if val and not date_regex.match(val):
|
||||||
|
errors.append(f"Invalid ISO 8601 date in publication '{p.get('title')}': {field}='{val}'")
|
||||||
|
|
||||||
# 3. Volume Cross-Check for Publications (Ensures volume date candidates match publishedAt)
|
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}'")
|
||||||
|
|
||||||
|
# 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)'
|
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:
|
for pub in intl_p + dom_p:
|
||||||
|
|||||||
@@ -1,44 +1,64 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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:
|
Verifies:
|
||||||
0. Clean step (npm run clean -> removes .next and tsconfig.tsbuildinfo)
|
0. Clean step (npm run clean -> removes .next and tsconfig.tsbuildinfo)
|
||||||
1. ESLint (npm run lint)
|
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)
|
3. TypeScript type check (npx tsc --noEmit)
|
||||||
4. Existence and content of static SSG routes HTML/body files
|
4. Live Server Route Verification (Live HTTP status 200 & content)
|
||||||
5. Data count losslessness & integrity gate (verify_counts.py)
|
5. Data count losslessness & fallback dataset integrity (verify_counts.py)
|
||||||
6. Light/Dark mode theme tokens and CSS attributes in source and build assets
|
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 os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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")
|
REFER_DIR = os.path.join(ROOT_DIR, "refer_landing_page")
|
||||||
|
|
||||||
# Expected Static SSG Routes HTML/Body output paths relative to REFER_DIR
|
EXPECTED_DYNAMIC_ROUTES = [
|
||||||
EXPECTED_SSG_ROUTES = [
|
"/",
|
||||||
os.path.join(".next", "server", "app", "index.html"),
|
"/members",
|
||||||
os.path.join(".next", "server", "app", "members.html"),
|
"/publications",
|
||||||
os.path.join(".next", "server", "app", "publications.html"),
|
"/publications/[category]",
|
||||||
os.path.join(".next", "server", "app", "publications", "intl-journal-conf.html"),
|
"/lectures",
|
||||||
os.path.join(".next", "server", "app", "publications", "domestic-journal-conf.html"),
|
"/standardization",
|
||||||
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"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
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})")
|
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
|
return res
|
||||||
|
|
||||||
def test_clean():
|
def test_clean():
|
||||||
@@ -60,22 +80,44 @@ def test_lint():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def test_build():
|
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"])
|
res = run_cmd(["npm", "run", "build"])
|
||||||
if res.returncode != 0:
|
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
|
return False
|
||||||
|
|
||||||
# Check stdout/stderr for dynamic routes (f Dynamic)
|
# Parse exact Next.js route table output lines (e.g. "┌ ƒ /", "├ ○ /_not-found", "├ ƒ /publications", "├ ƒ /publications/[category]")
|
||||||
if "ƒ" in res.stdout or "Dynamic" in res.stdout:
|
route_table_map = {}
|
||||||
# Search specifically for dynamic routes output line
|
for line in res.stdout.splitlines():
|
||||||
lines = res.stdout.splitlines()
|
m = re.match(r'^[┌├└]\s+([ƒ○●])\s+(\S+)', line.strip())
|
||||||
dynamic_lines = [l for l in lines if "ƒ" in l or "Dynamic" in l]
|
if m:
|
||||||
if dynamic_lines:
|
mode, route = m.group(1), m.group(2)
|
||||||
print(f"❌ Found dynamic route(s) in build report: {dynamic_lines}")
|
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
|
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
|
return True
|
||||||
|
|
||||||
def test_tsc():
|
def test_tsc():
|
||||||
@@ -87,35 +129,81 @@ def test_tsc():
|
|||||||
print("✅ Gate 3 PASSED: TypeScript type check clean (exit code 0)")
|
print("✅ Gate 3 PASSED: TypeScript type check clean (exit code 0)")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test_ssg_routes():
|
import socket
|
||||||
print("\n--- Gate 4: Static SSG HTML Route Verification ---")
|
|
||||||
missing = []
|
def get_free_port():
|
||||||
for rel_path in EXPECTED_SSG_ROUTES:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
full_path = os.path.join(REFER_DIR, rel_path)
|
s.bind(('', 0))
|
||||||
if not os.path.exists(full_path):
|
return s.getsockname()[1]
|
||||||
missing.append(rel_path)
|
|
||||||
|
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:
|
else:
|
||||||
size = os.path.getsize(full_path)
|
raise e
|
||||||
print(f" [OK] {rel_path} ({size} bytes)")
|
return bodies
|
||||||
|
|
||||||
if missing:
|
def test_live_routes(bodies):
|
||||||
print(f"❌ Missing {len(missing)} SSG routes:")
|
print("\n--- Gate 4: Live Server Route Verification ---")
|
||||||
for m in missing:
|
for route, body in bodies.items():
|
||||||
print(f" - {m}")
|
if len(body) == 0:
|
||||||
|
print(f"❌ Empty response body for route '{route}'")
|
||||||
return False
|
return False
|
||||||
|
print(f" [OK] {route} ({len(body)} bytes)")
|
||||||
|
|
||||||
print(f"✅ Gate 4 PASSED: All {len(EXPECTED_SSG_ROUTES)} static SSG routes exist and are non-empty")
|
print(f"✅ Gate 4 PASSED: All {len(bodies)} live routes served non-empty responses")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test_data_counts():
|
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")
|
script_path = os.path.join(REFER_DIR, "scripts", "extract", "verify_counts.py")
|
||||||
res = run_cmd([sys.executable, script_path], cwd=ROOT_DIR)
|
res = run_cmd([sys.executable, script_path], cwd=ROOT_DIR)
|
||||||
if res.returncode != 0:
|
if res.returncode != 0:
|
||||||
print(f"❌ Data count verification failed:\n{res.stdout}\n{res.stderr}")
|
print(f"❌ Data count verification failed:\n{res.stdout}\n{res.stderr}")
|
||||||
return False
|
return False
|
||||||
print(res.stdout.strip())
|
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
|
return True
|
||||||
|
|
||||||
def test_theme_tokens():
|
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")
|
print("✅ Gate 6 PASSED: Light/Dark theme CSS variables and Tailwind tokens present")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test_unittest_suite():
|
def test_unittest_suite(port=3000):
|
||||||
print("\n--- Gate 7: DOM & HTML Content Assertions (Unittest) ---")
|
print("\n--- Gate 7: DOM & HTML Content Assertions (Unittest) ---")
|
||||||
test_path = os.path.join(REFER_DIR, "tests", "e2e_test_suite.py")
|
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:
|
if res.returncode != 0:
|
||||||
print(f"❌ Unittest suite failed:\n{res.stdout}\n{res.stderr}")
|
print(f"❌ Unittest suite failed:\n{res.stdout}\n{res.stderr}")
|
||||||
return False
|
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
|
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) ---")
|
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)
|
# 1) Check live HTML responses for container-content inside main across all routes (C2, AC-25)
|
||||||
routes_to_check = EXPECTED_SSG_ROUTES
|
|
||||||
|
|
||||||
main_hashes = {}
|
main_hashes = {}
|
||||||
for rel_p in routes_to_check:
|
for route, html in bodies.items():
|
||||||
if not rel_p.endswith(".html"):
|
if "_not-found" in route or "404" in route:
|
||||||
continue
|
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)
|
main_match = re.search(r'<main(?:\s+[^>]*)?>(.*?)</main>', html, flags=re.S)
|
||||||
if not main_match:
|
if not main_match:
|
||||||
print(f"❌ <main> tag not found in {rel_p}")
|
print(f"❌ <main> tag not found in {route}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
main_inner = main_match.group(1)
|
main_inner = main_match.group(1)
|
||||||
|
|
||||||
# C1: Verify child wrapper inside <main> has container-content
|
# 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:
|
if "container-content" not in main_inner:
|
||||||
print(f"❌ container-content missing inside <main> in {rel_p}")
|
print(f"❌ container-content missing inside <main> in {route}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# AC-41: Check for duplicate <main> normalized content hashes (except 404 pages)
|
# AC-41: Check for duplicate <main> normalized content hashes
|
||||||
if "404" not in rel_p and "_not-found" not in rel_p:
|
|
||||||
norm_content = re.sub(r'\s+', '', main_inner)
|
norm_content = re.sub(r'\s+', '', main_inner)
|
||||||
if norm_content in main_hashes:
|
if norm_content in main_hashes:
|
||||||
print(f"❌ AC-41 Violation: Duplicate <main> content found between {rel_p} and {main_hashes[norm_content]}")
|
print(f"❌ AC-41 Violation: Duplicate <main> content found between {route} and {main_hashes[norm_content]}")
|
||||||
return False
|
return False
|
||||||
main_hashes[norm_content] = rel_p
|
main_hashes[norm_content] = route
|
||||||
|
|
||||||
# 2) Check source for forbidden escaped arbitrary classes (AC-26)
|
# 2) Check source for forbidden escaped arbitrary classes (AC-26)
|
||||||
forbidden_terms = ["w-screen", "-mx-[50vw]", "min-[1240px]:"]
|
forbidden_terms = ["w-screen", "-mx-[50vw]", "min-[1240px]:"]
|
||||||
@@ -276,21 +354,20 @@ def test_color_contrast_gate():
|
|||||||
|
|
||||||
def parse_tokens(block):
|
def parse_tokens(block):
|
||||||
res = {}
|
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)))
|
res[m.group(1)] = (int(m.group(2)), int(m.group(3)), int(m.group(4)))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
light_tokens = parse_tokens(root_match.group(1))
|
light_tokens = parse_tokens(root_match.group(1))
|
||||||
dark_tokens = parse_tokens(dark_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")
|
cd_light = light_tokens.get("cobalt-dark")
|
||||||
vd_light = light_tokens.get("vermillion-dark")
|
vd_light = light_tokens.get("vermillion-dark")
|
||||||
ivory_light = light_tokens.get("ivory")
|
ivory_light = light_tokens.get("ivory")
|
||||||
paper_light = light_tokens.get("paper")
|
paper_light = light_tokens.get("paper")
|
||||||
|
|
||||||
if not all([cd_light, vd_light, ivory_light, paper_light]):
|
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
|
return False
|
||||||
|
|
||||||
ratio_cd_ivory = contrast_ratio(cd_light, ivory_light)
|
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)")
|
print(f"❌ AC-45 Violation: Light/Dark vermillion-dark inversion contrast too low ({inversion_ratio_vd:.2f} < 2.0)")
|
||||||
return False
|
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
|
white_count = 0
|
||||||
small_size_pattern = re.compile(r'text-(xs|sm|\[0\.65rem\]|\[0\.78rem\])')
|
small_size_pattern = re.compile(r'text-(xs|sm|\[0\.65rem\]|\[0\.78rem\])')
|
||||||
default_color_pattern = re.compile(r'text-(cobalt|vermillion)(?![a-z-])')
|
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:
|
with open(fpath, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# AC-47 count
|
|
||||||
white_count += content.count("text-white")
|
white_count += content.count("text-white")
|
||||||
|
|
||||||
# AC-44 & C11-9 check line by line
|
|
||||||
for line_idx, line in enumerate(content.splitlines(), start=1):
|
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):
|
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()}")
|
print(f"❌ C11-9 (AC-48) Violation: Forbidden dark: modifier on accent color at {fpath}:{line_idx}\n {line.strip()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if small_size_pattern.search(line) and default_color_pattern.search(line):
|
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:
|
if "Pillar #" in line or "w-1.5 h-1.5" in line or "text-3xl" in line:
|
||||||
continue
|
continue
|
||||||
print(f"❌ AC-44 Violation: Small text combined with DEFAULT accent color at {fpath}:{line_idx}\n {line.strip()}")
|
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():
|
def main():
|
||||||
print("=" * 70)
|
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)
|
print("=" * 70)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
@@ -373,14 +443,34 @@ def main():
|
|||||||
success &= test_lint()
|
success &= test_lint()
|
||||||
success &= test_build()
|
success &= test_build()
|
||||||
success &= test_tsc()
|
success &= test_tsc()
|
||||||
success &= test_ssg_routes()
|
|
||||||
success &= test_data_counts()
|
success &= test_data_counts()
|
||||||
success &= test_theme_tokens()
|
success &= test_theme_tokens()
|
||||||
success &= test_unittest_suite()
|
|
||||||
success &= test_layout_architecture()
|
|
||||||
success &= test_token_contracts()
|
success &= test_token_contracts()
|
||||||
success &= test_color_contrast_gate()
|
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)
|
print("\n" + "-" * 70)
|
||||||
if success:
|
if success:
|
||||||
print("ALL E2E TEST SUITES PASSED CLEAN (Exit Code 0).")
|
print("ALL E2E TEST SUITES PASSED CLEAN (Exit Code 0).")
|
||||||
|
|||||||
@@ -1,50 +1,111 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Python Unittest Suite for ANL Homepage E2E Verification
|
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 unittest
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
SERVER_APP_DIR = os.path.join(BASE_DIR, ".next", "server", "app")
|
TEST_SERVER_URL = os.environ.get("TEST_SERVER_URL", "http://localhost:3000")
|
||||||
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
|
|
||||||
|
|
||||||
class TestANLHomepageE2E(unittest.TestCase):
|
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):
|
def read_html(self, relative_path):
|
||||||
full_path = os.path.join(SERVER_APP_DIR, relative_path)
|
route_map = {
|
||||||
self.assertTrue(os.path.exists(full_path), f"File {relative_path} does not exist")
|
"index.html": "/",
|
||||||
with open(full_path, "r", encoding="utf-8") as f:
|
"members.html": "/members",
|
||||||
return f.read()
|
"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):
|
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")
|
html = self.read_html("index.html")
|
||||||
|
|
||||||
# 1. Brand & Header titles
|
# 1. Brand & Header titles
|
||||||
self.assertIn("AI 에이전트 네트워크 연구실", html)
|
self.assertIn("ANL", html)
|
||||||
self.assertIn("AI Agent Networking Lab (ANL)", html)
|
self.assertIn("AI Agent Networking LAB", html)
|
||||||
|
|
||||||
# 2. Core Pillars keywords
|
# 2. Research Areas keywords
|
||||||
self.assertIn("MCP", html)
|
self.assertIn("Quick UDP Internet Connection (QUIC)", html)
|
||||||
self.assertIn("Multi-Agent Systems", html)
|
self.assertIn("Constrained Application Protocol (CoAP)", html)
|
||||||
self.assertIn("Interoperability Standards", html)
|
self.assertIn("Stream Control Transmission Protocol (SCTP)", html)
|
||||||
self.assertIn("High-Performance Backbone", html)
|
self.assertIn("Tabu Search and Genetic Algorithm", html)
|
||||||
|
|
||||||
# 3. Location / Address section
|
# 3. Location / Address section
|
||||||
self.assertIn("address", html.lower())
|
self.assertIn("address", html.lower())
|
||||||
self.assertIn("경북대학교 IT대학 5호관", html)
|
self.assertIn("Kyungpook National University", html)
|
||||||
|
self.assertIn("College of IT Engineering", html)
|
||||||
|
self.assertIn("Prof. Seok-Joo Koh", html)
|
||||||
self.assertIn("sjkoh@knu.ac.kr", html)
|
self.assertIn("sjkoh@knu.ac.kr", html)
|
||||||
|
|
||||||
def test_02_members_page_content(self):
|
def test_02_members_page_content(self):
|
||||||
@@ -52,15 +113,19 @@ class TestANLHomepageE2E(unittest.TestCase):
|
|||||||
html = self.read_html("members.html")
|
html = self.read_html("members.html")
|
||||||
|
|
||||||
# Professor profile
|
# Professor profile
|
||||||
self.assertIn("고석주", html)
|
self.assertIn("Prof. Seok-Joo Koh", html)
|
||||||
self.assertIn("Seok-Joo Koh", html)
|
self.assertIn("Seok-Joo Koh", html)
|
||||||
self.assertIn("sjkoh@knu.ac.kr", html)
|
self.assertIn("sjkoh@knu.ac.kr", html)
|
||||||
|
|
||||||
# Members content sanity checks
|
# Sections & Degree formatting checks
|
||||||
self.assertIn("Members", html)
|
self.assertIn("LAB MEMBERS", html)
|
||||||
|
self.assertIn("Active Researchers", html)
|
||||||
|
self.assertIn("Graduates", html)
|
||||||
|
self.assertIn("Ph. D. Candidate", html)
|
||||||
|
self.assertIn("Project Editor", html)
|
||||||
|
|
||||||
def test_03_publications_category_routes(self):
|
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 = [
|
cats = [
|
||||||
("intl-journal-conf.html", "International Journal"),
|
("intl-journal-conf.html", "International Journal"),
|
||||||
("domestic-journal-conf.html", "Domestic Journal"),
|
("domestic-journal-conf.html", "Domestic Journal"),
|
||||||
@@ -85,8 +150,6 @@ class TestANLHomepageE2E(unittest.TestCase):
|
|||||||
def test_06_theme_css_variables(self):
|
def test_06_theme_css_variables(self):
|
||||||
"""TC-T1-F2: Verify globals.css and built assets contain theme tokens."""
|
"""TC-T1-F2: Verify globals.css and built assets contain theme tokens."""
|
||||||
globals_path = os.path.join(BASE_DIR, "app", "globals.css")
|
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:
|
with open(globals_path, "r", encoding="utf-8") as f:
|
||||||
css = f.read()
|
css = f.read()
|
||||||
|
|
||||||
@@ -94,22 +157,77 @@ class TestANLHomepageE2E(unittest.TestCase):
|
|||||||
self.assertIn(token, css, f"Missing CSS variable {token} in globals.css")
|
self.assertIn(token, css, f"Missing CSS variable {token} in globals.css")
|
||||||
|
|
||||||
def test_07_research_projects_pageview(self):
|
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")
|
html = self.read_html("index.html")
|
||||||
|
|
||||||
# 1. Project PageView Title & Projects
|
# 1. Project PageView Title & Projects
|
||||||
self.assertIn("주요 연구 프로젝트 (Research Projects)", html)
|
self.assertIn("Research Projects", html)
|
||||||
self.assertIn("IoT Standards", html)
|
self.assertIn("AI-RAN", html)
|
||||||
self.assertIn("CCIS", html)
|
self.assertIn("MCM", html)
|
||||||
self.assertIn("VLC-IoT", html)
|
self.assertIn("MVQM", html)
|
||||||
|
|
||||||
# 2. Key Deliverables references (Top visible items)
|
# 2. Standards track labels
|
||||||
self.assertIn("ISO/IEC TR 30189-1", html)
|
self.assertIn("National Research Foundation of Korea", html)
|
||||||
self.assertIn("IEC 63246-1", html)
|
self.assertIn("IEC/TC 100/WG 12 supported by KEIT", html)
|
||||||
self.assertIn("ITU-T Recommendation Y.4465", html)
|
|
||||||
|
|
||||||
# 3. Assert zero placeholder Funder labels (AC-31)
|
# 3. International Standardizations Section
|
||||||
|
self.assertIn("International Standardizations", html)
|
||||||
|
self.assertIn("IEC TC100: Multimedia Systems and Equipment", html)
|
||||||
|
self.assertIn("ISO/IEC JTC1/SC6: Reliable Multicasting", html)
|
||||||
|
|
||||||
|
# 4. Assert zero placeholder Funder labels
|
||||||
self.assertNotIn("Funder:", html)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,536 @@
|
|||||||
|
|
||||||
|
# REQUIREMENTS FOR MULTILATERAL AND COLLABORATIVE METAVERSE – Part 1: General
|
||||||
|
|
||||||
|
## CONTENTS
|
||||||
|
|
||||||
|
<!-- 원본 PDF(p.3)의 목차는 Clause 4 재구조화 이후 갱신되지 않아 구 표제(4.1.2 Key Characteristics / 4.2 Technical Background)를 표시하며 두 항목의 페이지 번호가 "Error! Bookmark not defined." 로 출력됩니다. 본 목차는 본문을 기준으로 생성했습니다. -->
|
||||||
|
|
||||||
|
- [FOREWORD](#foreword)
|
||||||
|
- [INTRODUCTION](#introduction)
|
||||||
|
- [1 Scope](#1-scope)
|
||||||
|
- [2 Normative references](#2-normative-references)
|
||||||
|
- [3 Terms and definitions](#3-terms-and-definitions)
|
||||||
|
- [3.1 Terms and definitions](#31-terms-and-definitions)
|
||||||
|
- [3.2 Abbreviated terms](#32-abbreviated-terms)
|
||||||
|
- [4 General Considerations for Multilateral and Collaborative Metaverse Systems](#4-general-considerations-for-multilateral-and-collaborative-metaverse-systems)
|
||||||
|
- [4.1 Overview of MCM Systems](#41-overview-of-mcm-systems)
|
||||||
|
- [4.1.1 Definition and Concept of MCM](#411-definition-and-concept-of-mcm)
|
||||||
|
- [4.1.2 Position of MCM within the Metaverse Ecosystem](#412-position-of-mcm-within-the-metaverse-ecosystem)
|
||||||
|
- [4.2 Fundamental Characteristics of MCM Systems](#42-fundamental-characteristics-of-mcm-systems)
|
||||||
|
- [4.2.1 Collaboration-centered Service Objectives](#421-collaboration-centered-service-objectives)
|
||||||
|
- [4.2.2 Simultaneous Multilateral Participation](#422-simultaneous-multilateral-participation)
|
||||||
|
- [4.2.3 Shared Virtual Context](#423-shared-virtual-context)
|
||||||
|
- [4.2.4 Coordinated Roles and Interdependent Activities](#424-coordinated-roles-and-interdependent-activities)
|
||||||
|
- [4.2.5 Real-time Reciprocal Interaction](#425-real-time-reciprocal-interaction)
|
||||||
|
- [4.2.6 Coherence of Shared Session State](#426-coherence-of-shared-session-state)
|
||||||
|
- [4.2.7 Participant Presence and Mutual Awareness](#427-participant-presence-and-mutual-awareness)
|
||||||
|
- [4.2.8 Continuity of Collaborative Participation](#428-continuity-of-collaborative-participation)
|
||||||
|
- [4.3 Representative MCM Service Domains](#43-representative-mcm-service-domains)
|
||||||
|
- [4.3.1 Professional Work and Organizational Collaboration](#431-professional-work-and-organizational-collaboration)
|
||||||
|
- [4.3.2 Education and Training](#432-education-and-training)
|
||||||
|
- [4.3.3 Healthcare and Care Services](#433-healthcare-and-care-services)
|
||||||
|
- [4.3.4 Industrial Engineering and Operations](#434-industrial-engineering-and-operations)
|
||||||
|
- [4.3.5 Commerce and Customer Services](#435-commerce-and-customer-services)
|
||||||
|
- [4.3.6 Public and Civic Services](#436-public-and-civic-services)
|
||||||
|
- [4.3.7 Entertainment, Culture, and Creative Participation](#437-entertainment-culture-and-creative-participation)
|
||||||
|
- [4.4 Technical Considerations for MCM Systems](#44-technical-considerations-for-mcm-systems)
|
||||||
|
- [4.4.1 Heterogeneous Access and Participant Representation](#441-heterogeneous-access-and-participant-representation)
|
||||||
|
- [4.4.2 Multimodal Interaction and Temporal Coordination](#442-multimodal-interaction-and-temporal-coordination)
|
||||||
|
- [4.4.3 Shared state and Data Consistency](#443-shared-state-and-data-consistency)
|
||||||
|
- [4.4.4 Identity, Roles, and Access](#444-identity-roles-and-access)
|
||||||
|
- [4.4.5 Interoperability and Service Continuity](#445-interoperability-and-service-continuity)
|
||||||
|
- [4.4.6 Distributed Processing and Service Scalability](#446-distributed-processing-and-service-scalability)
|
||||||
|
- [4.4.7 AI-assisted and Autonomous Participation](#447-ai-assisted-and-autonomous-participation)
|
||||||
|
- [4.4.8 Physical-digital Integration](#448-physical-digital-integration)
|
||||||
|
- [4.4.9 Trust, Security, Privacy, and Safety](#449-trust-security-privacy-and-safety)
|
||||||
|
- [5 Classification of MCM Services](#5-classification-of-mcm-services)
|
||||||
|
|
||||||
|
- [5.1 Classification Framework Overview](#51-classification-framework-overview)
|
||||||
|
- [5.2 Domain-based Classification](#52-domain-based-classification)
|
||||||
|
- [5.3 Media-based Classification](#53-media-based-classification)
|
||||||
|
- [5.4 Technology-based Classification](#54-technology-based-classification)
|
||||||
|
- [6 Gap Analysis](#6-gap-analysis)
|
||||||
|
- [6.1 Current international Standardization Activities](#61-current-international-standardization-activities)
|
||||||
|
- [6.2 Gaps in Existing Standards](#62-gaps-in-existing-standards)
|
||||||
|
- [6.3 Recommendations for Future Standardization](#63-recommendations-for-future-standardization)
|
||||||
|
- [Annex A (informative) Roadmap for MCM Standard Development](#annex-a-informative-roadmap-for-mcm-standard-development)
|
||||||
|
- [Appendix Z — IEC Word Template Scaffolding (not part of the Technical Report)](#appendix-z--iec-word-template-scaffolding-not-part-of-the-technical-report)
|
||||||
|
|
||||||
|
## FOREWORD
|
||||||
|
|
||||||
|
1) The International Electrotechnical Commission (IEC) is a worldwide organization for standardization comprising all national electrotechnical committees (IEC National Committees). The object of IEC is to promote international co-operation on all questions concerning standardization in the electrical and electronic fields. To this end and in addition to other activities, IEC publishes International Standards, Technical Specifications, Technical Reports, Publicly Available Specifications (PAS) and Guides (hereafter referred to as "IEC Publication(s)"). Their preparation is entrusted to technical committees; any IEC National Committee interested in the subject dealt with may participate in this preparatory work. International, governmental and non-governmental organizations liaising with the IEC also participate in this preparation. IEC collaborates closely with the International Organization for Standardization (ISO) in accordance with conditions determined by agreement between the two organizations.
|
||||||
|
|
||||||
|
2) The formal decisions or agreements of IEC on technical matters express, as nearly as possible, an international consensus of opinion on the relevant subjects since each technical committee has representation from all interested IEC National Committees.
|
||||||
|
|
||||||
|
3) IEC Publications have the form of recommendations for international use and are accepted by IEC National Committees in that sense. While all reasonable efforts are made to ensure that the technical content of IEC Publications is accurate, IEC cannot be held responsible for the way in which they are used or for any misinterpretation by any end user.
|
||||||
|
|
||||||
|
4) In order to promote international uniformity, IEC National Committees undertake to apply IEC Publications transparently to the maximum extent possible in their national and regional publications. Any divergence between any IEC Publication and the corresponding national or regional publication shall be clearly indicated in the latter.
|
||||||
|
|
||||||
|
5) IEC itself does not provide any attestation of conformity. Independent certification bodies provide conformity assessment services and, in some areas, access to IEC marks of conformity. IEC is not responsible for any services carried out by independent certification bodies.
|
||||||
|
|
||||||
|
6) All users should ensure that they have the latest edition of this publication.
|
||||||
|
|
||||||
|
7) No liability shall attach to IEC or its directors, employees, servants or agents including individual experts and members of its technical committees and IEC National Committees for any personal injury, property damage or other damage of any nature whatsoever, whether direct or indirect, or for costs (including legal fees) and expenses arising out of the publication, use of, or reliance upon, this IEC Publication or any other IEC Publications.
|
||||||
|
|
||||||
|
8) Attention is drawn to the Normative references cited in this publication. Use of the referenced publications is indispensable for the correct application of this publication.
|
||||||
|
|
||||||
|
9) IEC draws attention to the possibility that the implementation of this document may involve the use of (a) patent(s). IEC takes no position concerning the evidence, validity or applicability of any claimed patent rights in respect thereof. As of the date of publication of this document, IEC had not received notice of (a) patent(s), which may be required to implement this document. However, implementers are cautioned that this may not represent the latest information, which may be obtained from the patent database available at https://patents.iec.ch. IEC shall not be held responsible for identifying any or all such patent rights.
|
||||||
|
|
||||||
|
IEC 6XXXX has been prepared by IEC technical Committee TC 100: Audio, video and multimedia systems and equipment. It is Technical Report.
|
||||||
|
|
||||||
|
The text of this Technical Report is based on the following documents:
|
||||||
|
|
||||||
|
| Draft | Report on voting |
|
||||||
|
|---|---|
|
||||||
|
| XX/XX/FDIS | XX/XX/RVD |
|
||||||
|
|
||||||
|
Full information on the voting for its approval can be found in the report on voting indicated in the above table.
|
||||||
|
|
||||||
|
The language used for the development of this Technical Report is English.
|
||||||
|
|
||||||
|
This document was drafted in accordance with ISO/IEC Directives, Part 2, and developed in accordance with ISO/IEC Directives, Part 1 and ISO/IEC Directives, IEC Supplement, available at www.iec.ch/members_experts/refdocs. The main document types developed by IEC are described in greater detail at www.iec.ch/publications.
|
||||||
|
|
||||||
|
The committee has decided that the contents of this document will remain unchanged until the stability date indicated on the IEC website under webstore.iec.ch in the data related to the specific document. At this date, the document will be
|
||||||
|
- reconfirmed,
|
||||||
|
- withdrawn, or
|
||||||
|
- revised.
|
||||||
|
|
||||||
|
## INTRODUCTION
|
||||||
|
|
||||||
|
As metaverse technologies have matured, a distinct class of services has emerged in which the simultaneous presence and active collaboration of multiple participants forms the fundamental premise of the service itself. These services, referred to in this document as Multilateral and Collaborative Metaverse (MCM) systems, constitute a subset of the metaverse specifically designed to enable purposeful, real-time collaboration among two or more simultaneous participants. MCM systems differ structurally and functionally from general metaverse platforms designed primarily for individual use or incidental social interaction, in that collaboration is not incidental but the fundamental condition for service delivery.
|
||||||
|
|
||||||
|
MCM systems are increasingly adopted across a broad range of domains, including remote work and collaboration, education and training, industrial manufacturing, healthcare, entertainment, commerce, and public administration. In each of these contexts, the realization of a coherent collaborative experience depends on the consistent integration of spatial, audio, video, avatar, and identity information across heterogeneous platforms and devices. This integration challenge is not adequately addressed by existing international standards, which have largely focused on platform architecture, device specifications, and content formats rather than on the service-level requirements specific to MCM. The absence of dedicated international standards for MCM systems has resulted in fragmented implementations across the industry, leaving cross-platform interoperability, avatar and digital asset portability, and consistent identity management largely unresolved.
|
||||||
|
|
||||||
|
MCM systems are implemented and realized through multimedia systems and equipment. This positions IEC TC 100 as the appropriate forum for MCM-specific standardization activities, given its established role as the leading standardization body in the area of multimedia systems and equipment.
|
||||||
|
|
||||||
|
The IEC 6XXXX — Requirements for Multilateral and Collaborative Metaverse (MCM) Systems series consists of the following parts:
|
||||||
|
|
||||||
|
Part 1 : General;
|
||||||
|
Part 2 : Service Requirements; and
|
||||||
|
Part 3 : Media Requirements.
|
||||||
|
|
||||||
|
Part 1 of IEC TR 6XXXX (this document) defines MCM systems and establishes their conceptual boundaries relative to general metaverse services, presents a classification of MCM services across three complementary dimensions of domain of application, media type, and enabling technology, and identifies gaps in existing international standards with respect to MCM service requirements, with recommendations for future standardization priorities. This Technical Report is informative in nature and serves as the analytical and conceptual foundation upon which subsequent parts of this series are developed.
|
||||||
|
|
||||||
|
|
||||||
|
Part 2 of IEC 6XXXX describes common service requirements applicable to MCM systems across major domain categories, including but not limited to work and collaboration, education, commerce, entertainment, and healthcare. Domain-specific requirements that extend beyond the common framework are addressed in dedicated sub-parts, organized in accordance with the service classifications established in Part 1.
|
||||||
|
|
||||||
|
Part 3 of IEC 6XXXX describes common media requirements applicable to MCM systems, addressing the general technical characteristics of media types including audio, video, 3D objects, haptics, and spatial data. Media-specific requirements that extend beyond the common framework are addressed in dedicated sub-parts, organized in accordance with the media classifications established in Part 1.
|
||||||
|
|
||||||
|
## 1 Scope
|
||||||
|
|
||||||
|
This document describes the general considerations for Multilateral and Collaborative Metaverse (MCM) systems, including the definition and key characteristics of MCM, the classification of MCM services by domain, media, and technology, and a gap analysis of existing international standards with respect to MCM service requirements.
|
||||||
|
|
||||||
|
For the purposes of this document, MCM is defined as a subset of the metaverse in which two or more participants simultaneously interact within a shared virtual space for a common collaborative purpose. The following are explicitly excluded from the scope of this document:
|
||||||
|
|
||||||
|
- single-user metaverse experiences in which collaboration is absent or incidental;
|
||||||
|
- multiplayer environments in which interaction is competitive rather than collaborative;
|
||||||
|
- asynchronous virtual environments in which participants do not share simultaneous presence; and
|
||||||
|
- general metaverse platform infrastructure not specific to MCM service requirements.
|
||||||
|
|
||||||
|
## 2 Normative references
|
||||||
|
|
||||||
|
The following documents are referred to in the text in such a way that some or all of their content constitutes requirements of this document. For dated references, only the edition cited applies. For undated references, the latest edition of the referenced document (including any amendments) applies.
|
||||||
|
|
||||||
|
- IEC TR 63614-1, Multimedia systems and equipment for metaverse – Part 1: General
|
||||||
|
- IEC TS 63614-2, Multimedia systems and equipment for metaverse – Part 2: Classification
|
||||||
|
- IEC TR 63614-3, Multimedia systems and equipment for metaverse – Part 3: Gap analysis
|
||||||
|
|
||||||
|
## 3 Terms and definitions
|
||||||
|
|
||||||
|
For the purposes of this document, the following terms and definitions apply.
|
||||||
|
|
||||||
|
ISO and IEC maintain terminology databases for use in standardization at the following addresses:
|
||||||
|
|
||||||
|
- IEC Electropedia: available at https://www.electropedia.org/
|
||||||
|
- ISO Online browsing platform: available at https://www.iso.org/obp
|
||||||
|
|
||||||
|
### 3.1 Terms and definitions
|
||||||
|
|
||||||
|
#### 3.1.1 metaverse
|
||||||
|
digital-based virtual environment in which users, represented by avatars, access a virtual space through a network using a device to experience various activities through digital content and platforms
|
||||||
|
|
||||||
|
#### 3.1.2 multilateral and collaborative metaverse (MCM)
|
||||||
|
subset of the metaverse specifically designed to enable purposeful, real-time collaboration among two or more simultaneous participants within a shared virtual space, where collaborative participation is the fundamental condition for service delivery
|
||||||
|
|
||||||
|
> **Note 1** — MCM systems are distinguished from general metaverse services in that the service itself cannot be constituted without the presence of multiple collaborative participants.
|
||||||
|
|
||||||
|
#### 3.1.3 avatar
|
||||||
|
digital representation of a participant within a virtual space, capable of conveying spatial presence, movement, and non-verbal expression
|
||||||
|
|
||||||
|
#### 3.1.4 session
|
||||||
|
bounded instance of MCM service operation during which multiple participants engage in real-time collaborative interaction within a shared virtual space
|
||||||
|
|
||||||
|
#### 3.1.5 spatial presence
|
||||||
|
perceptual experience of being co-located with other participants in a shared virtual space, enabled by spatial audio, positional awareness, and immersive rendering
|
||||||
|
|
||||||
|
#### 3.1.6 cross-platform interoperability
|
||||||
|
capability of MCM systems to maintain a coherent collaborative experience across heterogeneous platforms and devices without loss of functional or experiential continuity
|
||||||
|
|
||||||
|
#### 3.1.7 digital asset
|
||||||
|
digitally represented object within an MCM environment that holds functional or economic value, including but not limited to avatars, virtual goods, and environmental content
|
||||||
|
|
||||||
|
#### 3.1.8 Quality of Experience (QoE)
|
||||||
|
degree to which a user perceives the overall performance and suitability of an MCM service, encompassing technical, contextual, and perceptual dimensions
|
||||||
|
|
||||||
|
#### 3.1.9 AI agent
|
||||||
|
autonomous software entity that participates in an MCM environment, capable of perceiving context, generating responses, and interacting with human participants in real time
|
||||||
|
|
||||||
|
### 3.2 Abbreviated terms
|
||||||
|
|
||||||
|
| Abbreviation | Term |
|
||||||
|
|---|---|
|
||||||
|
| AI | Artificial intelligence |
|
||||||
|
| AR | Augmented Reality |
|
||||||
|
| DID | Decentralized identifier |
|
||||||
|
| IoT | Internet of Things |
|
||||||
|
| MCM | Multilateral and Collaborative Metaverse |
|
||||||
|
| MR | Mixed Reality |
|
||||||
|
| NPC | Non-Player Character |
|
||||||
|
| QoE | Quality of Experience |
|
||||||
|
| QoS | Quality of Service |
|
||||||
|
| VR | Virtual Reality |
|
||||||
|
| WoT | Web of Things |
|
||||||
|
| XR | Extended reality |
|
||||||
|
|
||||||
|
## 4 General Considerations for Multilateral and Collaborative Metaverse Systems
|
||||||
|
|
||||||
|
### 4.1 Overview of MCM Systems
|
||||||
|
|
||||||
|
#### 4.1.1 Definition and Concept of MCM
|
||||||
|
|
||||||
|
The metaverse encompasses digitally mediated virtual environments in which users, represented directly or indirectly through digital identities or avatars, participate in activities within a virtual space. These activities include individual content consumption, social interaction, entertainment, commerce, education, industrial operations, and other forms of digitally mediated experience. Within this broader concept, Multilateral and Collaborative Metaverse (MCM) systems constitute a distinct category of metaverse systems in which purposeful and coordinated collaboration among two or more simultaneous participants is the primary objective and an essential condition of service delivery.
|
||||||
|
|
||||||
|
An MCM service provides a shared virtual environment in which participants perceive and interact with a common context, including other participants, digital objects, information resources, and environmental states. Participants perform coordinated activities toward a shared or mutually aligned objective. Such activities include joint decision-making, co-creation, learning, training, consultation, design, operation, performance, service provision, or public participation. In the context of MCM, the term “multilateral” refers to the involvement of two or more interacting participants whose actions can affect a shared session, shared objects, or other participants. The term “collaborative” refers to coordinated interaction directed toward a common activity, objective, or outcome. Collaborative participation does not require all participants to perform identical functions. Participants have different roles, responsibilities, access privileges, or levels of authority within the same MCM session.
|
||||||
|
|
||||||
|
MCM is defined primarily from a service perspective and is not dependent on a particular device, platform, degree of immersion, or implementation architecture. An MCM service is accessed through head-mounted displays, personal computers, mobile devices, spatial computing devices, or other multimedia systems and equipment. Similarly, the use of extended reality, avatars, spatial audio, artificial intelligence, digital twins, or other enabling technologies does not by itself constitute an MCM service. The presence of multiple users alone is insufficient to classify a metaverse service as MCM. Multi-user environments in which interaction is incidental, loosely connected, exclusively competitive, or primarily based on independent content consumption do not fall within the conceptual boundary of MCM. Competitive or individual activities are included in an MCM service, provided that they form part of a broader collaborative process and that the primary service objective depends on coordinated participation. Accordingly, the defining condition of an MCM system is that the intended collaborative function or service outcome cannot be fully realized through the participation of a single user acting independently.
|
||||||
|
|
||||||
|
#### 4.1.2 Position of MCM within the Metaverse Ecosystem
|
||||||
|
|
||||||
|
MCM systems form a subset of the broader metaverse ecosystem. They do not represent a separate type of virtual environment defined by a specific platform or technology. Rather, they represent a service-oriented category distinguished by the structural role of collaboration in the design and operation of the service. General metaverse services support multiple users, social interaction, avatar communication, shared events, commerce, or content creation. However, such services remain operable for an individual user, and interaction with other users may be optional, incidental, or secondary to the principal service objective. By contrast, an MCM system is designed around a shared collaborative session. Participants interact within a common virtual environment, maintain awareness of shared activities and states, and contribute to a collaborative process or outcome. The service value arises primarily from coordinated interaction among participants rather than from the independent experience of each participant.
|
||||||
|
|
||||||
|
MCM systems are related to several adjacent service and technology areas, including virtual worlds, immersive communication, collaborative computing, digital twin environments, online multiplayer services, and conventional video collaboration systems. However, no single one of these areas is equivalent to MCM. MCM is distinguished by the combined presence of the following conditions:
|
||||||
|
|
||||||
|
- two or more simultaneous participants;
|
||||||
|
- a shared virtual environment or shared digitally mediated context;
|
||||||
|
- coordinated interaction among participants;
|
||||||
|
- a common or mutually aligned objective; and
|
||||||
|
- a service function or outcome that depends on multilateral participation.
|
||||||
|
|
||||||
|
Table 1 summarizes the principal differences between general metaverse services and MCM systems.
|
||||||
|
|
||||||
|
**Table 1. Comparison of general metaverse services and MCM Systems**
|
||||||
|
|
||||||
|
| Attribute | General Metaverse | Multilateral and Collaborative Metaverse |
|
||||||
|
|---|---|---|
|
||||||
|
| Primary service objective | Individual experience, content consumption, social interaction, entertainment, or other activities are performed independently | Purposeful collaboration and coordinated participation toward a shared activity, objective, or outcome |
|
||||||
|
| Participation structure | Support either individual or multiple user participation | Requires two or more simultaneous participants for the collaborative function |
|
||||||
|
| Role of interaction | Interaction with other users is optional, incidental, social, or competitive | Interaction among participants is integral to service delivery |
|
||||||
|
| Shared context | Users occupy the same environment without sharing a common task, state, or objective | Users share an environment, task context, resources, objects, or service state |
|
||||||
|
| Participant roles | Roles are undefined or unrelated to the service outcome | Participants have differentiated but coordinated roles, responsibilities, permissions, or authority |
|
||||||
|
| Service operability | Generally used or experienced by a single user | Generally used or experienced by multi-users |
|
||||||
|
| Value creation | Generated primarily through individual experience or optional interaction | Generated through the collaborative process, coordinated activity, or shared result |
|
||||||
|
|
||||||
|
As shown in Table 1, general metaverse services provide shared virtual spaces and support interaction among multiple users without requiring coordinated participation. Individual users independently consume content, explore virtual environments, create digital items, or engage in optional social interaction, while the principal service remains available regardless of the participation of others. In an MCM system, participants are connected through a shared activity, task, or service process. Their actions occur within a common context and influence shared objects, environmental states, other participants, or the outcome of the collaborative session. Participants assume different roles, responsibilities, permissions, and levels of authority, while these differentiated roles remain coordinated toward a common or mutually aligned objective. The participation structure and service operability identified in Table 1 provide the principal basis for distinguishing MCM systems from general metaverse services. A service qualifies as an MCM service when its intended function, collaborative process, or shared outcome depends on the simultaneous and coordinated participation of two or more participants. The mere presence of multiple users within the same virtual environment does not satisfy this condition.
|
||||||
|
|
||||||
|
The conceptual boundary established in this subclause provides the basis for the fundamental characteristics described in 4.2, the representative service domains described in 4.3, and the multidimensional classification framework specified in Clause 5.
|
||||||
|
|
||||||
|
|
||||||
|
### 4.2 Fundamental Characteristics of MCM Systems
|
||||||
|
|
||||||
|
MCM systems exhibit a set of common characteristics related to the structure of collaborative participation, the organization of shared activities, and the continuity of interaction within a shared virtual environment. These characteristics apply across the representative service domains described in 4.3, although their relative importance and detailed realization differ according to the purpose and operational context of each domain.
|
||||||
|
|
||||||
|
#### 4.2.1 Collaboration-centered Service Objectives
|
||||||
|
|
||||||
|
An MCM service is organized around a collaborative objective that connects the activities of all participants within a shared session. The objective represents a common activity, task, process, or outcome toward which participants contribute through coordinated interaction.
|
||||||
|
|
||||||
|
Collaborative objectives include joint decision-making, co-creation, instruction and learning, consultation, design review, operational coordination, performance, and public participation. The specific objective differs across service domains, but the collaborative process remains central to the operation and value of the service. The collaborative objective also provides the basis for the organization of participant roles, interaction procedures, shared resources, and session activities. Individual actions acquire meaning through their relationship with the actions of other participants and with the shared objective. Interaction that remains optional, incidental, or unrelated to the principal service outcome does not constitute collaboration in the context of MCM.
|
||||||
|
|
||||||
|
#### 4.2.2 Simultaneous Multilateral Participation
|
||||||
|
|
||||||
|
MCM services involve the simultaneous participation of two or more participants within a shared virtual session. Simultaneous participation refers to overlapping active presence during which participants exchange information, observe actions, and adjust their own activities in response to others. The number of participants varies according to the service domain and use case. An MCM session ranges from a small collaborative group, such as a medical consultation or design review, to a large-scale session, such as a virtual public event or civic consultation.
|
||||||
|
|
||||||
|
The participant group consists primarily of human participants and, in certain services, includes AI agents that perform collaborative or supporting functions. Regardless of participant type, each active participant has an identifiable relationship with the shared activity and with other participants in the session. Asynchronous activities, including preparation, review, recording, and follow-up, form supplementary elements of an MCM service. The principal collaborative phase, however, depends on simultaneous interaction among multiple participants.
|
||||||
|
|
||||||
|
#### 4.2.3 Shared Virtual Context
|
||||||
|
|
||||||
|
MCM participants interact within a shared virtual context that provides a common basis for collaborative activity. The shared virtual context includes the virtual environment, participant representations, digital objects, information resources, task status, and other session-related elements perceived or accessed by participants. A shared virtual context does not require identical visual presentation for every participant. Views and interaction interfaces differ according to participant role, access device, permissions, or service function.
|
||||||
|
|
||||||
|
Despite these differences, participants retain a consistent understanding of the shared environment, collaborative activity, and relevant object or task states. The shared context supports situational awareness by enabling participants to recognize where an activity takes place, who participates, which objects or information are involved, and how the collaborative process progresses. This common understanding distinguishes an MCM session from independent user activities that occur within the same platform without a shared task or coordinated context.
|
||||||
|
|
||||||
|
#### 4.2.4 Coordinated Roles and Interdependent Activities
|
||||||
|
|
||||||
|
Participants in an MCM system perform roles associated with the collaborative objective. These roles define relationships among participants and structure responsibilities, permissions, authority, access to information, and control over shared resources. Participant roles differ according to the service domain. Examples include employer and employee in work environments, instructor and learner in education, performer and audience member in entertainment, healthcare professional and patient in medical services, operator and remote expert in industrial environments, and public official and citizen in public services.
|
||||||
|
|
||||||
|
Different roles do not imply equal functions or identical access. MCM collaboration involves coordinated contributions in which participants perform complementary activities toward a shared objective. The output or action of one participant influences the activities, decisions, or available information of other participants. Role relationships also change during a session. A participant transitions between observing, presenting, controlling, assisting, approving, or performing functions according to the progression of the collaborative activity. Clear representation of roles and role transitions supports coherent interaction among participants.
|
||||||
|
|
||||||
|
#### 4.2.5 Real-time Reciprocal Interaction
|
||||||
|
|
||||||
|
MCM systems support reciprocal interaction in which participants exchange information and respond to one another during the shared session. Reciprocal interaction includes verbal communication, non-verbal expression, manipulation of shared objects, navigation within the virtual environment, and actions related to the shared task. Real-time interaction refers to a level of temporal responsiveness that preserves the continuity and meaning of collaborative activity. It does not imply the complete absence of transmission or processing delay. The acceptable level of responsiveness depends on the interaction type and service context. Conversation, collaborative object manipulation, remote operation, live performance, and clinical interaction present different temporal sensitivities.
|
||||||
|
|
||||||
|
Reciprocal interaction also involves mutual influence. A participant action changes the shared context, produces information for other participants, or affects the progression of the collaborative process. Other participants recognize the action and respond within the temporal conditions of the service.
|
||||||
|
|
||||||
|
#### 4.2.6 Coherence of Shared Session State
|
||||||
|
|
||||||
|
An MCM session maintains a coherent shared state across participating users, devices, and service components. The shared session state includes participant presence, role status, object state, task progress, environmental conditions, interaction history, and other information relevant to the collaborative activity.
|
||||||
|
|
||||||
|
State coherence does not require every participant device to render the virtual environment in an identical manner. It requires consistent interpretation of the meaning and effect of shared actions and objects. For example, when one participant moves, modifies, assigns, approves, or removes a shared object, other participants receive a corresponding representation of that change appropriate to their roles and interfaces. Loss of state coherence results in different understandings of the same collaborative situation. Such inconsistency disrupts coordination, reduces trust in the shared environment, and affects the validity of collaborative outcomes.
|
||||||
|
|
||||||
|
#### 4.2.7 Participant Presence and Mutual Awareness
|
||||||
|
|
||||||
|
MCM services provide participants with awareness of the presence, identity, role, and activity of other participants. Mutual awareness enables participants to interpret who is present, who is speaking or acting, where attention is directed, and how individual actions relate to the collaborative process. Participant presence is represented through avatars, video representations, digital humans, spatial indicators, cursors, status information, or other forms of digital representation. The form and fidelity of representation differ according to the service domain, media configuration, access device, and interaction objective.
|
||||||
|
|
||||||
|
Mutual awareness extends beyond the visual representation of participants. It includes awareness of participant availability, location, focus, current activity, communication status, and relationship to shared objects or tasks. This awareness supports coordination and reduces ambiguity during multilateral interaction. Non-verbal information, including gesture, posture, gaze direction, facial expression, and spatial orientation, contributes to mutual awareness in services where such information carries collaborative meaning. The significance of each form of non-verbal information differs across domains and use cases.
|
||||||
|
|
||||||
|
#### 4.2.8 Continuity of Collaborative Participation
|
||||||
|
|
||||||
|
MCM systems preserve the continuity of collaborative participation throughout the lifecycle of a session. Continuity includes the association of each participant with an identity, role, permission set, interaction state, and contribution to the shared activity. Participants enter, leave, reconnect to, or transition between access environments during an MCM session. The collaborative context retains relevant information concerning participant roles, shared objects, task progress, and previous actions throughout these transitions. Continuity also applies to collaborative outcomes that remain available after the real-time session. Records, jointly created content, decisions, annotations, digital assets, and task results form part of the service context where persistence supports subsequent activities.
|
||||||
|
|
||||||
|
The scope and duration of continuity differ across service domains. A live entertainment event emphasizes continuity during the active session, while education, industrial design, healthcare, and public administration often involve persistent records and collaborative processes extending across multiple sessions.
|
||||||
|
|
||||||
|
|
||||||
|
### 4.3 Representative MCM Service Domains
|
||||||
|
|
||||||
|
MCM services appear across different social, economic, institutional, and industrial contexts. For the purposes of this document, service domains are distinguished according to the principal collaborative objective, the operational context of the activity, the roles and relationships of participants, and the type of shared outcome produced through the session. The domain classification focuses on the purpose of the service rather than on the device, platform, media configuration, or enabling technology used for implementation. The same technical environment supports services in different domains, while services within one domain use different technical configurations.
|
||||||
|
|
||||||
|
The domains in this subclause are representative rather than exhaustive. A service involving more than one operational context is treated as a cross-domain MCM service. Its primary domain corresponds to the principal collaborative objective, while additional domains describe complementary activities or service functions. This approach preserves a consistent basis for the domain-oriented requirements without restricting emerging MCM services to a fixed industry structure.
|
||||||
|
|
||||||
|
This document identifies the following representative domains:
|
||||||
|
|
||||||
|
- professional work and organizational collaboration;
|
||||||
|
- education and training;
|
||||||
|
- healthcare and care services;
|
||||||
|
- industrial engineering and operations;
|
||||||
|
- commerce and customer services;
|
||||||
|
- public and civic services; and
|
||||||
|
- entertainment, culture, and creative participation.
|
||||||
|
|
||||||
|
#### 4.3.1 Professional Work and Organizational Collaboration
|
||||||
|
|
||||||
|
The professional work and organizational collaboration domain covers MCM services that support coordinated activities among participants engaged in professional, administrative, research, or organizational work. The principal objective is the production of a shared work result, decision, plan, design, or organizational action.
|
||||||
|
|
||||||
|
Representative activities include virtual meetings, collaborative planning, document and content development, brainstorming, design discussion, research collaboration, professional consultation, project coordination, onboarding, and distributed teamwork. Participants include employees, managers, researchers, consultants, clients, external partners, and AI agents performing facilitation or assistance functions.
|
||||||
|
|
||||||
|
The shared virtual context contains work-related information, common resources, task status, participant roles, and jointly produced outputs. The distinction from conventional communication services lies in the dependence of the service outcome on coordinated work within the shared context rather than on communication alone.
|
||||||
|
|
||||||
|
#### 4.3.2 Education and Training
|
||||||
|
|
||||||
|
The education and training domain covers MCM services that support instruction, collaborative learning, practice, assessment, and skill development. The principal objective is the acquisition, application, or evaluation of knowledge and competence through interaction among instructors, learners, peers, assessors, and supporting agents.
|
||||||
|
|
||||||
|
Representative activities include virtual classrooms, collaborative laboratories, simulation-based training, procedural rehearsal, technical instruction, group problem-solving, mentoring, role-playing, and guided practice. The shared virtual context provides learning resources, simulated objects or environments, task progress, demonstrations, and feedback.
|
||||||
|
|
||||||
|
This domain includes formal education, vocational training, professional development, and safety or operational training. Services that reproduce a workplace or industrial process for learning purposes remain within this domain when learning and competence development represent the principal objective.
|
||||||
|
|
||||||
|
#### 4.3.3 Healthcare and Care Services
|
||||||
|
|
||||||
|
The healthcare and care services domain covers MCM services that support clinical, therapeutic, rehabilitative, preventive, and care-related collaboration. The principal objective is the assessment, treatment, support, coordination, or improvement of an individual or group health condition.
|
||||||
|
|
||||||
|
Representative activities include multidisciplinary consultation, remote clinical assessment, collaborative treatment planning, rehabilitation, group therapy, clinical case review, patient education, caregiver coordination, and medical simulation associated with care delivery. Participants include healthcare professionals, patients, caregivers, technicians, specialists, and AI-based support agents.
|
||||||
|
|
||||||
|
The shared virtual context contains health-related information, participant observations, treatment or care activities, virtual representations, and records of collaborative decisions. Differentiated professional authority, restricted information access, participant consent, and continuity across related sessions form central aspects of this domain.
|
||||||
|
|
||||||
|
#### 4.3.4 Industrial Engineering and Operations
|
||||||
|
|
||||||
|
The industrial engineering and operations domain covers MCM services associated with the design, production, operation, inspection, maintenance, and lifecycle management of products, facilities, equipment, and technical systems. The principal objective is the coordinated execution or improvement of an engineering or operational process.
|
||||||
|
|
||||||
|
Representative activities include collaborative design review, virtual prototyping, factory and facility planning, remote expert assistance, equipment maintenance, process simulation, quality inspection, operational coordination, and interaction with digital twins. Participants include engineers, designers, operators, technicians, inspectors, managers, suppliers, and AI-based operational agents.
|
||||||
|
|
||||||
|
The shared virtual context often represents physical assets, operational conditions, technical data, and task states. Relationships between virtual actions and physical processes distinguish this domain from general professional collaboration. Industrial training remains classified under education and training when learning represents the principal service objective.
|
||||||
|
|
||||||
|
#### 4.3.5 Commerce and Customer Services
|
||||||
|
|
||||||
|
The commerce and customer services domain covers MCM services that support commercial consultation, product or service exploration, configuration, evaluation, negotiation, and transaction-related decision-making. The principal objective is the collaborative exchange of commercial information or the completion of a customer-oriented service process.
|
||||||
|
|
||||||
|
Representative activities include virtual showrooms, assisted shopping, collaborative product configuration, property or facility tours, live product demonstrations, customer consultation, group purchasing, and professional service delivery. Participants include customers, sales representatives, product specialists, service providers, advisors, designers, and AI-based customer service agents.
|
||||||
|
|
||||||
|
The shared virtual context contains product or service representations, configuration states, preferences, annotations, recommendations, and transaction-related information. This domain emphasizes the coordinated relationship between customers and service providers, or among multiple customers participating in a shared decision.
|
||||||
|
|
||||||
|
#### 4.3.6 Public and Civic Services
|
||||||
|
|
||||||
|
The public and civic services domain covers MCM services that support public administration, civic participation, community consultation, institutional coordination, and public service delivery. The principal objective is the execution of a public function or the participation of stakeholders in a civic or administrative process.
|
||||||
|
|
||||||
|
Representative activities include virtual public hearings, participatory planning, administrative consultation, emergency response coordination, inter-agency collaboration, public education, community engagement, and review of public proposals. Participants include public officials, civil servants, citizens, community representatives, experts, emergency personnel, and AI-based public service agents.
|
||||||
|
|
||||||
|
The shared virtual context contains public information, proposals, service records, spatial or administrative data, stakeholder input, and records of collective activity. Accessibility, inclusion, representation of authority, transparency of process, and traceability of decisions form significant aspects of this domain.
|
||||||
|
|
||||||
|
#### 4.3.7 Entertainment, Culture, and Creative Participation
|
||||||
|
|
||||||
|
The entertainment, culture, and creative participation domain covers MCM services that support shared performance, recreation, cultural engagement, artistic creation, and interaction with cultural heritage. The principal objective is the production or experience of entertainment, cultural, or creative value through coordinated participation.
|
||||||
|
|
||||||
|
Representative activities include virtual concerts, interactive performances, collaborative games, participatory storytelling, fan events, virtual exhibitions, guided museum or heritage experiences, collaborative art creation, rehearsals, and cultural workshops. Participants include performers, creators, audiences, curators, educators, researchers, moderators, and AI-driven characters, guides, or creative agents.
|
||||||
|
|
||||||
|
This domain combines entertainment and culture because both rely on participatory experience, creative expression, performance, interpretation, and audience engagement within a shared context. A service based solely on independent content consumption or adversarial activity without a collaborative objective remains outside the MCM boundary. Competitive elements remain relevant where they form part of team cooperation, collective production, or a shared participatory event.
|
||||||
|
|
||||||
|
|
||||||
|
### 4.4 Technical Considerations for MCM Systems
|
||||||
|
|
||||||
|
MCM systems combine multiple participants, media types, devices, platforms, and service components within a shared collaborative environment. Their technical configuration differs according to the service domain, collaborative objective, participant structure, media configuration, and operational environment.
|
||||||
|
|
||||||
|
This subclause identifies common technical consideration areas associated with MCM systems. These areas provide a general reference for understanding the technical aspects of MCM and for organizing the classifications and subsequent parts of this series. They do not define a specific implementation architecture, technology, or performance level.
|
||||||
|
|
||||||
|
#### 4.4.1 Heterogeneous Access and Participant Representation
|
||||||
|
|
||||||
|
Participants in an MCM session access the shared environment through different types of devices and interfaces, including immersive and non-immersive systems. The form of participant representation also differs according to the service context and access environment.
|
||||||
|
|
||||||
|
Despite differences in devices and representations, participants retain a consistent relationship with the shared session, including their identity, role, activity, and interaction with shared resources. Heterogeneous access therefore represents an important consideration for maintaining a coherent collaborative experience across different participation environments.
|
||||||
|
|
||||||
|
#### 4.4.2 Multimodal Interaction and Temporal Coordination
|
||||||
|
|
||||||
|
MCM services involve combinations of media and interaction information, including audio, video, participant representations, gestures, spatial information, shared objects, and other forms of digital content.
|
||||||
|
|
||||||
|
Collaborative interaction depends not only on the individual quality of each media element but also on the temporal relationship among them. Appropriate coordination among communication, participant actions, and changes in the shared environment supports consistent interpretation of collaborative activities.
|
||||||
|
|
||||||
|
#### 4.4.3 Shared state and Data Consistency
|
||||||
|
|
||||||
|
Participants in an MCM session interact with shared information, objects, tasks, and environmental states. Actions performed by one participant influence the collaborative context experienced by other participants.
|
||||||
|
|
||||||
|
Consistency of shared state supports a common understanding of participant activities, object conditions, task progress, and collaborative outcomes. The representation of a shared state differs across devices or participant roles, while the meaning and effect of the state remain consistent within the collaborative context.
|
||||||
|
|
||||||
|
#### 4.4.4 Identity, Roles, and Access
|
||||||
|
|
||||||
|
MCM services involve participants with different identities, roles, responsibilities, and levels of authority. Participant roles influence access to information, shared objects, service functions, and collaborative activities.
|
||||||
|
|
||||||
|
Identity and role information therefore form part of the technical context of an MCM session. This context also includes participant authentication, access control, role transitions, ownership relationships, and the association between participants and their activities within the shared environment. The detailed significance of these elements differs according to the service domain and collaborative objective.
|
||||||
|
|
||||||
|
#### 4.4.5 Interoperability and Service Continuity
|
||||||
|
|
||||||
|
MCM environments involve heterogeneous platforms, devices, service components, and digital resources. Interoperability supports coherent interaction among these elements and contributes to continuity of collaborative participation across different technical environments.
|
||||||
|
|
||||||
|
In the context of MCM, interoperability extends beyond the exchange of individual data elements. It also relates to continuity of participant identity, roles, shared objects, interaction context, session information, and collaborative activities. The scope of interoperability differs according to the service configuration and forms an important reference area for the classification and gap analysis presented in subsequent clauses.
|
||||||
|
|
||||||
|
#### 4.4.6 Distributed Processing and Service Scalability
|
||||||
|
|
||||||
|
MCM services involve processing and communication functions distributed across participant devices, network resources, edge systems, cloud environments, and service platforms. These functions include media processing, shared-state management, rendering, session management, and other operations associated with collaborative interaction.
|
||||||
|
|
||||||
|
The distribution of these functions influences responsiveness, service continuity, and the number and geographical distribution of participants supported within an MCM environment. Different service domains present different processing and scalability characteristics according to their collaborative activities and media configurations.
|
||||||
|
|
||||||
|
#### 4.4.7 AI-assisted and Autonomous Participation
|
||||||
|
|
||||||
|
Artificial intelligence performs various functions within MCM environments. AI-based functions include assistance with communication, content generation, information processing, interaction support, and management of collaborative activities.
|
||||||
|
|
||||||
|
AI agents also participate directly in an MCM session as identifiable entities associated with specific roles or functions. The involvement of AI introduces additional considerations regarding participant representation, role assignment, interaction context, authority, and distinction between human and AI participants. The role and level of AI involvement differ according to the collaborative objective and service domain.
|
||||||
|
|
||||||
|
#### 4.4.8 Physical-digital Integration
|
||||||
|
|
||||||
|
Some MCM services incorporate information and objects representing physical environments, assets, devices, or operational processes. Such integration connects collaborative activities in the virtual environment with states or events originating from physical environments.
|
||||||
|
|
||||||
|
Digital twins, sensing systems, connected devices, and spatial information represent examples of elements associated with physical-digital integration. Their relevance varies across domains, with stronger relationships appearing in industrial operations, healthcare, public services, and other services involving physical environments or assets. Physical-digital integration also supports cross-domain MCM scenarios in which participants interact with shared representations of real-world environments and systems.
|
||||||
|
|
||||||
|
#### 4.4.9 Trust, Security, Privacy, and Safety
|
||||||
|
|
||||||
|
MCM services involve interactions among multiple participants and the exchange of participant, service, media, and environmental information. The significance of trust, security, privacy, and safety differs according to the service domain, participant relationship, type of information, and effect of collaborative activities.
|
||||||
|
|
||||||
|
Relevant considerations include protection of participant and service information, reliability of identity and role information, integrity of shared states and collaborative outcomes, and management of risks arising from interactions between participants, AI agents, digital resources, and physical environments. These aspects represent cross-cutting considerations across MCM service domains rather than characteristics of a particular implementation technology.
|
||||||
|
|
||||||
|
The technical consideration areas described in 4.4 provide a general reference for the classification of MCM services in Clause 5. Individual MCM services combine these areas in different ways according to their domain, media configuration, enabling technologies, and collaborative objectives. Detailed technical and service aspects are addressed in subsequent parts of this series according to their respective scopes.
|
||||||
|
|
||||||
|
## 5 Classification of MCM Services
|
||||||
|
|
||||||
|
### 5.1 Classification Framework Overview
|
||||||
|
|
||||||
|
As described in Clause 4, MCM services span a heterogeneous landscape across application domains, media types, and enabling technologies. MCM systems are applied across multiple domains, combine different media types, and depend on several technology areas. This diversity introduces challenges for standardization. A single set of requirements is not sufficient to address the range of operational contexts, media configurations, and technical architectures associated with MCM services.
|
||||||
|
|
||||||
|
The operational constraints that determine service adequacy differ substantially between MCM domains. An immersive virtual concert distributes high-fidelity audio and visual media to a large audience, where sustained media quality, rendering fidelity, and scalability of concurrent participation govern the participant experience. A remote medical consultation involves a small number of participants, where end-to-end latency, temporal precision, and fidelity of clinical detail govern the validity of the collaborative outcome. Both are MCM services under the conditions established in Clause 4, and both depend on simultaneous multilateral participation, yet the requirements that determine their adequacy are not the same. As noted in 4.2.5, live performance and clinical interaction present different temporal sensitivities. A single undifferentiated set of requirements therefore either constrains one domain beyond its operational need or leaves the other insufficiently specified. Classification by domain of application allows requirements to be expressed at the level at which these constraints actually differ.
|
||||||
|
|
||||||
|
Participants access a shared MCM session through heterogeneous terminals, including head-mounted displays, personal computers, mobile devices, and spatial computing devices. As described in 4.2.3 and 4.2.6, participants do not require identical presentation of the shared environment, but they do require consistent interpretation of shared actions, objects, and session state. Interoperability across these terminals therefore depends not only on the mapping of capabilities between devices, but also on common baseline constraints that bound the permissible variation in timing, media quality, representation of participants, and interaction semantics. Where such constraints are absent, differences between access environments propagate into the collaborative process itself and produce divergent understandings of the same session. These constraints are not uniform across MCM services; they depend on the domain, media configuration, and enabling technologies involved.
|
||||||
|
|
||||||
|
This clause introduces a classification framework to support the identification of standardization requirements and gaps. The framework organizes MCM services along three dimensions: domain of application, media type, and enabling technology. The framework serves the following purposes within this Technical Report and the IEC TR series.
|
||||||
|
|
||||||
|
**Scope definition**
|
||||||
|
- The classification provides a structured basis for defining the scope of MCM services and their relation to adjacent technology domains. It distinguishes MCM services from adjacent forms of digital interaction, including conventional metaverse services and non-immersive collaboration systems. The classification provides a consistent basis for determining the applicability of standardization activities across different MCM service contexts.
|
||||||
|
|
||||||
|
**Analytical reference**
|
||||||
|
- The classification provides a structured vocabulary and framework for describing and comparing MCM services across contexts. It supports the identification of common requirements across categories and category-specific requirements within particular domains, media types, or technologies.
|
||||||
|
|
||||||
|
**Gap analysis foundation**
|
||||||
|
- The classification dimensions define the basis for identifying and assessing gaps in existing international standards, as described in Clause 6. Each gap identified in 6.2 is mapped to one or more classification categories. This mapping supports systematic evaluation of limitations in current standards relative to MCM service requirements.
|
||||||
|
|
||||||
|
**Requirements structuring**
|
||||||
|
- The classification informs the structure of subsequent parts in this series. Part 2 (Service Requirements) follows the domain classification defined in 5.2. Part 3 (Media Requirements) follows the media classification defined in 5.3.
|
||||||
|
|
||||||
|
Classification by domain alone does not account for convergence between MCM services. Services in different domains draw on the same enabling technologies, and capabilities developed in one domain are transferable to others, as noted in 4.3.3.3. Classification by enabling technology, applied in parallel with the domain and media dimensions, identifies requirements that are common across domains and avoids the duplication that would result from specifying each domain independently. The technology dimension defined in 5.4 therefore complements the domain and media dimensions rather than subdividing them.
|
||||||
|
|
||||||
|
The classification in this clause is informative and analytical. It serves as a reference framework and does not define a normative taxonomy. Category boundaries are not fixed. MCM services span multiple domains, media types, and enabling technologies. The three dimensions are treated as complementary perspectives for analysis.
|
||||||
|
|
||||||
|
### 5.2 Domain-based Classification
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### 5.3 Media-based Classification
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### 5.4 Technology-based Classification
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
## 6 Gap Analysis
|
||||||
|
|
||||||
|
### 6.1 Current international Standardization Activities
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### 6.2 Gaps in Existing Standards
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### 6.3 Recommendations for Future Standardization
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
## Annex A (informative) Roadmap for MCM Standard Development
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Appendix Z — IEC Word Template Scaffolding (not part of the Technical Report)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This section contains the IEC Word template guidelines and unselected boilerplate options
|
||||||
|
isolated from the main text in accordance with Policy D1'.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Z.1 How to use this document (source: p.1–2)
|
||||||
|
|
||||||
|
This document provides you with the general structure of an IEC publication and some information about individual elements. Links to more detailed information can be found in the individual sections.
|
||||||
|
Basic formatting instructions are provided for Word 2007 and later.
|
||||||
|
|
||||||
|
**Typographical conventions in this template**
|
||||||
|
- Content in red needs to be provided by you. Please revert the font colour to black once you have adapted it.
|
||||||
|
- Content in black is boilerplate text and cannot be modified.
|
||||||
|
- Additional explanations appear in blue with a red border; a special style (IEC INSTRUCTIONS) was created for this purpose.
|
||||||
|
|
||||||
|
To format text, use the styles from the IEC template.
|
||||||
|
Most of those required are available in the IEC tools tab.
|
||||||
|
Others can be accessed from Word's Styles panel – consult the user guide to the IEC template for more information.
|
||||||
|
Download the user guide for in-depth instructions and brief video tutorials (in progress).
|
||||||
|
|
||||||
|
**Table of contents guidance**
|
||||||
|
- To update the table of contents below, click on it and press F9. Do the same for the list of figures and the list of tables.
|
||||||
|
- To create a new table of contents, delete the old one and click on Table of contents in the IEC tools tab.
|
||||||
|
- If updating the table of contents produces an error message, please use Word's built-in feature to create a table of contents (in the Reference tab, click on Table of contents and select one you like). At CDV stage, we will insert the correct table at the IEC.
|
||||||
|
|
||||||
|
### Z.2 Unselected Clause 3 boilerplate variants (source: p.7–8)
|
||||||
|
|
||||||
|
> Note: The document author selected Variant ① (Standard boilerplate text) for Clause 3 because the TR defines 9 internal terms and does not cite an external terms document. Variants ② and ③ were left unselected in the template.
|
||||||
|
|
||||||
|
**Variant ② — Terms and definitions listed in a different document:**
|
||||||
|
|
||||||
|
For the purposes of this document, the terms and definitions given in [external document reference] apply.
|
||||||
|
|
||||||
|
ISO and IEC maintain terminology databases for use in standardization at the following addresses:
|
||||||
|
- IEC Electropedia: available at https://www.electropedia.org/
|
||||||
|
- ISO Online browsing platform: available at https://www.iso.org/obp
|
||||||
|
|
||||||
|
**Variant ③ — Terms and definitions listed in a different document and in this document:**
|
||||||
|
|
||||||
|
For the purposes of this document, the terms and definitions given in [external document reference] and the following apply.
|
||||||
|
|
||||||
|
ISO and IEC maintain terminology databases for use in standardization at the following addresses:
|
||||||
|
- IEC Electropedia: available at https://www.electropedia.org/
|
||||||
|
- ISO Online browsing platform: available at https://www.iso.org/obp
|
||||||
|
|
||||||
|
### Z.3 Authoring instruction (source: p.8)
|
||||||
|
|
||||||
|
To insert terms and definitions, use the Insert term form from the IEC tools tab.
|
||||||
|
If the tab does not appear on your screen, make sure the IEC template is attached to your working document, and consult the user guide.
|
||||||
File diff suppressed because one or more lines are too long
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# generate-env.sh — create a local .mam.env from the committed .mam.env.example template.
|
||||||
|
#
|
||||||
|
# Behaviour:
|
||||||
|
# - .mam.env absent → copy .mam.env.example to .mam.env, print the path.
|
||||||
|
# - .mam.env present → no-op (leaves your edits intact), exit 0.
|
||||||
|
# - .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 deploy/),
|
||||||
|
# so it works regardless of the caller's cwd.
|
||||||
|
#
|
||||||
|
# Usage: deploy/generate-env.sh [--force] [--migrate-legacy] [-h|--help]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SRC="$REPO_ROOT/.mam.env.example"
|
||||||
|
DST="$REPO_ROOT/.mam.env"
|
||||||
|
LEGACY_ENV="$REPO_ROOT/.env"
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
MIGRATE=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--force) FORCE=1; shift ;;
|
||||||
|
--migrate-legacy) MIGRATE=1; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--force] [--migrate-legacy]"
|
||||||
|
echo " Create .mam.env from .mam.env.example."
|
||||||
|
echo " --force overwrites an existing .mam.env (backs up to .mam.env.bak)."
|
||||||
|
echo " --migrate-legacy explicitly renames an existing legacy .env to .mam.env."
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "ERROR: unknown arg: $1" >&2; echo "Usage: $0 [--force] [--migrate-legacy]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$MIGRATE" = "1" ]; then
|
||||||
|
if [ -f "$DST" ]; then
|
||||||
|
echo "ERROR: cannot migrate: $DST already exists." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$LEGACY_ENV" ]; then
|
||||||
|
echo "ERROR: cannot migrate: legacy file $LEGACY_ENV not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mv -f "$LEGACY_ENV" "$DST"
|
||||||
|
chmod 0600 "$DST" 2>/dev/null || true
|
||||||
|
echo "migrated: $LEGACY_ENV -> $DST"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$SRC" ] || { echo "ERROR: template not found: $SRC" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ -f "$DST" ] && [ "$FORCE" != "1" ]; then
|
||||||
|
echo "no-op: $DST already exists (use --force to overwrite)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$DST" ] && [ "$FORCE" = "1" ]; then
|
||||||
|
cp -p "$DST" "$DST.bak"
|
||||||
|
echo "backed up existing .mam.env -> $DST.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$SRC" "$DST"
|
||||||
|
chmod 0600 "$DST" 2>/dev/null || true
|
||||||
|
echo "created: $DST"
|
||||||
|
echo "Next: edit $DST and fill in any secrets (look for 'replace_me')."
|
||||||
Reference in New Issue
Block a user