Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ jobs:
echo "KAKAO_LOGIN_REDIRECT_URI=${{ secrets.KAKAO_LOGIN_REDIRECT_URI }}" >> .env
echo "KAKAO_REST_API_KEY=${{ secrets.KAKAO_REST_API_KEY }}" >> .env

echo "KAKAO_JS_KEY=${{ secrets.KAKAO_JS_KEY }}" >> .env
echo "KAKAOPAY_SECRET_KEY_DEV=${{ secrets.KAKAOPAY_SECRET_KEY_DEV }}" >> .env
echo "NTS_SERVICE_KEY=${{ secrets.NTS_SERVICE_KEY }}" >> .env

# 3. 네트워크 생성 확인
sudo docker network inspect cuk-compasser-net >/dev/null 2>&1 || sudo docker network create cuk-compasser-net

Expand All @@ -89,7 +93,7 @@ jobs:
# 5. 미사용 이미지 정리
sudo docker image prune -f

# 6. 디스코드 알림 (성공 시) - SSH 액션 밖으로 분리
# 6. 디스코드 알림 (성공 시)
- name: Discord Notification - Success
if: success()
uses: sarisia/actions-status-discord@v1
Expand All @@ -103,7 +107,7 @@ jobs:
성공적으로 EC2에 배포되었습니다!
color: 0x00ff00

# 7. 디스코드 알림 (실패 시) - SSH 액션 밖으로 분리
# 7. 디스코드 알림 (실패 시)
- name: Discord Notification - Failure
if: failure()
uses: sarisia/actions-status-discord@v1
Expand Down
24 changes: 20 additions & 4 deletions docker-compose-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ services:
image: shawn9272/cuk-compasser:${IMAGE_TAG}
container_name: cuk-compasser-service
restart: always
ports:
- "8080:8080"
env_file: .env
environment:
- DB_HOST=mysql-db
Expand All @@ -25,6 +23,9 @@ services:
- KAKAO_LOCAL_BASE_URL=https://dapi.kakao.com
- KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY}
- JAVA_TOOL_OPTIONS=-Xms256m -Xmx256m
- KAKAO_JS_KEY=${KAKAO_JS_KEY}
- KAKAOPAY_SECRET_KEY_DEV=${KAKAOPAY_SECRET_KEY_DEV}
- NTS_SERVICE_KEY=${NTS_SERVICE_KEY}
depends_on:
mysql-db:
condition: service_healthy
Expand All @@ -48,6 +49,23 @@ services:
limits:
memory: 400M

nginx:
image: nginx:latest
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443" # HTTPS 포트 개방
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
# ✨ 핵심: EC2 호스트의 인증서 폴더를 Nginx 컨테이너 내부로 연결 (읽기 전용: ro)
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- app
- grafana
networks:
- cuk-compasser-net

