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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ mongodb
.DS_Store
elasticsearch/
.cursorrules
application-stageing.yml
application-stage.yml
application-production.yml
application-prod.yml
application-local.yml
!/docker/elasticsearch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import lombok.extern.slf4j.Slf4j;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.request.BasePublicDataRequest;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.BasePublicDataItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.PublicDataResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.request.BasePublicDataRequest;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.BasePublicDataItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.PublicDataResponse;
import org.atdev.artrip.external.culturalapi.properties.PublicDataProperties;
import org.atdev.artrip.global.apipayload.exception.ExternalApiException;
import org.springframework.core.ParameterizedTypeReference;
Expand Down Expand Up @@ -79,7 +79,6 @@ protected URI buildUri(R request) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(properties.getBaseUrl())
.path(getApiPath())
// .queryParam("serviceKey", properties.getServiceKey())
.queryParam("PageNo", request.getPageNo())
.queryParam("numOfrows", request.getNumOfRows())
.queryParam("sortStdr", request.getSortStdr());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import io.github.resilience4j.retry.RetryRegistry;
import lombok.extern.slf4j.Slf4j;
import org.atdev.artrip.domain.exhibit.reponse.ExhibitDetailResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.request.CultureInfoRequest;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoDetailResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoListResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.request.CultureInfoRequest;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoListResponse;
import org.atdev.artrip.external.culturalapi.properties.PublicDataProperties;
import org.atdev.artrip.global.apipayload.exception.ExternalApiException;
import org.springframework.core.ParameterizedTypeReference;
Expand All @@ -20,16 +19,16 @@

