-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
60 changed files
with
1,718 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
root = '.' | ||
tmp_dir = 'tmp' | ||
|
||
[build] | ||
cmd = "go build -o ./tmp/main ." #Go 프로젝트를 빌드 | ||
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"] | ||
include_dir = [] | ||
exclude_file = [] | ||
exclude_regex = ["_test.go"] | ||
exclude_unchanged = true | ||
exclude_underscore = false | ||
follow_symlink = false | ||
|
||
#로깅 설정 : 로그파일 경로, 파일 변경 감지 후 재빌드 대기 시간 | ||
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.git | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#최종 배포할 실행 파일 만드는 과정 | ||
#multi-stage build에서 이 단계의 이름을 'deploy-builder'로 지정 | ||
FROM golang:1.23.1-bullseye AS deploy-builder | ||
|
||
#app 디렉토리 생성 및 작업 디렉토리로 설정 | ||
WORKDIR /app | ||
|
||
#종속성 파일(go.mod go.sum)만 복사해서 캐시 활용 | ||
#소스코드가 변경되어도 종속성이 변경되지 않았다면 도커의 캐시 활용 | ||
COPY go.mod go.sum ./ | ||
#프로젝트의 모든 종속성 다운로드 | ||
RUN go mod download | ||
|
||
#전체 소스 파일을 /app으로 복사하고 최적화된 바이너리 생성 | ||
COPY . . | ||
RUN go build -trimpath -ldflags="-w -s" -o app | ||
|
||
# 최종 배포 단계 : 실제 운영 환경에서 실행될 최소한의 이미지 생성 | ||
#경량화된 Degian 이미지 사용 | ||
FROM debian:bullseye-slim AS deploy | ||
|
||
#시스템 패키지 최신화 | ||
RUN apt-get update | ||
|
||
#첫 번째 단계에서 만든 실행 파일만 복사 | ||
COPY --from=deploy-builder /app/app . | ||
|
||
#컨테이너 시작 시 애플리케이션 자동 실행 | ||
CMD ["./app"] | ||
|
||
#개발자의 로컬 환경을 위한 설정 | ||
FROM golang:1.23 AS dev | ||
|
||
#/app 디렉토리를 작업공간으로 설정 | ||
WORKDIR /app | ||
|
||
#air 도구 설치 (코드 변경 시 자동 재빌드 지원) | ||
RUN go install github.com/air-verse/air@latest | ||
|
||
#개발 서버 자동 시작 | ||
CMD ["air"] | ||
|
||
## docker-compose down -v <= 실행중인 컨테이너를 멈추고 매핑된 볼륨들을 제거 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
.PHONY: help build build-local up down logs ps test | ||
.DEFAULT_GOAL := help | ||
|
||
DOCKER_TAG := latest | ||
build: ## Build the docker image | ||
docker build -t gleaming9/go_todo_app:${DOCKER_TAG} \ | ||
--target deploy ./ | ||
|
||
build-local: ## Build the docker image for local development | ||
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}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package config | ||
|
||
import "github.com/caarlos0/env/v6" | ||
|
||
// Config 구조체는 애플리케이션 설정을 위한 구조체이다. | ||
// 환경 변수에서 값을 가져와서 필드를 초기화한다. | ||
type Config struct { | ||
Env string `env:"TODO_ENV" envDefault:"dev"` | ||
Port int `env:"PORT" envDefault:"80"` | ||
} | ||
|
||
// New 함수는 Config 구조체를 생성하고 환경 변수를 파싱하여 필드를 초기화 | ||
func New() (*Config, error) { | ||
cfg := &Config{} // config 구조체 초기화 | ||
if err := env.Parse(cfg); err != nil { | ||
return nil, err | ||
} | ||
return cfg, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestNeow(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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
services: | ||
|
||
# 'app'이라는 이름의 서비스(컨테이너)를 정의 | ||
app: | ||
# 사용할 Docker 이미지 이름 : 'gotodo' | ||
image: gotodo | ||
|
||
build: | ||
#빌드할 파일들이 있는 위치를 지정 : .. <- 한칸 위 폴더에 go.mod 위치 | ||
#go_todo_app 폴더를 컨텍스트로 설정 | ||
context: . | ||
dockerfile: Dockerfile | ||
# target=dev로 설정하여 Dockerfile의 dev stage를 빌드 | ||
args: | ||
target: dev | ||
environment: # 환경 변수 설정 | ||
TODO_ENV: dev | ||
PORT: 8080 | ||
|
||
# 로컬 컴퓨터의 파일과 컨테이너 안의 파일을 연결, 코드 수정 시 바로 컨테이너에 반영 | ||
volumes: | ||
- .:/app # go_todo_app 루트 디렉터리를 컨테이너의 /app에 마운트 | ||
|
||
# 컨테이너가 사용할 포트를 지정 | ||
ports: | ||
# 웹 브라우저에서 localhost:18000으로 접속하면 컨테이너의 80번 포트로 연결 | ||
# 왼쪽은 내 컴퓨터의 포트, 오른쪽은 컨테이너 안의 포트 | ||
- "18000:8080" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
module go_todo_app | ||
|
||
go 1.23.1 | ||
|
||
require ( | ||
github.com/caarlos0/env/v6 v6.10.1 // indirect | ||
github.com/go-chi/chi/v5 v5.1.0 // indirect | ||
github.com/go-playground/locales v0.14.1 // indirect | ||
github.com/go-playground/universal-translator v0.18.1 // indirect | ||
github.com/go-playground/validator v9.31.0+incompatible // indirect | ||
github.com/google/go-cmp v0.6.0 // indirect | ||
github.com/leodido/go-urn v1.4.0 // indirect | ||
golang.org/x/sync v0.9.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= | ||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | ||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= | ||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package handler | ||
|
||
import ( | ||
"encoding/json" | ||
"github.com/go-playground/validator" | ||
"go_todo_app/entity" | ||
"go_todo_app/store" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
type AddTask struct { | ||
Store *store.TaskStore | ||
Validator *validator.Validate | ||
} | ||
|
||
func (at *AddTask) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
ctx := r.Context() | ||
|
||
//요청 본문을 JSON으로 파싱하여 Title 필드를 가져옵니다. | ||
var b struct { | ||
// Title 필드는 필수입니다. | ||
Title string `json:"title" validate:"required"` | ||
} | ||
if err := json.NewDecoder(r.Body).Decode(&b); err != nil { | ||
// JSON 파싱 중 오류가 발생하면 500 상태 코드와 오류 메시지를 반환 | ||
RespondJSON(ctx, w, &ErrResponse{ | ||
Message: err.Error(), | ||
}, http.StatusBadRequest) | ||
return | ||
} | ||
// Validator를 사용하여 Title 필드가 있는지 검증 | ||
// Unmarshal한 타입에 대한 검증을 위해 Validator를 사용, JSON 구조가 방대하거나 복잡한 경우 자주 사용 | ||
if err := at.Validator.Struct(b); err != nil { | ||
// 필수 필드가 없으면 400 상태 코드와 오류 메시지를 반환 | ||
RespondJSON(ctx, w, &ErrResponse{ | ||
Message: err.Error(), | ||
}, http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// Task 구조체를 생성하여, Title, Status, Created 필드를 설정 | ||
t := &entity.Task{ | ||
Title: b.Title, // 입력받은 Title을 설정 | ||
Status: "todo", // 초기 상태는 "todo"로 설정 | ||
Created: time.Now(), // 생성된 시간을 현재 시간으로 설정 | ||
} | ||
// TaskStore에 Task를 추가하고 ID를 반환 | ||
id, err := store.Tasks.Add(t) | ||
if err != nil { | ||
// Task 추가 중 오류가 발생하면 500 상태 코드와 오류 메시지를 반환 | ||
RespondJSON(ctx, w, &ErrResponse{ | ||
Message: err.Error(), | ||
}, http.StatusInternalServerError) | ||
return | ||
} | ||
// Task의 ID를 JSON 응답으로 반환 | ||
rsp := struct { | ||
ID int `json:"id"` // JSON 응답에서 ID 필드의 이름을 "id"로 설정 | ||
}{ID: int(id)} | ||
RespondJSON(ctx, w, rsp, http.StatusOK) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package handler | ||
|
||
import ( | ||
"bytes" | ||
"github.com/go-playground/validator" | ||
"go_todo_app/entity" | ||
"go_todo_app/store" | ||
"go_todo_app/testutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
) | ||
|
||
func TestAddTask(t *testing.T) { | ||
type want struct { | ||
status int // 예상되는 HTTP 상태 코드 | ||
rspFile string // 예상되는 응답 JSON 파일 경로 | ||
} | ||
//테스트 케이스 정의 | ||
tests := map[string]struct { | ||
reqFile string // 요청 JSON 파일 경로 | ||
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 // 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{}, // 빈 TaskStore를 사용하여 저장 | ||
}, Validator: validator.New()} // 입력 검증을 위한 validator 생성 | ||
// AddTask 핸들러 실행 | ||
sut.ServeHTTP(w, r) | ||
|
||
// 예상 응답과 비교 | ||
resp := w.Result() | ||
testutil.AssertResponse(t, | ||
resp, tt.want.status, testutil.LoadFile(t, tt.want.rspFile)) | ||
}) | ||
} | ||
} |
Oops, something went wrong.