diff --git a/build.gradle b/build.gradle index e05fcd8..76ff45c 100644 --- a/build.gradle +++ b/build.gradle @@ -28,12 +28,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.0' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.batch:spring-batch-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // AWS SDK for S3 + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.500' } tasks.named('test') { diff --git a/src/main/java/com/curateme/clacobatchserver/batch/ConcertBatch.java b/src/main/java/com/curateme/clacobatchserver/batch/ConcertBatch.java new file mode 100644 index 0000000..7fa9a9c --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/batch/ConcertBatch.java @@ -0,0 +1,305 @@ +package com.curateme.clacobatchserver.batch; + +import com.curateme.clacobatchserver.config.s3.S3Service; +import com.curateme.clacobatchserver.dto.CategoryScoreDto; +import com.curateme.clacobatchserver.entity.Concert; +import com.curateme.clacobatchserver.repository.ConcertCategoryRepository; +import com.curateme.clacobatchserver.repository.ConcertRepository; +import com.curateme.clacobatchserver.service.ConcertCategoryExtractor; +import com.curateme.clacobatchserver.service.ConcertSummaryExtractor; +import com.curateme.clacobatchserver.service.KopisConcertApiReader; +import com.curateme.clacobatchserver.service.KopisDetailApiReader; +import com.curateme.clacobatchserver.service.KopisEntityWriter; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.config.Task; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class ConcertBatch { + + private final JobRepository jobRepository; + private final PlatformTransactionManager platformTransactionManager; + private final ConcertRepository concertRepository; + private final ConcertCategoryRepository concertCategoryRepository; + + private final KopisConcertApiReader kopisApiReader; + private final KopisDetailApiReader kopisDetailApiReader; + private final ConcertCategoryExtractor concertCategoryExtractor; + private final S3Service s3Service; + private final ConcertSummaryExtractor concertSummaryExtractor; + + public ConcertBatch(JobRepository jobRepository, + PlatformTransactionManager platformTransactionManager, ConcertRepository concertRepository, + ConcertCategoryRepository concertCategoryRepository, + KopisConcertApiReader kopisApiReader, + KopisDetailApiReader kopisDetailApiReader, + ConcertCategoryExtractor concertCategoryExtractor, + S3Service s3Service, + ConcertSummaryExtractor concertSummaryExtractor) { + this.jobRepository = jobRepository; + this.platformTransactionManager = platformTransactionManager; + this.concertRepository = concertRepository; + this.concertCategoryRepository = concertCategoryRepository; + this.kopisApiReader = kopisApiReader; + this.kopisDetailApiReader = kopisDetailApiReader; + this.concertCategoryExtractor = concertCategoryExtractor; + this.s3Service = s3Service; + this.concertSummaryExtractor = concertSummaryExtractor; + } + + @Bean + public Job kopisJob(KopisEntityWriter writer) { + return new JobBuilder("kopisJob", jobRepository) + .start(firstStep(writer)) + .next(secondStep()) + .next(thirdStep()) + .next(fourthStep()) + .next(fifthStep()) + .build(); + } + + // 1. Kopis에서 해당 기간에 대한 공연 정보 가져 오기 + @Bean + public Step firstStep(KopisEntityWriter writer) { + return new StepBuilder("firstStep", jobRepository) + .chunk(10, platformTransactionManager) + .reader(kopisApiReader) + .writer(writer) + .build(); + } + + // 2. Step1 에서 가져온 공연에 대해 상세 내역 가져 오기 + @Bean + public Step secondStep() { + return new StepBuilder("secondStep", jobRepository) + .tasklet(kopisDetailApiReader, platformTransactionManager) + .build(); + } + + // 3. 공연 포스터 이미지를 통해서 Flask 서버로 부터 카테고리 추출 + @Bean + public Step thirdStep() { + return new StepBuilder("thirdStep", jobRepository) + .tasklet(concertCategoryExtractorTasklet(), platformTransactionManager) + .build(); + } + + // 4. 추출한 카테고리 값을 CSV 파일에 저장 + @Bean + public Step fourthStep() { + return new StepBuilder("fourthStep", jobRepository) + .tasklet((StepContribution contribution, ChunkContext chunkContext) -> { + + String folderPath = "datasets"; + String fileName = "concerts.csv"; + + String localFilePath = s3Service.downloadCsvFile(folderPath, fileName); + if (!isFileExists(localFilePath)) { + return RepeatStatus.FINISHED; + } + + StringJoiner csvContent = readExistingCsvContent(localFilePath); + appendNewConcertData(csvContent); + + saveAndUploadCsvFile(csvContent, folderPath, fileName); + + return RepeatStatus.FINISHED; + }, platformTransactionManager).build(); + } + + // 5. 클라코 큐레이션: 공연 정보 요약 + @Bean + public Step fifthStep() { + return new StepBuilder("fifthStep", jobRepository) + .tasklet(concertSummaryExtractorTasklet(), platformTransactionManager) + .build(); + } + + // 5. 해당 Batch에서 가져온 값들을 전부 삭제하는 로직 + @Bean + public Step finalStep() { + return new StepBuilder("finalStep", jobRepository) + .tasklet((StepContribution contribution, ChunkContext chunkContext) -> { + // 데이터베이스에서 ConcertEntity와 관련된 모든 데이터를 삭제 + deleteAllConcertData(); + return RepeatStatus.FINISHED; + }, platformTransactionManager).build(); + } + + // ConcertEntity와 관련된 모든 데이터를 삭제하는 메서드 + private void deleteAllConcertData() { + try { + concertRepository.deleteAll(); + } catch (Exception e) { + System.err.println("ConcertEntity 데이터 삭제 오류: " + e.getMessage()); + } + } + + + // 파일 존재 확인 메서드 + private boolean isFileExists(String localFilePath) { + File downloadedFile = new File(localFilePath); + if (!downloadedFile.exists()) { + System.err.println("파일이 존재하지 않습니다: " + localFilePath); + return false; + } + System.out.println("다운로드된 파일 경로: " + localFilePath); + return true; + } + + // 기존 CSV 파일 내용을 읽는 메서드 + private StringJoiner readExistingCsvContent(String localFilePath) { + StringJoiner csvContent = new StringJoiner("\n"); + try (BufferedReader reader = new BufferedReader(new FileReader(localFilePath))) { + String line; + while ((line = reader.readLine()) != null) { + csvContent.add(line); + } + } catch (IOException e) { + System.err.println("기존 CSV 파일 읽기 오류: " + e.getMessage()); + } + return csvContent; + } + + // 새로운 Concert 데이터를 CSV에 추가하는 메서드 + private void appendNewConcertData(StringJoiner csvContent) { + List columns = Arrays.asList("concertId", "grand", "delicate", "classical", + "modern", + "lyrical", "dynamic", "romantic", "tragic", "familiar", "novel"); + + Map translationMap = Map.of( + "웅장한", "grand", + "섬세한", "delicate", + "고전적인", "classical", + "현대적인", "modern", + "서정적인", "lyrical", + "역동적인", "dynamic", + "낭만적인", "romantic", + "비극적인", "tragic", + "친숙한", "familiar", + "새로운", "novel" + ); + + List concertIds = concertRepository.findAllConcertIds(); + + for (Long concertId : concertIds) { + // 각 Concert ID에 대해 category와 score 조회 + List categoryScores = concertCategoryRepository.findByConcertId( + concertId); + System.out.println("categoryScores = " + categoryScores); + // 각 카테고리의 기본 값을 0.0으로 초기화한 Map 생성 + Map categoryScoreMap = new HashMap<>(); + for (String column : columns.subList(1, columns.size())) { + categoryScoreMap.put(column, 0.0); + } + + // categoryScores에서 각 카테고리의 점수를 Map에 업데이트 + for (CategoryScoreDto categoryScore : categoryScores) { + String koreanCategory = categoryScore.getCategory(); + System.out.println("koreanCategory = " + koreanCategory); + String englishCategory = translationMap.get(koreanCategory); + System.out.println("englishCategory = " + englishCategory); + if (categoryScoreMap.containsKey(englishCategory)) { + categoryScoreMap.put(englishCategory, categoryScore.getScore()); + } + } + + // CSV의 한 행을 구성하는 StringJoiner 생성 + StringJoiner rowContent = new StringJoiner(","); + rowContent.add(String.valueOf(concertId)); // concertId 추가 + + // 각 카테고리의 점수를 순서대로 추가 + for (String column : columns.subList(1, columns.size())) { + rowContent.add(String.valueOf(categoryScoreMap.get(column))); + } + + // 완성된 행을 csvContent에 추가 + csvContent.add(rowContent.toString()); + } + } + + // 새로운 Concert 데이터 행을 초기화하는 메서드 + private Map initializeRowData(List columns, Concert concert) { + Map row = new HashMap<>(); + row.put("concertId", concert.getMt20id()); + + for (String column : columns.subList(1, columns.size())) { + row.put(column, 0.0); + } + + if (concert.getCategories() != null) { + for (Map.Entry entry : concert.getCategories().entrySet()) { + if (columns.contains(entry.getKey())) { + row.put(entry.getKey(), entry.getValue()); + } + } + } + + return row; + } + + // 임시 파일에 저장하고 S3에 업로드하는 메서드 + private void saveAndUploadCsvFile(StringJoiner csvContent, String folderPath, String fileName) { + File tempFile = null; + try { + tempFile = File.createTempFile("concerts_", ".csv"); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { + writer.write(csvContent.toString()); + } + + s3Service.updateAndUploadCsvFile(folderPath, fileName, tempFile.getAbsolutePath()); + + } catch (IOException e) { + System.err.println("파일 쓰기 오류: " + e.getMessage()); + } finally { + // 임시 파일 정리 + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + } + + @Bean + public Tasklet concertCategoryExtractorTasklet() { + return new Tasklet() { + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + return concertCategoryExtractor.execute(contribution, chunkContext); + } + }; + } + + @Bean + public Tasklet concertSummaryExtractorTasklet() { + return new Tasklet() { + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + return concertSummaryExtractor.execute(contribution, chunkContext); + } + }; + } +} diff --git a/src/main/java/com/curateme/clacobatchserver/batch/FirstBatch.java b/src/main/java/com/curateme/clacobatchserver/batch/FirstBatch.java deleted file mode 100644 index 88593f5..0000000 --- a/src/main/java/com/curateme/clacobatchserver/batch/FirstBatch.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.curateme.clacobatchserver.batch; - -import com.curateme.clacobatchserver.entity.AfterEntity; -import com.curateme.clacobatchserver.entity.BeforeEntity; -import com.curateme.clacobatchserver.repository.AfterRepository; -import com.curateme.clacobatchserver.repository.BeforeRepository; -import java.util.Map; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.data.RepositoryItemReader; -import org.springframework.batch.item.data.RepositoryItemWriter; -import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; -import org.springframework.batch.item.data.builder.RepositoryItemWriterBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.domain.Sort; -import org.springframework.transaction.PlatformTransactionManager; - -@Configuration -public class FirstBatch { - private final JobRepository jobRepository; - private final PlatformTransactionManager platformTransactionManager; - - private final BeforeRepository beforeRepository; - private final AfterRepository afterRepository; - - public FirstBatch(JobRepository jobRepository, - PlatformTransactionManager platformTransactionManager, BeforeRepository beforeRepository, - AfterRepository afterRepository) { - this.jobRepository = jobRepository; - this.platformTransactionManager = platformTransactionManager; - this.beforeRepository = beforeRepository; - this.afterRepository = afterRepository; - } - - @Bean - public Job firstJob() { - - return new JobBuilder("firstJob", jobRepository) - .start(firstStep()) - .build(); - } - - @Bean - public Step firstStep() { - - return new StepBuilder("firstStep", jobRepository) - . chunk(10, platformTransactionManager) - .reader(beforeReader()) - .processor(middleProcessor()) - .writer(afterWriter()) - .build(); - } - - @Bean - public RepositoryItemReader beforeReader() { - - return new RepositoryItemReaderBuilder() - .name("beforeReader") - .pageSize(10) - .methodName("findAll") - .repository(beforeRepository) - .sorts(Map.of("id", Sort.Direction.ASC)) - .build(); - } - - @Bean - public ItemProcessor middleProcessor() { - - return new ItemProcessor<>() { - - @Override - public AfterEntity process(BeforeEntity item) throws Exception { - - AfterEntity afterEntity = new AfterEntity(); - afterEntity.setUsername(item.getUsername()); - - return afterEntity; - } - }; - } - - @Bean - public RepositoryItemWriter afterWriter() { - - return new RepositoryItemWriterBuilder() - .repository(afterRepository) - .methodName("save") - .build(); - } -} diff --git a/src/main/java/com/curateme/clacobatchserver/batch/SecondBatch.java b/src/main/java/com/curateme/clacobatchserver/batch/SecondBatch.java deleted file mode 100644 index 3dd2924..0000000 --- a/src/main/java/com/curateme/clacobatchserver/batch/SecondBatch.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.curateme.clacobatchserver.batch; - -import com.curateme.clacobatchserver.entity.WinEntity; -import com.curateme.clacobatchserver.repository.WinRepository; -import java.util.Collections; -import java.util.Map; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.data.RepositoryItemReader; -import org.springframework.batch.item.data.RepositoryItemWriter; -import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; -import org.springframework.batch.item.data.builder.RepositoryItemWriterBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.domain.Sort; -import org.springframework.transaction.PlatformTransactionManager; - -@Configuration -public class SecondBatch { - private final JobRepository jobRepository; - private final PlatformTransactionManager platformTransactionManager; - private final WinRepository winRepository; - - public SecondBatch(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager, WinRepository winRepository) { - this.jobRepository = jobRepository; - this.platformTransactionManager = platformTransactionManager; - this.winRepository = winRepository; - } - - @Bean - public Job secondJob() { - - return new JobBuilder("secondJob", jobRepository) - .start(secondStep()) - .build(); - } - - @Bean - public Step secondStep() { - - return new StepBuilder("secondStep", jobRepository) - . chunk(10, platformTransactionManager) - .reader(winReader()) - .processor(trueProcessor()) - .writer(winWriter()) - .build(); - } - - @Bean - public RepositoryItemReader winReader() { - - return new RepositoryItemReaderBuilder() - .name("winReader") - .pageSize(10) - .methodName("findByWinGreaterThanEqual") - .arguments(Collections.singletonList(10L)) - .repository(winRepository) - .sorts(Map.of("id", Sort.Direction.ASC)) - .build(); - } - - @Bean - public ItemProcessor trueProcessor() { - - return item -> { - item.setReward(true); - return item; - }; - } - - @Bean - public RepositoryItemWriter winWriter() { - - return new RepositoryItemWriterBuilder() - .repository(winRepository) - .methodName("save") - .build(); - } -} diff --git a/src/main/java/com/curateme/clacobatchserver/config/RestTemplateConfig.java b/src/main/java/com/curateme/clacobatchserver/config/RestTemplateConfig.java new file mode 100644 index 0000000..dde3f36 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/config/RestTemplateConfig.java @@ -0,0 +1,27 @@ +package com.curateme.clacobatchserver.config; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + List> messageConverters = new ArrayList<>(); + + messageConverters.add(new MappingJackson2XmlHttpMessageConverter()); + messageConverters.add(new MappingJackson2HttpMessageConverter()); + + restTemplate.setMessageConverters(messageConverters); + + return restTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/curateme/clacobatchserver/config/s3/S3Config.java b/src/main/java/com/curateme/clacobatchserver/config/s3/S3Config.java new file mode 100644 index 0000000..1ffa93b --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/config/s3/S3Config.java @@ -0,0 +1,33 @@ +package com.curateme.clacobatchserver.config.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } + +} diff --git a/src/main/java/com/curateme/clacobatchserver/config/s3/S3Service.java b/src/main/java/com/curateme/clacobatchserver/config/s3/S3Service.java new file mode 100644 index 0000000..23bdad3 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/config/s3/S3Service.java @@ -0,0 +1,60 @@ +package com.curateme.clacobatchserver.config.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Service +@RequiredArgsConstructor +@Slf4j +public class S3Service { + + private final AmazonS3 amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + public String downloadCsvFile(String folderPath, String fileName) { + String s3Key = folderPath + "/" + fileName; + String localFilePath = System.getProperty("java.io.tmpdir") + fileName; + + try { + log.info("Downloading file from S3: {}/{}", bucketName, s3Key); + amazonS3Client.getObject(new GetObjectRequest(bucketName, s3Key), new File(localFilePath)); + log.info("File downloaded successfully to: {}", localFilePath); + } catch (Exception e) { + log.error("Error downloading file from S3", e); + } + + return localFilePath; + } + + public void updateAndUploadCsvFile(String folderPath, String fileName, String filePath) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) { + log.info("File updated successfully: {}", filePath); + } catch (IOException e) { + log.error("Error updating file", e); + return; + } + + // S3에 파일 업로드 + try (InputStream inputStream = Files.newInputStream(Paths.get(filePath))) { + String s3Key = folderPath + "/" + fileName; + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(Files.size(Paths.get(filePath))); + + amazonS3Client.putObject(bucketName, s3Key, inputStream, metadata); + log.info("File uploaded successfully to S3: {}/{}", bucketName, s3Key); + } catch (Exception e) { + log.error("Error uploading file to S3", e); + } + } +} diff --git a/src/main/java/com/curateme/clacobatchserver/dto/CategoryScoreDto.java b/src/main/java/com/curateme/clacobatchserver/dto/CategoryScoreDto.java new file mode 100644 index 0000000..6619b2e --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/dto/CategoryScoreDto.java @@ -0,0 +1,19 @@ +package com.curateme.clacobatchserver.dto; + +public class CategoryScoreDto { + private String category; + private Double score; + + public CategoryScoreDto(String category, Double score) { + this.category = category; + this.score = score; + } + + public String getCategory() { + return category; + } + + public Double getScore() { + return score; + } +} \ No newline at end of file diff --git a/src/main/java/com/curateme/clacobatchserver/entity/AfterEntity.java b/src/main/java/com/curateme/clacobatchserver/entity/AfterEntity.java deleted file mode 100644 index 0a659db..0000000 --- a/src/main/java/com/curateme/clacobatchserver/entity/AfterEntity.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.curateme.clacobatchserver.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Getter; - -@Entity -@Getter -public class AfterEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String username; - - public void setUsername(String username) { - this.username = username; - } -} \ No newline at end of file diff --git a/src/main/java/com/curateme/clacobatchserver/entity/BeforeEntity.java b/src/main/java/com/curateme/clacobatchserver/entity/BeforeEntity.java deleted file mode 100644 index d28aca6..0000000 --- a/src/main/java/com/curateme/clacobatchserver/entity/BeforeEntity.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.curateme.clacobatchserver.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Getter; - -@Entity -@Getter -public class BeforeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String username; -} \ No newline at end of file diff --git a/src/main/java/com/curateme/clacobatchserver/entity/WinEntity.java b/src/main/java/com/curateme/clacobatchserver/entity/Category.java similarity index 51% rename from src/main/java/com/curateme/clacobatchserver/entity/WinEntity.java rename to src/main/java/com/curateme/clacobatchserver/entity/Category.java index e06f4fa..8d1e656 100644 --- a/src/main/java/com/curateme/clacobatchserver/entity/WinEntity.java +++ b/src/main/java/com/curateme/clacobatchserver/entity/Category.java @@ -4,19 +4,23 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; @Entity @Getter -@Setter -public class WinEntity { - +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "category") +public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String username; - private Long win; - private Boolean reward; -} \ No newline at end of file + private String category; + + private String imageUrl; +} diff --git a/src/main/java/com/curateme/clacobatchserver/entity/Concert.java b/src/main/java/com/curateme/clacobatchserver/entity/Concert.java new file mode 100644 index 0000000..40aa780 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/entity/Concert.java @@ -0,0 +1,174 @@ +package com.curateme.clacobatchserver.entity; + +import com.curateme.clacobatchserver.global.BaseEntity; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapKeyColumn; +import jakarta.persistence.Table; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "concert") +public class Concert extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "concert_id") + private String mt20id; + + @Column(name = "concert_name") + private String prfnm; + + @Column(name = "start_date") + private String prfpdfrom; + + @Column(name = "end_date") + private String prfpdto; + + @Column(name = "facility_name") + private String fcltynm; + + @Column(name = "poster") + private String poster; + + @Column(name = "area") + private String area; + + @Column(name = "genre") + private String genrenm; + + @Column(name = "openrun") + private String openrun; + + @Column(name = "status") + private String prfstate; + + @Column(name = "cast") + private String prfcast; + + @Column(name = "crew") + private String prfcrew; + + @Column(name = "runtime") + private String prfruntime; + + @Column(name = "age") + private String prfage; + + @Column(name = "company_name") + private String entrpsnm; + + @Column(name = "company_namep") + private String entrpsnmP; + + @Column(name = "company_namea") + private String entrpsnmA; + + @Column(name = "company_nameh") + private String entrpsnmH; + + @Column(name = "company_names") + private String entrpsnmS; + + @Column(name = "seat_guidance") + private String pcseguidance; + + @Column(name = "visit") + private String visit; + + @Column(name = "child") + private String child; + + @Column(name = "daehakro") + private String daehakro; + + @Column(name = "festival") + private String festival; + + @Column(name = "musical_license") + private String musicallicense; + + @Column(name = "musical_create") + private String musicalcreate; + + @Column(name = "update_date") + private String updatedate; + + @Column(name = "schedule_guidance", length = 1000) + private String dtguidance; + + @Column(name = "introduction") + private String styurl; + + @Column(name = "summary", length = 1500) + private String summary; + + @ElementCollection + @CollectionTable(name = "concert_category", joinColumns = @JoinColumn(name = "concert_id")) + @MapKeyColumn(name = "category") + @Column(name = "score") + private Map categories; + public void setConcertDetails(String mt20id, String prfnm, String prfpdfrom, String prfpdto, + String fcltynm, String poster, String area, String genrenm, + String openrun, String prfstate) { + this.mt20id = mt20id; + this.prfnm = prfnm; + this.prfpdfrom = prfpdfrom; + this.prfpdto = prfpdto; + this.fcltynm = fcltynm; + this.poster = poster; + this.area = area; + this.genrenm = genrenm; + this.openrun = openrun; + this.prfstate = prfstate; + } + + public void setAdditionalConcertDetails(String prfcast, String prfcrew, String prfruntime, String prfage, + String entrpsnm, String entrpsnmP, String entrpsnmA, String entrpsnmH, + String entrpsnmS, String pcseguidance, String visit, String child, + String daehakro, String festival, String musicallicense, + String musicalcreate, String updatedate, String dtguidance) { + this.prfcast = prfcast; + this.prfcrew = prfcrew; + this.prfruntime = prfruntime; + this.prfage = prfage; + this.entrpsnm = entrpsnm; + this.entrpsnmP = entrpsnmP; + this.entrpsnmA = entrpsnmA; + this.entrpsnmH = entrpsnmH; + this.entrpsnmS = entrpsnmS; + this.pcseguidance = pcseguidance; + this.visit = visit; + this.child = child; + this.daehakro = daehakro; + this.festival = festival; + this.musicallicense = musicallicense; + this.musicalcreate = musicalcreate; + this.updatedate = updatedate; + this.dtguidance = dtguidance; + } + + public void setStyurl(String styurl){ + this.styurl =styurl; + } + + public void setCategories(Map categories) { + this.categories = categories; + } + + public void setSummary(String summary) {this.summary = summary; } +} diff --git a/src/main/java/com/curateme/clacobatchserver/entity/ConcertCategory.java b/src/main/java/com/curateme/clacobatchserver/entity/ConcertCategory.java new file mode 100644 index 0000000..f4f2cc6 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/entity/ConcertCategory.java @@ -0,0 +1,34 @@ +package com.curateme.clacobatchserver.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "concert_category") +public class ConcertCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "score") + private Double score; + + @ManyToOne + @JoinColumn(name = "concert_id", nullable = false) + private Concert concert; + + @ManyToOne + @JoinColumn(name = "category_id", nullable = false) + private Category category; + public ConcertCategory(Category category, Double score, Concert concert) { + this.category = category; + this.score = score; + this.concert = concert; + } +} diff --git a/src/main/java/com/curateme/clacobatchserver/global/ActiveStatus.java b/src/main/java/com/curateme/clacobatchserver/global/ActiveStatus.java new file mode 100644 index 0000000..c6e68f0 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/global/ActiveStatus.java @@ -0,0 +1,27 @@ +package com.curateme.clacobatchserver.global; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @packageName : com.curateme.claco.global.entity + * @fileName : ActiveStatus.java + * @author : 이 건 + * @date : 2024.10.15 + * @author devkeon(devkeon123@gmail.com) + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 2024.10.15 이 건 최초 생성 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ActiveStatus { + + // ACTIVE: 활성, DELETED: 비활성 + ACTIVE("ACTIVE"), DELETED("DELETED"); + + private final String activeStatus; + +} diff --git a/src/main/java/com/curateme/clacobatchserver/global/BaseEntity.java b/src/main/java/com/curateme/clacobatchserver/global/BaseEntity.java new file mode 100644 index 0000000..f5749c8 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/global/BaseEntity.java @@ -0,0 +1,62 @@ +package com.curateme.clacobatchserver.global; + +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import java.time.LocalDateTime; +import lombok.Getter; + +/** + * @packageName : com.curateme.claco.global.entity + * @fileName : BaseEntity.java + * @author : 이 건 + * @date : 2024.10.15 + * @author devkeon(devkeon123@gmail.com) + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 2024.10.15 이 건 최초 생성 + */ +@Getter +@MappedSuperclass +public abstract class BaseEntity { + + // 활성 여부 + @Enumerated(value = EnumType.STRING) + @Column(name = "active_status") + private ActiveStatus activeStatus = ActiveStatus.ACTIVE; + // 생성일 + @Column(name = "created_at") + private LocalDateTime createdAt; + // 수정일 + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void createdAt() { + LocalDateTime currentTime = LocalDateTime.now(); + this.createdAt = currentTime; + this.updatedAt = currentTime; + } + + @PreUpdate + public void updatedAt() { + this.updatedAt = LocalDateTime.now(); + } + + public void deleteEntity() { + this.activeStatus = ActiveStatus.DELETED; + } + + public void restoreEntity() { + this.activeStatus = ActiveStatus.ACTIVE; + } + + public void updateActiveStatus(ActiveStatus activeStatus) { + this.activeStatus = activeStatus; + } + +} diff --git a/src/main/java/com/curateme/clacobatchserver/repository/AfterRepository.java b/src/main/java/com/curateme/clacobatchserver/repository/AfterRepository.java deleted file mode 100644 index e9b23ed..0000000 --- a/src/main/java/com/curateme/clacobatchserver/repository/AfterRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.curateme.clacobatchserver.repository; - -import com.curateme.clacobatchserver.entity.AfterEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AfterRepository extends JpaRepository { - -} diff --git a/src/main/java/com/curateme/clacobatchserver/repository/BeforeRepository.java b/src/main/java/com/curateme/clacobatchserver/repository/BeforeRepository.java deleted file mode 100644 index 8e6b16b..0000000 --- a/src/main/java/com/curateme/clacobatchserver/repository/BeforeRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.curateme.clacobatchserver.repository; - -import com.curateme.clacobatchserver.entity.BeforeEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface BeforeRepository extends JpaRepository { - -} diff --git a/src/main/java/com/curateme/clacobatchserver/repository/ConcertCategoryRepository.java b/src/main/java/com/curateme/clacobatchserver/repository/ConcertCategoryRepository.java new file mode 100644 index 0000000..1fee3b1 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/repository/ConcertCategoryRepository.java @@ -0,0 +1,20 @@ +package com.curateme.clacobatchserver.repository; + +import com.curateme.clacobatchserver.dto.CategoryScoreDto; +import com.curateme.clacobatchserver.entity.ConcertCategory; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ConcertCategoryRepository extends JpaRepository { + @Query("SELECT new com.curateme.clacobatchserver.dto.CategoryScoreDto(cc.category.category, cc.score) " + + "FROM ConcertCategory cc WHERE cc.concert.id = :concertId") + List findByConcertId(@Param("concertId") Long concertId); + + @Query("SELECT cc FROM ConcertCategory cc WHERE cc.concert.id = :concertId") + List findAllByConcertId(@Param("concertId") Long concertId); + +} + + diff --git a/src/main/java/com/curateme/clacobatchserver/repository/ConcertRepository.java b/src/main/java/com/curateme/clacobatchserver/repository/ConcertRepository.java new file mode 100644 index 0000000..d55c40d --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/repository/ConcertRepository.java @@ -0,0 +1,14 @@ +package com.curateme.clacobatchserver.repository; + +import com.curateme.clacobatchserver.entity.Concert; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ConcertRepository extends JpaRepository { + @Query("SELECT c FROM Concert c LEFT JOIN FETCH c.categories") + List getAllConcertsWithCategories(); + + @Query("SELECT c.id FROM Concert c") + List findAllConcertIds(); +} diff --git a/src/main/java/com/curateme/clacobatchserver/repository/WinRepository.java b/src/main/java/com/curateme/clacobatchserver/repository/WinRepository.java deleted file mode 100644 index d262af1..0000000 --- a/src/main/java/com/curateme/clacobatchserver/repository/WinRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.curateme.clacobatchserver.repository; - -import com.curateme.clacobatchserver.entity.WinEntity; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface WinRepository extends JpaRepository { - - Page findByWinGreaterThanEqual(Long win, Pageable pageable); -} diff --git a/src/main/java/com/curateme/clacobatchserver/schedule/FirstSchedule.java b/src/main/java/com/curateme/clacobatchserver/schedule/ConcertSchedule.java similarity index 77% rename from src/main/java/com/curateme/clacobatchserver/schedule/FirstSchedule.java rename to src/main/java/com/curateme/clacobatchserver/schedule/ConcertSchedule.java index 4933cb0..bc645b9 100644 --- a/src/main/java/com/curateme/clacobatchserver/schedule/FirstSchedule.java +++ b/src/main/java/com/curateme/clacobatchserver/schedule/ConcertSchedule.java @@ -10,17 +10,18 @@ import org.springframework.scheduling.annotation.Scheduled; @Configuration -public class FirstSchedule { +public class ConcertSchedule { private final JobLauncher jobLauncher; private final JobRegistry jobRegistry; - public FirstSchedule(JobLauncher jobLauncher, JobRegistry jobRegistry) { + public ConcertSchedule(JobLauncher jobLauncher, JobRegistry jobRegistry) { this.jobLauncher = jobLauncher; this.jobRegistry = jobRegistry; } - @Scheduled(cron = "10 * * * * *", zone = "Asia/Seoul") // 한국시간에 맞춰서 스케줄 실행 + @Scheduled(cron = "10 * * * * *", zone = "Asia/Seoul") + //@Scheduled(cron = "0 0 0 1 * *", zone = "Asia/Seoul") public void runFirstJob() throws Exception { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss"); @@ -30,6 +31,6 @@ public void runFirstJob() throws Exception { .addString("date", date) .toJobParameters(); - jobLauncher.run(jobRegistry.getJob("firstJob"), jobParameters); + jobLauncher.run(jobRegistry.getJob("kopisJob"), jobParameters); } } \ No newline at end of file diff --git a/src/main/java/com/curateme/clacobatchserver/schedule/SecondSchedule.java b/src/main/java/com/curateme/clacobatchserver/schedule/SecondSchedule.java deleted file mode 100644 index a832ac9..0000000 --- a/src/main/java/com/curateme/clacobatchserver/schedule/SecondSchedule.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.curateme.clacobatchserver.schedule; - -import java.text.SimpleDateFormat; -import java.util.Date; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.JobRegistry; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -public class SecondSchedule { - private final JobLauncher jobLauncher; - private final JobRegistry jobRegistry; - - public SecondSchedule(JobLauncher jobLauncher, JobRegistry jobRegistry) { - this.jobLauncher = jobLauncher; - this.jobRegistry = jobRegistry; - } - - @Scheduled(cron = "5 * * * * *", zone = "Asia/Seoul") // 한국시간에 맞춰서 스케줄 실행 - public void runFirstJob() throws Exception { - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss"); - String date = dateFormat.format(new Date()); - - JobParameters jobParameters = new JobParametersBuilder() - .addString("date", date) - .toJobParameters(); - - jobLauncher.run(jobRegistry.getJob("secondJob"), jobParameters); - } -} diff --git a/src/main/java/com/curateme/clacobatchserver/service/ConcertCategoryExtractor.java b/src/main/java/com/curateme/clacobatchserver/service/ConcertCategoryExtractor.java new file mode 100644 index 0000000..945841a --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/service/ConcertCategoryExtractor.java @@ -0,0 +1,90 @@ +package com.curateme.clacobatchserver.service; + +import com.curateme.clacobatchserver.entity.Concert; +import com.curateme.clacobatchserver.repository.ConcertRepository; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class ConcertCategoryExtractor { + + private final RestTemplate restTemplate; + private final ConcertRepository concertRepository; + + @Autowired + public ConcertCategoryExtractor(RestTemplate restTemplate, ConcertRepository concertRepository) { + this.restTemplate = restTemplate; + this.concertRepository = concertRepository; + } + + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + List concerts = concertRepository.findAll(); + + for (Concert concert : concerts) { + try { + String introduction = concert.getStyurl(); + + Map requestBody = new HashMap<>(); + requestBody.put("image_url", introduction); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // Flask 서버에 요청 보내기 + ResponseEntity response = restTemplate.postForEntity("http://localhost:8081/categories", requestEntity, Map.class); + + if (response.getBody() != null) { + Map responseBody = (Map) response.getBody(); + Object clovaResponse = responseBody.get("clova_response"); + + Map categories = new HashMap<>(); + + // clova_response 처리 + if (clovaResponse instanceof Map) { + // clova_response가 Map인 경우 + Map categoryData = (Map) clovaResponse; + String koreanCategory = categoryData.get("name").toString(); + Double score = Double.parseDouble(categoryData.get("score").toString()); + + categories.put(koreanCategory, score); + } else if (clovaResponse instanceof List) { + // clova_response가 List인 경우 + List> categoryList = (List>) clovaResponse; + + for (Map categoryData : categoryList) { + String koreanCategory = categoryData.get("name").toString(); + Double score = Double.parseDouble(categoryData.get("score").toString()); + + categories.put(koreanCategory, score); + } + } + + concert.setCategories(categories); + + // 업데이트된 concert 엔티티를 저장 + concertRepository.save(concert); + } + } catch (Exception e) { + // 특정 concert에서 예외 발생 시 로그를 남기고, 다음 concert로 계속 진행 + System.err.println("concert ID " + concert.getMt20id() + " 처리 중 오류 발생: " + e.getMessage()); + e.printStackTrace(); + } + } + + return RepeatStatus.FINISHED; + } + + +} diff --git a/src/main/java/com/curateme/clacobatchserver/service/ConcertSummaryExtractor.java b/src/main/java/com/curateme/clacobatchserver/service/ConcertSummaryExtractor.java new file mode 100644 index 0000000..a9049af --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/service/ConcertSummaryExtractor.java @@ -0,0 +1,64 @@ +package com.curateme.clacobatchserver.service; + +import com.curateme.clacobatchserver.entity.Concert; +import com.curateme.clacobatchserver.repository.ConcertRepository; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class ConcertSummaryExtractor { + private final RestTemplate restTemplate; + private final ConcertRepository concertRepository; + + @Autowired + public ConcertSummaryExtractor(RestTemplate restTemplate, ConcertRepository concertRepository) { + this.restTemplate = restTemplate; + this.concertRepository = concertRepository; + } + + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + List concerts = concertRepository.findAll(); + + for (Concert concert : concerts) { + try { + String introduction = concert.getStyurl(); + + Map requestBody = new HashMap<>(); + requestBody.put("image_url", introduction); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // Flask 서버에 요청 보내기 + ResponseEntity response = restTemplate.postForEntity("http://localhost:8081/summaries", requestEntity, Map.class); + + if (response.getBody() != null) { + String clovaResponse = (String) response.getBody().get("clova_response"); + + concert.setSummary(clovaResponse); + concertRepository.save(concert); + } + + } catch (Exception e) { + // 특정 concert에서 예외 발생 시 로그를 남기고, 다음 concert로 계속 진행 + System.err.println("concert ID " + concert.getMt20id() + " 처리 중 오류 발생: " + e.getMessage()); + e.printStackTrace(); + } + } + + return RepeatStatus.FINISHED; + } + +} diff --git a/src/main/java/com/curateme/clacobatchserver/service/KopisConcertApiReader.java b/src/main/java/com/curateme/clacobatchserver/service/KopisConcertApiReader.java new file mode 100644 index 0000000..f756104 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/service/KopisConcertApiReader.java @@ -0,0 +1,97 @@ +package com.curateme.clacobatchserver.service; + +import com.curateme.clacobatchserver.entity.Concert; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.springframework.batch.item.ItemReader; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.HttpClientErrorException; + +@Component +public class KopisConcertApiReader implements ItemReader { + + private final RestTemplate restTemplate; + private final String apiUrl = "http://www.kopis.or.kr/openApi/restful/pblprfr?service=f222668534db409b8769f640387de9c3"; + private int currentPage = 1; + private List beforeEntities = new ArrayList<>(); + private int index = 0; + + public KopisConcertApiReader(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public Concert read() throws Exception { + + if (index >= beforeEntities.size()) { + loadNextPage(); + index = 0; + } + + if (beforeEntities.isEmpty()) { + return null; + } + + return beforeEntities.get(index++); + } + + private void loadNextPage() { + LocalDate startDate = LocalDate.now(); + LocalDate endDate = startDate.plusDays(30); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + String formattedStartDate = startDate.format(formatter); + String formattedEndDate = endDate.format(formatter); + + String requestUrl = String.format("%s&stdate=%s&eddate=%s&rows=10&cpage=%d", + apiUrl, formattedStartDate, formattedEndDate, currentPage++); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML)); + HttpEntity entity = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, entity, Concert[].class); + System.out.println("response = " + response); + + if (response.getBody() != null && response.getBody().length > 0) { + beforeEntities.clear(); + + for (Concert beforeentity : response.getBody()) { + if ("무용".equals(beforeentity.getGenrenm()) || "서양음악(클래식)".equals(beforeentity.getGenrenm())) { + Concert concert = new Concert(); + concert.setConcertDetails( + beforeentity.getMt20id(), + beforeentity.getPrfnm(), + beforeentity.getPrfpdfrom(), + beforeentity.getPrfpdto(), + beforeentity.getFcltynm(), + beforeentity.getPoster(), + beforeentity.getArea(), + beforeentity.getGenrenm(), + beforeentity.getOpenrun(), + beforeentity.getPrfstate() + ); + + beforeEntities.add(concert); + } + } + } else { + beforeEntities.clear(); + } + } catch (HttpClientErrorException e) { + System.err.println("Error: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + beforeEntities.clear(); + } + + } +} diff --git a/src/main/java/com/curateme/clacobatchserver/service/KopisDetailApiReader.java b/src/main/java/com/curateme/clacobatchserver/service/KopisDetailApiReader.java new file mode 100644 index 0000000..6dad892 --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/service/KopisDetailApiReader.java @@ -0,0 +1,141 @@ +package com.curateme.clacobatchserver.service; + +import com.curateme.clacobatchserver.entity.Concert; +import com.curateme.clacobatchserver.repository.ConcertRepository; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Component +public class KopisDetailApiReader implements Tasklet { + + private final ConcertRepository concertRepository; + + public KopisDetailApiReader(ConcertRepository concertRepository) { + this.concertRepository = concertRepository; + } + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + List beforeEntities = concertRepository.findAll(); + + for (Concert concert : beforeEntities) { + fetchAndSave(concert.getMt20id(), concert); + } + + return RepeatStatus.FINISHED; + } + + private void fetchAndSave(String mt20id, Concert concert) { + try { + String urlString = String.format( + "http://www.kopis.or.kr/openApi/restful/pblprfr/%s?service=f222668534db409b8769f640387de9c3", + mt20id + ); + System.out.println("url = " + urlString); + + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/xml"); + connection.setRequestProperty("Accept-Charset", "UTF-8"); + + int responseCode = connection.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + if (responseCode == HttpURLConnection.HTTP_OK) { + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + String xmlResponse = response.toString(); + System.out.println("Response: " + xmlResponse); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes(StandardCharsets.UTF_8))); + + document.getDocumentElement().normalize(); + NodeList nodeList = document.getElementsByTagName("db"); + + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + + String prfcast = getTagValue("prfcast", element); + String prfcrew = getTagValue("prfcrew", element); + String prfruntime = getTagValue("prfruntime", element); + String prfage = getTagValue("prfage", element); + String entrpsnm = getTagValue("entrpsnm", element); + String entrpsnmP = getTagValue("entrpsnmP", element); + String entrpsnmA = getTagValue("entrpsnmA", element); + String entrpsnmH = getTagValue("entrpsnmH", element); + String entrpsnmS = getTagValue("entrpsnmS", element); + String pcseguidance = getTagValue("pcseguidance", element); + String visit = getTagValue("visit", element); + String child = getTagValue("child", element); + String daehakro = getTagValue("daehakro", element); + String festival = getTagValue("festival", element); + String musicallicense = getTagValue("musicallicense", element); + String musicalcreate = getTagValue("musicalcreate", element); + String updatedate = getTagValue("updatedate", element); + String dtguidance = getTagValue("dtguidance", element); + + NodeList styurlsList = element.getElementsByTagName("styurls"); + if (styurlsList.getLength() > 0) { + Element styurlsElement = (Element) styurlsList.item(0); + NodeList styurlList = styurlsElement.getElementsByTagName("styurl"); + if (styurlList.getLength() > 0) { + String styurl = styurlList.item(0).getTextContent(); + concert.setStyurl(styurl); + } + } + + concert.setAdditionalConcertDetails( + prfcast, prfcrew, prfruntime, prfage, entrpsnm, entrpsnmP, entrpsnmA, entrpsnmH, + entrpsnmS, pcseguidance, visit, child, daehakro, festival, musicallicense, + musicalcreate, updatedate, dtguidance + ); + + concertRepository.save(concert); + } + } + } else { + System.out.println("Error: Received HTTP " + responseCode); + } + + connection.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private String getTagValue(String tag, Element element) { + NodeList nodeList = element.getElementsByTagName(tag).item(0).getChildNodes(); + Node node = nodeList.item(0); + return node != null ? node.getNodeValue() : ""; + } +} diff --git a/src/main/java/com/curateme/clacobatchserver/service/KopisEntityWriter.java b/src/main/java/com/curateme/clacobatchserver/service/KopisEntityWriter.java new file mode 100644 index 0000000..fb736ce --- /dev/null +++ b/src/main/java/com/curateme/clacobatchserver/service/KopisEntityWriter.java @@ -0,0 +1,26 @@ +package com.curateme.clacobatchserver.service; + +import com.curateme.clacobatchserver.entity.Concert; +import com.curateme.clacobatchserver.repository.ConcertRepository; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +@Component +public class KopisEntityWriter implements ItemWriter { + + private final ConcertRepository concertRepository; + + public KopisEntityWriter(ConcertRepository concertRepository) { + + this.concertRepository = concertRepository; + } + + @Override + public void write(Chunk items) throws Exception { + + concertRepository.saveAll(items.getItems()); + } + + +} \ No newline at end of file