Skip to content

Commit 3262b6f

Browse files
soonge2yunabyteYuna
authored
Release: v1.0.0 기능 배포 (#72)
* [Feat] 소셜 로그인 및 토큰 재발급 API 개발 (#24) * feat: DB 연결 및 Spring Security 기본 설정 완료 * feat: User 도메인 추가 (#16) * feat: Bast Entity 추가 (#16) * feat: 카카오 OAuth 로그인 기능 구현 (#16) - 인가 코드로 액세스 토큰 요청 - 카카오 사용자 정보 조회 후 회원가입 또는 로그인 - JWT 액세스/리프레시 토큰 발급 - 리프레시 토큰은 HttpOnly 쿠키에 저장 - 전략 패턴 기반 구조 설계 (OAuthLoginStrategy) * feat: 리프레시 토큰 저장 로직 구현 (#16) * feat: 토큰 재발급 api 구현 (#16) * feat: 리프레시 토큰 만료 시 DB에서 삭제하는 코드 추가 (#16) * feat: 리프레시 토큰 UUID 방식으로 변경 (#16) * refactor: 서비스 클래스 이름 통일 (JwtTokenProvider → JwtTokenService) (#16) * feat: 커스텀 예외 처리 도입 및 분기 적용 - 소셜 로그인, 토큰 재발급 (#16) - AuthException, UserException 등 도메인별 BaseException 상속 구조 설계 - AuthErrorCode, UserErrorCode enum 정의 및 상태 코드 분리 - JwtTokenService, RefreshTokenService, UserValidator 등에서 커스텀 예외로 전환 - 로그인, 토큰 재발급 등에서 NO_TOKEN, INVALID_TOKEN, USER_WITHDRAWN 등 세분화된 예외 분기 처리 * refactor: createAccessToken → issueAccessToken으로 이름 통일 및 만료 시간 상수화 (#16) * refactor: 리프레시 토큰 발급 시 기존 리프레시 토큰 삭제 후 새 토큰 발급 로직 추가 (#16) * [Chore] dev 배포 환경 준비 및 테스트용 기능 추가 (#26) * feat: 프론트엔드 연동 테스트용 GET /api/v1/test/ping API 구현 (#22) * feat: AI 서버 연동 테스트용 POST /api/v1/test/ai-votes API 추가 (#22) * feat: AI 서버 요청용 서비스 로직 구현 (#22) - POST /api/v1/moderation 요청을 위한 RestTemplate 기반 클라이언트 작성 - 요청 실패 시 예외 처리 포함 - ModerationRequest/Response DTO 정의 * test: Mock AI 컨트롤러 및 테스트 환경 구성 (#22) - POST /mock-ai/api/v1/moderation 엔드포인트 추가 - CSRF 비활성화된 테스트 전용 SecurityConfig 구성 - MockMvc 테스트 작성 (정상 요청/잘못된 요청) - @activeprofiles("test") 설정 적용 * chore: 환경별 application 설정 파일 분리 (local, dev, prod) (#22) * chore: 환경별 SecurityConfig 파일 분리 (local, dev, prod) (#22) * chore: 환경별 설정 파일 분리 및 환경변수 적용으로 설정 관리 개선 (#22) * feat: dev 환경에 프론트엔드 도메인 기반 CORS 설정 추가 (#22) * chore: AI 콜백 api 주소를 실제 주소로 변경하고 ai 도메인 하위로 이동 (#22) * [Feat] 프론트-백-AI 서버 간 연동 테스트를 위한 엔드포인트 추가 (#27) * feat: 프론트엔드 연동 테스트용 GET /api/v1/test/ping API 구현 (#22) * feat: AI 서버 연동 테스트용 POST /api/v1/test/ai-votes API 추가 (#22) * feat: AI 서버 요청용 서비스 로직 구현 (#22) - POST /api/v1/moderation 요청을 위한 RestTemplate 기반 클라이언트 작성 - 요청 실패 시 예외 처리 포함 - ModerationRequest/Response DTO 정의 * test: Mock AI 컨트롤러 및 테스트 환경 구성 (#22) - POST /mock-ai/api/v1/moderation 엔드포인트 추가 - CSRF 비활성화된 테스트 전용 SecurityConfig 구성 - MockMvc 테스트 작성 (정상 요청/잘못된 요청) - @activeprofiles("test") 설정 적용 * chore: 환경별 application 설정 파일 분리 (local, dev, prod) (#22) * chore: 환경별 SecurityConfig 파일 분리 (local, dev, prod) (#22) * chore: 환경별 설정 파일 분리 및 환경변수 적용으로 설정 관리 개선 (#22) * feat: dev 환경에 프론트엔드 도메인 기반 CORS 설정 추가 (#22) * chore: AI 콜백 api 주소를 실제 주소로 변경하고 ai 도메인 하위로 이동 (#22) * feat: AI 서버 연동 테스트용 엔드포인트 추가 (/test/ping-ai) (#22) * chore: MockAiControllerTest 삭제 및 local 프로필 추가 * chore: .gitignore에 .env 파일 및 로컬 실행 스크립트(.sh) 추가 * [Feat] JWT 인증 필터 구현 (#39) * [Feat] 로그아웃 API 구현 및 인증 실패 시 401 응답 처리 (#40) * feat: 리프레시 토큰 삭제 기반 로그아웃 API 구현 (#28) - JwtAuthenticationFilter: 요청 헤더에서 액세스 토큰 추출 및 인증 처리 - JwtTokenService: validateAccessToken → validateAndExtractUserId 통합 - SecurityConfig: 인증 필터 등록 및 세션 관리 정책 설정 - 로그아웃 API: 리프레시 토큰 삭제 처리, ALREADY_LOGGED_OUT 응답 분기 * fix: JWT 인증 실패 시 403 → 401 응답으로 수정 (#28) - JwtAuthenticationFilter에서 AuthException을 AuthenticationException으로 wrapping하여 Spring Security 흐름에 맞게 예외 전달 - CustomAuthenticationEntryPoint를 통해 401 응답 및 커스텀 메시지(JSON) 반환 - TOKEN_EXPIRED, INVALID_TOKEN 등의 JWT 오류를 JSON 응답으로 구분하여 전달 가능 - 기존 403 응답 문제 해결 (Http403ForbiddenEntryPoint → CustomAuthenticationEntryPoint로 정상 위임됨) * [Feat] 투표 도메인 엔티티 및 리포지토리 생성 (#41) * feat: Vote 엔티티 생성 (#31) * feat: VoteResponse 엔티티 생성 (#31) * feat: VoteResult 엔티티 생성 (#31) * feat: VoteModerationLog 엔티티 생성 (#31) * feat: VoteModerationLog 리포지토리 생성 (#31) * feat: Vote 리포지토리 생성 (#31) * feat: VoteResponse 리포지토리 생성 (#31) * feat: VoteResult 리포지토리 생성 (#31) * fix(auth): TokenRepository의 PK 타입 Long → String으로 수정 (#31) * feat: Vote 연관관계 설정을 위한 Group 엔티티, 리포지토리 생성 (#31) * fix(user): User 엔티티에서 'user' 백틱(`) 처리 (#31) * [Feat] 투표 등록 API 개발 (#42) * feat: 투표 등록 RequestDto, ResponseDto 생성 (#32) * feat: 투표 등록 API 컨트롤러 구현 및 서비스 골격 구현 (#32) * refactor: DTO 클래스명에서 'Dto' 접미사 제거 (#32) * feat: 투표 등록 서비스 구현 (#32) * feat: 사용자 투표 생성 팩토리 메서드 도입 (#32) * feat: 투표 등록 서비스에 유저 유효성 검사 추가 (`AuthUserValidator`) (#32) * feat: 투표 등록 시 그룹 멤버십 확인 기능 추가 (#32) * chore: 초기 시스템 유저 및 공개 그룹 데이터 삽입용 SQL 스크립트 추가 (#32) * feat: 초기 데이터(시스템 유저 및 공개 그룹) 존재 여부 검증 로직 추가 (#32) * feat: 공개 그룹인 경우 투표 등록 시 멤버 확인 생략 로직 추가 (#32) * feat: 투표 등록 커스텀 예외 처리 적용 (#32) * refactor: 도메인 공통 에러코드 인터페이스(BaseErrorCode) 도입 (#32) * [Feat] 투표 내용 조회 API 개발 (#43) * feat(global): 전역 예외 코드(FORBIDDEN, UNEXPECTED_ERROR) 및 GlobalException 정의 (#33) * feat: 투표 내용 조회 커스텀 예외 코드 추가 (#33) * feat: 투표 내용 조회 컨트롤러, 서비스, DTO 구현 (200 확인) (#33) * feat: 투표 참여 여부 검증 로직 추가 (#33) * [Feat] 투표 참여 API 개발 (#45) * feat: 투표 참여 API 컨트롤러 및 DTO 작성 (#34) * feat: 투표 참여 커스텀 예외 추가 (#34) - INVALID_OPTION, ALREADY_VOTED * feat: 정적 팩토리 메서드 (`create`) 추가 (#34) * feat: 투표 참여 서비스 로직 구현 (#34) * feat: 투표 상태(OPEN) 체크 로직 추가 (#34) * [Feat] 투표 결과 조회 API 개발 (#46) * feat: 투표 결과 응답 DTO 작성 (#35) * feat: 투표 결과 조회 컨트롤러 및 서비스 틀 작성 (#35) * feat: 그룹 조회 권한 검사 함수를 투표 상세 조회 권한 검사 함수로 변경 (공개그룹 권한 제거) (#35) * feat: 투표 결과 조회 서비스 로직 구현 (#35) * feat: 투표 상태 검사 추가 (#35) * fix: totalCount에서 기권 응답 제외, results에 모든 항목 포함 (#35) * feat: 기권으로 응답한 참여자는 조회 권한에서 제외 (#35) * [Feat] Dev 환경에 Jwt 인증 및 예외 처리 필터 적용 (#48) * feat: Jwt 인증 필터 및 커스텀 엔트리 포인트 추가 (#47) * feat: 디버그 모드 추가 (#47) * [CICD] Dev 서버용 배포 파이프라인 생성 (#49) Co-authored-by: Yuna <yuna@Yunaui-MacBookPro.local> * [Feat] 진행 중인 투표 목록 조회 API 개발 (#50) * feat: 진행 중인 카드 목록 조회 API 응답 DTO 생성 (#36) * refactor: 투표 응답 DTO를 기능별로 list/와 result/ 하위 패키지로 분리 (#36) * feat: 커스텀 에러 코드 추가 (#36) * feat: 진행 중 투표 목록용 커서 파서 추가(#36) * feat: QueryDSL 환경 설정 및 Q 클래스 생성 설정 추가 (#36) - querydsl-core, querydsl-jpa(jakarta) 의존성 추가 - annotationProcessor 설정을 통한 Q 클래스 자동 생성 설정 - generated/querydsl 디렉토리 설정 및 sourceSets 등록 - JPAQueryFactory 빈 등록을 위한 QuerydslConfig 생성 - QueryDSL 관련 캐시 및 빌드 이슈 해결 * refactor: config 패키지 구조를 기능별로 분리 (security, querydsl, jpa) (#36) * feat: 진행 중인 투표 목록 조회용 QueryDSL 리포지토리 구현 (#36) - 사용자가 접근 가능한 그룹(공개 그룹 + 참여 그룹)의 진행 중인 투표만 조회 - 커서 기반 페이지네이션 조건 (closedAt, createdAt) - 사용자 참여 여부 필터링 (응답하지 않은 투표만) * chore: Hibernate SQL 디버깅을 위한 trace 로그 레벨 설정 (#36) - org.hibernate.type.descriptor.sql 로그 레벨을 trace로 설정하여 바인딩 값 확인 가능 * feat: 진행 중인 투표 목록 조회 서비스 로직 구현 (#36) - getActiveVotes(): 비로그인/로그인 분기 처리 및 커서 기반 페이지네이션 지원 - getAccessibleGroups(): 공개 그룹 + 사용자 소속 그룹 권한 기반 접근 목록 반환 - VoteListResponse 구성 및 hasNext, nextCursor 처리 포함 * feat: 진행 중인 투표 목록 조회 API 컨트롤러 구현 및 비로그인 접근 허용 설정 (#36) * feat: 미래 시간 커서 사용 시 성공 응답 처리 및 INVALID_CURSOR 예외 삭제(#36) * [Feat] 내가 만든 투표 목록 조회 API 개발 (#57) * feat: 내가 만든 투표 목록용 커서 파서 추가 (#37) * refactor: 투표 리스트 응답 DTO를 기능별 패키지로 분리 (active, mine, participated) (#37) * feat: 내가 만든 투표 목록 조회 API 응답 DTO 생성 (#37) * feat: 내가 만든 투표 목록 조회용 QueryDSL 리포지토리 구현 (#37) * refactor: 공개 그룹 조회 로직 GroupService로 분리 (#37) * feat: VoteOptionResultWithId DTO 추가 (voteId 포함) (#37) * feat: 내가 만든 투표 목록 조회 서비스 로직 구현 (#37) - getMyVotes 서비스 구현 - VoteResultService.getResultsWithVoteId() 메서드 추가 및 사용 - 투표에 대해 결과 집계 포함한 응답 구성 * feat: 내가 만든 투표 목록 조회 API 컨트롤러 구현 (#37) * refactor: VoteResultService를 사용해 getVoteResult 결과 집계 로직 분리 (#37) * feat: 커서에 포함된 voteId 유효성 검증 추가 (#37) - MyVote 목록 조회 시 전달된 커서의 voteId가 실제로 존재하는지 확인 - 존재하지 않으면 404(VOTE_NOT_FOUND) 예외 발생시킴 - 불필요한 쿼리 실행 방지 및 에러 원인 명확화 * [Feat] 내가 참여한 투표 목록 조회 API 개발 (#58) * feat: 내가 참여한 투표 목록용 커서 파서 추가 (#38) * feat: 내가 참여한 투표 목록 조회 API 응답 DTO 생성 (#38) * feat: 내가 참여한 투표 목록 조회용 QueryDSL 리포지토리 구현 (#38) * feat: 내가 참여한 투표 목록 조회 서비스 로직 구현 (#38) * feat: 내가 참여한 투표 목록 조회 API 컨트롤러 구현 (#38) * fix: 참여한 투표 목록 조회 시 nextCursor 생성 오류 수정 (#38) - nextCursor 생성 시 잘못된 필드(createdAt)를 사용하던 문제 수정 - closedAt 기준 커서 정렬 원칙에 맞게 closedAt으로 nextCursor 생성하도록 수정 * [Feat] 가입한 그룹 라벨 조회 API 개발 (#59) * feat: 가입한 그룹 이름/목록 조회용 커서 파서 추가 (#56) * feat: 가입한 그룹 이름 조회 API 응답 DTO 구현 (#56) * feat: 가입한 그룹 이름 조회용 QueryDSL 리포지토리 구현 (#56) * feat: 가입한 그룹 이름 조회 서비스 로직 구현 (#56) * feat: 가입한 그룹 이름 조회 API 컨트롤러 구현 (#56) * [Feat] 가입한 그룹 목록 조회 API 개발 (#60) * feat: 가입한 그룹 목록 조회 API 응답 DTO 구현 (#55) * feat: 가입한 그룹 목록 조회용 QueryDSL 리포지토리 구현 (#55) * feat: 가입한 그룹 목록 조회 서비스 구현 (#55) * feat: 가입한 그룹 목록 조회 API 컨트롤러 구현 (#55) * [Feat] 그룹 가입 API 개발 (#61) * feat: 그룹 가입 커스텀 예외 추가 (#51) * feat: 초대 코드 유효성 검사 메서드 추가 (#51) * feat: 그룹 가입 요청 및 응답 DTO 구현 (#51) * feat: 그룹 가입 리포지토리 구현 (#51) * feat: 그룹 가입 서비스 구현 (#51) * feat: 그룹 가입 컨트롤러 구현 (#51) * feat: 공개 그룹 가입 불가 로직 추가 (#51) * fix: soft-deleted 멤버 조회 로직 개선 및 투표 권한 검증 수정 (#51) - findByGroupAndUserIncludingDeleted 메서드를 native query로 수정하여 @where(clause = "deleted_at IS NULL") 무시 - voteService 내부 그룹 권한 검증 로직에서 soft delete 멤버도 조회할 수 있도록 수정 * feat: 그룹 탈퇴 이력이 있는 유저는 기존 데이터 복구 방식으로 재가입 처리 (#51) - 소프트 딜리트된 GroupMember가 존재하면 복구하여 재가입 처리 - 중복 삽입 방지를 위해 기존 가입 여부 먼저 확인 * docs: Group 조회 시 @where에 대한 주석 추가 (#51) * [Feat] 새 그룹 생성 API 개발 (#62) * feat: 새그룹 생성 요청 및 응답 DTO 구현 (#52) * feat: 그룹 생성 입력값 유효성 검증 로직 및 커스텀 예외 추가 (#52) * fix: 그룹/투표 이미지 URL에 도메인 prefix 검증 로직 추가 (#52) - VoteValidator.validateUrl() 로직을 도메인 기반으로 변경 - application.yml에 file.upload-url-prefix 추가 * feat: 새그룹 생성 서비스 로직 구현 (#52) * feat: 초대 코드 생성 로직 추가 (#52) * feat: 새그룹 생성 컨트롤러 구현 (#52) * fix: 투표/그룹 이미지 URL prefix를 application 설정 대신 하드코딩으로 변경 (#52) * feat: 그룹 생성 시 이름 중복 방지 로직 추가 (#52) * [Feat] 회원정보 조회/수정 API 개발 (#63) * feat: 회원정보 조회 응답 DTO 구현 (#53) * feat: 회원정보 조회 서비스 구현 (#53) * feat: 회원정보 조회 컨트롤러 구현 (#53) * feat: 회원정보 수정 요청 및 응답 DTO 구현 (#54) * feat: 닉네임 유효성 검사 로직 구현 (#54) * feat: 회원정보 수정 서비스 구현 (#54) * fix: 닉네임이 기존과 동일한 경우 중복 검사 없이 바로 반환 (#54) * feat: 회원정보 수정 컨트롤러 구현 (#54) * [Feat] 회원 탈퇴 API 개발 (#64) * feat: 회원 탈퇴 서비스 로직 구현 및 관련 삭제 메서드 추가 (#29) * feat: 회원 탈퇴 컨트롤러 구현 (#29) * feat: 카카오 연동 해제 로직 추가 (#29) - 카카오 연동 해제 로직 추가 (OAuth 전략 패턴 내 구현) - RestTemplate Bean 전역 설정 (config 패키지 이동) - OAuth 연동 ID 기반 unlink 요청 및 응답 로깅 처리 - 관련 커스텀 예외 코드 추가 - 관련 리포지토리 코드 추가 * feat: 회원 탈퇴 시 그룹 소유자 승계 로직 추가 (#29) * fix: 회원 탈퇴 시 처리 순서 조정 및 유저 상태 변경 시점 수정 (#29) - 그룹 소유자 승계 후 그룹 멤버 삭제로 순서 변경 (유효한 멤버 기반 승계 보장) - 카카오 연동 해제, 토큰/OAuth 삭제 이후에 회원 상태 변경 수행 - user.withdraw()를 가장 마지막에 호출하여 도메인 정합성 유지 * fix: 회원 탈퇴 시 그룹 멤버 삭제 방식을 soft delete에서 hard delete로 수정 (#29) * [CICD] 환경변수 추가 주입 과정 추가 * [Feat] 랜덤 닉네임 생성 기능 구현 (#65) * [Chore] Dev CORS에 localhost 추가 * [Fix] 투표 결과 비율 계산에 double 타입 적용 * [Feat] 카카오 로그인 시 동적 redirect URI 허용 및 검증 추가 (#67) * [Refactor] OAuth ID 타입 Long → String으로 변경 * [Feat] 투표 등록 시 익명 기능 추가 (#70) * feat: 투표 등록 시 익명 정보 저장 (#69) * feat: 투표 조회 시 익명 투표일 경우 닉네임 대신 '익명' 반환 (#69) * [Feat] AI 기반 투표 내용 검열 기능 추가 (#71) * feat: 투표 등록 시 AI 서버로 검열 요청하는 로직 추가 (#68) * feat: feat: AI 검열 결과 수신 및 상태 반영 서비스/컨트롤러 구현 (#68) * [Fix] USER_NOT_FOUND 에러 상태 코드 변경 (404 → 401) * [Fix] 진행 중인 투표 조회 시 PENDING/REJECTED 필터링 추가 * [Chore] prod 환경의 JPA ddl-auto 옵션을 none → update로 변경 --------- Co-authored-by: yunabyte <yunabyte@gmail.com> Co-authored-by: Yuna <yuna@Yunaui-MacBookPro.local>
1 parent 05b09b3 commit 3262b6f

File tree

121 files changed

+4414
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+4414
-10
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: SpringBoot BigBang CI/CD - Dev V1
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
8+
jobs:
9+
build-and-deploy:
10+
runs-on: ubuntu-latest
11+
environment: dev
12+
13+
steps:
14+
- name: 🐵 Checkout Repository
15+
uses: actions/checkout@v3
16+
17+
- name: 🐵 Set up JDK 21
18+
uses: actions/setup-java@v3
19+
with:
20+
distribution: 'temurin'
21+
java-version: '21'
22+
23+
- name: 🐵 Grant execute permission to Gradle
24+
run: chmod +x gradlew
25+
26+
- name: 🐵 Build with Gradle (Without Tests)
27+
run: |
28+
echo "🐵 Building JAR without tests..."
29+
./gradlew clean build -x test || { echo '🚨 Build failed!'; exit 1; }
30+
echo "✅ Build completed successfully!"
31+
32+
- name: 🐵 Save SSH Key
33+
run: |
34+
echo "🐵 Saving SSH key..."
35+
echo "${{ secrets.GCP_CICD_SSH_KEY }}" > key.pem
36+
chmod 600 key.pem
37+
echo "✅ SSH key saved!"
38+
39+
- name: 🐵 Deploy to GCP VM
40+
run: |
41+
echo "🐵 Sending JAR to GCP..."
42+
43+
scp -i key.pem -o StrictHostKeyChecking=no \
44+
build/libs/moa-server-0.0.1-SNAPSHOT.jar \
45+
cicd@${{ secrets.GCP_BE_DEV_HOST }}:/home/cicd/moa-server-0.0.1-SNAPSHOT.jar || {
46+
echo "🚨 Failed to upload JAR!"; exit 1;
47+
}
48+
echo "✅ JAR uploaded successfully!"
49+
50+
echo "🐵 Writing .env on remote server..."
51+
ssh -i key.pem -o StrictHostKeyChecking=no cicd@${{ secrets.GCP_BE_DEV_HOST }} << 'EOF'
52+
cat <<EENV | sudo tee /home/cicd/moa-backend.env > /dev/null
53+
SPRING_PROFILES_ACTIVE=${{ secrets.SPRING_PROFILE }}
54+
SPRING_DATASOURCE_URL=${{ secrets.DB_URL }}
55+
SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }}
56+
SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}
57+
JWT_SECRET=${{ secrets.JWT_SECRET }}
58+
FRONTEND_URL=${{ secrets.FRONTEND_URL }}
59+
AI_SERVER_URL=${{ secrets.AI_SERVER_URL }}
60+
KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}
61+
KAKAO_ADMIN_KEY=${{ secrets.KAKAO_ADMIN_KEY }}
62+
EENV
63+
sudo chmod 600 /home/cicd/moa-backend.env
64+
sudo chown cicd:cicd /home/cicd/moa-backend.env
65+
echo "✅ .env file written and secured!"
66+
EOF
67+
68+
- name: 🐵 Restart systemd Service
69+
run: |
70+
echo "🐵 Restarting Spring Boot systemd service..."
71+
ssh -i key.pem -o StrictHostKeyChecking=no cicd@${{ secrets.GCP_BE_DEV_HOST }} << 'EOF'
72+
echo "🐵 Running daemon-reload and restarting moa-backend..."
73+
sudo systemctl daemon-reload || { echo "🚨 daemon-reload failed!"; exit 1; }
74+
sudo systemctl restart moa-backend || { echo "🚨 Failed to restart service!"; exit 1; }
75+
76+
sleep 7
77+
echo "✅ Service restarted successfully!"
78+
79+
echo "🐵 Checking process..."
80+
if ! sudo pgrep -f 'java -jar' > /dev/null; then
81+
echo "🚨 Spring Boot process not running!"
82+
exit 1
83+
fi
84+
85+
echo "✅ Spring Boot process is running!"
86+
87+
echo "🐵 Tail last 20 lines of server.log:"
88+
sudo tail -n 20 /home/cicd/server.log || echo "🚨 Failed to read server.log"
89+
EOF

