Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 27 additions & 24 deletions src/main/java/com/likelion/picklbe/domain/brand/Brand.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.likelion.picklbe.domain.brand;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

public enum Brand {
Expand Down Expand Up @@ -69,20 +73,12 @@ public enum Brand {
Pattern.compile("hanaro", Pattern.CASE_INSENSITIVE))),
GS_SUPER(
"gs-super",
"GS์ˆ˜ํผ๋งˆ์ผ“",
"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());
Pattern.compile("์ง€์—์Šค๋ฆฌํ…Œ์ผ"),
Pattern.compile("\\bGS\\b\\s*(THE\\s*FRESH|์Šˆํผ)?", Pattern.CASE_INSENSITIVE))),
DEFAULT("default", "๊ธฐํƒ€", "mart_default.png", List.of());

private final String code;
private final String displayName;
Expand All @@ -108,10 +104,29 @@ public String displayName() {
return displayName;
}

private static final Map<String, Brand> BY_CODE;

static {
Map<String, Brand> m = new HashMap<>();
for (Brand b : values()) {
m.put(b.code().toLowerCase(Locale.ROOT), b);
}
BY_CODE = Collections.unmodifiableMap(m);
}

public static Brand fromCodeSafe(String code) {
if (code == null || code.isBlank()) {
return DEFAULT;
}
Brand b = BY_CODE.get(code.toLowerCase(Locale.ROOT));
return (b != null) ? b : DEFAULT;
}

