-
Notifications
You must be signed in to change notification settings - Fork 0
EC2, RDS 마이그레이션 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
EC2, RDS 마이그레이션 #19
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| name: Partition CI/CD | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main, develop ] | ||
|
|
||
| jobs: | ||
| build-and-push: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up JDK 21 | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '21' | ||
| distribution: 'temurin' | ||
|
|
||
| - name: Grant execute permission for gradlew | ||
| run: chmod +x gradlew | ||
|
|
||
| - name: Build jar | ||
| run: ./gradlew clean build -x 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@v6 | ||
| with: | ||
| context: . | ||
| push: true | ||
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/partition-spring:latest | ||
|
Comment on lines
+33
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Docker 이미지에 버전 태그 추가 필요
♻️ 버전 태그 추가 제안 - name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/partition-spring:latest
+ tags: |
+ ${{ secrets.DOCKERHUB_USERNAME }}/partition-spring:latest
+ ${{ secrets.DOCKERHUB_USERNAME }}/partition-spring:${{ github.sha }}🤖 Prompt for AI Agents |
||
|
|
||
| deploy: | ||
| needs: build-and-push | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Deploy to EC2 via SSH | ||
| uses: appleboy/ssh-action@v1.1.0 | ||
| with: | ||
| host: ${{ secrets.EC2_HOST }} | ||
| username: ubuntu | ||
| key: ${{ secrets.EC2_PRIVATE_KEY }} | ||
| script: | | ||
| cd /home/ubuntu/partition | ||
|
|
||
| # 지금 푸시된 브랜치 기준으로 체크아웃 (main or develop) | ||
| git fetch origin | ||
| git checkout ${{ github.ref_name }} | ||
| git pull origin ${{ github.ref_name }} | ||
|
|
||
| # 최신 이미지 받아서 컨테이너 재시작 | ||
| sudo docker compose pull | ||
| sudo docker compose up -d | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.partition.domain.auth.exception; | ||
|
|
||
| import com.partition.global.exception.BaseErrorCode; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum AuthErrorCode implements BaseErrorCode { // 인터페이스 구현 | ||
|
|
||
| // 21xx: Auth 관련 | ||
| EMPTY_ACCESS_TOKEN(400, "액세스 토큰이 없습니다."), | ||
| INVALID_ACCESS_TOKEN(401, "유효하지 않은 액세스 토큰입니다."), | ||
| EXPIRED_ACCESS_TOKEN(401, "액세스 토큰이 만료되었습니다."), | ||
| EMPTY_REFRESH_TOKEN(400, "리프레시 토큰이 없습니다."), | ||
| INVALID_REFRESH_TOKEN(401, "유효하지 않은 리프레시 토큰입니다."), | ||
| EXPIRED_REFRESH_TOKEN(401, "리프레시 토큰이 만료되었습니다."), | ||
| KAKAO_LOGIN_FAILED(502, "카카오 로그인에 실패했습니다."); | ||
|
Comment on lines
+11
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: AuthErrorCode가 실제로 사용되는지 확인
# AuthErrorCode 사용처 검색
rg -n "AuthErrorCode\." --type javaRepository: Partition-app/Backend Length of output: 47 🏁 Script executed: #!/bin/bash
# Check if AuthErrorCode class exists and is defined
find . -name "AuthErrorCode.java" -type f
# Search for AuthErrorCode imports in Java files
rg "import.*AuthErrorCode" --type java
# Search for AuthErrorCode references (any form)
rg "AuthErrorCode" --type java
# Look for how CustomException is actually used
rg "CustomException" --type java -A 2 -B 2 | head -50
# Check AuthService for token-related error handling
find . -name "AuthService.java" -type f -exec cat {} \;Repository: Partition-app/Backend Length of output: 12985 🏁 Script executed: #!/bin/bash
# Check JwtTokenProvider to see token validation handling
find . -name "JwtTokenProvider.java" -type f -exec cat {} \;
# Check if there are any exception handlers/controllers that might use AuthErrorCode
rg "UserErrorCode|ScheduleErrorCode" --type java -l | head -20
# Search for token validation in filter/interceptor
find . -name "*Filter.java" -o -name "*Interceptor.java" | xargs cat 2>/dev/null | head -100Repository: Partition-app/Backend Length of output: 6177 정의된 에러 코드가 실제로 사용되지 않고 있습니다. 코드 전체를 검토한 결과, 반면 🤖 Prompt for AI Agents |
||
|
|
||
| private final int status; | ||
| private final String message; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.partition.domain.calender.controller; | ||
|
|
||
| import com.partition.domain.calender.dto.response.CalendarDailyResponse; | ||
| import com.partition.domain.calender.dto.response.CalendarMonthlyResponse; | ||
| import com.partition.domain.calender.service.CalendarService; | ||
| import com.partition.domain.common.dto.response.ApiResponse; | ||
| import com.partition.global.config.security.CustomUserDetails; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/calendars") | ||
| @RequiredArgsConstructor | ||
| public class CalendarController { | ||
|
|
||
| private final CalendarService calendarService; | ||
|
|
||
| @Operation(summary = "월간 캘린더 조회", description = "특정 월의 날짜별 일정(일정, 집안일, 공과금) 개수를 반환합니다.") | ||
| @GetMapping("/monthly") | ||
| public ResponseEntity<ApiResponse<List<CalendarMonthlyResponse>>> getMonthlyCalendar( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam int year, | ||
| @RequestParam int month) { | ||
|
|
||
| Long userId = Long.parseLong(userDetails.getUsername()); | ||
| List<CalendarMonthlyResponse> result = calendarService.getMonthlyCalendar(userId, year, month); | ||
|
|
||
| return ResponseEntity.ok( | ||
| ApiResponse.onSuccess("200", "월간 캘린더 조회 성공", result) | ||
| ); | ||
| } | ||
|
|
||
| @Operation(summary = "일간 상세 조회", description = "특정 날짜의 상세 일정(집안일, 일정) 목록을 반환합니다.") | ||
| @GetMapping("/daily") | ||
| public ResponseEntity<ApiResponse<List<CalendarDailyResponse>>> getDailyCalendar( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam LocalDate date) { // yyyy-MM-dd 형식 자동 매핑 | ||
|
|
||
| Long userId = Long.parseLong(userDetails.getUsername()); | ||
| List<CalendarDailyResponse> result = calendarService.getDailyCalendar(userId, date); | ||
|
|
||
| return ResponseEntity.ok( | ||
| ApiResponse.onSuccess("200", "일간 상세 조회 성공", result) | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.partition.domain.calender.dto.response; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class CalendarDailyResponse { | ||
| private String category; // "CHORE" or "SCHEDULE" | ||
| private Long id; // 각 엔티티의 PK | ||
| private String title; // 제목 (내용) | ||
| private String assigneeName; // 담당자 또는 작성자 이름 | ||
| private Boolean isCompleted; // 완료 여부 (일정은 false) | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.partition.domain.calender.dto.response; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class CalendarMonthlyResponse { | ||
| private LocalDate date; | ||
| private long choreCount; | ||
| private long scheduleCount; | ||
| private long utilityBillsCount; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package com.partition.domain.calender.service; | ||
|
|
||
| import com.partition.domain.calender.dto.response.CalendarDailyResponse; | ||
| import com.partition.domain.calender.dto.response.CalendarMonthlyResponse; | ||
| import com.partition.domain.chore.repository.ChoreRepository; | ||
| import com.partition.domain.schedule.repository.ScheduleRepository; | ||
| import com.partition.domain.user.exception.UserErrorCode; | ||
| import com.partition.domain.user.repository.UserRepository; | ||
| import com.partition.domain.utilitybill.repository.UtilityBillRepository; | ||
| import com.partition.entity.Chore; | ||
| import com.partition.entity.Schedule; | ||
| import com.partition.entity.User; | ||
| import com.partition.entity.UtilityBill; | ||
| import com.partition.global.exception.CustomException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.*; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CalendarService { | ||
|
|
||
| private final UserRepository userRepository; | ||
| private final ScheduleRepository scheduleRepository; | ||
| private final ChoreRepository choreRepository; | ||
| private final UtilityBillRepository utilityBillRepository; | ||
|
|
||
| // 월간 캘린더 조회 | ||
| @Transactional(readOnly = true) | ||
| public List<CalendarMonthlyResponse> getMonthlyCalendar(Long userId, int year, int month) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| if (user.getHouseholdId() == null) return new ArrayList<>(); | ||
| Long householdId = user.getHouseholdId(); | ||
|
|
||
| LocalDate startDate = LocalDate.of(year, month, 1); | ||
| LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth()); | ||
|
|
||
| List<Schedule> schedules = scheduleRepository.findAllByHouseholdIdAndDateRange(householdId, startDate, endDate); | ||
| List<Chore> chores = choreRepository.findAllByHouseholdIdAndDateRange(householdId, startDate, endDate); | ||
| List<UtilityBill> bills = utilityBillRepository.findAllByHouseholdIdAndDueDateBetween(householdId, startDate, endDate); | ||
|
|
||
| Map<LocalDate, MonthlyCounts> countMap = new HashMap<>(); | ||
|
|
||
| for (Schedule s : schedules) countMap.computeIfAbsent(s.getDate(), k -> new MonthlyCounts()).scheduleCount++; | ||
| for (Chore c : chores) countMap.computeIfAbsent(c.getDate(), k -> new MonthlyCounts()).choreCount++; | ||
| for (UtilityBill b : bills) countMap.computeIfAbsent(b.getDueDate(), k -> new MonthlyCounts()).utilityBillsCount++; | ||
|
|
||
| return countMap.entrySet().stream() | ||
| .sorted(Map.Entry.comparingByKey()) | ||
| .map(entry -> CalendarMonthlyResponse.builder() | ||
| .date(entry.getKey()) | ||
| .scheduleCount(entry.getValue().scheduleCount) | ||
| .choreCount(entry.getValue().choreCount) | ||
| .utilityBillsCount(entry.getValue().utilityBillsCount) | ||
| .build()) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // 일간 상세 조회 (집안일 + 일정 통합 리스트) | ||
| @Transactional(readOnly = true) | ||
| public List<CalendarDailyResponse> getDailyCalendar(Long userId, LocalDate date) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| if (user.getHouseholdId() == null) { | ||
| return new ArrayList<>(); | ||
| } | ||
| Long householdId = user.getHouseholdId(); | ||
|
|
||
| // 해당 날짜의 집안일(Chore) 조회 -> DTO 변환 | ||
| List<CalendarDailyResponse> chores = choreRepository.findAllByHouseholdIdAndDateRange(householdId, date, date) | ||
| .stream() | ||
| .map(chore -> CalendarDailyResponse.builder() | ||
| .category("CHORE") | ||
| .id(chore.getId()) | ||
| .title(chore.getType().getDescription()) | ||
| .assigneeName( | ||
| Optional.ofNullable(chore.getAssignee()) | ||
| .map(User::getName) | ||
| .orElse(null) | ||
| ) | ||
| .isCompleted(chore.isCompleted()) | ||
| .build()) | ||
| .toList(); | ||
|
|
||
| // 해당 날짜의 일정(Schedule) 조회 -> DTO 변환 | ||
| List<CalendarDailyResponse> schedules = scheduleRepository.findAllByHouseholdIdAndDateRange(householdId, date, date) | ||
| .stream() | ||
| .map(schedule -> CalendarDailyResponse.builder() | ||
| .category("SCHEDULE") | ||
| .id(schedule.getId()) | ||
| .title(schedule.getContent()) | ||
| .assigneeName(schedule.getUser().getName()) | ||
| .isCompleted(false) | ||
| .build()) | ||
| .toList(); | ||
|
|
||
| // 두 리스트 합치기 (정렬: CHORE -> SCHEDULE) | ||
| return Stream.concat(chores.stream(), schedules.stream()) | ||
| .sorted(Comparator.comparing(CalendarDailyResponse::getCategory)) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // 내부 클래스: 날짜별 개수 집계용 | ||
| private static class MonthlyCounts { | ||
| long scheduleCount = 0; | ||
| long choreCount = 0; | ||
| long utilityBillsCount = 0; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.partition.domain.chore.controller; | ||
|
|
||
| import com.partition.domain.chore.service.ChoreAssignmentService; | ||
| import com.partition.domain.common.dto.response.ApiResponse; | ||
| import com.partition.global.config.security.CustomUserDetails; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.temporal.ChronoUnit; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/chores") | ||
| @RequiredArgsConstructor | ||
| public class ChoreController { | ||
|
|
||
| private final ChoreAssignmentService assignmentService; | ||
|
|
||
| @Operation(summary = "집안일 자동 배정 요청", description = "AI 알고리즘(FastAPI)을 호출하여 지정된 기간 동안의 집안일을 배정하고 결과를 저장합니다.") | ||
| @PostMapping("/auto-assign") | ||
| public ResponseEntity<ApiResponse<String>> autoAssignChores( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @RequestParam LocalDate startDate, | ||
| @RequestParam LocalDate endDate) { | ||
|
|
||
| // 토큰에서 요청자 ID 추출 | ||
| Long userId = Long.parseLong(userDetails.getUsername()); | ||
|
|
||
| // 시작일과 종료일 사이의 기간(일수) 계산 (종료일 포함이므로 +1) | ||
| int periodDays = (int) ChronoUnit.DAYS.between(startDate, endDate) + 1; | ||
|
|
||
| if (periodDays < 1) { | ||
| throw new IllegalArgumentException("종료일은 시작일보다 같거나 이후여야 합니다."); | ||
| } | ||
|
|
||
| // 서비스 호출 (FastAPI 통신 -> DB 저장) | ||
| assignmentService.assignChores(userId, startDate, periodDays); | ||
|
Comment on lines
+35
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 배정 기간 상한이 없어 한 번의 호출이 너무 커질 수 있습니다. Line 36-43은 역순 날짜만 막고 기간 길이 자체는 제한하지 않습니다. 지금 구현은 곧바로 FastAPI 호출과 결과 저장으로 이어지기 때문에, 수개월/수년 범위 요청 하나가 외부 서비스 부하와 대량 쓰기를 동시에 만들 수 있습니다. 최대 허용 기간을 명시적으로 제한해 두는 편이 안전합니다. 🤖 Prompt for AI Agents |
||
|
|
||
| return ResponseEntity.ok( | ||
| ApiResponse.onSuccess("200", "집안일 배정이 완료되었습니다.", "Success") | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Partition-app/Backend
Length of output: 123
🏁 Script executed:
Repository: Partition-app/Backend
Length of output: 2007
🏁 Script executed:
Repository: Partition-app/Backend
Length of output: 252
🏁 Script executed:
Repository: Partition-app/Backend
Length of output: 233
CI/CD 파이프라인에서 테스트가 완전히 건너뛰어지고 있습니다
-x test플래그로 테스트가 제외되고 있으며, 워크플로우에 테스트를 실행하는 다른 단계가 없습니다. 테스트 코드가 존재함에도 불구하고 검증 없이 코드가 빌드되고 배포되고 있으므로, 별도의 테스트 단계를 추가하여 모든 테스트가 CI 단계에서 실행되도록 수정해야 합니다.🤖 Prompt for AI Agents