redis:
image: redis:latest
container_name: redis
Expand Down Expand Up @@ -103,8 +121,6 @@ services:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
- GF_SERVER_DOMAIN=compasser.site
Expand Down
48 changes: 48 additions & 0 deletions nginx/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
server {
listen 80;
server_name compasser.site;

# HTTP로 들어오는 모든 요청을 HTTPS로 강제 리다이렉트
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
server_name compasser.site;

# Certbot이 관리하는 SSL 인증서 설정 (경로 유지)
ssl_certificate /etc/letsencrypt/live/compasser.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/compasser.site/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

# 1. 그라파나 접속 설정
location /grafana/ {
# 127.0.0.1 대신 도커 서비스 이름 사용
proxy_pass http://grafana:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 웹소켓 지원
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

# 2. 기본 서비스 (스프링 부트) 접속 설정
location / {
# 127.0.0.1 대신 도커 서비스 이름 사용
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 웹소켓 지원
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.controller;

import Comprehensive_Design_Project.CUK_Compasser.domain.member.dto.MemberRespDTO;
import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.MemberRole;
import Comprehensive_Design_Project.CUK_Compasser.domain.member.service.MemberService;
import Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto.LoginReqDTO;
import Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto.SignUpReqDTO;
Expand Down Expand Up @@ -42,7 +43,7 @@ public class OAuth2Controller {

private final OAuth2Service oAuth2Service;

public record AccessTokenDTO(String accessToken) {}
public record AccessTokenDTO(String accessToken, MemberRole role) {}

@PostMapping("/sign-up")
@Operation(summary = "일반 회원가입 API", description = "이름, 닉네임, 이메일 등을 입력받아 회원가입을 진행합니다.")
Expand Down Expand Up @@ -84,7 +85,7 @@ public ApiResponse<AccessTokenDTO> login(

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

return ApiResponse.onSuccess(SuccessStatus.OK, new AccessTokenDTO(tokenResponse.accessToken()));
return ApiResponse.onSuccess(SuccessStatus.OK, new AccessTokenDTO(tokenResponse.accessToken(), tokenResponse.role()));
}

@PostMapping("/login-kakao")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto;

import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.MemberRole;
import lombok.Builder;

@Builder
public record TokenRespDTO (
String accessToken,
String refreshToken
String refreshToken,
MemberRole role
) { }
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ public TokenRespDTO login(LoginReqDTO request) {

Authentication authentication = authenticationManager.authenticate(authenticationToken);

Member member = memberRepository.findByEmail(authentication.getName())
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

JWT jwt = jwtProvider.generateToken(authentication);

redisTemplate.opsForValue().set(
Expand All @@ -125,6 +128,7 @@ public TokenRespDTO login(LoginReqDTO request) {
return TokenRespDTO.builder()
.accessToken(jwt.getAccessToken())
.refreshToken(jwt.getRefreshToken())
.role(member.getRole())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
import Comprehensive_Design_Project.CUK_Compasser.domain.store.repository.StoreImageRepository;
import Comprehensive_Design_Project.CUK_Compasser.domain.store.repository.StoreRepository;
import Comprehensive_Design_Project.CUK_Compasser.domain.storeManager.repository.StoreManagerRepository;
import Comprehensive_Design_Project.CUK_Compasser.global.aws.S3Service;
import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus;
import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class StoreImageService {
Expand All @@ -24,6 +27,7 @@ public class StoreImageService {
private final StoreImageRepository storeImageRepository;
private final StoreManagerRepository storeManagerRepository;
private final StoreImageConverter storeImageConverter;
private final S3Service s3Service;

@Transactional(readOnly = true)
public StoreImageRespDTO getRepresentativeImage(Long memberId) {
Expand All @@ -42,7 +46,8 @@ public StoreImageRespDTO uploadRepresentativeImage(Long memberId, MultipartFile
throw new GeneralException(ErrorStatus.STORE_IMAGE_NOT_FOUND);
}

String url = "uploaded://" + storeImage.getOriginalFilename();
String imageKey = createStoreImageKey(store.getId(), storeImage);
String url = s3Service.uploadImage(storeImage, imageKey);

storeImageRepository.deleteAllByStore_Id(store.getId());

Expand Down Expand Up @@ -70,4 +75,17 @@ private Store getMyStoreEntity(Long memberId) {
return storeRepository.findByStoreManager_MemberId(memberId)
.orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND));
}
}

private String createStoreImageKey(Long storeId, MultipartFile file) {
return "stores/" + storeId + "/representative/" + UUID.randomUUID() + getExtension(file);
}

private String getExtension(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !originalFilename.contains(".")) {
return "";
}

return originalFilename.substring(originalFilename.lastIndexOf("."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package Comprehensive_Design_Project.CUK_Compasser.global.aws;

import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus;
import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;

@Service
@RequiredArgsConstructor
public class S3Service {

@Value("${spring.cloud.aws.s3.bucket}")
private String bucket;

@Value("${spring.cloud.aws.region.static}")
private String region;

private final S3Client s3Client;

public String uploadImage(MultipartFile file, String key) {
validateImage(file);

try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();

s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));

return "https://" + bucket + ".s3." + region + ".amazonaws.com/" + key;
} catch (IOException e) {
throw new GeneralException(ErrorStatus.FILE_UPLOAD_FAILED);
}
}

private void validateImage(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new GeneralException(ErrorStatus.FILE_REQUIRED);
}

String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new GeneralException(ErrorStatus.UNSUPPORTED_IMAGE_TYPE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;

@Configuration
@RequiredArgsConstructor
Expand All @@ -31,6 +35,8 @@ public JWTAuthenticationFilter jwtAuthenticationFilter(JWTProvider jwtProvider,
SecurityFilterChain securityFilterChain(HttpSecurity http, JWTAuthenticationFilter jwtAuthenticationFilter) throws Exception {

http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))

.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
Expand Down Expand Up @@ -59,6 +65,33 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, JWTAuthenticationFilt
return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

// 로컬 프론트, 로컬 스웨거, 본 서버 도메인 모두 추가
configuration.setAllowedOriginPatterns(List.of(
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:8080", // 로컬 테스트용 스웨거 (포트 8080일 경우)
"http://localhost:8081", // 로컬 테스트용 스웨거 (포트 8081일 경우)
"https://compasser.site", // 본 서버 스웨거 및 배포된 프론트엔드 도메인
"https://www.compasser.site", // www가 붙은 도메인
"https://owner.compasser.co.kr/"
));

configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

// 모든 헤더 허용
configuration.setAllowedHeaders(List.of("*"));

// 쿠키(인증 정보) 전송 허용
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
@Component
public class JWTProvider {

// 일단 1시간으로 세팅
private static final Long ACCESS_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60;
private static final Long REFRESH_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60;
// 일단 1시간으로 세팅 -> access token만 30분 설정, refresh token은 1주일로 설정
private static final Long ACCESS_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 30;
private static final Long REFRESH_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60 * 24 * 7;

private final Key key;
private final CustomUserDetailsService customUserDetailsService;
Expand Down
10 changes: 5 additions & 5 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ spring:
- account_email

datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/${MYSQL_DB_NAME}
username: ${MYSQL_USERNAME}
url: jdbc:mysql://localhost:3306/${DB_NAME:cuk_compasser}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
username: root
password: ${MYSQL_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver

hikari:
maximum-pool-size: 5
Expand Down Expand Up @@ -108,7 +108,7 @@ kakao:

kakaopay:
cid: TC0ONETIME
secret-key: ${KAKAOPAY_SECRET_KEY_DEV}
secret-key: ${KAKAOPAY_SECRET_KEY_DEV:local-dummy}
base-url: https://open-api.kakaopay.com
approval-url: https://compasser.co.kr/payments/kakaopay/success
cancel-url: https://compasser.co.kr/payments/kakaopay/cancel
Expand All @@ -123,4 +123,4 @@ springdoc:
nts:
business:
base-url: https://api.odcloud.kr/api/nts-businessman/v1
service-key: ${NTS_SERVICE_KEY}
service-key: ${NTS_SERVICE_KEY:local-dummy}
Loading
Loading