public static Brand fromStoreName(String name) {
if (name == null || name.isBlank()) {
return DEFAULT;
}
// ์šฐ์„ ์ˆœ์œ„: ์—๋ธŒ๋ฆฌ๋ฐ์ด โ†’ GS โ†’ ์ด๋งˆํŠธ ๋“ฑ (์„ ์–ธ ์ˆœ์„œ ์œ ์ง€)
for (Brand b : values()) {
if (b == DEFAULT) {
continue;
Expand All @@ -124,16 +139,4 @@ public static Brand fromStoreName(String name) {
}
return DEFAULT;
}

public static Brand fromCodeSafe(String code) {
if (code == null || code.isBlank()) {
return DEFAULT;
}
for (Brand b : values()) {
if (b.code.equalsIgnoreCase(code)) {
return b;
}
}
return DEFAULT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class BrandImageResolver {

// ex) https://picklocal.s3.ap-northeast-2.amazonaws.com
@Value("${app.cdn.base-url:https://picklocal.s3.ap-northeast-2.amazonaws.com}")
private String baseUrl;

Expand All @@ -19,109 +17,50 @@ public class BrandImageResolver {

/** ์›๋ฌธ(์ง€์ ๋ช…/์ƒํ˜ธ๋ช…/๋ธŒ๋žœ๋“œ๋ช… ํฌํ•จ ๊ฐ€๋Šฅ)์—์„œ Brand enum์„ ์ถ”์ • */
public Brand resolveBrand(String raw) {
if (!StringUtils.hasText(raw)) {
if (raw == null || raw.isBlank()) {
return Brand.DEFAULT;
}
return Brand.fromStoreName(raw);
}

/** explicitBrand(์ปฌ๋Ÿผ) ์šฐ์„ , ์—†์œผ๋ฉด name(์ง€์ ๋ช…)์œผ๋กœ ์ถ”์ • */
public Brand resolveBrand(String explicitBrand, String name) {
if (StringUtils.hasText(explicitBrand)) {
// fromStoreName ๊ฐ€ ๋ธŒ๋žœ๋“œ๋ช…๋„ ํŒŒ์‹ฑํ•œ๋‹ค๋Š” ์ „์ œ
return resolveBrand(explicitBrand);
}
return resolveBrand(name);
}

/** Brand ์ฝ”๋“œ๋งŒ ํ•„์š”ํ•  ๋•Œ */
public String resolveBrandCode(String raw) {
return resolveBrand(raw).code();
}

/** Brand ์ฝ”๋“œ๋งŒ ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ(์˜ˆ: DB ์กฐํšŒ) */
public String resolveBrandCodeFromCode(String brandCode) {
Brand b = Brand.fromCodeSafe(brandCode); // ์—†์œผ๋ฉด DEFAULT ๋ฐ˜ํ™˜ํ•˜๋„๋ก enum์— ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด์•ผ ํ•จ
return b.code();
/** Brand ์ฝ”๋“œ๋งŒ ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ โ†’ ์ด๋ฏธ์ง€ URL */
public String imageUrlForCode(String brandCode) {
Brand b = Brand.fromCodeSafe(brandCode);
return imageUrlFor(b);
}

/** ์›๋ฌธ์—์„œ ๋ฐ”๋กœ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ URL */
public String resolveImageUrl(String raw) {
return imageUrlFor(resolveBrand(raw));
}

/** explicitBrand + name ๋™์‹œ ๊ณ ๋ ค */
public String resolveImageUrl(String explicitBrand, String name) {
return imageUrlFor(resolveBrand(explicitBrand, name));
}

/** Brand๊ฐ€ ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ URL */
public String imageUrlFor(Brand brand) {
// 1) brand๊ฐ€ null/DEFAULT ์ด๊ฑฐ๋‚˜ ํŒŒ์ผ๋ช…์ด ๋น„์–ด์žˆ์œผ๋ฉด ๊ธฐ๋ณธ ์ด๋ฏธ์ง€
String filename =
(brand == null || brand == Brand.DEFAULT || !StringUtils.hasText(brand.filename()))
(brand == null || brand.filename() == null || brand == Brand.DEFAULT)
? defaultFile
: brand.filename();

// 2) filename ์ž์ฒด๊ฐ€ ์ ˆ๋Œ€ URL์ด๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ (์šด์˜ ์ค‘ ์ „์ฒด URL์„ enum์— ๋„ฃ์€ ๊ฒฝ์šฐ ๋Œ€์‘)
if (isAbsoluteUrl(filename)) {
return filename;
}

// 3) baseUrl/brandPath ์•ˆ์ „ ์กฐํ•ฉ
return joinUrl(rtrimSlashes(baseUrl), rtrimSlashes(brandPath), filename);
}

/** Brand ์ฝ”๋“œ๋กœ ๋ฐ”๋กœ ์ด๋ฏธ์ง€ URL (์ฝ”๋“œ๋งŒ ๋‚ด๋ ค๋ฐ›๋Š” API/์ฟผ๋ฆฌ ๋Œ€์‘) */
public String imageUrlForCode(String brandCode) {
Brand b = Brand.fromCodeSafe(brandCode);
return imageUrlFor(b);
return String.format("%s/%s/%s", rtrim(baseUrl), rtrim(brandPath), filename);
}

/** ์›๋ฌธ์—์„œ Brand์™€ ์ด๋ฏธ์ง€ URL/์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ์— */
/** ์›๋ฌธ์—์„œ Brand์™€ ์ด๋ฏธ์ง€ URL์„ ํ•œ ๋ฒˆ์— */
public ResolvedBrand resolve(String raw) {
Brand b = resolveBrand(raw);
return new ResolvedBrand(b, imageUrlFor(b), b.code());
}

/** explicitBrand + name ๋™์‹œ ์ž…๋ ฅ ๋ฒ„์ „ */
public ResolvedBrand resolve(String explicitBrand, String name) {
Brand b = resolveBrand(explicitBrand, name);
return new ResolvedBrand(b, imageUrlFor(b), b.code());
}

// ---- helpers -------------------------------------------------------------

private static boolean isAbsoluteUrl(String s) {
if (!StringUtils.hasText(s)) {
return false;
}
String t = s.trim().toLowerCase();
return t.startsWith("http://") || t.startsWith("https://");
}

private static String rtrimSlashes(String s) {
if (!StringUtils.hasText(s)) {
private String rtrim(String s) {
if (s == null || s.isBlank()) {
return "";
}
return s.replaceAll("/+$", "");
}

private static String joinUrl(String... parts) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
String p = parts[i] == null ? "" : parts[i].trim();
if (p.isEmpty()) {
continue;
}
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '/') {
sb.append('/');
}
sb.append(p.replaceAll("^/+", "")); // ์•ž ์Šฌ๋ž˜์‹œ ์ œ๊ฑฐ
}
return sb.toString();
}

/** ํŽธ์˜ ๋ฐ˜ํ™˜ DTO */
public record ResolvedBrand(Brand brand, String imageUrl, String code) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.likelion.picklbe.domain.brand.controller;

import java.util.List;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.likelion.picklbe.domain.brand.Brand;
import com.likelion.picklbe.global.s3.service.BrandImagePromotionService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/brands")
@Tag(name = "Brand Admin", description = "๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€ ์Šน๊ฒฉ(๊ด€๋ฆฌ์ž) API")
public class BrandAdminController {

private final BrandImagePromotionService promotionService;

/** ๋‹จ๊ฑด ์Šน๊ฒฉ: uuid, brandCode */
@PostMapping(path = "/promote", produces = MediaType.TEXT_PLAIN_VALUE)
@Operation(
summary = "๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€ ๋‹จ๊ฑด ์Šน๊ฒฉ",
description =
"""
์—…๋กœ๋“œ๋œ UUID ํ‚ค ํŒŒ์ผ์„ ํ•ด๋‹น ๋ธŒ๋žœ๋“œ์˜ **๊ณ ์ • ํŒŒ์ผ๋ช…**์œผ๋กœ ๋ณต์‚ฌ(์Šน๊ฒฉ)ํ•ฉ๋‹ˆ๋‹ค.
- `uuid`๋Š” `images/brand/<uuid>`์—์„œ `<uuid>` **ํŒŒ์ผ๋ช… ๋ถ€๋ถ„**๋งŒ ๋„ฃ์œผ์„ธ์š”.
- ๊ฐ€๋Šฅํ•œ brandCode ์˜ˆ: emart, homeplus, lotte-mart, emart-everyday, no-brand, costco, lotte-super, lotte-fresh, traders, hanaro
๋ฐ˜ํ™˜๊ฐ’์€ ์ตœ์ข… S3 ํ‚ค(์˜ˆ: `images/brand/emart.png`)์ž…๋‹ˆ๋‹ค.
""")
@ApiResponse(responseCode = "200", description = "์Šน๊ฒฉ ์„ฑ๊ณต (ํ…์ŠคํŠธ ํ‚ค ๋ฐ˜ํ™˜)")
@ApiResponse(responseCode = "400", description = "์œ ํšจํ•˜์ง€ ์•Š์€ brandCode", content = @Content)
public ResponseEntity<String> promote(
@Parameter(
description = "UUID ํŒŒ์ผ๋ช… (์˜ˆ: 8799915c-9018-406a-...)",
example = "8799915c-9018-406a-ad2e-47678f87ca96")
@RequestParam("uuid")
String uuidKey,
@Parameter(description = "๋ธŒ๋žœ๋“œ ์ฝ”๋“œ (์˜ˆ: emart, homeplus, lotte-mart)", example = "emart")
@RequestParam("brandCode")
String brandCode) {
Brand brand = Brand.fromCodeSafe(brandCode);
if (brand == Brand.DEFAULT || brand.filename() == null) {
return ResponseEntity.badRequest().body("์œ ํšจํ•˜์ง€ ์•Š์€ brandCode: " + brandCode);
}
String dstKey = promotionService.promote(uuidKey, brand);
return ResponseEntity.ok(dstKey);
}

/** ๋ฐฐ์น˜ ์Šน๊ฒฉ: [{uuid, brandCode}, ...] */
@PostMapping(
path = "/promote-batch",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€ ๋ฐฐ์น˜ ์Šน๊ฒฉ",
description =
"""
์—ฌ๋Ÿฌ ๊ฐœ์˜ UUID ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— ์Šน๊ฒฉํ•ฉ๋‹ˆ๋‹ค.
์š”์ฒญ ๋ฐ”๋”” ์˜ˆ:
[
{"uuid":"8799915c-9018-406a-ad2e-47678f87ca96","brandCode":"emart"},
{"uuid":"b45a7445-be38-4573-89dc-c5afbebcb0e9","brandCode":"homeplus"}
]
๋ฐ˜ํ™˜๊ฐ’์€ ์ตœ์ข… S3 ํ‚ค ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค.
""")
@ApiResponse(
responseCode = "200",
description = "์Šน๊ฒฉ ์„ฑ๊ณต",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class))))
public ResponseEntity<List<String>> promoteBatch(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
required = true,
content = @Content(schema = @Schema(implementation = PromoteReq[].class)))
@RequestBody
List<PromoteReq> reqs) {
List<String> results =
reqs.stream()
.map(
req -> {
Brand brand = Brand.fromCodeSafe(req.brandCode());
if (brand == Brand.DEFAULT || brand.filename() == null) {
throw new IllegalArgumentException("invalid brandCode: " + req.brandCode());
}
return promotionService.promote(req.uuid(), brand);
})
.toList();
return ResponseEntity.ok(results);
}

@Schema(name = "PromoteReq", description = "์Šน๊ฒฉ ์š”์ฒญ ํ•ญ๋ชฉ")
public record PromoteReq(
@Schema(description = "UUID ํŒŒ์ผ๋ช…", example = "8799915c-9018-406a-ad2e-47678f87ca96")
String uuid,
@Schema(description = "๋ธŒ๋žœ๋“œ ์ฝ”๋“œ", example = "emart") String brandCode) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ public class MartQueryService {
private final PlaceRepository placeRepository;
private final BrandImageResolver brandImageResolver;

/** ๋ธŒ๋žœ๋“œ๋ช…์ด ์—†์œผ๋ฉด ์ง€์ ๋ช…(name)์œผ๋กœ ํ‚ค๋ฅผ ๋งŒ๋“ค์–ด ์ด๋ฏธ์ง€/๋ธŒ๋žœ๋“œ์ฝ”๋“œ ๋ชจ๋‘ ํ•ด์„ */
/**
* brand ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ธŒ๋žœ๋“œ ์ถ”์ •. brandCode๋ฅผ ํ•œ ๋ฒˆ๋งŒ ํ•ด์„ํ•ด์„œ imageUrl์€ imageUrlForCode(...)๋กœ
* ์ƒ์„ฑํ•ด ์ค‘๋ณต ํŒŒ์‹ฑ์„ ํ”ผํ•œ๋‹ค.
*/
private BrandInfo resolveBrandInfo(Place p) {
String key = (p.getBrand() != null && !p.getBrand().isBlank()) ? p.getBrand() : p.getName();
String img = brandImageResolver.resolveImageUrl(key);
String code = brandImageResolver.resolveBrandCode(key);
String code = brandImageResolver.resolveBrandCode(key); // ex) "emart", "gs-super", "default"
String img = brandImageResolver.imageUrlForCode(code); // baseUrl/brandPath/default ์ฒ˜๋ฆฌ ํฌํ•จ
return new BrandInfo(img, code);
}

Expand All @@ -44,7 +47,6 @@ public List<PlaceResponse> findInBounds(
.map(
p -> {
BrandInfo brand = resolveBrandInfo(p);
// PlaceResponse.of(Place, imageUrl, brandCode) ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์‚ฌ์šฉ
return PlaceResponse.of(p, brand.imageUrl(), brand.brandCode());
})
.collect(Collectors.toList());
Expand All @@ -56,12 +58,11 @@ public List<PlaceResponse> findNearby(double lng, double lat, int radiusMeters,
.map(
p -> {
BrandInfo brand = resolveBrandInfo(p);
// PlaceResponse.of(Place, imageUrl, brandCode) ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์‚ฌ์šฉ
return PlaceResponse.of(p, brand.imageUrl(), brand.brandCode());
})
.collect(Collectors.toList());
}

/** ๋‚ด๋ถ€ ์ „์šฉ DTO (Java 16+): ์ด๋ฏธ์ง€ URL๊ณผ ๋ธŒ๋žœ๋“œ ์ฝ”๋“œ ํ•œ ๋ฒˆ์— ์ „๋‹ฌ */
/** ๋‚ด๋ถ€ ์ „์šฉ DTO: ์ด๋ฏธ์ง€ URL๊ณผ ๋ธŒ๋žœ๋“œ ์ฝ”๋“œ ๋™์‹œ ์ „๋‹ฌ */
private record BrandInfo(String imageUrl, String brandCode) {}
}
Loading