Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/ISSUE_TEMPLATE/issue_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Issue template
about: Describe this issue purpose here.
title: ""
labels: ""
assignees: ""
---

## 목적

## 세부 내용 설명
50 changes: 50 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CD

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission
run: chmod +x ./gradlew

- name: Build
run: ./gradlew clean build -x test

- name: Login Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build & Push Image
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mua-backend:${{ github.sha }} .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/mua-backend:${{ github.sha }}

- name: Deploy to WAS
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.WAS_HOST }}
username: ${{ secrets.WAS_USERNAME }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.WAS_SSH_PORT }}
script: |
docker stop mua-backend || true
docker rm mua-backend || true
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/mua-backend:${{ github.sha }}
docker run -d -p 8080:8080 --name mua-backend \
${{ secrets.DOCKERHUB_USERNAME }}/mua-backend:${{ github.sha }}
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
pull_request:
push:
branches-ignore:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission
run: chmod +x ./gradlew

- name: Build (no deploy)
run: ./gradlew clean build -x test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ out/

### VS Code ###
.vscode/

*.yml
!application.yml
!ci.yml
!cd.yml
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 1. JDK 이미지를 기반으로 실행 환경 설정
FROM eclipse-temurin:17-jdk-alpine

# 2. JAR 파일을 컨테이너 안으로 복사
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

# 3. 컨테이너 실행 시 실행할 명령어
ENTRYPOINT ["java","-jar","/app.jar"]
33 changes: 28 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,34 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// Database
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// AWS
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0'

}

tasks.named('test') {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/Mua/Mua_backend/global/annotation/ApiExceptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package Mua.Mua_backend.global.annotation;

import Mua.Mua_backend.global.exception.ErrorCode;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiExceptions {
ErrorCode[] value();
}
19 changes: 19 additions & 0 deletions src/main/java/Mua/Mua_backend/global/annotation/EnumValid.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package Mua.Mua_backend.global.annotation;

import Mua.Mua_backend.global.common.EnumValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {

String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}
24 changes: 24 additions & 0 deletions src/main/java/Mua/Mua_backend/global/common/BaseTimeEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package Mua.Mua_backend.global.common;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseTimeEntity {

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
27 changes: 27 additions & 0 deletions src/main/java/Mua/Mua_backend/global/common/EnumValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package Mua.Mua_backend.global.common;

import Mua.Mua_backend.global.annotation.EnumValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.Arrays;

public class EnumValidator implements ConstraintValidator<EnumValid, String> {

private Class<? extends Enum<?>> enumClass;

@Override
public void initialize(EnumValid constraintAnnotation) {
this.enumClass = constraintAnnotation.enumClass();
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}

return Arrays.stream(enumClass.getEnumConstants())
.anyMatch(e -> e.name().equalsIgnoreCase(value)); // 대소문자 구분 없음
}
}
23 changes: 23 additions & 0 deletions src/main/java/Mua/Mua_backend/global/config/S3StorageConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package Mua.Mua_backend.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class S3StorageConfig {

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

@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
}
}
65 changes: 65 additions & 0 deletions src/main/java/Mua/Mua_backend/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package Mua.Mua_backend.global.config;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/oauth2/**", "/login/oauth2/**",
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 예외 처리 설정
.exceptionHandling(exception -> exception
.authenticationEntryPoint((req, res, ex) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.accessDeniedHandler((req, res, ex) -> res.sendError(HttpServletResponse.SC_FORBIDDEN))
);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of(
"http://localhost:3000"
));
config.addAllowedHeader("*");
config.addAllowedMethod("*");

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

}
30 changes: 30 additions & 0 deletions src/main/java/Mua/Mua_backend/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package Mua.Mua_backend.global.config;

import Mua.Mua_backend.global.swagger.OpenApiConfigurer;
import Mua.Mua_backend.global.swagger.SwaggerErrorExampleGenerator;
import Mua.Mua_backend.global.swagger.SwaggerOperationCustomizer;
import io.swagger.v3.oas.models.OpenAPI;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

@Value("${spring.swagger.url}")
private String baseUrl;

@Bean
public OpenAPI openAPI() { return new OpenApiConfigurer(baseUrl).createOpenAPI(); }

@Bean
public SwaggerErrorExampleGenerator swaggerErrorExampleGenerator() {
return new SwaggerErrorExampleGenerator();
}

@Bean
public OperationCustomizer swaggerOperationCustomizer(SwaggerErrorExampleGenerator generator) {
return new SwaggerOperationCustomizer(generator);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package Mua.Mua_backend.global.exception;

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException{

private final ErrorCode errorCode;

public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
22 changes: 22 additions & 0 deletions src/main/java/Mua/Mua_backend/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package Mua.Mua_backend.global.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

INVALID_INPUT(400, "잘못된 입력 값입니다."),
UNAUTHORIZED(401, "인증이 필요합니다."),
FORBIDDEN(403, "접근 권한이 없습니다."),
NOT_FOUND(404, "존재하지 않는 리소스입니다."),
INTERNAL_SERVER_ERROR(500, "서버 오류가 발생했습니다."),

// example
EXAMPLE_NOT_FOUND(404, "예시를 찾을 수 없습니다.");

private final int status;
private final String message;

}
Loading