diff --git a/.github/workflows/PROJECT-SPRING-SYNOLOGY-SIMPLE-CICD.yaml b/.github/workflows/PROJECT-SPRING-SYNOLOGY-SIMPLE-CICD.yaml new file mode 100644 index 0000000..4706b64 --- /dev/null +++ b/.github/workflows/PROJECT-SPRING-SYNOLOGY-SIMPLE-CICD.yaml @@ -0,0 +1,326 @@ +# =================================================================== +# Spring Boot 전용 CI/CD 배포 워크플로우 +# =================================================================== + +name: PROJECT-SPRING-SYNOLOGY-CICD + +# =================================================================== +# 📋 필수 GitHub Secrets 설정 가이드 +# =================================================================== +# +# ⚠️ 사용하기 전에 반드시 다음 GitHub Secrets를 설정하세요! +# (저장소 Settings > Secrets and variables > Actions에서 설정) +# +# 🔧 필수 Secrets: +# ┌─────────────────────────────┬────────────────────────────────────┐ +# │ Secret 이름 │ 설명 │ +# ├─────────────────────────────┼────────────────────────────────────┤ +# │ APPLICATION_PROD_YML │ Spring Boot 운영 설정 파일 내용 │ +# │ DOCKERHUB_USERNAME │ DockerHub 사용자명 │ +# │ DOCKERHUB_TOKEN │ DockerHub 액세스 토큰 │ +# │ SERVER_HOST │ 배포 대상 서버 IP/도메인 │ +# │ SERVER_USER │ 서버 SSH 접속 사용자명 │ +# │ SERVER_PASSWORD │ 서버 SSH 접속 비밀번호 │ +# └─────────────────────────────┴────────────────────────────────────┘ +# +# 🧪 선택적 Secrets (포트 커스터마이징시): +# ┌─────────────────────────────┬────────────────────────────────────┐ +# │ PROJECT_DEPLOY_PORT │ deploy 브랜치 배포 포트 (기본: 8081) │ +# └─────────────────────────────┴────────────────────────────────────┘ +# +# 💡 포트 설정 예시: +# - PROJECT_DEPLOY_PORT: 8080 (배포 서비스 포트) +# - PROJECT_TEST_PORT: 8081 (테스트 서비스 포트) +# +# ⚠️ 주의사항: +# 1. 포트는 서버에서 사용하지 않는 포트를 선택하세요 +# 2. 방화벽에서 해당 포트가 열려있는지 확인하세요 +# 3. 다른 프로젝트와 포트가 겹치지 않도록 주의하세요 +# +# 📝 APPLICATION_PROD_YML 예시: +# spring: +# datasource: +# url: jdbc:mysql://localhost:3306/your_db +# username: your_username +# password: your_password +# jpa: +# hibernate: +# ddl-auto: update +# +# =================================================================== +# +# 🚀 워크플로우 기능: +# - Spring Boot 프로젝트 Gradle 빌드 +# - Docker 이미지 생성 및 DockerHub 푸시 +# - SSH를 통한 원격 서버 배포 +# - 브랜치별 다른 포트 및 컨테이너 관리 +# - 포트 충돌 방지를 위한 환경변수 기반 포트 설정 +# +# 🌿 브랜치별 배포 전략: +# - deploy 브랜치: 배포 환경 (PROJECT_DEPLOY_PORT 사용, 기본: 8080) +# - main 브랜치: 배포 환경 (수동 실행시, PROJECT_DEPLOY_PORT 사용, 기본: 8080) +# - test 브랜치: 테스트 환경 (PROJECT_TEST_PORT 사용, 기본: 8081) +# +# 📝 사용 방법: +# 1. 위의 GitHub Secrets 설정 +# 2. 아래 PROJECT_NAME을 실제 프로젝트명으로 변경 +# 3. deploy와 test 브랜치에 push하면 자동으로 CI/CD 실행 (main 브랜치는 수동 실행만 가능) +# 4. GitHub Secrets에서 포트 설정 (기본값: deploy=8080, test=8081) +# +# =================================================================== + +# =================================================================== +# 트리거 설정 +# =================================================================== +# deploy와 test 브랜치에 push할 때 자동으로 CI/CD가 실행됩니다. +# main 브랜치는 workflow_dispatch로 수동 실행만 가능합니다. +on: + push: + branches: + - deploy # 배포 환경 (8080 포트) + - test # 테스트 환경 (8081 포트) + workflow_dispatch: # 수동 실행 허용 + +# =================================================================== +# 환경 변수 설정 +# =================================================================== +env: + # 🔧 프로젝트 설정 - 실제 프로젝트명으로 변경하세요 + PROJECT_NAME: "project" # 기본값: project (프로젝트명에 맞게 수정 필요) + + # 🐳 Docker 설정 + DOCKER_IMAGE_PREFIX: "back-container" + + # ☁️ Spring Boot 설정 + SPRING_PROFILE: "prod" + JAVA_VERSION: "17" + GRADLE_OPTS: "-Dspring.profiles.active=prod" + +jobs: + # =================================================================== + # 빌드 작업 + # =================================================================== + build: + name: Spring Boot 애플리케이션 빌드 + runs-on: ubuntu-latest + + steps: + # 1. 소스코드 체크아웃 + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + # 2. Java 개발 환경 설정 + - name: Java 설정 + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: 'gradle' + + # 3. Gradle 실행 권한 부여 + - name: Gradle Wrapper 실행권한 부여 + run: chmod +x gradlew + + # 4. Spring Boot 운영 환경 설정 파일 생성 + # GitHub Secrets에서 APPLICATION_PROD_YML 값을 읽어와서 + # src/main/resources/application-prod.yml 파일로 생성 + - name: application-prod.yml 생성 + run: | + # 리소스 디렉토리 생성 (프로젝트 구조에 따라 경로 조정 필요) + mkdir -p src/main/resources + + # GitHub Secrets의 APPLICATION_PROD_YML 내용을 파일로 저장 + cat << 'EOF' > ./src/main/resources/application-prod.yml + ${{ secrets.APPLICATION_PROD_YML }} + EOF + + echo "✅ application-prod.yml 파일 생성 완료" + + # 5. Gradle 빌드 실행 + # 테스트는 제외하고 운영 프로파일로 빌드 + - name: Build with Gradle + run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }} + + # 6. Docker 빌드 환경 설정 + - name: Docker 빌드환경 설정 + uses: docker/setup-buildx-action@v3 + + # 7. DockerHub 로그인 + - name: DockerHub 로그인 + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # 8. Docker 레이어 캐싱 설정 + # 빌드 속도 향상을 위한 캐시 설정 + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }} + restore-keys: | + ${{ runner.os }}-buildx- + + # 9. Docker 이미지 빌드 및 푸시 + # 브랜치명을 태그로 사용하여 main/test 환경별로 이미지 관리 + - name: Docker 이미지 빌드 및 푸시 + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}-${{ env.DOCKER_IMAGE_PREFIX }}:${{ github.ref_name }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # 10. Docker 캐시 정리 + - name: Move Docker cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + # =================================================================== + # 배포 작업 + # =================================================================== + deploy: + name: 원격 서버 배포 + needs: build + runs-on: ubuntu-latest + + steps: + # SSH를 통한 원격 서버 배포 실행 + - name: Deploy + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + password: ${{ secrets.SERVER_PASSWORD }} + port: 2022 + script: | + set -e + + # ============================================================ + # 배포 환경 변수 설정 + # ============================================================ + echo "🔧 환경변수 설정.." + export PATH=$PATH:/usr/local/bin + export PW=${{ secrets.SERVER_PASSWORD }} + + # GitHub에서 전달받은 브랜치명 + BRANCH=${{ github.ref_name }} + + # 프로젝트 설정 + PROJECT_NAME="${{ env.PROJECT_NAME }}" + + # ============================================================ + # 브랜치별 포트 및 컨테이너명 설정 + # ============================================================ + # 기본값 설정 + PORT=8080 + CONTAINER_NAME="${PROJECT_NAME}-back" + + # 브랜치에 따른 환경별 설정 + if [ "$BRANCH" == "deploy" ] || [ "$BRANCH" == "main" ]; then + # 🚀 배포 환경 (deploy 브랜치 또는 main 브랜치) + # GitHub Secrets의 PROJECT_DEPLOY_PORT 환경변수 사용 + # 설정되어 있지 않으면 기본값 8080 사용 + PORT=${{ secrets.PROJECT_DEPLOY_PORT || '8080' }} + CONTAINER_NAME="${PROJECT_NAME}-back-deploy" + echo "🚀 배포 환경으로 배포합니다 (브랜치: $BRANCH)" + + elif [ "$BRANCH" == "test" ]; then + # 🧪 테스트 환경 (test 브랜치) + # GitHub Secrets의 PROJECT_TEST_PORT 환경변수 사용 + # 설정되어 있지 않으면 기본값 8081 사용 + PORT=${{ secrets.PROJECT_TEST_PORT || '8081' }} + CONTAINER_NAME="${PROJECT_NAME}-back-test" + echo "🧪 테스트 환경으로 배포합니다" + + else + # ⚠️ 기타 브랜치 (예상치 못한 브랜치) + echo "⚠️ 지원하지 않는 브랜치입니다: $BRANCH" + echo "이 워크플로우는 deploy 브랜치와 test 브랜치만 지원합니다." + echo "on.push.branches에서 지원할 브랜치를 설정하세요." + exit 1 + fi + + # 설정 정보 출력 + echo "📋 배포 설정 정보:" + echo " - 브랜치: $BRANCH" + echo " - 프로젝트: $PROJECT_NAME" + echo " - 컨테이너 이름: $CONTAINER_NAME" + echo " - 포트: $PORT" + echo " - Docker 이미지: ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}" + + # ============================================================ + # Docker 이미지 풀 (Pull) + # ============================================================ + echo "⬇️ Docker 이미지 풀: ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}" + echo $PW | sudo -S docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH} + + # ============================================================ + # 기존 컨테이너 정리 + # ============================================================ + echo "🧹 컨테이너 $CONTAINER_NAME 존재 여부 확인 중..." + + # 동일한 이름의 컨테이너가 존재하는지 확인 + if sudo docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}\$"; then + echo "⚠️ 컨테이너 $CONTAINER_NAME 이(가) 존재합니다. 중지 및 삭제 중..." + echo $PW | sudo -S docker rm -f $CONTAINER_NAME + echo "✅ 컨테이너 $CONTAINER_NAME 이(가) 삭제되었습니다." + else + echo "ℹ️ 존재하는 컨테이너 $CONTAINER_NAME 이(가) 없습니다." + fi + + # ============================================================ + # 새 컨테이너 실행 + # ============================================================ + echo "🚀 새로운 컨테이너 $CONTAINER_NAME 실행 중..." + + # Docker 컨테이너 실행 + # - 포트 매핑: ${PORT}:8080 (외부포트:내부포트) + # - 시간대: Asia/Seoul + # - Spring 프로파일: prod + # - 볼륨 마운트: 로컬 시간 동기화 및 프로젝트 데이터 + echo $PW | sudo -S docker run -d \ + -p ${PORT}:8080 \ + --name $CONTAINER_NAME \ + -e TZ=Asia/Seoul \ + -e "SPRING_PROFILES_ACTIVE=${{ env.SPRING_PROFILE }}" \ + -v /etc/localtime:/etc/localtime:ro \ + -v /volume1/projects/${PROJECT_NAME}:/mnt/${PROJECT_NAME} \ + ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH} + + # ============================================================ + # 배포 완료 확인 + # ============================================================ + echo "✅ 배포가 성공적으로 완료되었습니다!" + echo "" + echo "📋 배포 결과 요약:" + echo " 🎯 프로젝트: $PROJECT_NAME" + echo " 🌿 브랜치: $BRANCH" + echo " 🐳 컨테이너: $CONTAINER_NAME" + echo " 🌐 포트: $PORT" + echo " ⏰ 배포 시간: $(date '+%Y-%m-%d %H:%M:%S')" + echo "" + echo "🔗 접속 URL: http://${{ secrets.SERVER_HOST }}:${PORT}" + +# =================================================================== +# 사용 예시 - deploy와 test 브랜치 사용 +# =================================================================== +# +# 현재 설정: deploy와 test 브랜치에 push할 때 자동 배포 +# main 브랜치는 수동 실행(workflow_dispatch)만 가능 +# +# 브랜치별 포트: +# - deploy: 8080 (PROJECT_DEPLOY_PORT secret으로 변경 가능) +# - main: 8080 (수동 실행시, PROJECT_DEPLOY_PORT secret으로 변경 가능) +# - test: 8081 (PROJECT_TEST_PORT secret으로 변경 가능) +# +# 필요한 Secrets: +# - APPLICATION_PROD_YML, DOCKERHUB_*, SERVER_* (필수) +# - PROJECT_DEPLOY_PORT (선택, 기본값: 8080) +# - PROJECT_TEST_PORT (선택, 기본값: 8081) +# +# =================================================================== \ No newline at end of file diff --git a/CHANGELOG.json b/CHANGELOG.json index fefad9a..843d024 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,11 +1,41 @@ { "metadata": { - "lastUpdated": "2026-01-18T15:29:36Z", - "currentVersion": "0.1.8", + "lastUpdated": "2026-01-19T04:22:34Z", + "currentVersion": "0.1.13", "projectType": "spring", - "totalReleases": 3 + "totalReleases": 4 }, "releases": [ + { + "version": "0.1.13", + "project_type": "spring", + "date": "2026-01-19", + "pr_number": 7, + "raw_summary": "## Summary by CodeRabbit\n\n* **새로운 기능**\n * 자동 배포 워크플로우 추가로 개발 프로세스 효율화\n\n* **변경사항**\n * 제품 브랜드명 \"Mapsy\"로 통일\n * API 문서 및 설정 업데이트\n\n* **기타**\n * 버전 v0.1.13으로 업그레이드\n * 기본 기술 스택 최적화 및 내부 구조 개선", + "parsed_changes": { + "새로운_기능": { + "title": "새로운 기능", + "items": [ + "자동 배포 워크플로우 추가로 개발 프로세스 효율화" + ] + }, + "변경사항": { + "title": "변경사항", + "items": [ + "제품 브랜드명 \"Mapsy\"로 통일", + "API 문서 및 설정 업데이트" + ] + }, + "기타": { + "title": "기타", + "items": [ + "버전 v0.1.13으로 업그레이드", + "기본 기술 스택 최적화 및 내부 구조 개선" + ] + } + }, + "parse_method": "markdown" + }, { "version": "0.1.8", "project_type": "spring", diff --git a/CHANGELOG.md b/CHANGELOG.md index c2edf32..ebe234f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,24 @@ # Changelog -**현재 버전:** 0.1.8 -**마지막 업데이트:** 2026-01-18T15:29:36Z +**현재 버전:** 0.1.13 +**마지막 업데이트:** 2026-01-19T04:22:34Z + +--- + +## [0.1.13] - 2026-01-19 + +**PR:** #7 + +**새로운 기능** +- 자동 배포 워크플로우 추가로 개발 프로세스 효율화 + +**변경사항** +- 제품 브랜드명 "Mapsy"로 통일 +- API 문서 및 설정 업데이트 + +**기타** +- 버전 v0.1.13으로 업그레이드 +- 기본 기술 스택 최적화 및 내부 구조 개선 --- diff --git a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/AiServerProperties.java b/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/AiServerProperties.java index 1112f17..41e0b1a 100644 --- a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/AiServerProperties.java +++ b/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/AiServerProperties.java @@ -17,7 +17,7 @@ public class AiServerProperties { /** * AI 서버 Base URL - * 예: https://ai.tripgether.suhsaechan.kr + * 예: https://ai.mapsy.suhsaechan.kr */ private String baseUrl; diff --git a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/PlaceProperties.java b/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/PlaceProperties.java deleted file mode 100644 index b88bd59..0000000 --- a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/properties/PlaceProperties.java +++ /dev/null @@ -1,38 +0,0 @@ -package kr.suhsaechan.mapsy.common.properties; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -/** - * 장소 검색 API 연동을 위한 설정 정보 - * application-dev.yml의 place 설정을 매핑합니다. - */ -@Component -@Getter -@Setter -public class PlaceProperties { - - /** - * Google Places API Key - * 환경변수: GOOGLE_PLACES_API_KEY - */ - @Value("${place.google.api-key:}") - private String googleApiKey; - - /** - * Kakao Places API Key - * 환경변수: KAKAO_PLACES_API_KEY - */ - @Value("${place.kakao.api-key:}") - private String kakaoApiKey; - - /** - * Naver Places API Key - * 환경변수: NAVER_PLACES_API_KEY - */ - @Value("${place.naver.api-key:}") - private String naverApiKey; -} - diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/GooglePlaceSearchDto.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/GooglePlaceSearchDto.java deleted file mode 100644 index 9ccb76e..0000000 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/GooglePlaceSearchDto.java +++ /dev/null @@ -1,114 +0,0 @@ -package kr.suhsaechan.mapsy.place.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.List; -import lombok.ToString; - -/** - * Google Places API 응답 DTO - */ -@Getter -@ToString -@NoArgsConstructor -public class GooglePlaceSearchDto { - - private List candidates; - private String status; - - /** - * Google Places API 검색 결과 - */ - @Getter - @NoArgsConstructor - public static class Candidate { - - @JsonProperty("place_id") - private String placeId; - - private String name; - - @JsonProperty("formatted_address") - private String formattedAddress; - - private Geometry geometry; - - private List types; - - @JsonProperty("business_status") - private String businessStatus; - - private String icon; - - private List photos; - - private BigDecimal rating; - - @JsonProperty("user_ratings_total") - private Integer userRatingsTotal; - } - - /** - * 좌표 정보 - */ - @Getter - @NoArgsConstructor - public static class Geometry { - private Location location; - } - - /** - * 위도/경도 - */ - @Getter - @NoArgsConstructor - public static class Location { - private BigDecimal lat; - private BigDecimal lng; - } - - /** - * 사진 정보 - */ - @Getter - @NoArgsConstructor - public static class Photo { - @JsonProperty("photo_reference") - private String photoReference; - - private Integer height; - private Integer width; - - @JsonProperty("html_attributions") - private List htmlAttributions; - } - - /** - * Service 레이어에서 사용할 장소 상세 정보 - */ - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class PlaceDetail { - private String placeId; - private String name; - private String address; - private BigDecimal latitude; - private BigDecimal longitude; - private String country; - - // 추가 정보 - private List types; - private String businessStatus; - private String iconUrl; - private BigDecimal rating; - private Integer userRatingsTotal; - private List photoUrls; - } -} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/GooglePlaceSearcher.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/GooglePlaceSearcher.java deleted file mode 100644 index 5109d7f..0000000 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/GooglePlaceSearcher.java +++ /dev/null @@ -1,359 +0,0 @@ -package kr.suhsaechan.mapsy.place.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import kr.suhsaechan.mapsy.common.exception.CustomException; -import kr.suhsaechan.mapsy.common.exception.constant.ErrorCode; -import kr.suhsaechan.mapsy.common.properties.PlaceProperties; -import kr.suhsaechan.mapsy.place.constant.PlacePlatform; -import kr.suhsaechan.mapsy.place.dto.GooglePlaceSearchDto; -import kr.suhsaechan.mapsy.place.entity.Place; -import kr.suhsaechan.mapsy.place.entity.PlacePlatformReference; -import kr.suhsaechan.mapsy.place.repository.PlacePlatformReferenceRepository; -import kr.suhsaechan.mapsy.place.repository.PlaceRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Google Places API를 통한 장소 검색 서비스 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class GooglePlaceSearcher implements PlacePlatformSearcher { - - private final OkHttpClient okHttpClient; - private final ObjectMapper objectMapper; - private final PlaceProperties placeProperties; - private final PlaceRepository placeRepository; - private final PlacePlatformReferenceRepository placePlatformReferenceRepository; - - private static final String GOOGLE_PLACES_BASE_URL = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json"; - private static final String SEARCH_FIELDS = "place_id,name,formatted_address,geometry,types,business_status,icon,photos,rating,user_ratings_total"; - - /** - * 상호명으로 Google Place 검색 - *

