Skip to content

Commit

Permalink
[1112] section68
Browse files Browse the repository at this point in the history
  • Loading branch information
myeunee committed Nov 12, 2024
1 parent d65db76 commit 5bd6be0
Show file tree
Hide file tree
Showing 24 changed files with 656 additions and 0 deletions.
33 changes: 33 additions & 0 deletions chapter17/section68/.air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
root = "."
tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/main ."
# 'cmd'에서 바이너리 파일 지정
bin = "tmp/main"

# 80번 포트를 사용하도록 실행 시 인수를 지정
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main 80"

include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "_tools", "cert", "testutil"]
exclude_regex = ["_test.go"]
exclude_unchanged = true
follow_symlink = true
log = "air.log"
delay = 1000
stop_on_error = true
send_interrupt = false
kill_delay = 500

[log]
time = false

[color]
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
clean_on_exit = true
2 changes: 2 additions & 0 deletions chapter17/section68/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git
.DS_Store
36 changes: 36 additions & 0 deletions chapter17/section68/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Build stage
# 최신 버전으로 수정 ******
FROM golang:1.23.1 AS deploy-builder

WORKDIR /app

# go.mod, go.sum 파일 복사 및 의존성 다운로드
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# 바이너리 빌드
RUN go build -trimpath -ldflags "-w -s" -o app

#--------------------------------
# Deployment stage
# 최신 GLIBC 버전을 사용하기 위해 우분투로 교체

FROM ubuntu:latest as deploy

RUN apt-get update && apt-get install -y libgcc-s1

# 빌드된 바이너리를 복사
COPY --from=deploy-builder /app/app .

# 실행 명령
CMD ["./app"]

#--------------------------------
# Development stage with Air for live reloading

# ******** 최신 버전으로 수정 ***********
FROM golang:1.23.1 as dev
WORKDIR /app
RUN go install github.com/air-verse/air@latest
CMD ["air"]
28 changes: 28 additions & 0 deletions chapter17/section68/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.PHONY: help build build-local up down logs ps test
.DEFAULT_GOAL := help

DOCKER_TAG := latest
build: ## 배포용 도커 이미지 빌드 + 내 모듈 이름에 맞게 수정 **********
docker build -t myeunee/golangstudy-chapter17-section68:${DOCKER_TAG} --target deploy ./

build-local: ## 로컬 환경용 도커 이미지 빌드
docker compose build --no-cache

up: ## 자동 새로고침을 사용한 도커 컴포즈 실행
docker compose up -d

down: ## 도커 컴포즈 종료
docker compose down

logs: ## 도커 컴포즈 로그 출력
docker compose logs -f

ps: ## 컨테이너 상태 확인
docker compose ps

test: ## 테스트 실행
go test -race -shuffle=on ./...

help: ## 옵션 보기
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
18 changes: 18 additions & 0 deletions chapter17/section68/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package config

import (
"github.com/caarlos0/env/v6"
)

type Config struct {
Env string `env:"TODO_ENV" envDefault:"dev"`
Port int `env:"PORT" envDefault:"80"`
}

func New() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}
23 changes: 23 additions & 0 deletions chapter17/section68/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"fmt"
"testing"
)

func TestNew(t *testing.T) {
wantPort := 3333
t.Setenv("PORT", fmt.Sprint(wantPort))

got, err := New()
if err != nil {
t.Fatalf("cannot create config: %v", err)
}
if got.Port != wantPort {
t.Errorf("want %d, but %d", wantPort, got.Port)
}
wantEnv := "dev"
if got.Env != wantEnv {
t.Errorf("want %s, but %s", wantEnv, got.Env)
}
}
14 changes: 14 additions & 0 deletions chapter17/section68/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3.9"
services:
app:
image: gotodo
build:
args:
- target=dev
environment:
TODO_ENV: dev
PORT: 8080
volumes:
- .:/app
ports:
- "18000:8080"
21 changes: 21 additions & 0 deletions chapter17/section68/entity/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package entity

import "time"

type TaskID int64
type TaskStatus string

const (
TaskStatusTodo TaskStatus = "todo"
TaskStatusDoing TaskStatus = "doing"
TaskStatusDone TaskStatus = "done"
)

