feat(console,api): implement /console Admin Dashboard and Go backend full CRUD REST APIs

- Add comprehensive /console admin web UI for managing research projects, areas, members, publications, patents, lectures, and standards
- Implement Admin token authentication middleware (RequireAdminToken) with Bearer token validation
- Implement POST, PUT, DELETE REST API endpoints for all database entities across 5 domains
- Add typed admin API client in refer_landing_page/lib/adminApi.ts with localStorage session persistence
- Enhance E2E test runner to manage backend server lifecycle during automated tests
- Pass all 12 quality gates cleanly with unanimous reviewer PASS verdicts
This commit is contained in:
2026-08-24 22:21:12 +09:00
parent dadd14feb1
commit 3f452468d6
32 changed files with 5466 additions and 279 deletions
+88
View File
@@ -55,6 +55,54 @@ func (r *HomeRepository) GetResearchProjects(ctx context.Context) ([]models.Rese
return projects, nil
}
func (r *HomeRepository) CreateResearchProject(ctx context.Context, p models.ResearchProject) (int, error) {
kwBytes, _ := json.Marshal(p.Keywords)
if p.Keywords == nil {
kwBytes = []byte("[]")
}
res, err := r.db.ExecContext(ctx, `
INSERT INTO research_projects (slug, title, abstract, keywords, organization, standards_org, period, funder)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, p.Slug, p.Title, p.Abstract, string(kwBytes), p.Organization, p.StandardsOrg, p.Period, p.Funder)
if err != nil {
return 0, fmt.Errorf("insert research project: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *HomeRepository) UpdateResearchProject(ctx context.Context, id int, p models.ResearchProject) error {
kwBytes, _ := json.Marshal(p.Keywords)
if p.Keywords == nil {
kwBytes = []byte("[]")
}
res, err := r.db.ExecContext(ctx, `
UPDATE research_projects
SET slug = ?, title = ?, abstract = ?, keywords = ?, organization = ?, standards_org = ?, period = ?, funder = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, p.Slug, p.Title, p.Abstract, string(kwBytes), p.Organization, p.StandardsOrg, p.Period, p.Funder, id)
if err != nil {
return fmt.Errorf("update research project: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *HomeRepository) DeleteResearchProject(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM research_projects WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete research project: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *HomeRepository) GetResearchAreas(ctx context.Context) ([]models.ResearchArea, error) {
query := `
SELECT id, name_en, name_kr, display_order
@@ -78,6 +126,46 @@ func (r *HomeRepository) GetResearchAreas(ctx context.Context) ([]models.Researc
return areas, rows.Err()
}
func (r *HomeRepository) CreateResearchArea(ctx context.Context, a models.ResearchArea) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO research_areas (name_en, name_kr, display_order)
VALUES (?, ?, ?)
`, a.NameEn, a.NameKr, a.DisplayOrder)
if err != nil {
return 0, fmt.Errorf("insert research area: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *HomeRepository) UpdateResearchArea(ctx context.Context, id int, a models.ResearchArea) error {
res, err := r.db.ExecContext(ctx, `
UPDATE research_areas
SET name_en = ?, name_kr = ?, display_order = ?
WHERE id = ?
`, a.NameEn, a.NameKr, a.DisplayOrder, id)
if err != nil {
return fmt.Errorf("update research area: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *HomeRepository) DeleteResearchArea(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM research_areas WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete research area: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *HomeRepository) GetStatsSummary(ctx context.Context) (*models.StatsSummary, error) {
var summary models.StatsSummary
+81 -3
View File
@@ -17,7 +17,6 @@ func NewLecturesRepository(db *sql.DB) *LecturesRepository {
}
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
@@ -30,7 +29,7 @@ func (r *LecturesRepository) GetSemestersWithCourses(ctx context.Context) ([]mod
defer semRows.Close()
var semesters []models.SemesterWithCourses
semMap := make(map[int]int) // semester_id -> index in slice
semMap := make(map[int]int)
for semRows.Next() {
var s models.SemesterWithCourses
@@ -45,7 +44,6 @@ func (r *LecturesRepository) GetSemestersWithCourses(ctx context.Context) ([]mod
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
@@ -69,3 +67,83 @@ func (r *LecturesRepository) GetSemestersWithCourses(ctx context.Context) ([]mod
return semesters, courseRows.Err()
}
func (r *LecturesRepository) CreateSemester(ctx context.Context, s models.Semester) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO semesters (year, term)
VALUES (?, ?)
`, s.Year, s.Term)
if err != nil {
return 0, fmt.Errorf("insert semester: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *LecturesRepository) UpdateSemester(ctx context.Context, id int, s models.Semester) error {
res, err := r.db.ExecContext(ctx, `
UPDATE semesters
SET year = ?, term = ?
WHERE id = ?
`, s.Year, s.Term, id)
if err != nil {
return fmt.Errorf("update semester: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *LecturesRepository) DeleteSemester(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM semesters WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete semester: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *LecturesRepository) CreateCourse(ctx context.Context, c models.Course) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO courses (semester_id, code, name_en, name_kr, note, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, c.SemesterID, c.Code, c.NameEn, c.NameKr, c.Note, c.DisplayOrder)
if err != nil {
return 0, fmt.Errorf("insert course: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *LecturesRepository) UpdateCourse(ctx context.Context, id int, c models.Course) error {
res, err := r.db.ExecContext(ctx, `
UPDATE courses
SET code = ?, name_en = ?, name_kr = ?, note = ?, display_order = ?
WHERE id = ?
`, c.Code, c.NameEn, c.NameKr, c.Note, c.DisplayOrder, id)
if err != nil {
return fmt.Errorf("update course: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *LecturesRepository) DeleteCourse(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM courses WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete course: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
+80
View File
@@ -42,6 +42,46 @@ func (r *MembersRepository) GetMembers(ctx context.Context) ([]models.Member, er
return members, rows.Err()
}
func (r *MembersRepository) CreateMember(ctx context.Context, m models.Member) (int, error) {
res, err := r.db.ExecContext(ctx, `
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 {
return 0, fmt.Errorf("insert member: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *MembersRepository) UpdateMember(ctx context.Context, id int, m models.Member) error {
res, err := r.db.ExecContext(ctx, `
UPDATE members
SET name_ko = ?, name_en = ?, degree = ?, affiliation = ?, email = ?, is_advisor = ?, display_order = ?
WHERE id = ?
`, m.NameKo, m.NameEn, m.Degree, m.Affiliation, m.Email, m.IsAdvisor, m.DisplayOrder, id)
if err != nil {
return fmt.Errorf("update member: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *MembersRepository) DeleteMember(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM members WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete member: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return 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
@@ -67,3 +107,43 @@ func (r *MembersRepository) GetAlumni(ctx context.Context) ([]models.Alumnus, er
}
return alumni, rows.Err()
}
func (r *MembersRepository) CreateAlumnus(ctx context.Context, a models.Alumnus) (int, error) {
res, err := r.db.ExecContext(ctx, `
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 {
return 0, fmt.Errorf("insert alumnus: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *MembersRepository) UpdateAlumnus(ctx context.Context, id int, a models.Alumnus) error {
res, err := r.db.ExecContext(ctx, `
UPDATE alumni
SET name_ko = ?, name_en = ?, degree = ?, graduated_at = ?, major = ?, current_position = ?
WHERE id = ?
`, a.NameKo, a.NameEn, a.Degree, a.GraduatedAt, a.Major, a.CurrentPosition, id)
if err != nil {
return fmt.Errorf("update alumnus: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *MembersRepository) DeleteAlumnus(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM alumni WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete alumnus: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
@@ -48,6 +48,46 @@ func (r *PublicationsRepository) GetPublications(ctx context.Context, category s
return pubs, rows.Err()
}
func (r *PublicationsRepository) CreatePublication(ctx context.Context, p models.Publication) (int, error) {
res, err := r.db.ExecContext(ctx, `
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 {
return 0, fmt.Errorf("insert publication: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *PublicationsRepository) UpdatePublication(ctx context.Context, id int, p models.Publication) error {
res, err := r.db.ExecContext(ctx, `
UPDATE publications
SET category = ?, title = ?, authors = ?, venue = ?, volume = ?, published_at = ?, kci = ?, doi = ?, is_highlight = ?
WHERE id = ?
`, p.Category, p.Title, p.Authors, p.Venue, p.Volume, p.PublishedAt, p.KCI, p.DOI, p.IsHighlight, id)
if err != nil {
return fmt.Errorf("update publication: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *PublicationsRepository) DeletePublication(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM publications WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete publication: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return 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
@@ -100,3 +140,43 @@ func (r *PublicationsRepository) GetPatents(ctx context.Context) ([]models.Paten
}
return patents, rows.Err()
}
func (r *PublicationsRepository) CreatePatent(ctx context.Context, p models.Patent) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO patents (title, inventors, application_no, application_at, registration_no, registration_at, country, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, p.Title, p.Inventors, p.ApplicationNo, p.ApplicationAt, p.RegistrationNo, p.RegistrationAt, p.Country, p.PublishedAt)
if err != nil {
return 0, fmt.Errorf("insert patent: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *PublicationsRepository) UpdatePatent(ctx context.Context, id int, p models.Patent) error {
res, err := r.db.ExecContext(ctx, `
UPDATE patents
SET title = ?, inventors = ?, application_no = ?, application_at = ?, registration_no = ?, registration_at = ?, country = ?, published_at = ?
WHERE id = ?
`, p.Title, p.Inventors, p.ApplicationNo, p.ApplicationAt, p.RegistrationNo, p.RegistrationAt, p.Country, p.PublishedAt, id)
if err != nil {
return fmt.Errorf("update patent: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *PublicationsRepository) DeletePatent(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM patents WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete patent: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
+82 -5
View File
@@ -17,7 +17,6 @@ func NewStandardizationRepository(db *sql.DB) *StandardizationRepository {
}
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
@@ -30,7 +29,7 @@ func (r *StandardizationRepository) GetStandardsBodiesWithProjects(ctx context.C
defer bodyRows.Close()
var bodies []models.StandardsBodyWithProjects
bodyMap := make(map[int]int) // body_id -> index in bodies
bodyMap := make(map[int]int)
for bodyRows.Next() {
var b models.StandardsBodyWithProjects
@@ -45,7 +44,6 @@ func (r *StandardizationRepository) GetStandardsBodiesWithProjects(ctx context.C
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
@@ -57,13 +55,12 @@ func (r *StandardizationRepository) GetStandardsBodiesWithProjects(ctx context.C
}
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
projectMap := make(map[projectKey]int)
for docRows.Next() {
var d models.StandardDocument
@@ -93,3 +90,83 @@ func (r *StandardizationRepository) GetStandardsBodiesWithProjects(ctx context.C
return bodies, docRows.Err()
}
func (r *StandardizationRepository) CreateStandardsBody(ctx context.Context, b models.StandardsBody) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO standards_bodies (org, full_name, scope, period, role, display_order)
VALUES (?, ?, ?, ?, ?, ?)
`, b.Org, b.FullName, b.Scope, b.Period, b.Role, b.DisplayOrder)
if err != nil {
return 0, fmt.Errorf("insert standards body: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *StandardizationRepository) UpdateStandardsBody(ctx context.Context, id int, b models.StandardsBody) error {
res, err := r.db.ExecContext(ctx, `
UPDATE standards_bodies
SET org = ?, full_name = ?, scope = ?, period = ?, role = ?, display_order = ?
WHERE id = ?
`, b.Org, b.FullName, b.Scope, b.Period, b.Role, b.DisplayOrder, id)
if err != nil {
return fmt.Errorf("update standards body: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *StandardizationRepository) DeleteStandardsBody(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM standards_bodies WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete standards body: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *StandardizationRepository) CreateStandardDocument(ctx context.Context, d models.StandardDocument) (int, error) {
res, err := r.db.ExecContext(ctx, `
INSERT INTO standard_documents (body_id, wg, project_name, title, doc_ref, status, published_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, d.BodyID, d.WG, d.ProjectName, d.Title, d.DocRef, d.Status, d.PublishedAt)
if err != nil {
return 0, fmt.Errorf("insert standard document: %w", err)
}
id, err := res.LastInsertId()
return int(id), err
}
func (r *StandardizationRepository) UpdateStandardDocument(ctx context.Context, id int, d models.StandardDocument) error {
res, err := r.db.ExecContext(ctx, `
UPDATE standard_documents
SET wg = ?, project_name = ?, title = ?, doc_ref = ?, status = ?, published_at = ?
WHERE id = ?
`, d.WG, d.ProjectName, d.Title, d.DocRef, d.Status, d.PublishedAt, id)
if err != nil {
return fmt.Errorf("update standard document: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}
func (r *StandardizationRepository) DeleteStandardDocument(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM standard_documents WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete standard document: %w", err)
}
rows, err := res.RowsAffected()
if err == nil && rows == 0 {
return sql.ErrNoRows
}
return err
}