Skip to content

Commit

Permalink
Merge pull request #35 from kakao-tech-campus-2nd-step3/develop
Browse files Browse the repository at this point in the history
[�Develop] 3주차 과제 PR
  • Loading branch information
sanghee0820 authored Oct 4, 2024
2 parents 1369557 + 9ced153 commit 5bea7c2
Show file tree
Hide file tree
Showing 84 changed files with 2,595 additions and 644 deletions.
53 changes: 35 additions & 18 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -1,50 +1,53 @@
name: Java CI with Gradle

# Master, develop 브랜치에 push 혹은 pull request가 발생할 경우 동작한다.
# Master, develop, weekly/* 브랜치에 push가 발생할 경우 동작한다.
on:
push:
branches: [ "Master", "develop" ]
pull_request:
branches: [ "Master", "develop" ]
branches: [ "Master", "develop", "weekly/*" ]

permissions:
contents: read

jobs:
# Spring Boot 애플리케이션을 빌드하여 도커허브에 푸시하는 과정
build-docker-image:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
# 1. Java 17 세팅
# Java 17 세팅
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

# Gradle Wrapper 파일 실행 권한 주기
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# 2. Spring Boot 애플리케이션 빌드
# Spring Boot 애플리케이션 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: clean bootJar

# 3. Docker 이미지 빌드
# 테스트 실행
# - name: Run tests
# run: ./gradlew test

# Docker 이미지 빌드
- name: docker image build
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/inplace .

# 4. DockerHub 로그인
# DockerHub 로그인
- name: docker login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

# 5. Docker Hub 이미지 푸시
# Docker Hub 이미지 푸시
- name: docker Hub push
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/inplace

Expand All @@ -55,23 +58,37 @@ jobs:
needs: build-docker-image
runs-on: self-hosted

# weekly/* 브랜치에서는 실행되지 않도록 조건 추가
if: startsWith(github.ref, 'refs/heads/Master') || startsWith(github.ref, 'refs/heads/develop')

steps:
# 1. 최신 이미지를 pull
# 최신 이미지를 pull
- name: docker pull
run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/inplace

# 2. 기존의 컨테이너를 중지
# 기존의 컨테이너를 중지
- name: docker stop container
run: sudo docker stop $(sudo docker ps -q) 2>/dev/null || true

# 3. 브랜치에 따라 다른 환경 변수를 설정하여 컨테이너 실행
# .env 파일 생성
- name: Create .env file
run: |
echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
echo "KAKAO_REDIRECT_URI=${{ secrets.KAKAO_REDIRECT_URI }}" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env
echo "JWT_ACCESS_TOKEN_EXPIRED_TIME=${{ secrets.JWT_ACCESS_TOKEN_EXPIRED_TIME }}" >> .env
echo "JWT_REFRESH_TOKEN_EXPIRED_TIME=${{ secrets.JWT_REFRESH_TOKEN_EXPIRED_TIME }}" >> .env
# 브랜치에 따라 다른 환경 변수를 설정하여 컨테이너 실행
- name: docker run new container
run: |
if [ "${{ github.ref }}" == "refs/heads/master" ]; then
sudo docker run --name inplace --rm -d -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/inplace
if [ "${{ github.ref }}" == "refs/heads/Master" ]; then
sudo docker run --name inplace --rm -d -p 8080:8080 --env-file .env ${{ secrets.DOCKERHUB_USERNAME }}/inplace
elif [ "${{ github.ref }}" == "refs/heads/develop" ]; then
sudo docker run --name inplace-dev --rm -d -p 8081:8080 ${{ secrets.DOCKERHUB_USERNAME }}/inplace
# 4. 미사용 이미지를 정리
sudo docker run --name inplace-dev --rm -d -p 8081:8080 --env-file .env ${{ secrets.DOCKERHUB_USERNAME }}/inplace
fi
# 미사용 이미지를 정리
- name: delete old docker image
run: sudo docker system prune -f
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ gradle-app.setting
*.hprof

