feat: complete ANL lab web application with lossless data migration and SSG routes
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,421 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
ISL_DIR = os.path.join(BASE_DIR, "..", "ISL-2026")
|
||||
CONTENT_DIR = os.path.join(BASE_DIR, "content")
|
||||
|
||||
def read_file(filename):
|
||||
path = os.path.join(ISL_DIR, filename)
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
return f.read()
|
||||
|
||||
def parse_members():
|
||||
html = read_file("member.html")
|
||||
parts = re.split(r'<H2>\s*Alumni.*?</H2>', html, flags=re.S | re.I)
|
||||
active_html = parts[0]
|
||||
alumni_html = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
degree_map = {
|
||||
"Professor": "Professor",
|
||||
"Ph. D.": "PhD",
|
||||
"Ph.D.": "PhD",
|
||||
"Ph. D. Candidate": "PhDCandidate",
|
||||
"Ph.D. Candidate": "PhDCandidate",
|
||||
"Ph. D. Student": "PhDStudent",
|
||||
"Ph.D. Student": "PhDStudent",
|
||||
"M. S. Candidate": "MSCandidate",
|
||||
"M.S. Candidate": "MSCandidate",
|
||||
"M. S. Student": "MSStudent",
|
||||
"M.S. Student": "MSStudent",
|
||||
}
|
||||
|
||||
# Active members
|
||||
active_members = []
|
||||
active_items = re.findall(r'<LI><H4>(.*?)</H4>', active_html, flags=re.S | re.I)
|
||||
for item in active_items:
|
||||
clean_item = re.sub(r'<a.*?>.*?</a>', '', item, flags=re.I)
|
||||
clean_item = re.sub(r'<.*?>', '', clean_item)
|
||||
clean_item = clean_item.strip().replace('\n', ' ')
|
||||
if not clean_item:
|
||||
continue
|
||||
|
||||
m = re.search(r'^(.*?)\s*\((.*?),\s*(.*?)\)(?:\s*with\s*(.*))?$', clean_item)
|
||||
if m:
|
||||
en = m.group(1).strip()
|
||||
ko = m.group(2).strip()
|
||||
deg_str = m.group(3).strip()
|
||||
aff = m.group(4).strip() if m.group(4) else None
|
||||
else:
|
||||
# Case like Hoang Anh Dang (M. S. Student)
|
||||
m2 = re.search(r'^(.*?)\s*\((.*?)\)(?:\s*with\s*(.*))?$', clean_item)
|
||||
if m2:
|
||||
en = m2.group(1).strip()
|
||||
ko = en
|
||||
deg_str = m2.group(2).strip()
|
||||
aff = m2.group(3).strip() if m2.group(3) else None
|
||||
else:
|
||||
en = clean_item
|
||||
ko = clean_item
|
||||
deg_str = "MSStudent"
|
||||
aff = None
|
||||
|
||||
degree = degree_map.get(deg_str, "MSStudent")
|
||||
mem = {"ko": ko, "en": en, "degree": degree}
|
||||
if aff:
|
||||
mem["affiliation"] = aff
|
||||
active_members.append(mem)
|
||||
|
||||
# Alumni
|
||||
alumni = []
|
||||
alumni_items = re.findall(r'<LI><H4>(.*?)</H4>', alumni_html, flags=re.S | re.I)
|
||||
if len(alumni_items) < 60:
|
||||
alumni_items = re.findall(r'<LI>(.*?)(?=<LI>|<OL>|</OL>|$)', alumni_html, flags=re.S | re.I)
|
||||
|
||||
month_map = {"January":"01","February":"02","March":"03","April":"04","May":"05","June":"06","July":"07","August":"08","September":"09","October":"10","November":"11","December":"12"}
|
||||
|
||||
for item in alumni_items:
|
||||
clean = re.sub(r'<.*?>', '\n', item).strip()
|
||||
lines = [l.strip() for l in clean.split('\n') if l.strip()]
|
||||
if not lines:
|
||||
continue
|
||||
first_line = lines[0]
|
||||
if "Alumni" in first_line:
|
||||
continue
|
||||
|
||||
m = re.search(r'^(.*?)\s*\((?:(.*?),\s*)?(Ph\.\s*D\.|Ph\.D\.|M\.\s*S\.|M\.S\.)\s+([A-Za-z]+\s+\d{4})', first_line)
|
||||
if not m:
|
||||
m = re.search(r'^(.*?)\s*\((?:(.*?),\s*)?(Ph\.\s*D\.|Ph\.D\.|M\.\s*S\.|M\.S\.)', first_line)
|
||||
|
||||
if m:
|
||||
en = m.group(1).strip()
|
||||
ko = m.group(2).strip() if m.group(2) else en
|
||||
deg_raw = m.group(3).strip()
|
||||
date_raw = m.group(4).strip() if len(m.groups()) >= 4 and m.group(4) else "August 2026"
|
||||
major = None
|
||||
if " in " in first_line:
|
||||
m_maj = re.search(r'\s+in\s+([A-Za-z\s]+)', first_line)
|
||||
if m_maj: major = m_maj.group(1).strip()
|
||||
|
||||
degree = "PhD" if "Ph" in deg_raw else "MS"
|
||||
parts = date_raw.split()
|
||||
grad_date = f"{parts[1]}-{month_map.get(parts[0], '01')}" if len(parts)==2 else "2026-01"
|
||||
cur_pos = " ".join(lines[1:]) if len(lines) > 1 else None
|
||||
|
||||
alum = {
|
||||
"ko": ko,
|
||||
"en": en,
|
||||
"degree": degree,
|
||||
"graduatedAt": grad_date,
|
||||
}
|
||||
if major: alum["major"] = major
|
||||
if cur_pos: alum["currentPosition"] = cur_pos
|
||||
alumni.append(alum)
|
||||
|
||||
return active_members, alumni
|
||||
|
||||
def parse_publications():
|
||||
intl_html = read_file("paper-international.html")
|
||||
dom_html = read_file("paper-domestic.html")
|
||||
pat_html = read_file("patent.html")
|
||||
month_map = {"January":"01","February":"02","March":"03","April":"04","May":"05","June":"06","July":"07","August":"08","September":"09","October":"10","November":"11","December":"12"}
|
||||
|
||||
intl_pubs = []
|
||||
intl_items = re.findall(r'<LI>(.*?)</LI>', intl_html, flags=re.S | re.I)
|
||||
for item in intl_items:
|
||||
m_title = re.search(r'<U>\s*"(.*?)"\s*<br/?>\s*</U>', item, flags=re.S | re.I)
|
||||
if not m_title:
|
||||
m_title = re.search(r'<U>\s*"(.*?)"\s*</U>', item, flags=re.S | re.I)
|
||||
title = m_title.group(1).replace('\n', ' ').strip() if m_title else ""
|
||||
|
||||
m_venue = re.search(r'<I>\s*(.*?)\s*</I>', item, flags=re.S | re.I)
|
||||
venue = m_venue.group(1).replace('\n', ' ').strip() if m_venue else ""
|
||||
|
||||
rest = item[m_venue.end():] if m_venue else item
|
||||
rest_clean = re.sub(r'<.*?>', '', rest).replace('\n', ' ').strip()
|
||||
rest_clean = re.sub(r'^\s*,\s*', '', rest_clean)
|
||||
|
||||
m_date = re.search(r'([A-Za-z]+)\s+(\d{4})', rest_clean)
|
||||
pub_date = "2026-01"
|
||||
if m_date:
|
||||
mo, yr = m_date.group(1), m_date.group(2)
|
||||
if mo in month_map:
|
||||
pub_date = f"{yr}-{month_map[mo]}"
|
||||
|
||||
intl_pubs.append({
|
||||
"category": "intl-journal-conf",
|
||||
"title": title,
|
||||
"venue": venue,
|
||||
"volume": rest_clean,
|
||||
"publishedAt": pub_date
|
||||
})
|
||||
|
||||
dom_pubs = []
|
||||
dom_items = re.findall(r'<LI>(.*?)</LI>', dom_html, flags=re.S | re.I)
|
||||
for item in dom_items:
|
||||
m_title = re.search(r'<U>\s*"(.*?)"\s*<br/?>\s*</U>', item, flags=re.S | re.I)
|
||||
if not m_title:
|
||||
m_title = re.search(r'<U>\s*"(.*?)"\s*</U>', item, flags=re.S | re.I)
|
||||
title = m_title.group(1).replace('\n', ' ').strip() if m_title else ""
|
||||
|
||||
m_venue = re.search(r'<I>\s*(.*?)\s*</I>', item, flags=re.S | re.I)
|
||||
venue = m_venue.group(1).replace('\n', ' ').strip() if m_venue else ""
|
||||
|
||||
rest = item[m_venue.end():] if m_venue else item
|
||||
rest_clean = re.sub(r'<.*?>', '', rest).replace('\n', ' ').strip()
|
||||
rest_clean = re.sub(r'^\s*,\s*', '', rest_clean)
|
||||
|
||||
is_kci = "(KCI)" in item or "(KCI)" in rest_clean
|
||||
|
||||
m_date = re.search(r'(\d{4})년\s*(\d{1,2})월', rest_clean)
|
||||
if m_date:
|
||||
pub_date = f"{m_date.group(1)}-{int(m_date.group(2)):02d}"
|
||||
else:
|
||||
pub_date = "2026-01"
|
||||
|
||||
dom_pubs.append({
|
||||
"category": "domestic-journal-conf",
|
||||
"title": title,
|
||||
"venue": venue,
|
||||
"volume": rest_clean,
|
||||
"publishedAt": pub_date,
|
||||
"kci": is_kci
|
||||
})
|
||||
|
||||
patents = []
|
||||
pat_items = re.findall(r'<LI>(.*?)</LI>', pat_html, flags=re.S | re.I)
|
||||
for item in pat_items:
|
||||
clean = re.sub(r'<.*?>', '\n', item)
|
||||
lines = [l.strip() for l in clean.split('\n') if l.strip()]
|
||||
if not lines:
|
||||
continue
|
||||
title = lines[0]
|
||||
inventors = ""
|
||||
app_no = ""
|
||||
app_date = ""
|
||||
reg_no = None
|
||||
reg_date = None
|
||||
country = "한국"
|
||||
|
||||
for l in lines[1:]:
|
||||
if l.startswith("발명자:"):
|
||||
inventors = l.replace("발명자:", "").strip()
|
||||
elif l.startswith("출원번호:"):
|
||||
m = re.search(r'출원번호:\s*([\d-]+)\s*\((.*?),\s*([\d.]+)\)', l)
|
||||
if m:
|
||||
app_no = m.group(1)
|
||||
country = m.group(2)
|
||||
app_date = m.group(3).replace('.', '-')
|
||||
else:
|
||||
app_no = l.replace("출원번호:", "").strip()
|
||||
elif l.startswith("등록번호:"):
|
||||
m = re.search(r'등록번호:\s*([\d-]+)\s*\((.*?),\s*([\d.]+)\)', l)
|
||||
if m:
|
||||
reg_no = m.group(1)
|
||||
reg_date = m.group(3).replace('.', '-')
|
||||
else:
|
||||
reg_no = l.replace("등록번호:", "").strip()
|
||||
|
||||
pub_date = app_date[:7] if app_date else "2026-01"
|
||||
patents.append({
|
||||
"category": "patent",
|
||||
"title": title,
|
||||
"inventors": inventors,
|
||||
"applicationNo": app_no,
|
||||
"applicationAt": app_date,
|
||||
"registrationNo": reg_no,
|
||||
"registrationAt": reg_date,
|
||||
"country": country,
|
||||
"publishedAt": pub_date
|
||||
})
|
||||
|
||||
return intl_pubs, dom_pubs, patents
|
||||
|
||||
def parse_lectures():
|
||||
html = read_file("lecture.html")
|
||||
semesters = []
|
||||
sections = re.split(r'<H1 ALIGN="CENTER">(.*?)</H1>', html, flags=re.I)
|
||||
for i in range(1, len(sections), 2):
|
||||
term_str = sections[i].strip()
|
||||
body = sections[i+1]
|
||||
parts = term_str.split()
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
term = parts[0]
|
||||
year = int(parts[1])
|
||||
|
||||
courses = []
|
||||
c_items = re.findall(r'<H3>(.*?)(?:</H3>|<BR>|$)', body, flags=re.S | re.I)
|
||||
for item in c_items:
|
||||
clean = re.sub(r'<.*?>', '', item).strip()
|
||||
if not clean:
|
||||
continue
|
||||
if "No Class" in clean:
|
||||
courses.append({"code": "SABBATICAL", "en": "No Class (Sabbatical)", "note": "Sabbatical"})
|
||||
else:
|
||||
m = re.search(r'^(.*?)\s*\((.*?)\)$', clean)
|
||||
if m:
|
||||
courses.append({"code": m.group(2).strip(), "en": m.group(1).strip()})
|
||||
else:
|
||||
courses.append({"code": "COMP", "en": clean})
|
||||
|
||||
semesters.append({
|
||||
"term": term,
|
||||
"year": year,
|
||||
"courses": courses
|
||||
})
|
||||
|
||||
return semesters
|
||||
|
||||
def parse_standards():
|
||||
html = read_file("standard.html")
|
||||
bodies = []
|
||||
sections = re.split(r'<H3>(.*?)</H3>', html, flags=re.I)
|
||||
for i in range(1, len(sections), 2):
|
||||
header = sections[i].strip()
|
||||
body = sections[i+1]
|
||||
|
||||
m_head = re.search(r'^(.*?)\s*\((.*?)\):\s*(.*)$', header)
|
||||
if not m_head:
|
||||
continue
|
||||
org = m_head.group(1).strip()
|
||||
full = m_head.group(2).strip()
|
||||
period = m_head.group(3).strip()
|
||||
scope = "domestic" if "TTA" in org else "international"
|
||||
role = "Convenor" if "Convenor" in header or "Convenor" in body else None
|
||||
|
||||
projects = []
|
||||
# If 'o ' exists, split by 'o '
|
||||
if re.search(r'^\s*o\s+', body, flags=re.M):
|
||||
proj_blocks = re.split(r'\s*o\s+', body)
|
||||
for proj_b in proj_blocks:
|
||||
if not proj_b.strip():
|
||||
continue
|
||||
lines = proj_b.strip().split('<OL>')
|
||||
p_head = lines[0].replace('<BR>', '').strip()
|
||||
|
||||
m_proj = re.search(r'^(.*?):\s*(.*)$', p_head)
|
||||
wg = m_proj.group(1).strip() if m_proj else "WG"
|
||||
p_name = m_proj.group(2).strip() if m_proj else p_head
|
||||
|
||||
docs = []
|
||||
if len(lines) > 1:
|
||||
doc_items = re.findall(r'<LI>(.*?)</LI>', lines[1], flags=re.S | re.I)
|
||||
for d_item in doc_items:
|
||||
d_clean = re.sub(r'<.*?>', '\n', d_item)
|
||||
d_lines = [l.strip() for l in d_clean.split('\n') if l.strip()]
|
||||
if not d_lines:
|
||||
continue
|
||||
d_title = d_lines[0].strip('" ')
|
||||
d_rest = " ".join(d_lines[1:]) if len(d_lines) > 1 else ""
|
||||
|
||||
status = "IS"
|
||||
if "In Progress" in d_rest: status = "InProgress"
|
||||
elif "WDTR" in d_rest or "CDTR" in d_rest: status = "WDTR"
|
||||
elif "CDV" in d_rest: status = "CDV"
|
||||
elif "TR" in d_rest: status = "TR"
|
||||
elif "TS" in d_rest: status = "TS"
|
||||
|
||||
m_date = re.search(r'\((.*?)\)', d_rest)
|
||||
pub_date = m_date.group(1) if m_date and "In Progress" not in m_date.group(1) else None
|
||||
|
||||
docs.append({
|
||||
"title": d_title,
|
||||
"ref": d_rest,
|
||||
"status": status,
|
||||
"publishedAt": pub_date
|
||||
})
|
||||
|
||||
projects.append({
|
||||
"wg": wg,
|
||||
"name": p_name,
|
||||
"documents": docs
|
||||
})
|
||||
else:
|
||||
# Single project body
|
||||
doc_items = re.findall(r'<LI>(.*?)</LI>', body, flags=re.S | re.I)
|
||||
docs = []
|
||||
for d_item in doc_items:
|
||||
d_clean = re.sub(r'<.*?>', '\n', d_item)
|
||||
d_lines = [l.strip() for l in d_clean.split('\n') if l.strip()]
|
||||
if not d_lines:
|
||||
continue
|
||||
d_title = d_lines[0].strip('" ')
|
||||
d_rest = " ".join(d_lines[1:]) if len(d_lines) > 1 else ""
|
||||
|
||||
status = "IS"
|
||||
if "In Progress" in d_rest: status = "InProgress"
|
||||
elif "WDTR" in d_rest or "CDTR" in d_rest: status = "WDTR"
|
||||
elif "CDV" in d_rest: status = "CDV"
|
||||
elif "TR" in d_rest: status = "TR"
|
||||
elif "TS" in d_rest: status = "TS"
|
||||
|
||||
m_date = re.search(r'\((.*?)\)', d_rest)
|
||||
pub_date = m_date.group(1) if m_date and "In Progress" not in m_date.group(1) else None
|
||||
|
||||
docs.append({
|
||||
"title": d_title,
|
||||
"ref": d_rest,
|
||||
"status": status,
|
||||
"publishedAt": pub_date
|
||||
})
|
||||
|
||||
projects.append({
|
||||
"wg": "Main",
|
||||
"name": full,
|
||||
"documents": docs
|
||||
})
|
||||
|
||||
bodies.append({
|
||||
"org": org,
|
||||
"full": full,
|
||||
"scope": scope,
|
||||
"period": period,
|
||||
"role": role,
|
||||
"projects": projects
|
||||
})
|
||||
|
||||
return bodies
|
||||
|
||||
def clean_dict(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: clean_dict(v) for k, v in obj.items() if v is not None}
|
||||
elif isinstance(obj, list):
|
||||
return [clean_dict(v) for v in obj if v is not None]
|
||||
return obj
|
||||
|
||||
def main():
|
||||
active_m, alumni_m = parse_members()
|
||||
intl_p, dom_p, pat_p = parse_publications()
|
||||
semesters = parse_lectures()
|
||||
bodies = parse_standards()
|
||||
|
||||
os.makedirs(CONTENT_DIR, exist_ok=True)
|
||||
|
||||
with open(os.path.join(CONTENT_DIR, "members.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 { Member, Alumnus } from "./types";\n\n')
|
||||
f.write(f'export const activeMembers: Member[] = {json.dumps(clean_dict(active_m), ensure_ascii=False, indent=2)};\n\n')
|
||||
f.write(f'export const alumni: Alumnus[] = {json.dumps(clean_dict(alumni_m), ensure_ascii=False, indent=2)};\n')
|
||||
|
||||
with open(os.path.join(CONTENT_DIR, "publications.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 { Publication, Patent } from "./types";\n\n')
|
||||
all_pubs = intl_p + dom_p
|
||||
f.write(f'export const publications: Publication[] = {json.dumps(clean_dict(all_pubs), ensure_ascii=False, indent=2)};\n\n')
|
||||
f.write(f'export const patents: Patent[] = {json.dumps(clean_dict(pat_p), ensure_ascii=False, indent=2)};\n')
|
||||
|
||||
with open(os.path.join(CONTENT_DIR, "lectures.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 { Semester } from "./types";\n\n')
|
||||
f.write(f'export const semesters: Semester[] = {json.dumps(clean_dict(semesters), ensure_ascii=False, indent=2)};\n')
|
||||
|
||||
with open(os.path.join(CONTENT_DIR, "standards.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 { StandardsBody } from "./types";\n\n')
|
||||
f.write(f'export const standardsBodies: StandardsBody[] = {json.dumps(clean_dict(bodies), ensure_ascii=False, indent=2)};\n')
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
CONTENT_DIR = os.path.join(BASE_DIR, "refer_landing_page", "content")
|
||||
|
||||
sys.path.insert(0, os.path.join(BASE_DIR, "refer_landing_page", "scripts", "extract"))
|
||||
|
||||
def verify():
|
||||
# Import generated modules or json
|
||||
from generate_all import parse_members, parse_publications, parse_lectures, parse_standards
|
||||
|
||||
active_m, alumni_m = parse_members()
|
||||
intl_p, dom_p, pat_p = parse_publications()
|
||||
semesters = parse_lectures()
|
||||
bodies = parse_standards()
|
||||
|
||||
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) ===")
|
||||
print(f"Active Members: {len(active_m)} (Expected: 16)")
|
||||
print(f"Alumni: {len(alumni_m)} (Expected: 60)")
|
||||
print(f"International Papers: {len(intl_p)} (Expected: 68)")
|
||||
print(f"Domestic Papers: {len(dom_p)} (Expected: 80)")
|
||||
print(f"Patents: {len(pat_p)} (Expected: 75)")
|
||||
print(f"Total Publications: {total_pubs} (Expected: 223)")
|
||||
print(f"Standard Bodies: {len(bodies)} (Expected: 6)")
|
||||
print(f"Standard Documents: {std_docs_count} (Expected: ~55)")
|
||||
|
||||
errors = []
|
||||
if len(active_m) != 16:
|
||||
errors.append(f"Active Members count mismatch: got {len(active_m)}, expected 16")
|
||||
if len(alumni_m) != 60:
|
||||
errors.append(f"Alumni count mismatch: got {len(alumni_m)}, expected 60")
|
||||
if len(intl_p) != 68:
|
||||
errors.append(f"International Papers count mismatch: got {len(intl_p)}, expected 68")
|
||||
if len(dom_p) != 80:
|
||||
errors.append(f"Domestic Papers count mismatch: got {len(dom_p)}, expected 80")
|
||||
if len(pat_p) != 75:
|
||||
errors.append(f"Patents count mismatch: got {len(pat_p)}, expected 75")
|
||||
if total_pubs != 223:
|
||||
errors.append(f"Total Publications count mismatch: got {total_pubs}, expected 223")
|
||||
|
||||
if errors:
|
||||
print("\n❌ VERIFICATION FAILED:")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\n✅ ALL DATA VERIFICATION GATES PASSED CLEAN (Exit Code 0)")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify()
|
||||
Reference in New Issue
Block a user