Skip to content

Commit

Permalink
Feat: Use thumbnail images for nearby markers
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfex4936 committed Oct 23, 2024
1 parent 301b11d commit 2b83fd4
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 36 deletions.
6 changes: 6 additions & 0 deletions backend/dto/search_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ type MarkerIndexData struct {
FullAddress string `json:"fullAddress"`
InitialConsonants string `json:"initialConsonants"` // 초성
}

type KoreaStation struct {
Name string
Latitude float64
Longitude float64
}
12 changes: 10 additions & 2 deletions backend/handler/kakao_bot_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,17 @@ func (h *KakaoBotHandler) HandleKakaoSearchMarkers(c *fiber.Ctx) error {
listCard.Title = utterance + " 결과"

for _, pullup := range response.Markers {
// Remove <mark> and </mark> tags from the address
cleanAddress := strings.ReplaceAll(pullup.Address, "<mark>", "")
cleanAddress = strings.ReplaceAll(cleanAddress, "</mark>", "")

// Add the cleaned address to the listCard
listCard.Items.Add(k.ListItemLink{}.New(
strconv.Itoa(pullup.MarkerID), pullup.Address, "",
"https://k-pullup.com/pullup/"+strconv.Itoa(pullup.MarkerID)))
strconv.Itoa(int(pullup.MarkerID)), // Ensure MarkerID is converted to string correctly
cleanAddress,
"",
"https://k-pullup.com/pullup/"+strconv.Itoa(int(pullup.MarkerID)),
))
}

listCard.Buttons.Add(k.ShareButton{}.New("공유하기"))
Expand Down
13 changes: 8 additions & 5 deletions backend/model/photo.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package model

import "time"
import (
"time"
)

// Photo corresponds to the Photos table in the database
type Photo struct {
UploadedAt time.Time `json:"uploadedAt" db:"UploadedAt"`
PhotoID int `json:"photoId" db:"PhotoID"`
MarkerID int `json:"markerId" db:"MarkerID"`
PhotoURL string `json:"photoUrl" db:"PhotoURL"`
UploadedAt time.Time `json:"uploadedAt" db:"UploadedAt"`
PhotoID int `json:"photoId" db:"PhotoID"`
MarkerID int `json:"markerId" db:"MarkerID"`
PhotoURL string `json:"photoUrl" db:"PhotoURL"`
ThumbnailURL *string `json:"thumbnailUrl,omitempty" db:"ThumbnailURL"`
}
2 changes: 1 addition & 1 deletion backend/service/marker_facility_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
updateAddressQuery = "UPDATE Markers SET Address = ? WHERE MarkerID = ?"

// userAgent
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
)