.env
/src/test/resources/application-test.yaml

# CommandLineRunner for loading dummy data
src/main/java/team7/inplace/place/config/DataLoader.java

# End of https://www.toptal.com/developers/gitignore/api/macos,java,intellij,gradle
# End of https://www.toptal.com/developers/gitignore/api/macos,java,intellij,gradle
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dockerfile

# jdk17 Image Start
#FROM --platform=linux/amd64 ubuntu:latest
FROM openjdk:17

# 인자 설정 - Jar_File
ARG JAR_FILE=build/libs/*.jar

# jar 파일 복제
COPY ${JAR_FILE} app.jar

# 인자 설정 부분과 jar 파일 복제 부분 합쳐서 진행해도 무방
#COPY build/libs/*.jar app.jar

# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.3'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/team7/inplace/cicd/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package team7.inplace.cicd;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
@GetMapping("/cicd")
public ResponseEntity test() {
return ResponseEntity.ok("ci/cd 테스트2");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package team7.inplace.crawling.application;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import team7.inplace.crawling.client.KakaoMapClient;
import team7.inplace.crawling.client.YoutubeClient;
import team7.inplace.crawling.persistence.YoutubeChannelRepository;

@Slf4j
@Service
@RequiredArgsConstructor
public class YoutubeCrawlingService {
private final YoutubeChannelRepository youtubeChannelRepository;
private final YoutubeClient youtubeClient;
private final KakaoMapClient kakaoMapClient;

/*
1. 유튜브 채널 정보를 모두 가져온다.
2. 마지막 비디오와, 유튜브 UUID를 이용하여 비디오 정보를 가져온다.
3. 마지막 비디오 UUID를 업데이트 한다.
4. 카카오 API를 호출해 장소 정보를 가져온다
*/
public void crawlAllVideos() {
var youtubeChannels = youtubeChannelRepository.findAll();
for (var channel : youtubeChannels) {
var rawVideoInfos = youtubeClient.getVideos(channel.getPlayListUUID(), channel.getLastVideoUUID());
channel.updateLastVideoUUID(rawVideoInfos.get(0).videoId());

var videos = rawVideoInfos.stream()
.map(rawVideoInfo -> kakaoMapClient.search(rawVideoInfo, channel.getChannelType().getCode()))
.filter(Objects::nonNull)
.toList();
}
}
}

58 changes: 58 additions & 0 deletions src/main/java/team7/inplace/crawling/client/KakaoMapClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package team7.inplace.crawling.client;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import team7.inplace.crawling.client.dto.RawPlace;
import team7.inplace.crawling.client.dto.RawVideoInfo;
import team7.inplace.global.kakao.config.KakaoApiProperties;

@Slf4j
@Component
@RequiredArgsConstructor
public class KakaoMapClient {
private static final String KAKAO_MAP_LOCATE_SEARCH_URL = "https://dapi.kakao.com/v2/local/search/keyword.json";
private static final String KAKAO_MAP_LOCATE_SEARCH_PARAMS = "?query=%s&sort=accuracy&page=1&size=15";
private static final String KAKAO_MAP_PLACE_SEARCH_URL = "https://place.map.kakao.com/main/v/";
private final KakaoApiProperties kakaoApiProperties;
private final RestTemplate restTemplate;

public RawPlace.Info search(RawVideoInfo videoInfo, String category) {
var address = videoInfo.address();
var locationInfo = getLocateInfo(address, category);
var placeId = locationInfo.has("documents") ?
locationInfo.get("documents").get(0).get("id").asText() : null;
if (Objects.isNull(placeId)) {
return null;
}

var placeInfo = getPlaceInfo(placeId);
return RawPlace.Info.from(locationInfo, placeInfo);
}

private JsonNode getLocateInfo(String address, String category) {
var url = KAKAO_MAP_LOCATE_SEARCH_URL + KAKAO_MAP_LOCATE_SEARCH_PARAMS.formatted(address);
url = url + (Objects.isNull(category) ? "" : "&category_group_code=" + category);

HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", kakaoApiProperties.getAuthorization());
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<JsonNode> response = restTemplate.exchange(url, HttpMethod.GET, entity, JsonNode.class);
return response.getBody();
}

private JsonNode getPlaceInfo(String placeId) {
var url = KAKAO_MAP_PLACE_SEARCH_URL + placeId;

ResponseEntity<JsonNode> response = restTemplate.getForEntity(url, JsonNode.class);
return response.getBody();
}
}
96 changes: 96 additions & 0 deletions src/main/java/team7/inplace/crawling/client/YoutubeClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package team7.inplace.crawling.client;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import team7.inplace.crawling.client.dto.RawVideoInfo;

@Slf4j
@Component
public class YoutubeClient {
private static final String PLAY_LIST_ITEMS_BASE_URL = "https://www.googleapis.com/youtube/v3/playlistItems";
private static final String PLAY_LIST_PARAMS = "?part=snippet&playlistId=%s&key=%s&maxResults=50";
private static final String ADDRESS_REGEX = "[가-힣0-9]+(?:도|시|구|군|읍|면|동|리|로|길)[^#,\\n()]+(?:동|읍|면|리|로|길|호|층|번지)[^#,\\n()]+";
private final RestTemplate restTemplate;
private final String apiKey;

public YoutubeClient(@Value("${youtube.api.key}") String apiKey, RestTemplate restTemplate) {
log.info("Youtube API Key: {}", apiKey);
this.restTemplate = restTemplate;
this.apiKey = apiKey;
}

public List<RawVideoInfo> getVideos(String playListId, String finalVideoUUID) {
List<RawVideoInfo> videoInfos = new ArrayList<>();
String nextPageToken = null;
while (true) {
String url = PLAY_LIST_ITEMS_BASE_URL + String.format(PLAY_LIST_PARAMS, playListId, apiKey);

JsonNode response = null;
if (Objects.nonNull(nextPageToken)) {
url += "&pageToken=" + nextPageToken;
}
try {
response = restTemplate.getForObject(url, JsonNode.class);
} catch (Exception e) {
log.error("Youtube API 호출이 실패했습니다. Youtuber Id {}", playListId);
log.info(e.getMessage());
break;
}
if (Objects.isNull(response)) {
log.error("Youtube API Response가 NULL입니다 {}.", playListId);
break;
}

var containsLastVideo = extractRawVideoInfo(videoInfos, response.path("items"), finalVideoUUID);
if (containsLastVideo) {
break;
}
nextPageToken = response.path("nextPageToken").asText();
if (isLastPage(nextPageToken)) {
break;
}
}
return videoInfos;
}

private boolean isLastPage(String nextPageToken) {
return Objects.isNull(nextPageToken) || nextPageToken.isEmpty();
}

private boolean extractRawVideoInfo(List<RawVideoInfo> videoInfos, JsonNode items, String finalVideoUUID) {
for (JsonNode item : items) {
var snippet = item.path("snippet");
var videoId = snippet.path("resourceId").path("videoId").asText();
var videoTitle = snippet.path("title").asText();
var videoDescription = snippet.path("description").asText();
if (videoId.equals(finalVideoUUID)) {
return true;
}

var address = extractAddress(videoDescription);
if (Objects.nonNull(address)) {
videoInfos.add(new RawVideoInfo(videoId, videoTitle, address));
continue;
}
log.info("주소를 찾을 수 없습니다. {}", videoDescription);
}
return false;
}

private String extractAddress(String description) {
Pattern pattern = Pattern.compile(ADDRESS_REGEX);
Matcher matcher = pattern.matcher(description);
if (matcher.find()) {
return matcher.group();
}
return null;
}
}
Loading

0 comments on commit 5bea7c2

Please sign in to comment.