Skip to content

Commit

Permalink
[feat] csvファイルのアップロードAPIとrowの件数分取得APIの作成
Browse files Browse the repository at this point in the history
  • Loading branch information
hikahana committed Oct 9, 2024
1 parent 1662713 commit 2ff5a51
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 18 deletions.
76 changes: 59 additions & 17 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2087,26 +2087,69 @@ const docTemplate = `{
},
},
},
"/sponsors/periods/{year}": {
"get": {
tags: ["sponsor"],
"description": "年度で指定されたsponsorを取得",
"parameters": [
"/sponsors/periods/{year}": {
"get": {
tags: ["sponsor"],
"description": "年度で指定されたsponsorを取得",
"parameters": [
{
"name": "year",
"in": "path",
"description": "year",
"required": true,
"type": "integer"
}
],
"responses": {
"200": {
"description": "sponsorの取得完了",
}
}
}
},
"/sponsors/csv": {
"post": {
"tags": ["sponsor"],
"description": "CSVファイルを使ってsponsorを作成する",
"consumes": [
"multipart/form-data"
],
"parameters": [
{
"name": "year",
"in": "path",
"description": "year",
"required": true,
"type": "integer"
"name": "file",
"in": "formData",
"description": "CSVファイル",
"required": true,
"type": "file"
}
],
"responses": {
],
"responses": {
"200": {
"description": "sponsorの取得完了",
"description": "作成されたsponsorsの情報が返ってくる",
}
}
}
},
},
},
"/sponsors/rowAffected/{rowAffected}": {
"get": {
tags: ["sponsor"],
"description": "createdAt以降のsponsorの取得",
"parameters": [
{
"name": "rowAffected",
"in": "path",
"description": "rowAffected",
"required": true,
"type": "integer",
}
],
"responses": {
"200": {
"description": "sponsorの取得",
}
}
},
},
"/sponsorstyles": {
"get": {
tags: ["sponsorstyle"],
Expand Down Expand Up @@ -3012,7 +3055,6 @@ const docTemplate = `{
"year":{
"type": "int",
"example": 2024,
},
"startedAt":{
"type": "string",
Expand All @@ -3026,7 +3068,7 @@ const docTemplate = `{
"required":{
"year",
"startedAt",
"endedAt"
"endedAt",
},
},
},
Expand Down
31 changes: 30 additions & 1 deletion api/externals/controller/sponsor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type SponsorController interface {
UpdateSponsor(echo.Context) error
DestroySponsor(echo.Context) error
IndexSponsorByPeriod(echo.Context) error
CreateSponsorsByCsv(echo.Context) error
IndexSponsorsByRowAffected(echo.Context) error
}

func NewSponsorController(u usecase.SponsorUseCase) SponsorController {
Expand Down Expand Up @@ -92,7 +94,7 @@ func (s *sponsorController) DestroySponsor(c echo.Context) error {
return c.String(http.StatusOK, "Destroy Sponsor")
}

//年度別に取得
// 年度別に取得
func (s *sponsorController) IndexSponsorByPeriod(c echo.Context) error {
year := c.Param("year")
sponsors, err := s.u.GetSponsorByPeriod(c.Request().Context(), year)
Expand All @@ -101,3 +103,30 @@ func (s *sponsorController) IndexSponsorByPeriod(c echo.Context) error {
}
return c.JSON(http.StatusOK, sponsors)
}

// cavで一括登録
func (s *sponsorController) CreateSponsorsByCsv(c echo.Context) error {
file, err := c.FormFile("file")
if err != nil {
return err
}
csv, err := file.Open()
if err != nil {
return err
}
defer csv.Close()
csvSponsor, err := s.u.CreateSponsorsByCsv(c.Request().Context(), csv)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()})
}
return c.JSON(http.StatusOK, csvSponsor)
}

func (s *sponsorController) IndexSponsorsByRowAffected(c echo.Context) error {
row := c.Param("row")
sponsors, err := s.u.GetSponsorByRowAffected(c.Request().Context(), row)
if err != nil {
return err
}
return c.JSON(http.StatusOK, sponsors)
}
35 changes: 35 additions & 0 deletions api/externals/repository/sponsor_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package repository
import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/NUTFes/FinanSu/api/drivers/db"
"github.com/NUTFes/FinanSu/api/externals/repository/abstract"
"github.com/NUTFes/FinanSu/api/internals/domain"
)

type sponsorRepository struct {
Expand All @@ -21,6 +24,8 @@ type SponsorRepository interface {
Delete(context.Context, string) error
FindLatestRecord(context.Context) (*sql.Row, error)
AllByPeriod(context.Context, string) (*sql.Rows, error)
CreateByCsv(context.Context, []domain.Sponsor) (string, error)
FindByRowsAffected(context.Context, string) (*sql.Rows, error)
}

func NewSponsorRepository(c db.Client, ac abstract.Crud) SponsorRepository {
Expand Down Expand Up @@ -116,3 +121,33 @@ func (sr *sponsorRepository) AllByPeriod(c context.Context, year string) (*sql.R
" ORDER BY sponsors.id;"
return sr.crud.Read(c, query)
}

// csvで一括登録
func (sr *sponsorRepository) CreateByCsv(c context.Context, csvRecords []domain.Sponsor) (string, error) {
query := `
INSERT INTO
sponsors (name, tel, email, address, representative)
VALUES`
values := []string{}
for _, record := range csvRecords {
values = append(values, fmt.Sprintf("('%s', '%s', '%s', '%s', '%s')",
record.Name,
record.Tel,
record.Email,
record.Address,
record.Representative,
))
}
query += strings.Join(values, ", ")
rowAffected, err := sr.crud.UpdateAndReturnRows(c, query)
if err != nil {
return "", err
}
return rowAffected, err
}

// rowの件数分取得
func (sr *sponsorRepository) FindByRowsAffected(c context.Context, row string) (*sql.Rows, error) {
query := fmt.Sprintf("SELECT * FROM sponsors ORDER BY id DESC LIMIT %s", row)
return sr.crud.Read(c, query)
}
126 changes: 126 additions & 0 deletions api/internals/usecase/sponsor_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package usecase

import (
"context"
"encoding/csv"
"fmt"
"io"
"strings"
"unicode/utf8"

rep "github.com/NUTFes/FinanSu/api/externals/repository"
"github.com/NUTFes/FinanSu/api/internals/domain"
Expand All @@ -18,6 +23,8 @@ type SponsorUseCase interface {
UpdateSponsor(context.Context, string, string, string, string, string, string) (domain.Sponsor, error)
DestroySponsor(context.Context, string) error
GetSponsorByPeriod(context.Context, string) ([]domain.Sponsor, error)
CreateSponsorsByCsv(context.Context, io.Reader) ([]domain.Sponsor, error)
GetSponsorByRowAffected(context.Context, string) ([]domain.Sponsor, error)
}

func NewSponsorUseCase(rep rep.SponsorRepository) SponsorUseCase {
Expand Down Expand Up @@ -160,3 +167,122 @@ func (s *sponsorUseCase) GetSponsorByPeriod(c context.Context, year string) ([]d
}
return sponsors, nil
}

func (s *sponsorUseCase) CreateSponsorsByCsv(c context.Context, csvFile io.Reader) ([]domain.Sponsor, error) {
sponsor := domain.Sponsor{}
var sponsors []domain.Sponsor

r := csv.NewReader(csvFile)
r.TrimLeadingSpace = true
records, err := r.ReadAll()
if err != nil {
return nil, err
}

if len(records) == 0 {
return nil, fmt.Errorf("csvの中身が空です。")
}

header := []string{"Name", "Tel", "Email", "Address", "Representative"}
records = removeBOM(records)

for i, record := range records {
if i == 0 {
if !isHeaderMatch(header, record) {
return nil, fmt.Errorf("異なるヘッダーがあります。")
}
continue
}

for j := range record {
if isEmpty(record[j]) {
return nil, fmt.Errorf("空のレコードがあります。")
}
}

sponsor := domain.Sponsor{
Name: record[0],
Tel: record[1],
Email: record[2],
Address: record[3],
Representative: record[4],
}
sponsors = append(sponsors, sponsor)
}
rowAffected, err := s.rep.CreateByCsv(c, sponsors)
if err != nil {
return nil, err
}

sponsors = []domain.Sponsor{}
rows, err := s.rep.FindByRowsAffected(c, string(rowAffected))
if err != nil {
return nil, err
}
for rows.Next() {
err := rows.Scan(
&sponsor.ID,
&sponsor.Name,
&sponsor.Tel,
&sponsor.Email,
&sponsor.Address,
&sponsor.Representative,
&sponsor.CreatedAt,
&sponsor.UpdatedAt,
)
if err != nil {
return nil, err
}
sponsors = append(sponsors, sponsor)
}
return sponsors, nil
}

func (s *sponsorUseCase) GetSponsorByRowAffected(c context.Context, row string) ([]domain.Sponsor, error) {
sponsor := domain.Sponsor{}
var sponsors []domain.Sponsor
rows, err := s.rep.FindByRowsAffected(c, row)
if err != nil {
return nil, err
}
for rows.Next() {
err := rows.Scan(
&sponsor.ID,
&sponsor.Name,
&sponsor.Tel,
&sponsor.Email,
&sponsor.Address,
&sponsor.Representative,
&sponsor.CreatedAt,
&sponsor.UpdatedAt,
)
if err != nil {
return nil, err
}
sponsors = append(sponsors, sponsor)
}
return sponsors, nil
}

func isEmpty(s string) bool {
return s == ""
}

func removeBOM(header [][]string) [][]string {
for i, row := range header {
if len(row) > 0 && strings.HasPrefix(row[0], "\uFEFF") {
_, size := utf8.DecodeRuneInString(row[0])
header[i][0] = row[0][size:]
}
}
return header
}

func isHeaderMatch(headers []string, records []string) bool {
for i := range headers {
if headers[i] != records[i] {
return false
}
}
return true
}
2 changes: 2 additions & 0 deletions api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ func (r router) ProvideRouter(e *echo.Echo) {
e.PUT("/sponsors/:id", r.sponsorController.UpdateSponsor)
e.DELETE("/sponsors/:id", r.sponsorController.DestroySponsor)
e.GET("/sponsors/periods/:year", r.sponsorController.IndexSponsorByPeriod)
e.POST("/sponsors/csv", r.sponsorController.CreateSponsorsByCsv)
e.GET("/sponsors/rowAffected/:row", r.sponsorController.IndexSponsorsByRowAffected)

// sponsorstylesのRoute
e.GET("/sponsorstyles", r.sponsorStyleController.IndexSponsorStyle)
Expand Down

0 comments on commit 2ff5a51

Please sign in to comment.