type MarkerFacilityService struct {
Expand Down
4 changes: 2 additions & 2 deletions backend/service/marker_location_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ SELECT m.MarkerID,
m.Description,
ST_Distance_Sphere(m.Location, ST_GeomFromText(?, 4326)) AS Distance,
m.Address,
MAX(p.PhotoURL) AS Thumbnail
COALESCE(p.ThumbnailURL, p.PhotoURL) AS Thumbnail
FROM Markers m
LEFT JOIN (
SELECT p1.MarkerID, p1.PhotoURL
SELECT p1.MarkerID, p1.PhotoURL, p1.ThumbnailURL
FROM Photos p1
WHERE p1.UploadedAt = (
SELECT MAX(p2.UploadedAt)
Expand Down
4 changes: 2 additions & 2 deletions backend/service/marker_management_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func (s *MarkerManageService) CreateMarkerWithPhotos(markerDto *dto.MarkerReques
defer wg.Done()

// Upload the file to S3
fileURL, err := s.S3Service.UploadFileToS3(folder, file)
fileURL, err := s.S3Service.UploadFileToS3(folder, file, true)
if err != nil {
errorChan <- err
return
Expand Down Expand Up @@ -749,7 +749,7 @@ func (s *MarkerManageService) UploadMarkerPhotoToS3(markerID int, files []*multi
picUrls := make([]string, 0)
// Process file uploads from the multipart form
for _, file := range files {
fileURL, err := s.S3Service.UploadFileToS3(folder, file)
fileURL, err := s.S3Service.UploadFileToS3(folder, file, true)
if err != nil {
fmt.Printf("Failed to upload file to S3: %v\n", err)
continue // Skip this file and continue with the next
Expand Down
2 changes: 1 addition & 1 deletion backend/service/marker_story_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (s *StoryService) AddStory(markerID int, userID int, caption string, photo

// Upload the photo to S3
folder := fmt.Sprintf("stories/%d", markerID)
photoURL, err := s.S3Service.UploadFileToS3(folder, photo)
photoURL, err := s.S3Service.UploadFileToS3(folder, photo, true)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion backend/service/report_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (s *ReportService) CreateReport(report *dto.MarkerReportRequest, form *mult
wg.Add(1)
go func(file *multipart.FileHeader) {
defer wg.Done()
fileURL, err := s.S3Service.UploadFileToS3("reports", file)
fileURL, err := s.S3Service.UploadFileToS3("reports", file, false)
if err != nil {
errorChan <- fmt.Errorf("%w: %v", ErrFileUpload, err)
return
Expand Down
160 changes: 140 additions & 20 deletions backend/service/s3_service.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
package service

import (
"bytes"
"context"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"mime/multipart"
"net/url"
"path/filepath"
"strings"
"time"

myconfig "github.com/Alfex4936/chulbong-kr/config"
"github.com/Alfex4936/chulbong-kr/util"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
"go.uber.org/zap"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/google/uuid"
)

Expand Down Expand Up @@ -44,7 +54,7 @@ func NewS3Service(c *myconfig.S3Config, redis *RedisService, logger *zap.Logger)
}
}

func (s *S3Service) UploadFileToS3(folder string, file *multipart.FileHeader) (string, error) {
func (s *S3Service) UploadFileToS3(folder string, file *multipart.FileHeader, thumbnail bool) (string, error) {
// Open the uploaded file
fileData, err := file.Open()
if err != nil {
Expand Down Expand Up @@ -77,6 +87,20 @@ func (s *S3Service) UploadFileToS3(folder string, file *multipart.FileHeader) (s
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// If thumbnail is requested and file is an image, generate the thumbnail
if thumbnail && isImage(ext) {
err = s.GenerateThumbnail(ctx, fileData, folder, uuid.String(), ext)
if err != nil {
s.logger.Error("failed to generate or upload thumbnail", zap.Error(err))
}

// Reset fileData to the beginning for uploading the original file
_, err = fileData.Seek(0, io.SeekStart)
if err != nil {
return "", fmt.Errorf("failed to seek fileData: %w", err)
}
}

// Upload the file to S3
_, err = s.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &s.Config.S3BucketName,
Expand All @@ -102,7 +126,7 @@ func (s *S3Service) UploadFileToS3(folder string, file *multipart.FileHeader) (s
return fileURL, nil
}

// DeleteDataFromS3 deletes a photo from S3 given its URL.
// DeleteDataFromS3 deletes a photo and its thumbnail from S3 given its URL.
func (s *S3Service) DeleteDataFromS3(dataURL string) error {
var bucketName, key string

Expand All @@ -126,35 +150,41 @@ func (s *S3Service) DeleteDataFromS3(dataURL string) error {
return fmt.Errorf("invalid key")
}

// if isImage(filepath.Ext(key)) {
// s.Redis.ResetCache("image:" + key)
// }

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

// Load the AWS credentials
cfg, err := config.LoadDefaultConfig(
ctx,
config.WithRegion(s.Config.AwsRegion),
)
if err != nil {
return fmt.Errorf("could not load AWS credentials: %w", err)
// Delete the object(s)
keysToDelete := []string{key}

// Check if the key does not contain '_thumb' and is an image
ext := strings.ToLower(filepath.Ext(key))
if !strings.Contains(key, "_thumb") && isImage(ext) {
// Generate the thumbnail key
thumbKey := generateThumbnailKey(key)
keysToDelete = append(keysToDelete, thumbKey)
}

// Create an S3 client
s3Client := s3.NewFromConfig(cfg)
s3Client := s.s3Client // Reuse the existing client if available

// Delete the object
_, err = s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
// Delete the objects
deleteObjectsInput := &s3.DeleteObjectsInput{
Bucket: &bucketName,
Key: &key,
})
Delete: &types.Delete{
Objects: make([]types.ObjectIdentifier, len(keysToDelete)),
Quiet: aws.Bool(true),
},
}

for i, k := range keysToDelete {
deleteObjectsInput.Delete.Objects[i] = types.ObjectIdentifier{Key: &k}
}

_, err = s3Client.DeleteObjects(ctx, deleteObjectsInput)
if err != nil {
return fmt.Errorf("failed to delete object from S3: %w", err)
return fmt.Errorf("failed to delete object(s) from S3: %w", err)
}

// Wait until the object is deleted
return nil
}

Expand Down Expand Up @@ -266,6 +296,72 @@ func (s *S3Service) MoveFileInS3(sourceKey string, destinationKey string) error
return nil
}

// GenerateThumbnail generates a thumbnail for an image and uploads it to S3
func (s *S3Service) GenerateThumbnail(ctx context.Context, fileData multipart.File, folder, uuidStr, ext string) error {
// Reset the fileData to the beginning
_, err := fileData.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("failed to seek fileData: %w", err)
}

// Decode the image
img, _, err := image.Decode(fileData)
if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}

// Generate thumbnail
thumbImg := imaging.Thumbnail(img, 300, 300, imaging.Lanczos)

// Encode thumbnail to buffer
var buf bytes.Buffer
switch ext {
case ".jpg", ".jpeg":
err = jpeg.Encode(&buf, thumbImg, nil)
case ".png":
err = png.Encode(&buf, thumbImg)
case ".gif":
err = gif.Encode(&buf, thumbImg, nil)
case ".webp":
err = webp.Encode(&buf, thumbImg, nil)
default:
return fmt.Errorf("unsupported image format: %s", ext)
}
if err != nil {
return fmt.Errorf("failed to encode thumbnail: %w", err)
}

// Generate thumbnail key (e.g., append "_thumb" before extension)
thumbKey := fmt.Sprintf("%s/%s_thumb%s", folder, uuidStr, ext)

// Upload thumbnail to S3
_, err = s.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &s.Config.S3BucketName,
Key: &thumbKey,
Body: bytes.NewReader(buf.Bytes()),
})
if err != nil {
return fmt.Errorf("failed to upload thumbnail to S3: %w", err)
}

return nil
}

// ObjectExists checks if an object exists in S3
func (s *S3Service) ObjectExists(ctx context.Context, key string) (bool, error) {
_, err := s.s3Client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.Config.S3BucketName),
Key: aws.String(key),
})
if err != nil {
if strings.Contains(err.Error(), "NotFound") || strings.Contains(err.Error(), "404") {
return false, nil
}
return false, err
}
return true, nil
}

// Helper function to determine if a file extension corresponds to an image
func isImage(ext string) bool {
// Normalize the extension to lower case
Expand Down Expand Up @@ -324,3 +420,27 @@ func filepathExt(filename string) string {
}
return ""
}

// Helper function to generate the thumbnail key from the original key
func generateThumbnailKey(originalKey string) string {
ext := filepath.Ext(originalKey)
baseName := strings.TrimSuffix(originalKey, ext)
thumbKey := fmt.Sprintf("%s_thumb%s", baseName, ext)
return thumbKey
}

// getContentType returns the MIME type based on file extension
func getContentType(ext string) string {
switch ext {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
default:
return "application/octet-stream"
}
}
6 changes: 4 additions & 2 deletions backend/service/search_bleve_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import (
ristretto_store "github.com/eko/gocache/store/ristretto/v4"
)

const Analyzer = "koCJKEdgeNgram"
const (
Analyzer = "koCJKEdgeNgram"
nearDistance = "2km"
)

var (
// Map of Hangul initial consonant Unicode values to their corresponding Korean consonants.
Expand Down Expand Up @@ -622,7 +625,6 @@ func performWholeQuerySearch(index bleve.Index, t string, terms []string, result
var provinceTerm, cityTerm string
hasProvince := false
hasCity := false
nearDistance := "5km"

for _, assignment := range termAssignments {
var termQueries []query.Query
Expand Down

0 comments on commit 2b83fd4

Please sign in to comment.