.gitignore

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ out/
2626
!**/src/main/**/out/
2727
!**/src/test/**/out/
2828

29-
### NetBeans ###
30-
/nbproject/private/
31-
/nbbuild/
32-
/dist/
33-
/nbdist/
34-
/.nb-gradle/
35-
36-
### VS Code ###
37-
.vscode/
29+
*.env.*
30+
build-local.sh
31+
run-local.sh

build.gradle

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,51 @@ dependencies {
2727
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2828
implementation 'org.springframework.boot:spring-boot-starter-security'
2929
implementation 'org.springframework.boot:spring-boot-starter-web'
30-
implementation 'com.mysql:mysql-connector-j:8.0.33'
30+
implementation 'com.mysql:mysql-connector-j'
31+
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
32+
implementation 'org.apache.commons:commons-lang3:3.12.0'
33+
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
34+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
3135
compileOnly 'org.projectlombok:lombok'
3236
developmentOnly 'org.springframework.boot:spring-boot-devtools'
3337
runtimeOnly 'com.mysql:mysql-connector-j'
3438
annotationProcessor 'org.projectlombok:lombok'
3539
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3640
testImplementation 'org.springframework.security:spring-security-test'
3741
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
42+
43+
// querydsl 의존성
44+
implementation "com.querydsl:querydsl-core:5.1.0"
45+
implementation "com.querydsl:querydsl-jpa:5.1.0:jakarta"
46+
47+
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
48+
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
49+
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
50+
51+
// testImplementation 'org.springframework.boot:spring-boot-starter-test'
52+
// testImplementation 'jakarta.persistence:jakarta.persistence-api'
53+
// testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
3854
}
3955