type Task struct {
ID TaskID `json:"id"`
Title string `json:"title"`
Status TaskStatus `json:"status" `
Created time.Time `json:"created"`
}

type Tasks []*Task
21 changes: 21 additions & 0 deletions chapter17/section68/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module github.com/myeunee/GolangStudy/chapter17/section68

go 1.23.1

require (
github.com/caarlos0/env/v6 v6.10.1
github.com/go-playground/validator/v10 v10.22.1
github.com/google/go-cmp v0.6.0
golang.org/x/sync v0.8.0
)

require (
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
34 changes: 34 additions & 0 deletions chapter17/section68/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
53 changes: 53 additions & 0 deletions chapter17/section68/handler/add_task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package handler

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

"github.com/go-playground/validator/v10"
"github.com/myeunee/GolangStudy/chapter17/section68/entity"
"github.com/myeunee/GolangStudy/chapter17/section68/store"
)

type AddTask struct {
Store *store.TaskStore
Validator *validator.Validate
}

func (at *AddTask) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var b struct {
Title string `json:"title" validate:"required"`
}
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
RespondJSON(ctx, w, &ErrResponse{
Message: err.Error(),
}, http.StatusInternalServerError)
return
}
err := validator.New().Struct(b)
if err != nil {
RespondJSON(ctx, w, &ErrResponse{
Message: err.Error(),
}, http.StatusBadRequest)
return
}

t := &entity.Task{
Title: b.Title,
Status: "todo",
Created: time.Now(),
}
id, err := store.Tasks.Add(t)
if err != nil {
RespondJSON(ctx, w, &ErrResponse{
Message: err.Error(),
}, http.StatusInternalServerError)
return
}
rsp := struct {
ID int `json:"id"`
}{ID: int(id)}
RespondJSON(ctx, w, rsp, http.StatusOK)
}
62 changes: 62 additions & 0 deletions chapter17/section68/handler/add_task_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package handler

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

"github.com/go-playground/validator/v10"
"github.com/myeunee/GolangStudy/chapter17/section68/entity"
"github.com/myeunee/GolangStudy/chapter17/section68/store"
"github.com/myeunee/GolangStudy/chapter17/section68/testutil"
)

func TestAddTask(t *testing.T) {
type want struct {
status int
rspFile string
}
tests := map[string]struct {
reqFile string
want want
}{
"ok": {
reqFile: "testdata/add_task/ok_req.json.golden",
want: want{
status: http.StatusOK,
rspFile: "testdata/add_task/ok_rsp.json.golden",
},
},
"badRequest": {
reqFile: "testdata/add_task/bad_req.json.golden",
want: want{
status: http.StatusBadRequest,
rspFile: "testdata/add_task/bad_rsp.json.golden",
},
},
}
for n, tt := range tests {
tt := tt
t.Run(n, func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()
r := httptest.NewRequest(
http.MethodPost,
"/tasks",
bytes.NewReader(testutil.LoadFile(t, tt.reqFile)),
)

sut := AddTask{Store: &store.TaskStore{
Tasks: map[entity.TaskID]*entity.Task{},
}, Validator: validator.New()}
sut.ServeHTTP(w, r)

resp := w.Result()
testutil.AssertResponse(t,
resp, tt.want.status, testutil.LoadFile(t, tt.want.rspFile),
)
})
}
}
34 changes: 34 additions & 0 deletions chapter17/section68/handler/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package handler

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

type ErrResponse struct {
Message string `json:"message"`
Details []string `json:"details,omitempty"`
}

func RespondJSON(ctx context.Context, w http.ResponseWriter, body any, status int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
bodyBytes, err := json.Marshal(body)
if err != nil {
fmt.Printf("encode response error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
rsp := ErrResponse{
Message: http.StatusText(http.StatusInternalServerError),
}
if err := json.NewEncoder(w).Encode(rsp); err != nil {
fmt.Printf("write error response error: %v", err)
}
return
}

w.WriteHeader(status)
if _, err := fmt.Fprintf(w, "%s", bodyBytes); err != nil {
fmt.Printf("write response error: %v", err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Implement a handler"
}
Loading

0 comments on commit 5bd6be0

Please sign in to comment.