diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 43eea57..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dbf7477 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gradle +build/ +out/ +bin/ +Dockerfile +.dockerignore +README.md \ No newline at end of file diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 0000000..c1420d0 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,5 @@ +{ + "ui": { + "theme": "ANSI Light" + } +} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index fa7d32f..51cf244 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -13,4 +13,4 @@ assignees: "" ### 💡 기타 참고 사항 - + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8938d7d..d419620 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,19 @@ -### 📌 관련 이슈 +## 📌 관련 이슈 +
+ -### ✨ 작업 내용 요약 +## ✨ 작업 내용 요약 +
+ -### 🛠️ 주요 변경 사항 +## 🛠️ 주요 변경 사항 +
+ -### 📚 체크리스트 +## 📚 체크리스트 - [ ] 팀 컨벤션에 맞는 커밋 메시지를 작성했나요? - [ ] 로컬 환경에서 정상 작동하는지 확인했나요? - [ ] 불필요한 주석이나 print문은 제거했나요? diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..64ebc7d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,96 @@ +name: Pace Project CI/CD # 워크플로우 이름 설정 + +on: + push: + branches: [ "main", "develop" ] # 해당 브랜치에 코드가 push될 때만 실행 + +jobs: + deploy: + runs-on: ubuntu-latest # 깃허브가 제공하는 최신 우분투 가상 환경에서 작업 + + steps: + - name: Checkout code + uses: actions/checkout@v4 # 깃허브 서버로 내 소스 코드를 가져옴 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Tests with Gradle + run: ./gradlew test + + - name: Login to Docker Hub + uses: docker/login-action@v3 # 도커 허브 접속 시도 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} # 깃허브 시크릿에서 아이디 가져옴 + password: ${{ secrets.DOCKERHUB_TOKEN }} # 깃허브 시크릿에서 토큰 가져옴 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 # 도커 이미지 빌드 및 전송 + with: + context: . # 현재 위치의 파일을 바탕으로 빌드 + push: true # 빌드 성공 시 도커 허브로 즉시 보냄 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/pace-project:latest # 이미지 이름과 태그 붙임 + + - name: Prepare directory on EC2 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_SSH_KEY }} + script: mkdir -p ~/pace-project/nginx + + - name: Copy docker-compose.yml via scp + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_SSH_KEY }} + source: "docker-compose.yml,nginx/default.conf" # 내 깃허브에 있는 파일 이름 + target: "~/pace-project" # 서버에 저장될 폴더 이름 + + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1.0.3 # SSH를 통해 AWS EC2 서버에 접속 + with: + host: ${{ secrets.EC2_HOST }} # EC2의 IP 주소로 연결 + username: ${{ secrets.EC2_USERNAME }} # 접속 계정(보통 ubuntu) 사용 + key: ${{ secrets.EC2_SSH_KEY }} # .pem 키 내용으로 인증 + script: | # 접속 후 실행할 명령어들 시작 + mkdir -p ~/pace-project # 프로젝트 폴더가 없으면 생성 + cd ~/pace-project # 해당 폴더로 이동 + + docker pull ${{ secrets.DOCKERHUB_USERNAME }}/pace-project:latest # 창고에서 새 이미지 가져옴 + + # 서버에서 사용할 비밀 장부(.env)를 시크릿 값으로 새로 만듦 + echo "MYSQL_ROOT_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }}" > .env + echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> .env + echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> .env + echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> .env + echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env + echo "KAKAO_REDIRECT_URI=${{ secrets.KAKAO_REDIRECT_URI }}" >> .env + echo "GOOGLE_MAPS_API_KEY=${{ secrets.GOOGLE_MAPS_API_KEY }}" >> .env + echo "SEOUL_SUBWAY_API_KEY=${{ secrets.SEOUL_SUBWAY_API_KEY }}" >> .env + docker-compose up -d --remove-orphans # 새 이미지로 세트 메뉴(DB+앱) 다시 실행 + docker image prune -f + + # 헬스 체크: 서버가 제대로 떴는지 10번 확인함 + echo "Starting health check..." + for i in {1..10}; do + RESPONSE=$(curl -s -H "Host: pace-server.kro.kr" http://localhost/health) # 내 서버에 안부를 물어봄 + if [ "$RESPONSE" = "OK" ]; then # 대답이 "OK"면 성공 + echo "Health check passed!" + exit 0 + fi + echo "Waiting for server... ($i/10)" # 아직 안 떴으면 10초 기다림 + sleep 10 + done + echo "Health check failed." # 10번 다 실패하면 배포 실패 처리 + exit 1 diff --git a/.gitignore b/.gitignore index c2065bc..80d6f37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.gemini ### STS ### .apt_generated @@ -35,3 +36,6 @@ out/ ### VS Code ### .vscode/ + +.DS_Store +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d6cae52 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# 빌드 스테이지(eclipse-temurin을 사용하여 jar 파일 생성) +FROM eclipse-temurin:21-jdk-jammy AS build + +# 컨테이너 내부의 작업 디렉토리를 /app으로 설정 +WORKDIR /workspace + +# 의존성 캐시 단계 +COPY gradlew . +COPY gradle gradle +COPY settings.gradle build.gradle ./ + +# Gradle 래퍼(/gradlew)에 실행 권한 부여 및 라이브러리 다운로드 +RUN chmod +x ./gradlew && ./gradlew --no-daemon dependencies + +# 빌드 단계 +COPY src src +RUN ./gradlew --no-daemon clean bootJar -x test + +# 실행 단계 +FROM eclipse-temurin:21-jre-jammy AS runtime +WORKDIR /app + +# 보안을 위한 비관리자 유저 생성 +RUN useradd -ms /bin/bash appuser +USER appuser + +# 빌드 결과물만 복사 +COPY --from=build /workspace/build/libs/*.jar app.jar + +EXPOSE 8080 + +# 한국 시간 설정 및 실행 +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] diff --git a/README.md b/README.md index 050ab24..7589e72 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# Pace_server -UMC 9기 'Pace' 서비스 server 팀 +# Pace Server 🏃‍♂️ + +> **UMC 9기 'Pace' 서버팀** + +## 📖 프로젝트 개요 + +**Pace**는 사용자의 일정과 이동 경로를 효율적으로 관리할 수 있도록 돕는 모바일 애플리케이션입니다. **카카오 로그인**을 통한 간편한 인증을 지원하며, 대중교통 데이터를 활용한 일정 관리, 경로 추천, 장소 저장 등의 기능을 제공합니다. + +## 🛠 백엔드 기술 스택 (Tech Stack) + +### 환경 (Environment) +* ![Java](https://img.shields.io/badge/Java-21-ED8B00?style=flat&logo=openjdk&logoColor=white) +* ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.1-6DB33F?style=flat&logo=springboot&logoColor=white) +* ![Gradle](https://img.shields.io/badge/Gradle-8.x-02303A?style=flat&logo=gradle&logoColor=white) + +### 데이터베이스 & ORM +* ![MySQL](https://img.shields.io/badge/MySQL-8.0-4479A1?style=flat&logo=mysql&logoColor=white) +* **Spring Data JPA** +* **QueryDSL 5.0** + +### 보안 & 인증 (Security & Auth) +* **Spring Security** +* **OAuth 2.0 (Kakao Login)** +* **JWT (JSON Web Token)** + +### API & 문서화 +* **SpringDoc OpenAPI (Swagger UI)** +* **WebFlux (WebClient)** - 외부 API 연동 + +### 유틸리티 (Utilities) +* **Apache POI** - 엑셀 데이터 처리 (버스/지하철 정보) +* **Lombok** + +## 📂 프로젝트 구조 + +```bash +src/main/java/com/example/pace +├── PaceApplication.java +├── domain +│ ├── auth # 인증 (Kakao, JWT) +│ ├── member # 회원 관리, 장소 보관함, 설정 +│ ├── schedule # 일정 및 경로 관리 +│ ├── terms # 약관 관리 +│ └── transit # 대중교통 데이터 로직 +└── global + ├── apiPayload # 표준 API 응답 구조 + ├── auth # 시큐리티 설정, Custom User Details + ├── config # 앱 설정 (Swagger, WebClient 등) + └── entity # Base Entities +``` + +## ✨ 주요 기능 + +### 1. 인증 (Authentication) +* **카카오 로그인**: Kakao OAuth2를 이용한 간편 로그인. +* **토큰 관리**: Access/Refresh Token 발급 및 재발급(Reissue). +* **계정 관리**: 로그아웃 및 회원 탈퇴 기능. + +### 2. 일정 관리 (Schedule Management) +* **생성/삭제**: 개인 일정 등록 및 삭제. +* **조회**: 일별, 월별 일정 목록 조회. +* **경로**: 대중교통 정보를 활용한 일정 경로 관리. + +### 3. 회원 및 설정 (Member & Settings) +* **프로필**: 사용자 정보 관리. +* **장소 보관함**: 자주 가는 장소 즐겨찾기 및 그룹 관리. +* **온보딩**: 초기 사용자 설정 프로세스. + +### 4. 대중교통 통합 (Transit Integration) +* **데이터 로딩**: 버스 및 지하철 데이터(Excel/JSON)를 로드하여 경로 계산에 활용. diff --git a/build.gradle b/build.gradle index 1895f35..8502b1d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ plugins { - id 'java' - id 'org.springframework.boot' version '4.0.1' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '4.0.1' + id 'io.spring.dependency-management' version '1.1.7' + id 'org.jetbrains.kotlin.jvm' } group = 'com.example' @@ -9,38 +10,88 @@ version = '0.0.1-SNAPSHOT' description = 'Demo project for Spring Boot' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-security-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-webmvc' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' - testImplementation 'org.springframework.boot:spring-boot-starter-security-oauth2-client-test' - testImplementation 'org.springframework.boot:spring-boot-starter-security-test' - testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security-oauth2-client' + + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' + testImplementation 'org.springframework.boot:spring-boot-starter-security-oauth2-client-test' + testImplementation 'org.springframework.boot:spring-boot-starter-security-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'com.h2database:h2' + + // Swagger UI + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1") + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Excel (Apache POI) + implementation 'org.apache.poi:poi-ooxml:5.4.0' + + // QueryDSL : OpenFeign + implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" + implementation "io.github.openfeign.querydsl:querydsl-core:7.0" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + + // idk + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } + tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() +} + +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +// 소스 세트에 생성 경로 추가 +sourceSets { + main.java.srcDirs += [querydslDir] +} + +// 컴파일 시 생성 경로 지정 +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) } + +// clean 태스크에 생성 폴더 삭제 로직 +clean.doLast { + file(querydslDir).deleteDir() +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f119d60 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +services: + # Nginx + pace-nginx: + image: nginx:stable-alpine + container_name: pace-nginx + restart: always + depends_on: + - pace-app + networks: + - pace-network + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + + # Certbot 서비스(ssl 인증서 발급) + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + + # 데이터베이스 서비스 + pace-redis: + image: redis:alpine + command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes + container_name: pace-redis + restart: always + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis_data:/data + networks: + - pace-network + + pace-mysql: + image: mysql:8.0 + container_name: pace-mysql + restart: always + environment: + # .env 파일에서 변수를 읽어와 보안을 유지 + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + + TZ: Asia/Seoul + # 한글 및 이모지 지원을 위한 설정 + command: > + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + ports: + - "127.0.0.1:3306:3306" + # 컨테이너가 삭제되어도 데이터가 보존되도록 볼륨을 설정 + volumes: + - mysql_data:/var/lib/mysql + networks: + - pace-network + + # Spring Boot + pace-app: + # Docker Hub의 Dockerfile을 기반으로 이미지를 빌드 + image: shootingstar1020/pace-project:latest + container_name: pace-app + restart: always + # DB 컨테이너가 먼저 실행된 후 애플리케이션이 실행되도록 순서를 보장 + depends_on: + - pace-mysql + - pace-redis + environment: + # DB 접속 주소에 서비스 이름(pace-db)을 사용하여 내부 통신을 수행 + SPRING_DATASOURCE_URL: jdbc:mysql://pace-mysql:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} + SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} + REDIS_HOST: pace-redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + SPRING_PROFILES_ACTIVE: prod + JWT_SECRET: ${JWT_SECRET} + KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID} + KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI} + GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY} + SEOUL_SUBWAY_API_KEY: ${SEOUL_SUBWAY_API_KEY} + + networks: + - pace-network + +# 서비스들이 서로 소통할 수 있는 가상 네트워크 정의 +networks: + pace-network: + driver: bridge + +# 데이터 보존을 위한 명명된 볼륨 정의 +volumes: + mysql_data: + redis_data: \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew index adff685..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,6 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -171,6 +172,7 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -210,6 +212,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index c4bdd3a..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,10 +70,11 @@ goto fail :execute @rem Setup the command line +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..b8f4fdf --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,78 @@ +server { + listen 80; + server_name pace-server.kro.kr; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location = /health { + proxy_pass http://pace-app:8080/health; + access_log off; + } + + location / { + # host: 사용자가 접속한 도메인 + # request_uri: 도메인 뒤의 주소 + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + + # 인증서 파일 경로(Certbot이 만들어준 파일들) + ssl_certificate /etc/letsencrypt/live/pace-server.kro.kr/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/pace-server.kro.kr/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + server_name pace-server.kro.kr; + # 내 사이트를 다른 사이트의