@Slf4j
@Component
public class CultureInfoApiClient extends BasePublicDataClient<CultureInfoItem, CultureInfoRequest, CultureInfoListResponse>{
public class CultureInfoApiClient extends BasePublicDataClient<CultureInfoItem, CultureInfoRequest, CultureInfoListResponse> {

private static final String REALM_CODE_EXHIBITION = "D000";
private static final String SERVICE_TP = "A";
private static final String PATH_REALM2 = "/realm2";
private static final String PATH_DETAILS2 = "/detail2";

public CultureInfoApiClient(
WebClient publicDataWebClient,
PublicDataProperties properties,
RetryRegistry retryRegistry){
RetryRegistry retryRegistry) {
super(publicDataWebClient, properties, retryRegistry);
}

Expand All @@ -45,7 +44,8 @@ protected String getApiPath() {

@Override
protected ParameterizedTypeReference<CultureInfoListResponse> getResponseTypeRef() {
return new ParameterizedTypeReference<>() {};
return new ParameterizedTypeReference<>() {
};
}

@Override
Expand All @@ -55,7 +55,7 @@ protected Class<CultureInfoListResponse> getResponseClass() {

@Override
protected void addRequestParams(UriComponentsBuilder builder, CultureInfoRequest request) {
builder.queryParam("realmCode", REALM_CODE_EXHIBITION);
builder.queryParam("serviceTp", SERVICE_TP);

if (request.getFrom() != null) {
builder.queryParam("from", request.getFrom());
Expand Down Expand Up @@ -106,7 +106,7 @@ public CultureInfoListResponse fetchExhibits(int pageNo) {
CultureInfoRequest request = CultureInfoRequest.builder()
.serviceKey(properties.getServiceKey())
.pageNo(pageNo)
.numOfRows(pageNo)
.numOfRows(properties.getPageSize())
.build();
return fetch(request);
}
Expand Down Expand Up @@ -156,5 +156,4 @@ private void logDetailResponse(CultureInfoDetailResponse response) {
log.warn("상세 조회 실패 - error: {}", response.getErrorMessage());
}
}

}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.mapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.atdev.artrip.domain.Enum.Status;
import org.atdev.artrip.domain.exhibit.data.Exhibit;
import org.atdev.artrip.domain.exhibitHall.data.ExhibitHall;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoDetailItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoItem;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,22 @@
import org.atdev.artrip.domain.keyword.data.Keyword;
import org.atdev.artrip.elastic.service.ExhibitIndexService;
import org.atdev.artrip.external.culturalapi.cultureinfo.client.CultureInfoApiClient;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoDetailItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoDetailResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.dto.response.CultureInfoListResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoItem;
import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoListResponse;
import org.atdev.artrip.external.culturalapi.cultureinfo.mapper.CultureInfoMapper;
import org.atdev.artrip.global.apipayload.exception.ExternalApiException;
import org.atdev.artrip.global.s3.service.S3Service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.parameters.P;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;

@Slf4j
Expand All @@ -42,6 +39,7 @@ public class CultureInfoSyncService {
private final ExhibitHallRepository exhibitHallRepository;
private final KeywordMatchingService keywordMatchingService;
private final ExhibitIndexService exhibitIndexService;
private final S3Service s3Service;

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final int MAX_PAGES = 200;
Expand Down Expand Up @@ -173,6 +171,14 @@ private void processItem(CultureInfoItem item, Map<String, ExhibitHall> hallCach
}
}

String posterUrl = exhibit.getPosterUrl();
if (posterUrl != null && !s3Service.isInternalUrl(posterUrl)) {
String s3Url = s3Service.uploadPosterFromExternalUrl(posterUrl);
if (s3Url != null && !s3Url.equals(posterUrl)) {
exhibit.setPosterUrl(s3Url);
}
}

Optional<Exhibit> existing = exhibitRepository
.findByTitleAndStartDate(exhibit.getTitle(), exhibit.getStartDate());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,13 @@
public class CultureInfoSyncController {

private final CultureInfoSyncService CultureInfoSyncService;

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final ExhibitIndexService exhibitIndexService;

@GetMapping("exhibits/latest")
public CommonResponse<Map<String, Object>> syncLatest() {
String today = LocalDate.now().format(DATE_FORMATTER);
log.info("최신 전시 동기화 : {}", today);

CultureInfoSyncService.SyncResult result = CultureInfoSyncService.syncLatest();

return CommonResponse.onSuccess(Map.of(
"from", today,
"inserted", result.getInserted(),
"updated", result.getUpdated(),
"skipped", result.getSkipped(),
"failed", result.getFailed()
));

}

@GetMapping("/exhibits")
public CommonResponse<Map<String, Object>> syncByPeriod(
@RequestParam String from,
@RequestParam(required = false) String to) {

log.info("기간별 전시 동기화 시작 - 기간: {} ~ {}", from, to != null ? to : "전체");

CultureInfoSyncService.SyncResult result;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.request;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.request;

import lombok.*;
import lombok.experimental.SuperBuilder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.request;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.request;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

public interface BasePublicDataItem {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
Expand Down Expand Up @@ -60,7 +60,7 @@ public boolean isValid() {
}

public boolean isExhibition() {
return "전시".equals(realmName);
return "전시".equals(serviceName);
}

public String getFullAddress() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.atdev.artrip.external.culturalapi.cultureinfo.dto.response;
package org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response;

import java.util.List;

Expand Down
103 changes: 99 additions & 4 deletions src/main/java/org/atdev/artrip/global/s3/service/S3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

Expand Down Expand Up @@ -252,7 +249,105 @@ private String uploadToFolder(MultipartFile file, String folder) {
return uploadToS3(file, folder);
}

public String uploadFromExternalUrl(String externalUrl, String folder){
if (externalUrl == null || externalUrl.isBlank()) {
return null;
}

if (isInternalUrl(externalUrl)) {
return externalUrl;
}


try {
URL url = new URI(externalUrl).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
connection.setInstanceFollowRedirects(true);

int responseCode = connection.getResponseCode();

if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
responseCode == 307 || responseCode == 300
) {
String redirectUrl = connection.getHeaderField("Location");

if (redirectUrl != null && !redirectUrl.isBlank()) {
// 리다이렉트 URL로 재귀 호출
return uploadFromExternalUrl(redirectUrl, folder);
}
}

if (responseCode != 200) {
return externalUrl;
}

String contentType = connection.getContentType();
String extension = getExtensionFromContentType(contentType);
if (extension == null) {
extension = getExtensionFromUrl(externalUrl);
}

String s3Key = String.format("%s/%s.%s", folder, UUID.randomUUID().toString().substring(0, 10), extension);
try (InputStream inputStream = connection.getInputStream()){
byte[] imageBytes = inputStream.readAllBytes();

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(s3Key)
.contentType(contentType != null ? contentType : "image/" + extension)
.contentLength((long) imageBytes.length)
.cacheControl("public, max-age=31536000")
.build();

s3Client.putObject(putObjectRequest, RequestBody.fromBytes(imageBytes));

return buildImageUrl(s3Key);

}
} catch (Exception e) {
log.error("외부 이미지 업로드 실패 - URL: {}, 에러: {}", externalUrl, e.getMessage());
return externalUrl;
}
}

private String getExtensionFromUrl(String url) {
try {
String path = new URI(url).getPath();
int lastDot = path.lastIndexOf('.');
if (lastDot > 0) {
return path.substring(lastDot + 1).toLowerCase();
}
} catch (Exception ignored) {}
return "jpg";
}

private String getExtensionFromContentType(String contentType) {
if (contentType == null) return null;
return switch (contentType.toLowerCase()) {
case "image/jpeg", "image/jpg" -> "jpg";
case "image/png" -> "png";
case "image/webp" -> "webp";
case "image/gif" -> "gif";
default -> null;
};
}

public boolean isInternalUrl(String url) {
if (url == null || url.isBlank()) {
return false;
}
return url.contains("s3.ap-northeast-2.amazonaws.com") ||
url.contains("cloudfront.net") ||
url.contains(bucketName);
}

public String uploadPosterFromExternalUrl(String externalUrl) {
return uploadFromExternalUrl(externalUrl, FOLDER_POSTERS);
}

}
Loading