feat: implement Research Project PageView (v1.8) with static SSG pre-render and E2E gates

This commit is contained in:
2026-07-30 21:54:22 +09:00
parent 66fae1ebec
commit 1c772ae091
10 changed files with 623 additions and 21 deletions
+7 -7
View File
@@ -21,30 +21,30 @@
```
refer_landing_page/
├── app/
│ ├── _components/ # 서버/클라이언트 전용 컴포넌트 (ProjectPageView, ProjectPageViewControls)
│ ├── layout.tsx # 루트 레이아웃 (Header/Footer, 폰트, 메타데이터)
│ ├── globals.css # Tailwind 지시문 + 공통 유틸 클래스
│ ├── page.tsx # Home 라우트 (연구실 소개, 5대 기둥 3+2 매거진 조판, 오시는 길)
│ ├── page.tsx # Home 라우트 (연구실 소개, Research Project PageView, 4대 분야 조판, 오시는 길)
│ ├── members/page.tsx # 구성원 (16명 재학 연구원, 60명 졸업생 인벤토리)
│ ├── publications/
│ │ ├── page.tsx # 연구 실적 개요 및 Top 10 하이라이트
│ │ └── [category]/page.tsx # 카테고리 4종 정적 사전 생성 라우트 (● SSG)
│ │ └── [category]/page.tsx # 카테고리 3종 정적 사전 생성 라우트 (● SSG)
│ ├── lectures/page.tsx # 강의 이력 (2004 Spring ~ 2026 Spring 45개 학기)
│ ├── standardization/page.tsx# 표준화 활동 (6개 기구 / 44개 규격 문서)
│ ├── sitemap.ts # SEO 사이트맵
│ └── robots.ts # 검색엔진 크롤러 제어
├── content/ # 마이그레이션 실데이터 및 스키마 모듈
├── content/ # 마이그레이션 실데이터 및 스키마 모듈 (projects.ts 자동 생성)
├── scripts/ # 레거시 데이터 파싱 및 무손실 대조 검증 파이프라인
├── components/
│ ├── Header.tsx # 반응형 상단 내비게이션
│ ├── Header.tsx # 반응형 상단 내비게이션 (1240px desktop: 햄버거 토글)
│ ├── Footer.tsx # 하단 연락처/바로가기
│ ├── PageHeader.tsx # 페이지 배너 (한글 제목 + 영문 display)
│ ├── SectionLabel.tsx # 매거진 섹션 넘버링 (01 / 05)
│ ├── Reveal.tsx # 스크롤 등장 모션 래퍼
│ ├── Counter.tsx # 0→값 카운트업 숫자
│ └── Marquee.tsx # 키워드 러닝 티커
├── docs/
│ └── DESIGN.md # 디자인 시스템 문서 (토큰·타입·모션·컴포넌트)
├── tailwind.config.ts # 색상·폰트·타입스케일·모션 토큰
├── tailwind.config.ts # 색상·폰트·타입스케일·모션 토큰 (desktop: "1240px")
├── next.config.js
├── tsconfig.json
├── postcss.config.js
@@ -68,7 +68,7 @@ npm run dev
| 경로 | 설명 |
| --- | --- |
| `/` | Home — 연구실 소개 · 5대 핵심 연구 기둥(3+2 매거진 조판) · 오시는 길 |
| `/` | Home — 연구실 소개 · Research Project PageView(3대 실연구과제 인터랙티브 카드) · 4대 핵심 연구 분야 · 오시는 길 |
| `/intro` | Home (`/`)으로 308 permanent redirect |
| `/members` | 구성원 — 지도교수 및 재학 연구원 16명, 졸업생 60명 이력 인벤토리 |
| `/publications` | 연구 실적 개요 — 223건 카테고리 분류 및 Top 10 최신 성과 |
@@ -0,0 +1,132 @@
import React from "react";
import { researchProjects } from "../../content/projects";
import ProjectPageViewControls from "./ProjectPageViewControls";
export function ProjectPageView() {
const slugs = researchProjects.map((p) => p.slug);
const trackId = "research-projects-track";
return (
<section className="space-y-6">
<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-vermillion"></span>
<span className="font-mono text-xs tracking-widest uppercase text-vermillion font-bold">
Key Initiatives
</span>
</div>
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
(Research Projects)
</h2>
</div>
<p className="text-xs text-ink-mute font-mono">
{researchProjects.length} /
</p>
</div>
{/* Scroll-Snap Track: Mobile 1up, Tablet 2up, Desktop Static 3-col Grid */}
<div
id={trackId}
tabIndex={0}
role="group"
aria-roledescription="carousel"
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-vermillion/50 rounded"
>
{researchProjects.map((proj, idx) => (
<article
key={proj.slug}
id={`project-${proj.slug}`}
role="group"
aria-roledescription="slide"
aria-label={`${researchProjects.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-vermillion/60 transition-colors space-y-5 break-words [word-break:keep-all]"
>
<div className="space-y-4">
{/* Header Info */}
<div className="flex items-start justify-between gap-3 border-b border-line/60 pb-3">
<h3 className="font-serif text-xl font-normal text-ink leading-snug">
{proj.title}
</h3>
{proj.period && (
<span className="font-mono text-xs text-vermillion bg-ivory border border-line px-2 py-0.5 rounded shrink-0">
{proj.period}
</span>
)}
</div>
{/* Abstract */}
<p className="text-sm leading-relaxed text-ink/80">
{proj.abstract}
</p>
{/* Keywords */}
<div className="flex flex-wrap gap-1.5 pt-1">
{proj.keywords.map((kw) => (
<span
key={kw}
className="font-mono text-[0.72rem] text-ink-mute bg-ivory border border-line px-2 py-0.5 rounded"
>
#{kw}
</span>
))}
</div>
</div>
{/* Metadata Footer: Standards Track & Deliverables */}
<div className="space-y-3 pt-3 border-t border-line/60 text-xs font-mono">
{/* Standards Track (or Funder if present, never placeholder if undefined) */}
{proj.funder ? (
<div className="flex items-baseline justify-between gap-2">
<span className="text-ink-mute">Funder:</span>
<span className="text-ink font-semibold">{proj.funder}</span>
</div>
) : proj.standardsTrack ? (
<div className="flex items-baseline justify-between gap-2">
<span className="text-ink-mute">Standards Track:</span>
<span className="text-cobalt font-bold">{proj.standardsTrack}</span>
</div>
) : proj.standardsOrg ? (
<div className="flex items-baseline justify-between gap-2">
<span className="text-ink-mute">Organization:</span>
<span className="text-cobalt font-bold">{proj.standardsOrg}</span>
</div>
) : null}
{/* Key Deliverables */}
{proj.deliverables.length > 0 && (
<div className="space-y-1.5 pt-1">
<span className="text-ink-mute block">Key Deliverables:</span>
<ul className="space-y-1">
{proj.deliverables.slice(0, 3).map((item, dIdx) => (
<li
key={dIdx}
className="flex items-start gap-1.5 text-[0.75rem] text-ink/90"
>
<span className="text-vermillion font-bold"></span>
<span className="line-clamp-2 leading-tight">
<strong className="font-semibold text-ink">{item.ref}</strong> {item.title}
</span>
</li>
))}
{proj.deliverables.length > 3 && (
<li className="text-[0.7rem] text-ink-mute italic">
+ {proj.deliverables.length - 3}
</li>
)}
</ul>
</div>
)}
</div>
</article>
))}
</div>
{/* Interactive Controls (Hidden on desktop) */}
<ProjectPageViewControls slugs={slugs} trackId={trackId} />
</section>
);
}
export default ProjectPageView;
@@ -0,0 +1,122 @@
"use client";
import React, { useEffect, useState, useRef } from "react";
interface ProjectPageViewControlsProps {
slugs: string[];
trackId: string;
}
export default function ProjectPageViewControls({
slugs,
trackId,
}: ProjectPageViewControlsProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
useEffect(() => {
const track = document.getElementById(trackId);
if (!track) return;
const checkScroll = () => {
const { scrollLeft, scrollWidth, clientWidth } = track;
setCanScrollLeft(scrollLeft > 5);
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 5);
};
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
const index = slugs.findIndex((s) => `project-${s}` === id);
if (index !== -1) {
setActiveIndex(index);
}
}
});
},
{ root: track, threshold: 0.6 }
);
slugs.forEach((slug) => {
const el = document.getElementById(`project-${slug}`);
if (el) observer.observe(el);
});
track.addEventListener("scroll", checkScroll, { passive: true });
checkScroll();
return () => {
observer.disconnect();
track.removeEventListener("scroll", checkScroll);
};
}, [slugs, trackId]);
const scroll = (direction: "left" | "right") => {
const track = document.getElementById(trackId);
if (!track) return;
const cardWidth = track.firstElementChild?.clientWidth || 300;
const delta = direction === "left" ? -(cardWidth + 24) : cardWidth + 24;
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
track.scrollBy({
left: delta,
behavior: prefersReducedMotion ? "auto" : "smooth",
});
};
return (
<div className="flex items-center justify-between mt-6 pt-4 border-t border-line desktop:hidden">
{/* Indicator dots using semantic anchor links */}
<div className="flex items-center gap-2" role="tablist" aria-label="프로젝트 위치">
{slugs.map((slug, idx) => (
<a
key={slug}
href={`#project-${slug}`}
role="tab"
aria-selected={activeIndex === idx}
aria-label={`${slugs.length}개 중 ${idx + 1}번째 프로젝트로 이동`}
className={`h-2.5 rounded-full transition-all duration-300 ${
activeIndex === idx
? "w-8 bg-vermillion"
: "w-2.5 bg-line hover:bg-ink-mute"
}`}
/>
))}
</div>
{/* Navigation Buttons */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => scroll("left")}
disabled={!canScrollLeft}
aria-label="이전 프로젝트"
aria-controls={trackId}
className="inline-flex items-center justify-center w-9 h-9 border border-line bg-paper text-ink transition hover:border-vermillion hover:text-vermillion disabled:opacity-30 disabled:pointer-events-none rounded"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<button
type="button"
onClick={() => scroll("right")}
disabled={!canScrollRight}
aria-label="다음 프로젝트"
aria-controls={trackId}
className="inline-flex items-center justify-center w-9 h-9 border border-line bg-paper text-ink transition hover:border-vermillion hover:text-vermillion disabled:opacity-30 disabled:pointer-events-none rounded"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
);
}
+23 -7
View File
@@ -1,14 +1,27 @@
import React from "react";
import HeroComposition from "./_components/HeroComposition";
import { DirectionsBlock } from "./_components/DirectionsBlock";
import { ProjectPageView } from "./_components/ProjectPageView";
import { publications, patents } from "../content/publications";
const PILLARS = [
{
no: "01",
en: "Agent Core Engineering",
ko: "AI 에이전트 3대 핵심 엔지니어링",
theme: "vermillion" as const,
topics: [
"프롬프트 엔지니어링 (System Prompt Optimization)",
"맥락 엔지니어링 (Dynamic Context & Memory Packaging)",
"하네스 엔지니어링 (Agent Control Loop & Sandbox Isolation)",
],
desc: "AI 에이전트의 구동 성능과 안전성을 좌우하는 프롬프트·맥락·하네스 3대 핵심 엔지니어링 기술을 연구합니다.",
},
{
no: "02",
en: "Extensibility & Protocol",
ko: "AI Agent 기능 확장 프로토콜",
theme: "vermillion" as const,
theme: "cobalt" as const,
topics: [
"Model Context Protocol (MCP) 서버/클라이언트 아키텍처",
"SKILL 도구 캡슐화 및 재사용 패키징",
@@ -17,10 +30,10 @@ const PILLARS = [
desc: "에이전트가 외부 도구 및 시스템과 표준화된 방식으로 연동할 수 있도록 MCP와 SKILL 패키징 기술을 개발합니다.",
},
{
no: "02",
no: "03",
en: "Multi-Agent Systems",
ko: "멀티 에이전트 협업 및 오케스트레이션",
theme: "cobalt" as const,
theme: "vermillion" as const,
topics: [
"HERDR 백그라운드 프로세스 컨테이너 격리 제어",
"crewAI 기반 역할 분담 및 태스크 위임 체계",
@@ -29,10 +42,10 @@ const PILLARS = [
desc: "다중 에이전트 환경에서 독립적인 역할 분담과 선순환 오케스트레이션을 보장하는 자율 협업 플랫폼을 연구합니다.",
},
{
no: "03",
no: "04",
en: "Interoperability Standards",
ko: "에이전트 상호운용성 개방형 표준",
theme: "vermillion" as const,
theme: "cobalt" as const,
topics: [
"Agent-to-Agent (A2A) 프로토콜 규격 정의 및 검증",
"Agent Communication Protocol (ACP) 메시지 교환 규격",
@@ -41,10 +54,10 @@ const PILLARS = [
desc: "서로 다른 프레임워크 기반의 에이전트 간의 상호운용성을 보장하는 국제 표준 개방형 통신 프로토콜을 구현합니다.",
},
{
no: "04",
no: "05",
en: "High-Performance Backbone",
ko: "AIoT & 고성능 통신 백본",
theme: "cobalt" as const,
theme: "vermillion" as const,
topics: [
"gRPC / QUIC 양방향 스트리밍 에이전트 백본 통신",
"Protocol Buffers (Protobuf) 직렬화 통신 최적화",
@@ -115,6 +128,9 @@ export default function HomePage() {
</div>
</section>
{/* Research Project PageView Component */}
<ProjectPageView />
{/* Core Research Areas Grid */}
<section className="space-y-8 pt-8 border-t border-line">
<div className="flex justify-between items-end">
+142
View File
@@ -0,0 +1,142 @@
// 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": "Multimedia Systems and Equipment for Metaverse - Part 1: Genaral",
"ref": "IEC TR 63614-1 (May 2026)",
"status": "TR"
},
{
"title": "Multimedia Systems and Equipment for Metaverse - Part 2: Classification",
"ref": "IEC TS 63614-2 (May 2026)",
"status": "TS"
},
{
"title": "Multimedia Systems and Equipment for Metaverse - Part 3: Gap Analysis",
"ref": "IEC TR 63614-3 (March 2026)",
"status": "TR"
},
{
"title": "Multimedia Systems and Equipment for Metaverse - Part 4: AI Use Cases",
"ref": "IEC WDTR 63614-4 (In Progress)",
"status": "InProgress"
},
{
"title": "User's QoE for Multimedia Conference Services - Part 1: General",
"ref": "IEC TR 63478-1 (November 2023)",
"status": "TR"
},
{
"title": "User's QoE for Multimedia Conference Services - Part 2: Requirments",
"ref": "IEC IS 63478-2 (October 2025)",
"status": "IS"
},
{
"title": "User's QoE for Multimedia Conference Services - Part 3: Measurement Methods",
"ref": "IEC CDV 63478-3 (In Progress)",
"status": "InProgress"
},
{
"title": "Infotainment Services for Public Vehicles (PVIS) - Part 1: General",
"ref": "IEC TR 63479-1 (November 2023)",
"status": "TR"
},
{
"title": "Infotainment Services for Public Vehicles (PVIS) - Part 2: Requirements",
"ref": "IEC IS 63479-2 (February 2026)",
"status": "IS"
},
{
"title": "Infotainment Services for Public Vehicles (PVIS) - Part 3: Framework",
"ref": "IEC IS 63479-3 (January 2026)",
"status": "IS"
},
{
"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"
}
]
}
];
+18
View File
@@ -80,3 +80,21 @@ export interface StandardsBody {
role?: string;
projects: StandardProject[];
}
export interface ProjectDeliverable {
title: string;
ref: string;
status: DocStatus;
}
export interface ResearchProject {
slug: string;
title: string;
abstract: string;
keywords: string[];
standardsTrack?: string;
standardsOrg?: string;
period?: string;
deliverables: ProjectDeliverable[];
funder?: string;
}
@@ -443,6 +443,77 @@ def parse_standards():
return bodies
def parse_projects():
bodies = parse_standards()
body_map = {b["org"]: b for b in bodies}
# Extract deliverables for CCIS (IEC TC100)
ccis_docs = []
if "IEC TC100" in body_map:
for p in body_map["IEC TC100"]["projects"]:
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 = [
{
"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": body_map.get("ISO/IEC JTC1/SC41", {}).get("period", "2020 ~ Present"),
"deliverables": iot_docs,
},
{
"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": body_map.get("IEC TC100", {}).get("period", "2018 ~ Present"),
"deliverables": ccis_docs,
},
{
"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": body_map.get("ITU-T SG20", {}).get("period", "2018 ~ 2020"),
"deliverables": vlc_docs,
}
]
return projects
def clean_dict(obj):
if isinstance(obj, dict):
return {k: clean_dict(v) for k, v in obj.items() if v is not None}
@@ -455,6 +526,7 @@ def main():
intl_p, dom_p, pat_p = parse_publications()
semesters = parse_lectures()
bodies = parse_standards()
projects = parse_projects()
os.makedirs(CONTENT_DIR, exist_ok=True)
@@ -481,5 +553,10 @@ def main():
f.write('import { StandardsBody } from "./types";\n\n')
f.write(f'export const standardsBodies: StandardsBody[] = {json.dumps(clean_dict(bodies), ensure_ascii=False, indent=2)};\n')
with open(os.path.join(CONTENT_DIR, "projects.ts"), "w", encoding="utf-8") as f:
f.write('// AUTO-GENERATED by scripts/extract/generate_all.py — DO NOT EDIT BY HAND\n')
f.write('import { ResearchProject } from "./types";\n\n')
f.write(f'export const researchProjects: ResearchProject[] = {json.dumps(clean_dict(projects), ensure_ascii=False, indent=2)};\n')
if __name__ == "__main__":
main()
@@ -14,17 +14,18 @@ MONTH_MAP = {
}
def verify():
from generate_all import parse_members, parse_publications, parse_lectures, parse_standards
from generate_all import parse_members, parse_publications, parse_lectures, parse_standards, parse_projects
active_m, alumni_m = parse_members()
intl_p, dom_p, pat_p = parse_publications()
semesters = parse_lectures()
bodies = parse_standards()
projects = parse_projects()
total_pubs = len(intl_p) + len(dom_p) + len(pat_p)
std_docs_count = sum(len(p["documents"]) for b in bodies for p in b["projects"])
print("=== DATA VERIFICATION GATE (AC-07 / AC-08 / AC-10 / AC-11 / VOLUME CROSS-CHECK) ===")
print("=== DATA VERIFICATION GATE (AC-07 / AC-08 / AC-10 / AC-11 / AC-30 / VOLUME CROSS-CHECK) ===")
print(f"Active Members: {len(active_m)} (Expected: 16)")
print(f"Alumni: {len(alumni_m)} (Expected: 60)")
print(f"International Papers: {len(intl_p)} (Expected: 68)")
@@ -34,9 +35,20 @@ def verify():
print(f"Lectures Semesters: {len(semesters)} (Expected: 45)")
print(f"Standard Bodies: {len(bodies)} (Expected: 6)")
print(f"Standard Documents: {std_docs_count} (Expected: 44)")
print(f"Research Projects: {len(projects)} (Expected: 3)")
errors = []
if len(projects) != 3:
errors.append(f"Research Projects count mismatch: got {len(projects)}, expected 3")
# AC-30: Keyword Substring Verification (Ensures zero hallucinated/created keywords)
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")
# 1. Count checks
if len(active_m) != 16:
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
@@ -93,5 +93,23 @@ class TestANLHomepageE2E(unittest.TestCase):
for token in ["--ink", "--ivory", "--paper", "--line", "--vermillion", "--cobalt"]:
self.assertIn(token, css, f"Missing CSS variable {token} in globals.css")
def test_07_research_projects_pageview(self):
"""TC-T1-F7: Verify Research Project PageView content in Home static SSG HTML (AC-27 / AC-31)."""
html = self.read_html("index.html")
# 1. Project PageView Title & Projects
self.assertIn("주요 연구 프로젝트 (Research Projects)", html)
self.assertIn("IoT Standards", html)
self.assertIn("CCIS", html)
self.assertIn("VLC-IoT", html)
# 2. Key Deliverables references (Top visible items)
self.assertIn("ISO/IEC TR 30189-1", html)
self.assertIn("IEC TR 63614-1", html)
self.assertIn("ITU-T Recommendation Y.4465", html)
# 3. Assert zero placeholder Funder labels (AC-31)
self.assertNotIn("Funder:", html)
if __name__ == "__main__":
unittest.main()