Files
Godopu fb6881070c feat(backend,docker,docs): implement Go/Gin/SQLite REST API server, Docker containerization, and architecture specifications
- Implement standalone Go backend API server using Gin and pure-Go SQLite (modernc.org/sqlite)
- Add 11-table schema DDL migrations (000001_init.up.sql) based on DATA_TABLE.md
- Add seed data ingestion tool (cmd/seed) and static TypeScript content exporter (cmd/export-content)
- Add configuration loader (internal/config) supporting environment variables and .env files (HOST, PORT, DB_PATH, GIN_MODE, CORS_ALLOW_ORIGINS, AUTO_MIGRATE)
- Add lightweight multi-stage Dockerfile and docker-compose.yml with persistent SQLite volume
- Add comprehensive architecture specification (ARCHITECTURE.md) and REST API interface specification (API_INTERFACE.md)
- Harmonize frontend publications page to consume isHighlight flag from database-synced content
2026-08-24 17:34:51 +09:00

346 lines
11 KiB
Go

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! ===")
}