feat(arch): unify frontend and backend into single Go backend server with static export

- Configure Next.js static export (output: 'export') with client-side dynamic fetching
- Implement Go static serving with SPA fallback in backend/internal/router/router.go
- Unify multi-stage build in backend/Dockerfile (Node -> Go -> minimal Alpine runtime)
- Simplify root docker-compose.yml to single anl-app service on port 8080
- Update REQUIREMENTS.md DM-03 and AC-17 normative contracts to v3.0 Unified Go Backend
- Restore strict regression verification in Gate 9 (AC-35/36/37) and unittest suite
- All Go unit tests, TypeScript type checks, ESLint, and E2E gates passed clean
This commit is contained in:
2026-08-25 12:54:39 +09:00
parent 5f87631530
commit 9bdc9bdd9a
27 changed files with 597 additions and 812 deletions
+41
View File
@@ -521,6 +521,47 @@ func TestCascadeDeletes(t *testing.T) {
assert.Equal(t, 0, courseCount, "Courses should be cascaded when parent semester is deleted")
}
func TestStaticServingAndRedirects(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
r := router.SetupRouter(db, testAdminToken)
// 1. /intro 308 permanent redirect to /
wIntro := doRequest(r, "GET", "/intro", nil, "")
assert.Equal(t, http.StatusPermanentRedirect, wIntro.Code)
assert.Equal(t, "/", wIntro.Header().Get("Location"))
// 2. Unknown API route returns 404 JSON (not HTML)
wAPI404 := doRequest(r, "GET", "/api/v1/unknown-endpoint", nil, "")
assert.Equal(t, http.StatusNotFound, wAPI404.Code)
var apiErr map[string]any
require.NoError(t, json.Unmarshal(wAPI404.Body.Bytes(), &apiErr))
assert.Contains(t, apiErr, "error")
// 3. Create mock web directory to verify static HTML serving
webDir := "./web"
require.NoError(t, os.MkdirAll(filepath.Join(webDir, "test-page"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(webDir, "index.html"), []byte("<html>Home</html>"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(webDir, "test-page", "index.html"), []byte("<html>Test Page</html>"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(webDir, "404.html"), []byte("<html>404 Not Found</html>"), 0644))
defer os.RemoveAll(webDir)
// Test root URL /
wRoot := doRequest(r, "GET", "/", nil, "")
assert.Equal(t, http.StatusOK, wRoot.Code)
assert.Contains(t, wRoot.Body.String(), "<html>Home</html>")
// Test sub-route /test-page
wTestPage := doRequest(r, "GET", "/test-page", nil, "")
assert.Equal(t, http.StatusOK, wTestPage.Code)
assert.Contains(t, wTestPage.Body.String(), "<html>Test Page</html>")
// Test unknown page -> 404 HTML
wUnknown := doRequest(r, "GET", "/completely-unknown-route", nil, "")
assert.Equal(t, http.StatusNotFound, wUnknown.Code)
assert.Contains(t, wUnknown.Body.String(), "<html>404 Not Found</html>")
}
func strconvItoa(i int) string {
return strconv.Itoa(i)
}