Skip to content

Commit 7d9b071

Browse files
authored
Merge branch 'dev' into refactor/v2-calendar-exception
2 parents 669efa6 + 4622915 commit 7d9b071

22 files changed

Lines changed: 1242 additions & 82 deletions

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI/CD Pipeline to EC2 with Blue/Green
22

33
on:
44
push:
5-
branches: [main, dev, ci/env]
5+
branches: [main, dev]
66

77
# ───────────────────────────────────────────────────────────────
88
concurrency:

deploy.sh

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,35 +56,9 @@ $COMPOSE -f "$COMPOSE_FILE" rm -fs "$NEW_APP_SERVICE" || true
5656

5757
echo "컨테이너 기동…"
5858
# (compose v2.21+ 사용 시 주석 해제해 헬스체크 통합)
59-
# $COMPOSE -f "$COMPOSE_FILE" up -d --wait -t 180 "$NEW_APP_SERVICE"
60-
$COMPOSE -f "$COMPOSE_FILE" up -d "$NEW_APP_SERVICE"
59+
$COMPOSE -f "$COMPOSE_FILE" up -d --wait -t 180 "$NEW_APP_SERVICE"
6160

62-
############################ 3. 헬스체크 (curl 방식) ###########################
63-
echo "헬스체크 대기…"
64-
HEALTH_URL="http://localhost:${NEW_APP_PORT}/actuator/health"
65-
MAX=20; INTERVAL=10
66-
67-
for ((i=1;i<=MAX;i++)); do
68-
echo "--- 헬스 체크 시도 $i/$MAX ---"
69-
# curl의 전체 응답을 변수에 저장 (오류 시에도 스크립트가 중단되지 않도록)
70-
HEALTH_OUTPUT=$(curl -s "$HEALTH_URL" || echo "curl_failed")
71-
72-
# 전체 응답을 로그에 출력
73-
echo "응답: $HEALTH_OUTPUT"
74-
75-
# 응답 내용에 "status":"UP"이 포함되어 있는지 확인
76-
if echo "$HEALTH_OUTPUT" | grep -q '"status":"UP"'; then
77-
echo "✅ 헬스 통과 ($i/$MAX)"
78-
break
79-
fi
80-
81-
# 마지막 시도였다면 실패 처리
82-
[[ $i -eq $MAX ]] && { echo "🚨 헬스 실패"; exit 1; }
83-
84-
echo "⏳ 재시도 $i/$MAX"; sleep $INTERVAL
85-
done
86-
87-
############################ 4. Nginx 스위치 ##################################
61+
############################ 3. Nginx 스위치 ##################################
8862
sudo sed -E -i '/upstream current_app/,+1 s/127\.0\.0\.1:[0-9]+/127.0.0.1:'"$NEW_APP_PORT"'/' \
8963
"$NGINX_CONF"
9064
sudo nginx -t
@@ -98,7 +72,7 @@ DEPLOY_OK=true # ★ 여기서 성공 플래그 ON
9872

9973
echo "🎉 Blue/Green 전환 완료"
10074

101-
############################ 5. 모니터링 스택 재배포 ##########################
75+
############################ 4. 모니터링 스택 재배포 ##########################
10276
cd "$MONITORING_DIR"
10377

10478
echo "→ Rendering Alertmanager config with SLACK_WEBHOOK_URL"

monitoring/loki/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ ingester:
2323
num_tokens: 512
2424
final_sleep: 0s
2525
chunk_idle_period: 12h # 청크가 S3에 쓰여지기 전까지 유휴 상태로 유지되는 시간 (9-to-6 환경 최적화)
26-
chunk_block_size: 262144 # 청크의 블록 크기 (바이트)
26+
chunk_block_size: 524288 # 청크의 블록 크기 (바이트)
2727
chunk_retain_period: 72h # 청크가 로컬에 유지되는 최대 시간 (3일로 줄임)
28-
max_chunk_age: 168h # 청크가 S3에 쓰여지기 전까지 로컬에 유지되는 최대 시간 (기존 24h에서 7d으로 늘림)
28+
max_chunk_age: 336h # 청크가 S3에 쓰여지기 전까지 로컬에 유지되는 최대 시간 (기존 24h에서 7d으로 늘림)
2929

