Live at : bonitto.eunchan.com
Youtube : https://www.youtube.com/watch?v=7c8_krY4jr4
실무 도메인 문제 구현을 위한 코딩 채점 사이트 입니다.
실무 도메인 문제의 예시는 아래와 같습니다.
- User Authentication and Authorization
- Contents Management System
- Realtime Chat Server
- Image Resizing, Caching, Serving and Uploading
이 프로젝트는 모노레포(단일저장소)로서 프론트와 백엔드 그리고 여러 마이크로서비스들을 포함하고 있습니다.
개발 실행과 배포 실행 두가지 종류가 있습니다.
- 개발실행
- dockerizing backend & push to docker repo
make docker-back docker push bonitto-back:lateset 도커저장소이미지주소
- setup kubernetes with backend image url you uploaded
vi infra/kubernetes/configmap.yaml # example 따라 잘 만들어주시고 kubectl apply -k infra/kubernetes
- run backend
go run cmd/preparer/preparer.go go run cmd/recorder/recorder.go go run cmd/tester/tester.go go run cmd/web/web.go
- run frontend
cd front yarn yarn start
- go to
http://localhost:3000
- dockerizing backend & push to docker repo
- 배포실행
- dockerizing backend, frontend
make docker-back docker push bonitto-back:latest 도커저장소이미지주소 make docker-front docker push bonitto-front:latest 도커저장소이미지주소
- setup kubernetes
vi infra/kubernetes/configmap.yaml # example 따라 잘 만들어주시고 kubectl apply -k infra/kubernetes
- go to service endpoint
- dockerizing backend, frontend
사용자의 소스를 실행시키고 평가하는게 주 업무입니다.
위와 같은 Event Driven 구조로 되어있습니다.
더 자세한 설명은 PPT 를 참고해주세요.
Project Directory Structure
.
├── cmd # microservices' entry point
│ ├── preparer
│ ├── recorder
│ ├── tester
│ └── web
├── front # CRA 앱
├── infra
│ └── kubernetes # kubernetes kustomization 한방 인프라
└── internal # microservice share codes
├── model
├── notifier
├── preparer
├── problems
├── queue
└── recorder
Directory Structure:
infra/kubernetes
├── cluster-role-binding.yaml
*** configmap.yaml # 밑의 example 기반으로 직접 만들어야 합니다
├── configmap.yaml.example
├── deployment-preparer.yaml
├── deployment-recorder.yaml
├── deployment-web.yaml
├── kustomization.yaml
├── service-account.yaml
└── service.yaml
configmap.yaml
: 직접 만들어주셔야 합니다
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bonitto
data:
REDIS_URL: 포트번호까지 포함한 redis url
WORKER_INTERVAL_MS: 마이크로서비스들 폴링 인터벌 (1000)
KUBERNETES_SERVICE_HOST: 쿠버네티스 master url
KUBERNETES_SERVICE_PORT: 쿠버네티스 master port
IMAGE_TESTER: "ckcks12/bonitto-back:v0.17" # tester 이미지 입니다
IMAGE_JAVASCRIPT: "node:14.8.0-alpine3.11"
IMAGE_JAVA: "alpine3"
IMAGE_GOLANG: "golang:1.15.0-alpine3.12"
IMAGE_CPP: "alpine3A"
COMMAND_JAVASCRIPT: 'echo "${CODE}" > /code.js && node /code.js& echo start; sleep 300; echo end'
COMMAND_JAVA: "whoami"
COMMAND_GOLANG: 'echo "${CODE}" > /code.go && go run /code.go& echo start; sleep 300; echo end'
COMMAND_CPP: "whoami"
kustomization.yaml
: 커스텀 이미지를 구워 사용할 경우 수정해주셔야 합니다.
# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
resources:
- cluster-role-binding.yaml
- deployment-web.yaml
- deployment-preparer.yaml
- deployment-recorder.yaml
- service-account.yaml
- configmap.yaml
- service.yaml
images:
- name: bonitto-front
newName: ckcks12/bonitto-front # 프론트 이미지 입니다.
newTag: v0.11 # 프론트 이미지 태그 입니다.
- name: bonitto-back
newName: ckcks12/bonitto-back # 백엔드 이미지 입니다.
newTag: v0.17 # 백엔드 이미지 태그 입니다.
아래 명령어로 kustomization 을 apply 하여 쿠버네티스에 띄울 수 있습니다.
cd infra/kubernetes
kubectl apply -k .
Directory Structure:
.
├── cmd # microservices' entry point
│ ├── preparer
│ ├── recorder
│ ├── tester
│ └── web
└── internal # microservice share codes
├── model # 기본 모델들 정의되어 있는 곳
├── notifier # notifier producer/consumer 인터페이스 구현
├── preparer # preparer producer/consumer 인터페이스 구현
├── problems # 문제 정의(설명, 테스트케이스 등)
├── queue # producer/consumer 인터페이스 정의 및 RedisProducer RedisConsumer 정의
└── recorder # recorder producer/consumer 인터페이스 구현
Build and Run:
# preparer
go run cmd/preparer/preparer.go
# recorder
go run cmd/recorder/recorder.go
# tester
go run cmd/tester/tester.go
# web
go run cmd/web/web.go
Dockerizing:
make docker-back
docker run --rm -p 8080:8080 bonitto-back:latest /preparer
docker run --rm -p 8080:8080 bonitto-back:latest /recorder
docker run --rm -p 8080:8080 bonitto-back:latest /tester
docker run --rm -p 8080:8080 bonitto-back:latest /web
Directory Structure:
front/src
├── App.tsx # router 설정 등 React App Main Entry Point
├── lib
│ ├── api.ts # 서버 통신하는 API 함수
│ └── type.ts # 모델 정의
└── page
├── MainPage.tsx # 메인 페이지
├── ProblemListPage.tsx # 문제 목록 페이지
└── ProblemPage.tsx # 문제 상세보기 페이지
Run:
cd front
yarn start
Build:
cd front
yarn build
Dockerizing:
주의할 점: nginx.conf 에서 websocket proxy 주소가 localhost 입니다. 항상 세트로 front 와 back 이 배포된다 설계한 것 입니다. 따라서 loopback 이 다른 컨테이너에게 연결되게 해야합니다. Linux 라면
network_mode: host
으로 간단히 해결 가능합니다. Mac 과 Window 라면 차라리 nginx.conf 에서 proxy 주소를 바꾸신 뒤 이에 맞게 설정해주세요. (예를 들어 docker-compose 라면 service 이름을 바꾸기)
가장 추천드리는 방법은 로컬에서는 위의 yarn start
로, 배포할때만 docker 를 사용하시는 것 입니다.
make docker-front
docker run --rm -p 80:80 bonitto-front
Youtube : https://www.youtube.com/watch?v=7c8_krY4jr4
8번 문제 User 에 대한 정답 코드 golang 입니다.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
)
type User struct {
ID string `json:"id"`
PW string `json:"pw"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Visit int `json:"visit"`
Deleted bool `json:"deleted"`
}
var DB = make([]User, 0)
func add(u User) {
DB = append(DB, u)
}
func find(id string) (User, error) {
for _, u := range DB {
if u.ID == id {
return u, nil
}
}
return User{}, errors.New("not found")
}
func del(id string) error {
idx := -1
for i, u := range DB {
if u.ID == id {
idx = i
break
}
}
if idx < 0 {
return errors.New("not found")
}
l := len(DB)
DB[idx] = DB[l-1]
DB = DB[:l-1]
return nil
}
func update(u User) (User, error) {
u2, err := find(u.ID)
if err != nil {
return User{}, nil
}
if u.Name != "" {
u2.Name = u.Name
}
if u.Email != "" {
u2.Email = u.Email
}
if u.Phone != "" {
u2.Phone = u.Phone
}
if err := del(u.ID); err != nil {
return User{}, err
}
add(u2)
return u2, nil
}
func main() {
server := &http.Server{Addr: ":80"}
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
GetUser(w, r)
case http.MethodPost:
PostUser(w, r)
case http.MethodPut:
PutUser(w, r)
case http.MethodDelete:
DeleteUser(w, r)
}
})
if err := http.ListenAndServe(":80", nil); err != nil {
panic(err)
}
server.Close()
}
func encodeJSON(a interface{}) []byte {
b, err := json.Marshal(a)
if err != nil {
panic(err)
}
return b
}
func GetUser(w http.ResponseWriter, r *http.Request) {
ids, ok := r.URL.Query()["id"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}
id := ids[0]
if !checkID(id) {
w.WriteHeader(http.StatusBadRequest)
return
}
u, err := find(id)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
io.WriteString(w, string(encodeJSON(u)))
}
func PostUser(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
u := User{}
if err := json.Unmarshal(body, &u); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if !(checkID(u.ID) &&
checkPW(u.PW) &&
checkName(u.Name) &&
checkEmail(u.Email) &&
checkPhone(u.Phone)) {
w.WriteHeader(http.StatusBadRequest)
return
}
add(u)
io.WriteString(w, string(encodeJSON(u)))
}
func PutUser(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
u := User{}
if err := json.Unmarshal(body, &u); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if u.Name != "" && !checkName(u.Name) {
w.WriteHeader(http.StatusBadRequest)
return
}
if u.Email != "" && !checkEmail(u.Email) {
w.WriteHeader(http.StatusBadRequest)
return
}
if u.Phone != "" && !checkPhone(u.Phone) {
w.WriteHeader(http.StatusBadRequest)
return
}
u2, err := update(u)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
io.WriteString(w, string(encodeJSON(u2)))
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
u := User{}
if err := json.Unmarshal(body, &u); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := del(u.ID); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusOK)
}
func checkOnlyAlphaNumeric(s string) bool {
sources := []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_'}
for _, a := range s {
not := true
for _, r := range sources {
if a == r {
not = false
break
}
}
if not {
return false
}
}
return true
}
func checkLength(s string, min, max int) bool {
return min <= len(s) && len(s) <= max
}
func checkID(id string) bool {
return checkLength(id, 5, 12) && checkOnlyAlphaNumeric(id)
}
func checkPW(pw string) bool {
return checkLength(pw, 0, 50)
}
func checkName(name string) bool {
return checkLength(name, 2, 50) && checkOnlyAlphaNumeric(name)
}
func checkEmail(email string) bool {
return checkLength(email, 1, 50) && strings.Contains(email, "@")
}
func checkPhone(phone string) bool {
return checkLength(phone, 1, 20) && strings.Contains(phone, "+")
}
Eunchan Lee(@ckcks12) - 3unch4n@gmail.com
Beer License