Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
max_line_length = 100
trim_trailing_whitespace = true

[*.go]
indent_style = tab

[*.md]
trim_trailing_whitespace = false
81 changes: 81 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
frontend:
name: Frontend
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install development tools
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
with:
version: 2026.8.3
sha256: 66585ac496c10bf6fbf13272e3e550c1813aed0e1cb780b9bb73c1751de49289
install_args: --locked node@24.19.0 pnpm@11.20.0
cache: false

- name: Install dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile

- name: Run checks
working-directory: frontend
run: pnpm check

- name: Build
working-directory: frontend
run: pnpm build

backend:
name: Backend
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install development tools
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
with:
version: 2026.8.3
sha256: 66585ac496c10bf6fbf13272e3e550c1813aed0e1cb780b9bb73c1751de49289
install_args: --locked go@1.26.4
cache: false

- name: Check formatting
working-directory: backend
run: test -z "$(gofmt -l .)"

- name: Run go vet
working-directory: backend
run: go vet ./...

- name: Run tests
working-directory: backend
run: go test ./...

- name: Build
working-directory: backend
run: go build ./...
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.DS_Store
.idea/

.env
.env.*
!.env.example

frontend/node_modules/
frontend/dist/
frontend/coverage/

backend/server
11 changes: 11 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"recommendations": [
"Vue.volar",
"oxc.oxc-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"EditorConfig.EditorConfig",
"golang.go",
"vitest.explorer"
]
}
26 changes: 26 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"[css]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[vue]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"eslint.validate": ["javascript", "typescript", "vue"],
"oxc.fmt.configPath": "./frontend/oxfmt.config.ts",
"stylelint.validate": ["css", "vue"]
}
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 1m26_13

traP 1-Monthon 2026 13班のプロジェクトです。

バックエンドはGo、フロントエンドはVueで開発します。UIには
[BasiQ UI](https://github.com/traP-jp/basiq-ui)を使用する予定です。

## 開発環境

- mise 2026.8.3以降
- Go 1.26.4
- Node.js 24.19.0
- pnpm 11.20.0

Go、Node.js、pnpmのバージョンはmiseで管理しています。miseを使用しない場合も、
`mise.toml`と同じバージョンを用意してください。

## セットアップ

```sh
mise install --locked

cd frontend
pnpm install --frozen-lockfile
```

miseをshellで有効化していない場合は、`go`や`pnpm`の代わりに
`mise exec -- go`、`mise exec -- pnpm`を使用できます。

## 開発サーバー

バックエンドを起動します。

```sh
cd backend
go run ./cmd/server
```

別のターミナルでフロントエンドを起動します。

```sh
cd frontend
pnpm dev
```

フロントエンドは <http://localhost:5173>、バックエンドは
<http://localhost:8080> で起動します。Viteは`/api`へのリクエストをバックエンドへ
プロキシします。

## チェック

フロントエンド:

```sh
cd frontend
pnpm check
pnpm build
```

バックエンド:

```sh
cd backend
test -z "$(gofmt -l .)"
go vet ./...
go test ./...
go build ./...
```

## ディレクトリ構成

```text
.
├── backend/ # Goバックエンド
└── frontend/ # Vueフロントエンド
```

## ブランチ運用

- 統合ブランチは`main`のみとする
- 変更は短命な作業ブランチからPull Requestを作成する
- ブランチ名は`chore/...`、`feat/...`、`fix/...`を基本とする
- Pull Requestは原則としてSquash mergeし、merge後に作業ブランチを削除する
- `main`を常にbuild可能な状態に保つ
71 changes: 71 additions & 0 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/traP-jp/1m26_13/backend/internal/httpapi"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if err := run(ctx); err != nil {
slog.Error("server stopped", "error", err)
os.Exit(1)
}
}

func run(ctx context.Context) error {
addr := os.Getenv("APP_ADDR")
if addr == "" {
addr = ":8080"
}

server := &http.Server{
Addr: addr,
Handler: httpapi.NewHandler(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}

serveError := make(chan error, 1)

slog.Info("starting server", "address", addr)
go func() {
serveError <- server.ListenAndServe()
}()

select {
case err := <-serveError:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
case <-ctx.Done():
slog.Info("shutting down server")
}

shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

if err := server.Shutdown(shutdownContext); err != nil {
return fmt.Errorf("shutdown server: %w", err)
}

if err := <-serveError; err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}

return nil
}
3 changes: 3 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/traP-jp/1m26_13/backend

go 1.26.0
24 changes: 24 additions & 0 deletions backend/internal/httpapi/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package httpapi

import (
"encoding/json"
"net/http"
)

type healthResponse struct {
Status string `json:"status"`
}

func NewHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/health", handleHealth)

return mux
}

func handleHealth(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

_ = json.NewEncoder(w).Encode(healthResponse{Status: "ok"})
}
54 changes: 54 additions & 0 deletions backend/internal/httpapi/router_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package httpapi

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestHealth(t *testing.T) {
t.Parallel()

request := httptest.NewRequest(http.MethodGet, "/api/health", nil)
response := httptest.NewRecorder()

NewHandler().ServeHTTP(response, request)

if response.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, response.Code)
}

if got, want := response.Header().Get("Content-Type"), "application/json"; got != want {
t.Fatalf("expected content type %q, got %q", want, got)
}

if got, want := response.Body.String(), "{\"status\":\"ok\"}\n"; got != want {
t.Fatalf("expected body %q, got %q", want, got)
}
}

func TestHealthRejectsPost(t *testing.T) {
t.Parallel()

request := httptest.NewRequest(http.MethodPost, "/api/health", nil)
response := httptest.NewRecorder()

NewHandler().ServeHTTP(response, request)

if response.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, response.Code)
}
}

func TestUnknownRoute(t *testing.T) {
t.Parallel()

request := httptest.NewRequest(http.MethodGet, "/unknown", nil)
response := httptest.NewRecorder()

NewHandler().ServeHTTP(response, request)

if response.Code != http.StatusNotFound {
t.Fatalf("expected status %d, got %d", http.StatusNotFound, response.Code)
}
}
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_PROXY_TARGET=http://localhost:8080
Loading