diff --git a/api/docs/docs.go b/api/docs/docs.go index bb884df32..692d11d31 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -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"], @@ -3012,7 +3055,6 @@ const docTemplate = `{ "year":{ "type": "int", "example": 2024, - }, "startedAt":{ "type": "string", @@ -3026,7 +3068,7 @@ const docTemplate = `{ "required":{ "year", "startedAt", - "endedAt" + "endedAt", }, }, }, diff --git a/api/externals/controller/sponsor_controller.go b/api/externals/controller/sponsor_controller.go index 87cc1a871..bb3a9610c 100644 --- a/api/externals/controller/sponsor_controller.go +++ b/api/externals/controller/sponsor_controller.go @@ -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 { @@ -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) @@ -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) +} diff --git a/api/externals/repository/sponsor_repository.go b/api/externals/repository/sponsor_repository.go index 1fe310074..9bbc90579 100644 --- a/api/externals/repository/sponsor_repository.go +++ b/api/externals/repository/sponsor_repository.go @@ -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 { @@ -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 { @@ -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) +} diff --git a/api/internals/usecase/sponsor_usecase.go b/api/internals/usecase/sponsor_usecase.go index f2be5c57a..fe0de1d53 100644 --- a/api/internals/usecase/sponsor_usecase.go +++ b/api/internals/usecase/sponsor_usecase.go @@ -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" @@ -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 { @@ -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 +} diff --git a/api/router/router.go b/api/router/router.go index cd05eba67..ebf943508 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -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)