4056
tasks.named('test') {
4157
useJUnitPlatform()
4258
}
59+
60+
// QueryDSL Q파일 생성용
61+
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
62+
63+
sourceSets {
64+
main {
65+
java {
66+
srcDir querydslDir
67+
}
68+
}
69+
}
70+
71+
tasks.withType(JavaCompile).configureEach {
72+
options.generatedSourceOutputDirectory.set(querydslDir)
73+
}
74+
75+
compileJava {
76+
options.annotationProcessorPath = configurations.annotationProcessor
77+
}

db/init-data.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- 시스템 유저
2+
INSERT INTO `user` (id, nickname, email, role, user_status, last_active_at, created_at, updated_at)
3+
SELECT 1, 'SYSTEM', 'system@moa.com', 'ADMIN', 'ACTIVE', NOW(), NOW(), NOW()
4+
WHERE NOT EXISTS (
5+
SELECT 1 FROM `user` WHERE id = 1
6+
);
7+
8+
-- 공개 그룹
9+
INSERT INTO `group` (id, user_id, name, description, invite_code, created_at, updated_at)
10+
SELECT 1, 1, '공개', '모든 사용자가 속한 공개 투표 그룹입니다.', 'public', NOW(), NOW()
11+
WHERE NOT EXISTS (
12+
SELECT 1 FROM `group` WHERE id = 1
13+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.moa.moa_server.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.client.RestTemplate;
6+
7+
@Configuration
8+
public class RestTemplateConfig {
9+
10+
@Bean
11+
public RestTemplate restTemplate() {
12+
return new RestTemplate();
13+
}
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.moa.moa_server.config.jpa;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
5+
6+
@Configuration
7+
@EnableJpaAuditing
8+
public class JpaAuditingConfig {
9+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.moa.moa_server.config.querydsl;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import jakarta.persistence.EntityManager;
5+
import jakarta.persistence.PersistenceContext;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class QuerydslConfig {
11+
12+
@PersistenceContext
13+
private EntityManager em;
14+
15+
@Bean
16+
public JPAQueryFactory jpaQueryFactory() {
17+
return new JPAQueryFactory(em);
18+
}
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.moa.moa_server.config.security;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.moa.moa_server.domain.global.dto.ApiResponse;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.security.core.AuthenticationException;
9+
import org.springframework.security.web.AuthenticationEntryPoint;
10+
import org.springframework.stereotype.Component;
11+
12+
import java.io.IOException;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
17+
18+
private final ObjectMapper objectMapper;
19+
20+
@Override
21+
public void commence(HttpServletRequest request,
22+
HttpServletResponse response,
23+
AuthenticationException authException) throws IOException {
24+
25+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
26+
response.setContentType("application/json");
27+
response.setCharacterEncoding("UTF-8");
28+
29+
ApiResponse apiResponse = new ApiResponse("INVALID_TOKEN", null);
30+
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
31+
}
32+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.moa.moa_server.config.security;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.Profile;
8+
import org.springframework.http.HttpMethod;
9+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
10+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
11+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
12+
import org.springframework.security.config.http.SessionCreationPolicy;
13+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
14+
import org.springframework.security.crypto.password.PasswordEncoder;
15+
import org.springframework.security.web.SecurityFilterChain;
16+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
17+
import org.springframework.web.cors.CorsConfiguration;
18+
import org.springframework.web.cors.CorsConfigurationSource;
19+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
20+
21+
import java.util.List;
22+
23+
import static com.moa.moa_server.config.security.SecurityConstants.ALLOWED_URLS;
24+
25+
@RequiredArgsConstructor
26+
@EnableWebSecurity
27+
@Configuration
28+
@Profile("dev")
29+
public class DevSecurityConfig {
30+
31+
@Value("${frontend.url}")
32+
private String frontendUrl;
33+
34+
private final JwtAuthenticationFilter jwtAuthenticationFilter;
35+
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
36+
37+
@Bean
38+
public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception {
39+
http
40+
.csrf(AbstractHttpConfigurer::disable)
41+
.cors(cors -> cors.configurationSource(corsConfigurationSource))
42+
.httpBasic(AbstractHttpConfigurer::disable)
43+
.formLogin(AbstractHttpConfigurer::disable)
44+
.exceptionHandling(ex -> ex.authenticationEntryPoint(customAuthenticationEntryPoint))
45+
.authorizeHttpRequests(auth -> auth
46+
.requestMatchers(HttpMethod.GET, "/api/v1/votes").permitAll()
47+
.requestMatchers(ALLOWED_URLS).permitAll()
48+
.anyRequest().authenticated()
49+
)
50+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
51+
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
52+
53+
return http.build();
54+
}
55+
56+
/**
57+
* CORS 정책을 설정하고, 이를 Spring Security에 등록하는 Bean을 반환
58+
*/
59+
@Bean
60+
public CorsConfigurationSource corsConfigurationSource() {
61+
CorsConfiguration config = new CorsConfiguration();
62+
63+
config.setAllowedOriginPatterns(List.of(frontendUrl, "http://localhost:5173")); // 요청을 허용할 출처(origin) 패턴을 설정
64+
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); // 허용할 HTTP 메서드 목록 지정
65+
config.setAllowedHeaders(List.of("*")); // 모든 요청 헤더 허용
66+
config.setAllowCredentials(true); // 인증 정보를 포함한 요청(Cookie 등)을 허용
67+
68+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 경로 별로 다른 CORS 설정을 적용할 수 있도록 지원하는 구현체
69+
source.registerCorsConfiguration("/**", config); // 모든 경로에 위에서 설정한 CORS 정책을 적용
70+
return source;
71+
}
72+
73+
@Bean
74+
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
75+
76+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.moa.moa_server.config.security;
2+
3+
import com.moa.moa_server.domain.auth.handler.AuthException;
4+
import com.moa.moa_server.domain.auth.service.JwtTokenService;
5+
import jakarta.servlet.FilterChain;
6+
import jakarta.servlet.ServletException;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
11+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
12+
import org.springframework.security.core.context.SecurityContextHolder;
13+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
14+
import org.springframework.stereotype.Component;
15+
import org.springframework.web.filter.OncePerRequestFilter;
16+
17+
import java.io.IOException;
18+
import java.util.Optional;
19+
20+
@RequiredArgsConstructor
21+
@Component
22+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
23+
24+
private final JwtTokenService jwtTokenService; // 토큰 유효성 검증 및 userId 추출
25+
26+
@Override
27+
protected void doFilterInternal(HttpServletRequest request,
28+
HttpServletResponse response,
29+
FilterChain filterChain)
30+
throws ServletException, IOException {
31+
32+
try {
33+
// 헤더에서 토큰 추출
34+
Optional<String> tokenOptional = extractToken(request);
35+
36+
if (tokenOptional.isPresent()) {
37+
// 토큰 검증 및 userId 추출
38+
String token = tokenOptional.get();
39+
Long userId = jwtTokenService.validateAndExtractUserId(token);
40+
41+
// 인증 객체 생성
42+
UsernamePasswordAuthenticationToken authentication =
43+
new UsernamePasswordAuthenticationToken(userId, null, null);
44+
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
45+
46+
// SecurityContext에 저장
47+
SecurityContextHolder.getContext().setAuthentication(authentication);
48+
}
49+
50+
filterChain.doFilter(request, response);
51+
} catch (AuthException e) {
52+
// AuthException을 AuthenticationException으로 감싸서 Spring Security로 위임
53+
throw new AuthenticationCredentialsNotFoundException(e.getCode(), e);
54+
}
55+
}
56+
57+
private Optional<String> extractToken(HttpServletRequest request) {
58+
String authHeader = request.getHeader("Authorization");
59+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
60+
return Optional.of(authHeader.substring(7));
61+
}
62+
return Optional.empty();
63+
}
64+
}

0 commit comments

Comments
 (0)