- * 1차: DB에서 캐싱된 Place 검색 (name + address 기반) - * 2차: DB에 없으면 Google API 호출 - * 3차: Google API 결과를 DB에 저장하여 캐싱 - * - * @param placeName 장소명 - * @param address 주소 (fallback용, 현재 미사용) - * @param language 언어 코드 (ko, en, ja, zh) - * @return Google Place 상세 정보 (place_id, 좌표 등) - * @throws CustomException Google Places API 호출 실패 또는 장소를 찾을 수 없을 때 - */ - @Override - public GooglePlaceSearchDto.PlaceDetail searchPlaceDetail(String placeName, String address, String language) { - log.info("Place Search Start: name={}, address={}, language={}", placeName, address, language); - - // 1️⃣ DB에서 먼저 검색 (정규화된 키워드로 캐싱 조회) - String normalizedName = (placeName != null) ? placeName.trim() : null; - String normalizedAddress = (address != null && !address.isEmpty()) ? address.trim() : null; - - if (normalizedName != null && !normalizedName.isEmpty()) { - Optional cachedPlace = placeRepository.findByNormalizedNameAndAddress( - normalizedName, normalizedAddress); - - if (cachedPlace.isPresent()) { - Place existing = cachedPlace.get(); - log.info("Place found in DB (cache hit): placeId={}, name={}", existing.getId(), existing.getName()); - - // 좌표 변경 검증 (Google API 재호출 필요 여부) - if (shouldUpdateCoordinates(existing)) { - log.info("Coordinates may have changed, refreshing from Google API"); - return searchGooglePlaceApi(placeName, address, language); - } - - // PlaceId 검증 및 재호출 처리 - String googlePlaceId = getGooglePlaceId(existing); - if (googlePlaceId == null) { - log.warn("PlacePlatformReference not found for Place: {}, refreshing from Google API", existing.getId()); - return searchGooglePlaceApi(placeName, address, language); - } - - // DB 캐시 히트 - PlaceDetail 변환하여 반환 - return convertPlaceToPlaceDetail(existing, googlePlaceId); - } - - log.info("Place not found in DB (cache miss), calling Google Places API"); - } - - // 2️⃣ DB에 없으면 Google API 호출 - return searchGooglePlaceApi(placeName, address, language); - } - - /** - * Google Places API 호출 - *

- * DB 캐시에 없을 때만 호출됨 - * - * @param placeName 장소명 - * @param address 주소 - * @param language 언어 코드 - * @return Google Place 상세 정보 - * @throws CustomException API 호출 실패 시 - */ - private GooglePlaceSearchDto.PlaceDetail searchGooglePlaceApi(String placeName, String address, String language) { - String googleApiKey = placeProperties.getGoogleApiKey(); - - if (googleApiKey == null || googleApiKey.isEmpty()) { - log.error("Google Places API key not configured: placeName={}", placeName); - throw new CustomException(ErrorCode.INVALID_API_KEY); - } - - try { - log.info("Calling Google Places API"); - log.info("Place Name: {}", placeName); - log.info("Address: {}", address); - log.info("Language: {}", language); - - // URL 생성 - String url = buildSearchUrl(placeName, language, googleApiKey); - - log.info("Request URL: {}", url); - - // OkHttp로 API 호출 - Request request = new Request.Builder() - .url(url) - .addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") - .addHeader("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7") - .addHeader("Accept", "application/json") - .get() - .build(); - - GooglePlaceSearchDto response; - try (Response httpResponse = okHttpClient.newCall(request).execute()) { - if (!httpResponse.isSuccessful()) { - log.error("Google Places API HTTP error: code={}", httpResponse.code()); - throw new CustomException(ErrorCode.GOOGLE_PLACE_API_ERROR); - } - - if (httpResponse.body() == null) { - log.error("Google Places API response body is null"); - throw new CustomException(ErrorCode.GOOGLE_PLACE_API_ERROR); - } - - String responseBody = httpResponse.body().string(); - response = objectMapper.readValue(responseBody, GooglePlaceSearchDto.class); - } - - // 결과 파싱 - String status = response.getStatus(); - log.info("API Response Status: {}", status); - log.info("Candidates Count: {}", - response.getCandidates() != null ? response.getCandidates().size() : 0); - - if ("OK".equals(status) - && response.getCandidates() != null - && !response.getCandidates().isEmpty()) { - - GooglePlaceSearchDto.Candidate candidate = response.getCandidates().get(0); - - log.info("Selected Candidate:"); - log.info(" Place ID: {}", candidate.getPlaceId()); - log.info(" Name: {}", candidate.getName()); - log.info(" Address: {}", candidate.getFormattedAddress()); - - GooglePlaceSearchDto.PlaceDetail placeDetail = GooglePlaceSearchDto.PlaceDetail.builder() - .placeId(candidate.getPlaceId()) - .name(candidate.getName()) - .address(candidate.getFormattedAddress()) - .latitude(candidate.getGeometry().getLocation().getLat()) - .longitude(candidate.getGeometry().getLocation().getLng()) - .country(extractCountryCode(candidate.getFormattedAddress())) - .types(candidate.getTypes()) - .businessStatus(candidate.getBusinessStatus()) - .iconUrl(candidate.getIcon()) - .rating(candidate.getRating()) - .userRatingsTotal(candidate.getUserRatingsTotal()) - .photoUrls(buildPhotoUrls(candidate.getPhotos(), googleApiKey)) - .build(); - - log.info("Google Places API Search Success"); - log.info("Final Result: placeId={}, name={}, rating={}", - placeDetail.getPlaceId(), placeDetail.getName(), placeDetail.getRating()); - return placeDetail; - - } else { - // Google Places API 상태 코드별 에러 처리 - log.error("Google Places API Search Failed"); - log.error("Place Name: {}", placeName); - log.error("Status: {}", status); - - if ("REQUEST_DENIED".equals(status)) { - throw new CustomException(ErrorCode.INVALID_API_KEY); - } else if ("INVALID_REQUEST".equals(status)) { - throw new CustomException(ErrorCode.INVALID_REQUEST); - } else if ("OVER_QUERY_LIMIT".equals(status)) { - throw new CustomException(ErrorCode.EXTERNAL_API_ERROR); - } else if ("ZERO_RESULTS".equals(status)) { - throw new CustomException(ErrorCode.GOOGLE_PLACE_NOT_FOUND); - } else { - throw new CustomException(ErrorCode.GOOGLE_PLACE_API_ERROR); - } - } - - } catch (CustomException e) { - // NetworkUtil에서 발생한 CustomException은 그대로 전파 - log.error("Google Places API error: placeName={}, error={}", placeName, e.getMessage()); - throw e; - } catch (Exception e) { - // 예상치 못한 예외 - log.error("Unexpected error during Google Places API call: placeName={}", placeName, e); - throw new CustomException(ErrorCode.GOOGLE_PLACE_API_ERROR); - } - } - - /** - * Google Places API 검색 URL 생성 - */ - private String buildSearchUrl(String placeName, String language, String googleApiKey) { - log.debug("Building search URL for: {}", placeName); - - String encodedPlaceName = URLEncoder.encode(placeName, StandardCharsets.UTF_8); - - String url = String.format("%s?input=%s&inputtype=textquery&fields=%s&language=%s&key=%s", - GOOGLE_PLACES_BASE_URL, - encodedPlaceName, - SEARCH_FIELDS, - language, - googleApiKey - ); - - return url; - } - - /** - * 사진 URL 배열 생성 - *

- * photo_reference를 실제 Google Photos API URL로 변환 - * - * @param photos 사진 정보 리스트 - * @param googleApiKey API 키 - * @return 사진 URL 배열 (최대 10개) - */ - private List buildPhotoUrls(List photos, String googleApiKey) { - if (photos == null || photos.isEmpty()) { - return null; - } - - return photos.stream() - .limit(10) // 최대 10개 - .map(photo -> String.format( - "https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=%s&key=%s", - photo.getPhotoReference(), - googleApiKey - )) - .collect(Collectors.toList()); - } - - /** - * 주소에서 국가 코드 추출 - *

- * 간단 구현: 주소 문자열에서 국가명 패턴 매칭 - * 추후 더 정교한 로직으로 개선 필요 - * - * @param address Google API의 formatted_address - * @return 국가 코드 (ISO 3166-1 alpha-2) - */ - private String extractCountryCode(String address) { - if (address == null || address.isEmpty()) { - return "XX"; - } - - // 한국 - if (address.contains("South Korea") || address.contains("대한민국") || address.contains("Korea")) { - return "KR"; - } - // 미국 - if (address.contains("United States") || address.contains("USA") || address.contains("US")) { - return "US"; - } - // 일본 - if (address.contains("Japan") || address.contains("日本")) { - return "JP"; - } - // 중국 - if (address.contains("China") || address.contains("中国")) { - return "CN"; - } - - // 기본값 - log.debug("Could not extract country code from address: {}", address); - return "XX"; - } - - /** - * Place 엔티티를 PlaceDetail DTO로 변환 - *

- * DB 캐시된 Place를 API 응답 형식으로 변환 - * - * @param place Place 엔티티 - * @param googlePlaceId Google place_id - * @return PlaceDetail DTO - */ - private GooglePlaceSearchDto.PlaceDetail convertPlaceToPlaceDetail(Place place, String googlePlaceId) { - return GooglePlaceSearchDto.PlaceDetail.builder() - .placeId(googlePlaceId) - .name(place.getName()) - .address(place.getAddress()) - .latitude(place.getLatitude()) - .longitude(place.getLongitude()) - .country(place.getCountry()) - .types(place.getTypes()) - .businessStatus(place.getBusinessStatus()) - .iconUrl(place.getIconUrl()) - .rating(place.getRating()) - .userRatingsTotal(place.getUserRatingsTotal()) - .photoUrls(place.getPhotoUrls()) - .build(); - } - - /** - * Place의 Google PlaceId 조회 - *

- * PlacePlatformReference 테이블에서 Google platform의 placeId를 조회 - * - * @param place Place 엔티티 - * @return Google placeId (없으면 null) - */ - private String getGooglePlaceId(Place place) { - return placePlatformReferenceRepository - .findByPlaceAndPlacePlatform(place, PlacePlatform.GOOGLE) - .map(PlacePlatformReference::getPlacePlatformId) - .orElse(null); - } - - /** - * 좌표 변경 검증 - *

- * Place가 오래되었거나 좌표 정확도가 낮으면 Google API 재호출 필요 - * 현재는 단순 구현 (항상 false 반환) - *

- * 향후 개선 방향: - * - createdAt이 1년 이상 지났으면 재조회 - * - 좌표 정확도가 낮으면 (소수점 자리수 부족) 재조회 - * - * @param place Place 엔티티 - * @return true: 재조회 필요, false: 캐시 사용 - */ - private boolean shouldUpdateCoordinates(Place place) { - // 현재는 항상 캐시 사용 (좌표 변경 감지 미구현) - // 필요시 아래 로직 활성화: - // LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); - // return place.getCreatedAt().isBefore(oneYearAgo); - return false; - } -} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlacePlatformSearcher.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlacePlatformSearcher.java deleted file mode 100644 index e0122b0..0000000 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlacePlatformSearcher.java +++ /dev/null @@ -1,21 +0,0 @@ -package kr.suhsaechan.mapsy.place.service; - -import kr.suhsaechan.mapsy.place.dto.GooglePlaceSearchDto; - -/** - * 플랫폼별 장소 검색 인터페이스 - *

- * Google Places API를 통한 장소 검색 기능을 제공 - */ -public interface PlacePlatformSearcher { - - /** - * 상호명으로 장소 상세 정보 검색 - * - * @param placeName 장소명 - * @param address 주소 (fallback용, 현재 미사용) - * @param language 언어 코드 (ko, en, ja, zh) - * @return 장소 상세 정보 (place_id, 좌표, 추가 정보 등) - */ - GooglePlaceSearchDto.PlaceDetail searchPlaceDetail(String placeName, String address, String language); -} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlaceSearchService.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlaceSearchService.java deleted file mode 100644 index a1f8841..0000000 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/PlaceSearchService.java +++ /dev/null @@ -1,35 +0,0 @@ -package kr.suhsaechan.mapsy.place.service; - -import kr.suhsaechan.mapsy.place.dto.GooglePlaceSearchDto; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -/** - * 장소 검색 서비스 - *

- * Google Places API를 통한 장소 검색 기능을 제공 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class PlaceSearchService { - - private final GooglePlaceSearcher googlePlaceSearcher; - - /** - * Google 플랫폼으로 장소 검색 - * - * @param placeName 장소명 - * @param address 주소 - * @param language 언어 코드 - * @return 장소 상세 정보 - */ - public GooglePlaceSearchDto.PlaceDetail searchGooglePlace( - String placeName, - String address, - String language) { - - return googlePlaceSearcher.searchPlaceDetail(placeName, address, language); - } -} diff --git a/MS-SNS/src/main/java/kr/suhsaechan/mapsy/sns/service/AiCallbackService.java b/MS-SNS/src/main/java/kr/suhsaechan/mapsy/sns/service/AiCallbackService.java index 42db62d..5662ccf 100644 --- a/MS-SNS/src/main/java/kr/suhsaechan/mapsy/sns/service/AiCallbackService.java +++ b/MS-SNS/src/main/java/kr/suhsaechan/mapsy/sns/service/AiCallbackService.java @@ -7,14 +7,12 @@ import kr.suhsaechan.mapsy.common.exception.constant.ErrorCode; import kr.suhsaechan.mapsy.place.constant.PlacePlatform; import kr.suhsaechan.mapsy.place.constant.PlaceSavedStatus; -import kr.suhsaechan.mapsy.place.dto.GooglePlaceSearchDto; import kr.suhsaechan.mapsy.place.entity.MemberPlace; import kr.suhsaechan.mapsy.place.entity.Place; import kr.suhsaechan.mapsy.place.entity.PlacePlatformReference; import kr.suhsaechan.mapsy.place.repository.MemberPlaceRepository; import kr.suhsaechan.mapsy.place.repository.PlacePlatformReferenceRepository; import kr.suhsaechan.mapsy.place.repository.PlaceRepository; -import kr.suhsaechan.mapsy.place.service.PlaceSearchService; import kr.suhsaechan.mapsy.sns.entity.Content; import kr.suhsaechan.mapsy.sns.entity.ContentMember; import kr.suhsaechan.mapsy.sns.entity.ContentPlace; @@ -46,7 +44,6 @@ public class AiCallbackService { private final ContentPlaceRepository contentPlaceRepository; private final PlacePlatformReferenceRepository placePlatformReferenceRepository; private final MemberPlaceRepository memberPlaceRepository; - private final PlaceSearchService placeSearchService; private final FcmService fcmService; /** @@ -105,7 +102,7 @@ public AiCallbackResponse processAiServerCallback(AiCallbackRequest request) { * * - Content가 COMPLETED 상태: 기존 ContentPlace 삭제 후 재생성 (업데이트 모드) * - Content가 PENDING/FAILED 상태: 신규 ContentPlace 생성 - * - Google Places API 호출하여 place_id 및 상세 정보 획득 + * - TODO: AI 서버에서 받은 Place 정보로 직접 Place 생성 (나중에 구현 예정) * * @param content 대상 Content * @param request AI Callback 요청 @@ -131,57 +128,17 @@ private void processAiServerSuccessCallback(Content content, AiCallbackRequest r contentRepository.save(content); - // AI 서버에서 받은 Place 정보로 ContentPlace 생성 + // TODO: AI 서버에서 받은 Place 정보로 직접 Place 생성하도록 수정 필요 + // 현재는 Google Places API 호출이 제거되어 Place 저장 로직이 임시로 비활성화됨 if (request.getPlaces() != null && !request.getPlaces().isEmpty()) { List places = request.getPlaces(); - log.info("Processing {} places for contentId={} (update mode: {})", + log.info("Received {} places for contentId={} (update mode: {}). Place saving temporarily disabled.", places.size(), content.getId(), isContentAlreadyCompleted); - // 각 Place 정보를 순회하며 저장 - for (int i = 0; i < places.size(); i++) { - AiCallbackRequest.PlaceInfo placeInfo = places.get(i); - - log.info("Processing place {}/{}", i + 1, places.size()); - log.info("Place Name: {}", placeInfo.getName()); - log.info("Address: {}", placeInfo.getAddress()); - - // language 필드가 null이면 기본값 "ko" 사용 - String language = placeInfo.getLanguage() != null ? placeInfo.getLanguage() : "ko"; - log.info("Language: {}", language); - - try { - // 1. Google Places API 호출 (실패 시 CustomException 발생) - GooglePlaceSearchDto.PlaceDetail googlePlace = placeSearchService.searchGooglePlace( - placeInfo.getName(), - placeInfo.getAddress(), - language - ); - - // 2. Google 응답으로 Place 생성/업데이트 - Place place = createOrUpdatePlace(googlePlace); - - // 3. PlacePlatformReference 저장 (Google place_id) - savePlacePlatformReference(place, googlePlace.getPlaceId()); - - // 4. Content와 Place 연결 생성 (position 포함) - createContentPlace(content, place, i); - - // 5. 해당 Content를 요청한 모든 회원에게 MemberPlace 생성 - createMemberPlaces(content, place); - - log.info("Place processed successfully: DB ID={}", place.getId()); - - - } catch (CustomException e) { - log.error("Failed to process place {}/{}", i + 1, places.size()); - log.error("Place Name: {}", placeInfo.getName()); - log.error("HTTP Status: {}", e.getStatus()); - log.error("Error Message: {}", e.getMessage()); - - // 예외 재발생으로 전체 트랜잭션 롤백 - throw e; - } - } + // TODO: AiCallbackRequest.PlaceInfo에서 직접 Place 생성하도록 구현 필요 + // - 위도/경도 정보가 AI 서버 응답에 포함되어야 함 + // - Place 생성 로직 재구현 필요 + log.warn("Place saving is temporarily disabled. Will be implemented later with AI server response data."); } else { // Place 데이터가 없는 경우 경고 로그 log.warn("No places found in callback for contentId={}", content.getId()); @@ -277,14 +234,9 @@ private void processFailedCallback(Content content, AiCallbackRequest request) { contentRepository.save(content); } - /** - * Google Places API 응답으로 Place 생성 또는 업데이트 - *

- * 이름+좌표로 중복 체크 후 없으면 신규 생성 - * - * @param googlePlace Google Places API 응답 - * @return 조회 또는 생성된 Place - */ + // TODO: Google Places API 제거로 인해 임시로 주석 처리 + // 나중에 AI 서버 응답 데이터(AiCallbackRequest.PlaceInfo)로 직접 Place 생성하도록 재구현 필요 + /* private Place createOrUpdatePlace(GooglePlaceSearchDto.PlaceDetail googlePlace) { // 이름+좌표로 중복 체크 Optional existing = placeRepository.findByNameAndLatitudeAndLongitude( @@ -330,14 +282,6 @@ private Place createOrUpdatePlace(GooglePlaceSearchDto.PlaceDetail googlePlace) } } - /** - * PlacePlatformReference 저장 - *

- * Google place_id를 PlacePlatformReference에 저장 (중복 체크) - * - * @param place 장소 - * @param googlePlaceId Google place_id - */ private void savePlacePlatformReference(Place place, String googlePlaceId) { log.info("Saving PlacePlatformReference: placeId={}, googlePlaceId={}", place.getId(), googlePlaceId); @@ -358,6 +302,7 @@ private void savePlacePlatformReference(Place place, String googlePlaceId) { existing.get().getId(), place.getId(), existing.get().getPlacePlatformId(), googlePlaceId); } } + */ /** * ContentPlace 연결 생성 diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/JacksonConfig.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/JacksonConfig.java new file mode 100644 index 0000000..f973f4a --- /dev/null +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/JacksonConfig.java @@ -0,0 +1,34 @@ +package kr.suhsaechan.mapsy.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Jackson 설정 + * Spring Boot 4.0.1의 JsonMapper를 ObjectMapper로 제공하여 기존 코드 호환성 유지 + */ +@Configuration +public class JacksonConfig { + + /** + * ObjectMapper 빈 정의 + * Spring Boot 4.0.1에서 JsonMapper가 자동 구성되면 이를 활용하고, + * 없으면 JsonMapper.builder()로 직접 생성합니다. + * + * @return ObjectMapper 빈 + */ + @Bean + @Primary + @ConditionalOnMissingBean(ObjectMapper.class) + public ObjectMapper objectMapper() { + // JsonMapper.builder()로 ObjectMapper 생성 + // JsonMapper는 ObjectMapper의 서브클래스이므로 호환성 보장 + return JsonMapper.builder() + .findAndAddModules() + .build(); + } +} diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/SwaggerConfig.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/SwaggerConfig.java index 296fcdd..3c1f263 100644 --- a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/SwaggerConfig.java +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/SwaggerConfig.java @@ -14,7 +14,7 @@ @OpenAPIDefinition( info = @io.swagger.v3.oas.annotations.info.Info( - title = "📚 Tripgether : 여행의 동반자 📚", + title = "📚 Mapsy 📚", description = """ ### http://suh-project.synology.me:8093 """ @@ -28,7 +28,7 @@ public class SwaggerConfig { private final SpringDocProperties springDocProperties; @Bean - public OpenAPI tripgetherOpenAPI() { + public OpenAPI mapsyOpenAPI() { SecurityScheme bearerAuth = new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") @@ -41,11 +41,8 @@ public OpenAPI tripgetherOpenAPI() { .addSecurityItem(new SecurityRequirement().addList("Bearer Token")) .servers(List.of( new io.swagger.v3.oas.models.servers.Server() - .url("https://api.tripgether.suhsaechan.kr") + .url("https://api.mapsy.suhsaechan.kr") .description("메인 서버"), - new io.swagger.v3.oas.models.servers.Server() - .url("https://api.test.tripgether.suhsaechan.kr") - .description("테스트 서버"), new io.swagger.v3.oas.models.servers.Server() .url("http://localhost:8080") .description("로컬 서버") diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/WebClientConfig.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/WebClientConfig.java index f5e9fe3..3e2ac2c 100644 --- a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/WebClientConfig.java +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/config/WebClientConfig.java @@ -24,7 +24,7 @@ public class WebClientConfig { * Timeout 설정 및 메모리 버퍼 크기 설정 포함 */ @Bean - public WebClient webClient(WebClient.Builder builder) { + public WebClient webClient() { HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) // 연결 타임아웃 10초 .responseTimeout(Duration.ofSeconds(30)) // 응답 타임아웃 30초 @@ -33,7 +33,7 @@ public WebClient webClient(WebClient.Builder builder) { .addHandlerLast(new WriteTimeoutHandler(30, TimeUnit.SECONDS)) // 쓰기 타임아웃 30초 ); - return builder + return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .codecs(configurer -> configurer .defaultCodecs() diff --git a/MS-Web/src/main/resources/db/migration/V0.2.1__insert_interests.sql b/MS-Web/src/main/resources/db/migration/V0.2.1__insert_interests.sql index 2fdfb92..b3ab340 100644 --- a/MS-Web/src/main/resources/db/migration/V0.2.1__insert_interests.sql +++ b/MS-Web/src/main/resources/db/migration/V0.2.1__insert_interests.sql @@ -1,5 +1,5 @@ -- ============================================ --- Tripgether Interest Data Migration +-- Mapsy Interest Data Migration -- Version: 0.2.1 -- Description: 14개 대분류, 123개 소분류 관심사 초기화 -- - 테이블이 없으면 아무 작업도 하지 않음 (JPA가 자동 생성) diff --git a/README.md b/README.md index f721e24..297fd76 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -## 최신 버전 : v0.1.2 (2026-01-18) +## 최신 버전 : v0.1.8 (2026-01-18) [전체 버전 기록 보기](CHANGELOG.md) diff --git a/build.gradle b/build.gradle index 4dbd3c6..eace6b8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ bootJar { allprojects { group = 'kr.suhsaechan.mapsy' - version = '0.1.8' + version = '0.1.13' repositories { mavenCentral() diff --git a/version.yml b/version.yml index bce5a1f..5106076 100644 --- a/version.yml +++ b/version.yml @@ -33,12 +33,11 @@ # - project_type은 최초 설정 후 변경하지 마세요 # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== - -version: "0.1.8" -version_code: 12 # app build number +version: "0.1.13" +version_code: 17 # app build number project_type: "spring" # spring, flutter, next, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-01-18 15:22:42" + last_updated: "2026-01-19 01:19:20" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE"