Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[최세연: Go(Fiber)] Good-Night-Hackathon-Backend 제출 #11

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3906e24
set: go 프로젝트 시작
barabobBOB Aug 26, 2023
90f04e0
feat: docker compose 세팅
barabobBOB Aug 26, 2023
9c52757
feat: 데이터베이스 테이블 스크립트
barabobBOB Aug 26, 2023
acf8ae5
fix: 스키마 수정
barabobBOB Aug 26, 2023
f9c7a42
feat: DB 연결 완료
barabobBOB Aug 26, 2023
bc6e906
feat: movie 도메인 선언
barabobBOB Aug 26, 2023
7f21a67
feat: movie 레포지토리 인터페이스 선언
barabobBOB Aug 26, 2023
4da4b69
fix: gitingnore 수정
barabobBOB Aug 26, 2023
435f67c
fix: movie repository 수정
barabobBOB Aug 26, 2023
974da7e
feat: controller 추가
barabobBOB Aug 26, 2023
a780377
feat: usecase 추가
barabobBOB Aug 26, 2023
946fcdb
fix: repository 추가
barabobBOB Aug 26, 2023
6b4d512
feat: 라우터 추가
barabobBOB Aug 26, 2023
3b8e7ce
feat: 버그 해결
barabobBOB Aug 26, 2023
282b1b3
fix: 도메인 수정
barabobBOB Aug 26, 2023
9074a83
fix: movie 엔티티 수정
barabobBOB Aug 26, 2023
49689b4
feat: review 도메인 추가
barabobBOB Aug 26, 2023
cf06898
feat: review 레포지토리 추가
barabobBOB Aug 26, 2023
44ed103
feat: usecase 추가
barabobBOB Aug 26, 2023
e7d3365
feat: review controller 추가
barabobBOB Aug 26, 2023
0afe8ff
feat: 라우터 추가
barabobBOB Aug 26, 2023
9c4cf0f
feat: 리뷰 목록 조회 기능 추가
barabobBOB Aug 26, 2023
b9afca3
feat: 영화 목록 평점 순 조회 추가
barabobBOB Aug 26, 2023
18bbf20
fix: print 제거
barabobBOB Aug 27, 2023
3c29447
fix: 현재 상영중 계산 추가
barabobBOB Aug 27, 2023
0c4fd74
fix: 빈 배열 리턴하도록 구현
barabobBOB Aug 27, 2023
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea
db
.env
go.sum
54 changes: 54 additions & 0 deletions database/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package database

import (
"context"
"database/sql"
"fmt"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"log"
"os"
"time"
)

func init() {
// Load .env file
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}

// Assign environment variables
host = os.Getenv("DB_HOST")
port = os.Getenv("DB_PORT")
user = os.Getenv("DB_USER")
password = os.Getenv("DB_PASSWORD")
dbname = os.Getenv("DB_NAME")
}

// Database settings
var (
host string
port string
user string
password string
dbname string
)

func DatabaseConnection() (*sql.DB, error) {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai", host, port, user, password, dbname)

db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal("Failed to connect to database. \n", err)
}

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

err = db.PingContext(ctx)
if err != nil {
return nil, err
}
return db, nil
}
22 changes: 22 additions & 0 deletions database/migrations/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE movies (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
genre VARCHAR(10) CHECK (genre IN ('스릴러', '로맨스', '코믹', '액션')) NOT NULL,
release_date DATE,
end_date DATE,
is_showing BOOLEAN,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ NULL
);

CREATE TABLE reviews (
id SERIAL PRIMARY KEY,
movie_id INT,
rating INT CHECK (rating >= 1 AND rating <= 5),
content TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ NULL,
FOREIGN KEY (movie_id) REFERENCES movies(id)
);
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'
services:
DB:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: 1234
ports:
- 5434:5432
volumes:
- ./db/data:/var/lib/postgresql/data
42 changes: 42 additions & 0 deletions domain/movie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package domain

import (
"database/sql"
"time"
)

type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
Genre string `json:"genre"`
ReleaseDate time.Time `json:"release_date"`
EndDate time.Time `json:"end_date"`
IsShowing bool `json:"is_showing"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at"`
}

type MovieRepository interface {
Insert(movie *Movie) error
FindAll(options *RatingQueryOptions) ([]Movie, error)
FindById(id int) (Movie, error)
FindAllByRating(options *PaginationOptions) ([]MovieWithRating, error)
Update(movie *Movie) error
Delete(movie *Movie) error
}

type MovieWithRating struct {
Movie Movie
AvgRating float64
}

type PaginationOptions struct {
Page int
PageSize int
}

type RatingQueryOptions struct {
Genre string
IsShowing *bool
}
26 changes: 26 additions & 0 deletions domain/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package domain

import (
"database/sql"
"time"
)

type Review struct {
ID int `json:"id"`
MovieID int `json:"movie_id"`
Rating int `json:"rating"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt sql.NullTime `json:"deleted_at,omitempty"`
}

type ReviewRepository interface {
Insert(review *Review) error
FindAllByMovie(options *ReviewQueryOptions) ([]Review, error)
}

type ReviewQueryOptions struct {
MovieId int
Rating float64
}
20 changes: 20 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module server

go 1.19

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/gofiber/fiber/v2 v2.48.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.10.0 // indirect
)
134 changes: 134 additions & 0 deletions interfaces/controller/movie_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package controller

import (
"server/interfaces/controller/response"
"strconv"

"github.com/gofiber/fiber/v2"
"server/domain"
"server/usecase"
)

type MovieController struct {
MovieUsecase usecase.MovieUsecase
}

func NewMovieController(movieUsecase usecase.MovieUsecase) *MovieController {
return &MovieController{MovieUsecase: movieUsecase}
}

func (c *MovieController) AddMovie(ctx *fiber.Ctx) error {
var movie domain.Movie
if err := ctx.BodyParser(&movie); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

if err := c.MovieUsecase.AddMovie(&movie); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

return ctx.Status(fiber.StatusCreated).JSON(response.MovieSuccessResponse(&movie, "Movie successfully added"))
}

func (c *MovieController) ListMovies(ctx *fiber.Ctx) error {
options := &domain.RatingQueryOptions{}

genre := ctx.Query("genre")
if genre != "" {
options.Genre = genre
}

isShowingStr := ctx.Query("is_showing")
if isShowingStr != "" {
isShowing, err := strconv.ParseBool(isShowingStr)
if err == nil {
options.IsShowing = &isShowing
}
}

movies, err := c.MovieUsecase.ListMovies(options)
if movies == nil {
movies = []domain.Movie{}
}

if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(response.MoviesErrorResponse(err))
}

return ctx.Status(fiber.StatusOK).JSON(response.MoviesSuccessResponse(&movies, "Movies list"))
}

func (c *MovieController) GetMovieById(ctx *fiber.Ctx) error {
idStr := ctx.Params("id")
id, err := strconv.Atoi(idStr)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

movie, err := c.MovieUsecase.GetMovieById(id)
if err != nil {
return ctx.Status(fiber.StatusNotFound).JSON(response.MovieErrorResponse(err))
}

return ctx.Status(fiber.StatusOK).JSON(response.MovieSuccessResponse(&movie, "Movie details"))
}

func (c *MovieController) ListMoviesByRating(ctx *fiber.Ctx) error {
options := &domain.PaginationOptions{}
page := ctx.Query("page")
if page != "" {
err := ctx.QueryParser(options)
options.Page, err = strconv.Atoi(page)
if err != nil {
options.Page = 1
}
}

pageSize := ctx.Query("pageSize")
if pageSize != "" {
err := ctx.QueryParser(options)
options.PageSize, err = strconv.Atoi(pageSize)
if err != nil {
options.PageSize = 10
}
}
movies, err := c.MovieUsecase.ListMoviesByRating(options)

if movies == nil {
movies = []domain.MovieWithRating{}
}

if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(response.MoviesErrorResponse(err))
}

return ctx.Status(fiber.StatusOK).JSON(response.MovieRatingSuccessResponse(movies, "Movies list by rating"))
}

func (c *MovieController) UpdateMovieDetails(ctx *fiber.Ctx) error {
var movie domain.Movie
if err := ctx.BodyParser(&movie); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

if err := c.MovieUsecase.UpdateMovieDetails(&movie); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

return ctx.Status(fiber.StatusOK).JSON(response.MovieSuccessResponse(&movie, "Movie successfully updated"))
}

func (c *MovieController) RemoveMovie(ctx *fiber.Ctx) error {
idStr := ctx.Params("id")
id, err := strconv.Atoi(idStr)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(response.MovieErrorResponse(err))
}

movie := &domain.Movie{ID: id}
if err := c.MovieUsecase.RemoveMovie(movie); err != nil {
return ctx.Status(fiber.StatusNotFound).JSON(response.MovieErrorResponse(err))
}

return ctx.Status(fiber.StatusNoContent).JSON(fiber.Map{"state": "Movie successfully removed"})
}
11 changes: 11 additions & 0 deletions interfaces/controller/request/movie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package request

import "time"

type MovieRequest struct {
Title string `json:"title"`
Genre string `json:"genre"`
ReleaseDate *time.Time `json:"release_date"`
EndDate *time.Time `json:"end_date"`
IsShowing bool `json:"is_showing"`
}
7 changes: 7 additions & 0 deletions interfaces/controller/request/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package request

type Review struct {
MovieID int `json:"movie_id"`
Rating int `json:"rating"`
Content string `json:"content"`
}
55 changes: 55 additions & 0 deletions interfaces/controller/response/movie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package response

import (
"github.com/gofiber/fiber/v2"
"server/domain"
"time"
)

type Movie struct {
Title string `json:"title"`
Genre string `json:"genre"`
ReleaseDate *time.Time `json:"release_date"`
EndDate *time.Time `json:"end_date"`
IsShowing bool `json:"is_showing"`
}

func MovieSuccessResponse(data *domain.Movie, message string) *fiber.Map {
movie := Movie{
Title: data.Title,
Genre: data.Genre,
ReleaseDate: &data.ReleaseDate,
EndDate: &data.EndDate,
IsShowing: data.IsShowing,
}
return &fiber.Map{
"state": message,
"data": movie,
}
}

func MovieRatingSuccessResponse(data []domain.MovieWithRating, message string) *fiber.Map {
return &fiber.Map{
"state": message,
"data": data,
}
}

func MoviesSuccessResponse(data *[]domain.Movie, message string) *fiber.Map {
return &fiber.Map{
"state": message,
"data": data,
}
}

func MovieErrorResponse(err error) *fiber.Map {
return &fiber.Map{
"error": err.Error(),
}
}

func MoviesErrorResponse(err error) *fiber.Map {
return &fiber.Map{
"error": err.Error(),
}
}
Loading