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
This commit is contained in:
@@ -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,344 @@
|
||||
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")
|
||||
flag.Parse()
|
||||
|
||||
log.Printf("Exporting content from SQLite (%s) to TypeScript files in %s...", *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! ===")
|
||||
}
|
||||
Reference in New Issue
Block a user