diff --git a/src/main/java/com/likelion/picklbe/domain/brand/Brand.java b/src/main/java/com/likelion/picklbe/domain/brand/Brand.java index edd633b..331047b 100644 --- a/src/main/java/com/likelion/picklbe/domain/brand/Brand.java +++ b/src/main/java/com/likelion/picklbe/domain/brand/Brand.java @@ -67,6 +67,21 @@ public enum Brand { Pattern.compile("(농협|하나로)\\s*마트"), Pattern.compile("하나로클럽"), Pattern.compile("hanaro", Pattern.CASE_INSENSITIVE))), + GS_SUPER( + "gs-super", + "GS수퍼마켓", + "gs_super.png", + List.of( + Pattern.compile("지에스\\s*리테일"), + Pattern.compile("\\bGS\\s*수퍼", Pattern.CASE_INSENSITIVE), + Pattern.compile("\\bGS\\s*super", Pattern.CASE_INSENSITIVE))), + GS_THE_FRESH( + "gs-the-fresh", + "GS더프레시", + "gs_the_fresh.png", + List.of( + Pattern.compile("더\\s*프레시"), + Pattern.compile("\\bGS\\s*the\\s*fresh", Pattern.CASE_INSENSITIVE))), DEFAULT("default", "기타", null, List.of()); private final String code; @@ -122,4 +137,3 @@ public static Brand fromCodeSafe(String code) { return DEFAULT; } } - diff --git a/src/main/java/com/likelion/picklbe/domain/brand/BrandImageResolver.java b/src/main/java/com/likelion/picklbe/domain/brand/BrandImageResolver.java index 216797c..e54272a 100644 --- a/src/main/java/com/likelion/picklbe/domain/brand/BrandImageResolver.java +++ b/src/main/java/com/likelion/picklbe/domain/brand/BrandImageResolver.java @@ -17,9 +17,7 @@ public class BrandImageResolver { @Value("${app.cdn.default-file:mart_default.png}") private String defaultFile; - /** - * 원문(지점명/상호명/브랜드명 포함 가능)에서 Brand enum을 추정 - */ + /** 원문(지점명/상호명/브랜드명 포함 가능)에서 Brand enum을 추정 */ public Brand resolveBrand(String raw) { if (!StringUtils.hasText(raw)) { return Brand.DEFAULT; @@ -27,9 +25,7 @@ public Brand resolveBrand(String raw) { return Brand.fromStoreName(raw); } - /** - * explicitBrand(컬럼) 우선, 없으면 name(지점명)으로 추정 - */ + /** explicitBrand(컬럼) 우선, 없으면 name(지점명)으로 추정 */ public Brand resolveBrand(String explicitBrand, String name) { if (StringUtils.hasText(explicitBrand)) { // fromStoreName 가 브랜드명도 파싱한다는 전제 @@ -38,38 +34,28 @@ public Brand resolveBrand(String explicitBrand, String name) { return resolveBrand(name); } - /** - * Brand 코드만 필요할 때 - */ + /** Brand 코드만 필요할 때 */ public String resolveBrandCode(String raw) { return resolveBrand(raw).code(); } - /** - * Brand 코드만 이미 있는 경우(예: DB 조회) - */ + /** Brand 코드만 이미 있는 경우(예: DB 조회) */ public String resolveBrandCodeFromCode(String brandCode) { Brand b = Brand.fromCodeSafe(brandCode); // 없으면 DEFAULT 반환하도록 enum에 구현되어 있어야 함 return b.code(); } - /** - * 원문에서 바로 대표 이미지 URL - */ + /** 원문에서 바로 대표 이미지 URL */ public String resolveImageUrl(String raw) { return imageUrlFor(resolveBrand(raw)); } - /** - * explicitBrand + name 동시 고려 - */ + /** explicitBrand + name 동시 고려 */ public String resolveImageUrl(String explicitBrand, String name) { return imageUrlFor(resolveBrand(explicitBrand, name)); } - /** - * Brand가 이미 있는 경우 이미지 URL - */ + /** Brand가 이미 있는 경우 이미지 URL */ public String imageUrlFor(Brand brand) { // 1) brand가 null/DEFAULT 이거나 파일명이 비어있으면 기본 이미지 String filename = @@ -86,25 +72,19 @@ public String imageUrlFor(Brand brand) { return joinUrl(rtrimSlashes(baseUrl), rtrimSlashes(brandPath), filename); } - /** - * Brand 코드로 바로 이미지 URL (코드만 내려받는 API/쿼리 대응) - */ + /** Brand 코드로 바로 이미지 URL (코드만 내려받는 API/쿼리 대응) */ public String imageUrlForCode(String brandCode) { Brand b = Brand.fromCodeSafe(brandCode); return imageUrlFor(b); } - /** - * 원문에서 Brand와 이미지 URL/코드를 한 번에 - */ + /** 원문에서 Brand와 이미지 URL/코드를 한 번에 */ public ResolvedBrand resolve(String raw) { Brand b = resolveBrand(raw); return new ResolvedBrand(b, imageUrlFor(b), b.code()); } - /** - * explicitBrand + name 동시 입력 버전 - */ + /** explicitBrand + name 동시 입력 버전 */ public ResolvedBrand resolve(String explicitBrand, String name) { Brand b = resolveBrand(explicitBrand, name); return new ResolvedBrand(b, imageUrlFor(b), b.code()); @@ -142,11 +122,6 @@ private static String joinUrl(String... parts) { return sb.toString(); } - /** - * 편의 반환 DTO - */ - public record ResolvedBrand(Brand brand, String imageUrl, String code) { - - } + /** 편의 반환 DTO */ + public record ResolvedBrand(Brand brand, String imageUrl, String code) {} } - diff --git a/src/main/java/com/likelion/picklbe/global/s3/controller/S3Controller.java b/src/main/java/com/likelion/picklbe/global/s3/controller/S3Controller.java index ccc9196..e2703eb 100644 --- a/src/main/java/com/likelion/picklbe/global/s3/controller/S3Controller.java +++ b/src/main/java/com/likelion/picklbe/global/s3/controller/S3Controller.java @@ -33,8 +33,7 @@ public class S3Controller { @Operation(summary = "이미지 업로드 API", description = "이미지를 업로드하고 URL을 리턴받는 API") @PostMapping(value = "/image-upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadImage( - @RequestParam PathName pathName, MultipartFile file - ) { + @RequestParam PathName pathName, MultipartFile file) { S3Response s3Response = s3Service.uploadImage(pathName, file); return ResponseEntity.ok(BaseResponse.success("이미지 업로드에 성공했습니다.", s3Response)); diff --git a/src/main/java/com/likelion/picklbe/global/s3/service/BrandImagePromotionService.java b/src/main/java/com/likelion/picklbe/global/s3/service/BrandImagePromotionService.java new file mode 100644 index 0000000..e670840 --- /dev/null +++ b/src/main/java/com/likelion/picklbe/global/s3/service/BrandImagePromotionService.java @@ -0,0 +1,40 @@ +package com.likelion.picklbe.global.s3.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.likelion.picklbe.domain.brand.Brand; + +import com.amazonaws.services.s3.AmazonS3; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BrandImagePromotionService { + + private final AmazonS3 s3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.path.brand:images/brand}") + private String brandPath; + + public String promote(String uuidKey, Brand brand) { + if (brand == null || brand.filename() == null) { + throw new IllegalArgumentException("고정 파일명이 없는 브랜드입니다: " + brand); + } + String srcKey = trimSlash(brandPath) + "/" + trimSlash(uuidKey); + String dstKey = trimSlash(brandPath) + "/" + brand.filename(); + + s3.copyObject(bucket, srcKey, bucket, dstKey); + s3.deleteObject(bucket, srcKey); + + return dstKey; + } + + private String trimSlash(String s) { + return s == null ? "" : s.replaceAll("^/+", "").replaceAll("/+$", ""); + } +} diff --git a/src/main/java/com/likelion/picklbe/global/s3/service/S3Service.java b/src/main/java/com/likelion/picklbe/global/s3/service/S3Service.java index 6127491..718b5c7 100644 --- a/src/main/java/com/likelion/picklbe/global/s3/service/S3Service.java +++ b/src/main/java/com/likelion/picklbe/global/s3/service/S3Service.java @@ -1,25 +1,29 @@ package com.likelion.picklbe.global.s3.service; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.DeleteObjectRequest; -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.likelion.picklbe.global.config.S3Config; -import com.likelion.picklbe.global.exception.CustomException; -import com.likelion.picklbe.global.s3.dto.S3Response; -import com.likelion.picklbe.global.s3.entity.PathName; -import com.likelion.picklbe.global.s3.exception.S3ErrorCode; import java.io.ByteArrayInputStream; import java.util.Base64; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import com.likelion.picklbe.global.config.S3Config; +import com.likelion.picklbe.global.exception.CustomException; +import com.likelion.picklbe.global.s3.dto.S3Response; +import com.likelion.picklbe.global.s3.entity.PathName; +import com.likelion.picklbe.global.s3.exception.S3ErrorCode; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ListObjectsV2Request; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + @Slf4j @Service @RequiredArgsConstructor @@ -95,17 +99,17 @@ public String base64UploadFile(PathName pathName, String base64Url) { public String createKeyName(PathName pathName) { return switch (pathName) { - case M01 -> s3Config.getM01Folder(); - case M02 -> s3Config.getM02Folder(); - case M03 -> s3Config.getM03Folder(); - case M04 -> s3Config.getM04Folder(); - case M05 -> s3Config.getM05Folder(); - case M06 -> s3Config.getM06Folder(); - case M07 -> s3Config.getM07Folder(); - case M08 -> s3Config.getM08Folder(); - case MARKET -> s3Config.getMARKETFolder(); - case BRAND -> s3Config.getBRANDFolder(); - } + case M01 -> s3Config.getM01Folder(); + case M02 -> s3Config.getM02Folder(); + case M03 -> s3Config.getM03Folder(); + case M04 -> s3Config.getM04Folder(); + case M05 -> s3Config.getM05Folder(); + case M06 -> s3Config.getM06Folder(); + case M07 -> s3Config.getM07Folder(); + case M08 -> s3Config.getM08Folder(); + case MARKET -> s3Config.getMARKETFolder(); + case BRAND -> s3Config.getBRANDFolder(); + } + '/' + UUID.randomUUID(); }