3030
compactor:
3131
compaction_interval: 10m # 인덱스 압축 주기

monitoring/prometheus/alert.rules

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ groups:
2121
# 2. CPU 사용률 높음 알림
2222
- alert: HighCpuUsage
2323
expr: |
24-
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
24+
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
2525
for: 10m
2626
labels:
2727
severity: warning
2828
annotations:
2929
summary: "인스턴스 {{ $labels.instance }} 의 CPU 사용률이 높습니다."
30-
description: "지난 10분간 평균 CPU 사용률이 80%를 초과했습니다. 현재: {{ printf \"%.2f\" $value }}%"
30+
description: "지난 10분간 평균 CPU 사용률이 90%를 초과했습니다. 현재: {{ printf \"%.2f\" $value }}%"
3131

3232
# 3. 메모리 사용률 높음 알림
3333
- alert: HighMemoryUsage
3434
expr: |
35-
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100 > 80
35+
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100 > 85
3636
for: 2m
3737
labels:
3838
severity: warning
3939
annotations:
4040
summary: "인스턴스 {{ $labels.instance }} 의 메모리 사용률이 높습니다."
41-
description: "지난 2분간 사용 가능한 메모리가 20% 미만입니다. 현재 사용률: {{ printf \"%.2f\" $value }}%"
41+
description: "지난 2분간 사용 가능한 메모리가 15% 미만입니다. 현재 사용률: {{ printf \"%.2f\" $value }}%"
4242

4343
# 4. 디스크 공간 부족 알림
4444
- alert: DiskAlmostFull
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package store.lastdance.controller.auth;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
5+
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import store.lastdance.dto.user.UserResponseDTO;
16+
import store.lastdance.security.oauth.CustomOAuth2User;
17+
import store.lastdance.service.auth.AuthService;
18+
import store.lastdance.service.user.UserV2QueryService;
19+
20+
import java.util.UUID;
21+
22+
@RestController
23+
@RequestMapping("/api/v2/auth")
24+
@RequiredArgsConstructor
25+
@Tag(name = "Authentication", description = "인증 관련 API")
26+
public class AuthV2Controller {
27+
28+
private final AuthService authService;
29+
private final UserV2QueryService userV2QueryService;
30+
31+
@PostMapping("/refresh")
32+
@Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 새 액세스 토큰 발급")
33+
public ResponseEntity<Void> refreshToken(HttpServletRequest request, HttpServletResponse response) {
34+
authService.refreshToken(request, response);
35+
return ResponseEntity.ok().build();
36+
}
37+
38+
@PostMapping("/logout")
39+
@Operation(summary = "로그아웃", description = "토큰 무효화 및 로그아웃 처리")
40+
public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
41+
authService.logout(request, response);
42+
return ResponseEntity.ok().build();
43+
}
44+
45+
@GetMapping("/me")
46+
@Operation(summary = "내 정보 조회", description = "현재 로그인한 사용자 정보 조회")
47+
public ResponseEntity<UserResponseDTO> getMe(
48+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User principal
49+
) {
50+
UUID userId = principal.getUserId();
51+
UserResponseDTO user = userV2QueryService.getUserWithProfileImage(userId);
52+
return ResponseEntity.ok(user);
53+
}
54+
}

src/main/java/store/lastdance/controller/user/UserController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1313
import org.springframework.web.bind.annotation.*;
1414
import org.springframework.web.multipart.MultipartFile;
15+
import store.lastdance.converter.UserConverter;
1516
import store.lastdance.domain.user.User;
1617
import store.lastdance.dto.response.ApiResponse;
1718
import store.lastdance.dto.user.UserResponseDTO;
@@ -28,6 +29,7 @@
2829
public class UserController {
2930

3031
private final UserService userService;
32+
private final UserConverter userConverter;
3133

3234
@PatchMapping("/me")
3335
@Operation(summary = "내 정보 수정", description = "닉네임 수정")
@@ -37,7 +39,7 @@ public ResponseEntity<ApiResponse<UserResponseDTO>> updateMyInfo(
3739
) {
3840
UUID userId = oAuth2User.getUserId();
3941
User updatedUser = userService.updateMyInfo(userId, requestDTO);
40-
UserResponseDTO dto = UserResponseDTO.from(updatedUser);
42+
UserResponseDTO dto = userConverter.toResponseDTO(updatedUser);
4143
return ResponseEntity.ok(ApiResponse.success(dto));
4244
}
4345

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package store.lastdance.controller.user;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
5+
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
13+
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.multipart.MultipartFile;
15+
import store.lastdance.converter.UserConverter;
16+
import store.lastdance.domain.user.User;
17+
import store.lastdance.dto.response.ApiResponse;
18+
import store.lastdance.dto.user.UserResponseDTO;
19+
import store.lastdance.dto.user.UserUpdateRequestDTO;
20+
import store.lastdance.security.oauth.CustomOAuth2User;
21+
import store.lastdance.service.user.UserV2QueryService;
22+
import store.lastdance.service.user.UserV2Service;
23+
24+
import java.util.UUID;
25+
26+
@RestController
27+
@RequestMapping("/api/v2/users")
28+
@RequiredArgsConstructor
29+
@Tag(name = "User", description = "사용자 관리 API")
30+
public class UserV2Controller {
31+
32+
private final UserV2Service userV2Service;
33+
private final UserV2QueryService userV2QueryService;
34+
private final UserConverter userConverter;
35+
36+
@PatchMapping("/me")
37+
@Operation(summary = "내 정보 수정", description = "닉네임 수정")
38+
public ResponseEntity<ApiResponse<UserResponseDTO>> updateMyInfo(
39+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User oAuth2User,
40+
@Valid @RequestBody UserUpdateRequestDTO requestDTO
41+
) {
42+
UUID userId = oAuth2User.getUserId();
43+
User updatedUser = userV2Service.updateMyInfo(userId, requestDTO);
44+
UserResponseDTO dto = userConverter.toResponseDTO(updatedUser);
45+
return ResponseEntity.ok(ApiResponse.success(dto));
46+
}
47+
48+
@PostMapping(value = "/me/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
49+
@Operation(summary = "프로필 이미지 업로드", description = "JPG, PNG 형식만 지원, 최대 5MB")
50+
public ResponseEntity<ApiResponse<UserResponseDTO>> uploadProfileImage(
51+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User oAuth2User,
52+
@RequestParam("file") MultipartFile file
53+
) {
54+
UUID userId = oAuth2User.getUserId();
55+
UserResponseDTO dto = userV2Service.updateProfileImage(userId, file);
56+
return ResponseEntity.ok(ApiResponse.success(dto));
57+
}
58+
59+
@DeleteMapping("/me/profile-image")
60+
@Operation(summary = "프로필 이미지 삭제", description = "기본 이미지로 변경")
61+
public ResponseEntity<ApiResponse<UserResponseDTO>> deleteProfileImage(
62+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User oAuth2User
63+
) {
64+
UUID userId = oAuth2User.getUserId();
65+
UserResponseDTO dto = userV2Service.deleteProfileImage(userId);
66+
return ResponseEntity.ok(ApiResponse.success(dto));
67+
}
68+
69+
@GetMapping("/nickname/check")
70+
@Operation(summary = "닉네임 중복 확인", description = "사용 가능 여부 체크")
71+
public ResponseEntity<ApiResponse<Boolean>> checkNickname(
72+
@RequestParam("nickname") String nickname,
73+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User oAuth2User
74+
) {
75+
UUID userId = oAuth2User.getUserId();
76+
boolean isAvailable = userV2QueryService.isNicknameAvailable(userId, nickname);
77+
return ResponseEntity.ok(ApiResponse.success(isAvailable));
78+
}
79+
80+
@DeleteMapping("/me")
81+
@Operation(summary = "계정 비활성화", description = "계정 비활성화 및 토큰 무효화")
82+
public ResponseEntity<ApiResponse<String>> deactivateAccount(
83+
@Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User oAuth2User,
84+
@Parameter(hidden = true) HttpServletRequest request,
85+
@Parameter(hidden = true) HttpServletResponse response
86+
) {
87+
UUID userId = oAuth2User.getUserId();
88+
userV2Service.deactivateUser(userId, request, response);
89+
return ResponseEntity.ok(ApiResponse.success("계정이 비활성화되었습니다."));
90+
}
91+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package store.lastdance.converter;
2+
3+
import org.springframework.stereotype.Component;
4+
import store.lastdance.domain.user.User;
5+
import store.lastdance.dto.user.UserResponseDTO;
6+
7+
@Component
8+
public class UserConverter {
9+
10+
public UserResponseDTO toResponseDTO(User user) {
11+
if (user == null) {
12+
return null;
13+
}
14+
15+
String profileImageUrl = (user.getProfileImageFile() != null)
16+
? user.getProfileImageFile().getFilePath()
17+
: null;
18+
19+
return new UserResponseDTO(
20+
user.getUserId(),
21+
user.getEmail(),
22+
user.getUsername(),
23+
user.getNickname(),
24+
profileImageUrl,
25+
user.getProvider().name(),
26+
user.getIsActive(),
27+
user.getIsBanned(),
28+
user.getUserBudget()
29+
);
30+
}
31+
}

src/main/java/store/lastdance/dto/user/UserResponseDTO.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package store.lastdance.dto.user;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4-
import store.lastdance.domain.user.User;
54

65
import java.util.UUID;
76

@@ -34,22 +33,4 @@ public record UserResponseDTO(
3433
@Schema(description = "월 예산", example = "1000000")
3534
Integer monthlyBudget
3635
) {
37-
public static UserResponseDTO from(User user) {
38-
String profileImageUrl = null;
39-
if (user.getProfileImageFile() != null) {
40-
profileImageUrl = user.getProfileImageFile().getFilePath();
41-
}
42-
43-
return new UserResponseDTO(
44-
user.getUserId(),
45-
user.getEmail(),
46-
user.getUsername(),
47-
user.getNickname(),
48-
profileImageUrl,
49-
user.getProvider().name(),
50-
user.getIsActive(),
51-
user.getIsBanned(),
52-
user.getUserBudget()
53-
);
54-
}
5536
}

src/main/java/store/lastdance/repository/group/GroupRepository.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@ public interface GroupRepository extends JpaRepository<Group, UUID> {
1717

1818
List<Group> findByMembers_User(User user);
1919

20-
/**
21-
* 특정 그룹에 특정 사용자가 멤버로 속해있는지 확인
22-
*/
2320
@Query(value = "SELECT CASE WHEN COUNT(gm.group_id) > 0 THEN true ELSE false END " +
2421
"FROM group_members gm " +
2522
"WHERE gm.group_id = :groupId AND gm.user_id = :userId",
2623
nativeQuery = true)
2724
boolean existsByGroupIdAndMemberId(@Param("groupId") UUID groupId,
2825
@Param("userId") UUID userId);
2926

30-
/**
31-
* 그룹 소유자인지 확인
32-
*/
27+
3328
@Query(value = "SELECT CASE WHEN COUNT(g.group_id) > 0 THEN true ELSE false END " +
3429
"FROM groups g " +
3530
"WHERE g.group_id = :groupId AND g.owner_id = :userId",

0 commit comments

Comments
 (0)