Skip to content

Commit

Permalink
충남대 BE_윤정훈 6주차 과제 (0단계) (#38)
Browse files Browse the repository at this point in the history
* add: 주문하기 코드를 옮겨온다.

옮겨온다.

* refactor: gitignore 변경

* remove: 동시성 테스트 진행으로 인해 발생하는 build 과부하 방지를 위해 동시성테스트 제거

동시성 테스트 제거

* add: 기존 동시성테스트 추가

동시성 테스트 추가

* add: ci-cd 파일 추가

ci-cd 파일 추가

* rename: workflows -> workflow

* rename: ci-cd to gradle

* Create gradle.yml

* remove: workflow remove

* add: CI with java

* refactor: 자동으로 application.yml 파일 생성하도록 추가

* add: make application.yml 설정파일 생성

* rename: application 설정파일 이름 재설정

* remove: 동시성 테스트 삭제

* change: setup jdk

* remove: deploy 코드 제거

* add: make application.yml 방식 추가

* refactor: application.yml 만드는 코드 수정

* add: deploy gradle 추가

* rename: secret deploy server로 ip 보호

* add: EC2 서버에 키를 알려줌

* add: 호스트키 신뢰하도록 설정

* rename: 호스트키 이름 변경

* add: HOST 추가

* add: 호스트키 검증 무시

* add: redirect-token-uri를 따로 관리해서 배포환경과 다른값 설정할 수 있도록 함

* refactor: 이름 명시

* refactor: 배포 코드 수정

* refactor: 절대 경로 설정

* add: 배포시 중간에 sleep 10 추가

* remove: 그냥 실행되는 부분 제거

* add: 기존 실행중인 8080포트 죽이기

* refactor: 절대경로로 설정
  • Loading branch information
yunjunghun0116 authored Jul 29, 2024
1 parent 68235b7 commit 531a67c
Show file tree
Hide file tree
Showing 98 changed files with 4,443 additions and 4 deletions.
72 changes: 72 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: JAVA CI with GRADLE

on:
push:
branches: [ "yunjunghun0116" ]
pull_request:
branches: [ "yunjunghun0116" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- name: 시작
uses: actions/checkout@v4

- name: JDK 21로 실행
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle

- name: gradle 생성
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5

- name: 설정파일 생성
run: |
touch ./src/main/resources/application.yml
echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.yml
cat ./src/main/resources/application.yml
- name: gradlew 권한 추가
run: chmod +x gradlew

- name: 빌드 작업
run: ./gradlew build

- name: 테스트 작업
run: ./gradlew test

- name: 서버에 배포하기
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
DEPLOY_SERVER: ${{ secrets.DEPLOY_SERVER }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
run: |
echo "$SSH_KEY" > key.pem
chmod 400 key.pem
scp -o StrictHostKeyChecking=no -i key.pem build/libs/*.jar $DEPLOY_USER@$DEPLOY_SERVER:~/spring-gift-point/build/libs/
scp -o StrictHostKeyChecking=no -i key.pem ./src/main/resources/application.yml $DEPLOY_USER@$DEPLOY_SERVER:~/spring-gift-point/src/main/resources/application.yml
ssh -o StrictHostKeyChecking=no -i key.pem $DEPLOY_USER@$DEPLOY_SERVER "
sudo lsof -t -i:8080 | xargs -r sudo kill -9
nohup java -jar ~/spring-gift-point/build/libs/spring-gift-0.0.1-SNAPSHOT.jar &"
dependency-submission:

runs-on: ubuntu-latest
permissions:
contents: write

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

- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

src/main/resources/application.yml
### STS ###
.apt_generated
.classpath
Expand Down
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# spring-gift-point
# spring-gift-point

### 과제 진행 요구 사항

- 미션은 [포인트](https://github.com/kakao-tech-campus-2nd-step2/spring-gift-point) 저장소를 포크하고 클론하는 것으로 시작한다.
- [온라인 코드 리뷰 요청 1단계 문서](https://github.com/next-step/nextstep-docs/blob/master/codereview/review-step1.md)를 참고하여 실습 환경을
구축한다.
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로
추가한다. [AngularJS Git Commit Message Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)을 참고해 커밋
메시지를 작성한다.

### 프로그래밍 요구 사항

- 자바 코드 컨벤션을 지키면서 프로그래밍 한다. (들여쓰기는 '4 spaces' 로 한다)
- indent (들여쓰기) depth 를 3이 넘지 않도록 구현한다.
- 3항 연산자를 사용하지 않는다.
- 함수는 한가지 일만 하도록 최대한 작게 만든다.
- 함수의 길이가 15 라인을 넘어가지 않도록 구현한다.
- JUnit 5 와 AssertJ 를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.
- else 예약어를 사용하지 않는다.
- 도메인 로직에 단위 테스트를 구현해야 한다.(핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.)

### 기능 요구 사항 (5주차)

#### 0단계

- [X] 주문하기 코드를 옮겨온다.

### 나만의 HTTP RULE

| HTTP Method | 사용상황 | 반환(상태코드) |
|-------------|--------------------------------|----------|
| GET | 리소스의 조회 | 200 |
| POST | 새로운 리소스 생성 | 201 |
| PUT | 리소스의 전체 업데이트 또는 ID를 통한 리소스 생성 | 204 |
| PATCH | 리소스의 일부분(일부 필드) 업데이트 | 204 |
| DELETE | 리소스의 삭제 | 204 |

### 나만의 계층 RULE

| 계층 | 역할 |
|------------|-------------------------------------------------------------|
| Controller | HTTP 요청을 받아 적절한 Service 호출, 입력 검증, 유효성 검사, HTTP 응답 생성 및 반환 |
| Service | 비즈니스 로직 수행, DTO 와 엔티티 변환, 다수 Repository 를 통한 하나의 트랜잭션 처리 작업 |
| Model | Entity, DTO 가 속하며 데이터구조, 데이터베이스와의 연동되는 객체 |
| Repository | DB 관련 CRUD 작업, DB 의 결과를 Entity 로 변환하는 작업 |

### 나만의 개행 RULE

- 지역변수는 사이에 개행을 두지 않는다. 하지만 첫 지역변수 전줄, 마지막 지역변수 다음줄에 개행을 추가한다.
- 생성자 전후에 개행을 추가한다.
- 추상체, 구현체 모두 메서드 전후에 개행을 추가한다. 단 마지막 메서드 후에는 추가하지 않는다.
- 클래스의 마지막 줄에는 개행을 추가한다.

### 연관관계 매핑 RULE

- M:1 관계에서는 M 에서 1 에 대한 정보까지 추가한다. ex) setProduct() 는 Product 가 아닌 WishProduct 에서 수행을 하는것 처럼
- save 를 하는 과정에서 우선 객체를 생성하고 연관관계를 맺어준 후에 repository.save() 를 호출한다.

### 계층간 의존 RULE

- M:1 관계에서 M 에서는 1에 대한 조회만을 수행하기에 서비스 계층에서는 레포지토리 계층을 의존한다.(R)
- M:1 관계에서 1 에서는 M에 대한 로직을 수행할 수 있기에(삭제 등) 서비스 계층을 의존한다.(CUD)
14 changes: 12 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
runtimeOnly 'com.h2database:h2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
testRuntimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
testRuntimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

tasks.named('test') {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/gift/Application.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package gift;

import gift.config.properties.JwtProperties;
import gift.config.properties.KakaoProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
@EnableConfigurationProperties({JwtProperties.class, KakaoProperties.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
Expand Down
128 changes: 128 additions & 0 deletions src/main/java/gift/client/KakaoApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package gift.client;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import gift.config.properties.KakaoProperties;
import gift.dto.giftorder.GiftOrderResponse;
import gift.dto.kakao.KakaoAuthResponse;
import gift.dto.kakao.KakaoTokenResponse;
import gift.dto.kakao.template.KakaoTemplate;
import gift.dto.kakao.template.KakaoTemplateCommerce;
import gift.dto.kakao.template.KakaoTemplateContent;
import gift.dto.kakao.template.KakaoTemplateLink;
import gift.exception.BadRequestException;
import gift.exception.InvalidKakaoTokenException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestClient;

import java.net.URI;

@Component
public class KakaoApiClient {

private final RestClient restClient;
private final KakaoProperties kakaoProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String INVALID_TOKEN_MESSAGE = "유효하지 않은 토큰입니다. 갱신이 필요합니다.";
private static final String TOKEN_BASE_URL = "https://kauth.kakao.com/oauth/token";

public KakaoApiClient(KakaoProperties kakaoProperties, RestClient restClient) {
this.kakaoProperties = kakaoProperties;
this.restClient = restClient;
}

public KakaoTokenResponse getTokenResponse(String code, String redirectUri) {
var body = new LinkedMultiValueMap<String, String>();
body.add("grant_type", "authorization_code");
body.add("client_id", kakaoProperties.restApiKey());
body.add("redirect_uri", redirectUri);
body.add("code", code);

var response = restClient.post()
.uri(URI.create(TOKEN_BASE_URL))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(String.class);

return convertDtoWithJsonString(response, KakaoTokenResponse.class);
}

public KakaoTokenResponse getRefreshedTokenResponse(String refreshToken) {
var body = new LinkedMultiValueMap<String, String>();
body.add("grant_type", "refresh_token");
body.add("client_id", kakaoProperties.restApiKey());
body.add("refresh_token", refreshToken);

var response = restClient.post()
.uri(URI.create(TOKEN_BASE_URL))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.onStatus(statusCode -> statusCode.equals(HttpStatus.UNAUTHORIZED), (req, res) -> {
throw new InvalidKakaoTokenException(INVALID_TOKEN_MESSAGE);
})
.onStatus(statusCode -> statusCode.equals(HttpStatus.BAD_REQUEST), (req, res) -> {
throw new InvalidKakaoTokenException(INVALID_TOKEN_MESSAGE);
})
.body(String.class);

return convertDtoWithJsonString(response, KakaoTokenResponse.class);
}

public KakaoAuthResponse getKakaoAuthResponse(KakaoTokenResponse kakaoTokenResponse) {
var url = "https://kapi.kakao.com/v2/user/me";
var header = "Bearer " + kakaoTokenResponse.accessToken();

var response = restClient.get()
.uri(URI.create(url))
.header("Authorization", header)
.retrieve()
.body(String.class);

return convertDtoWithJsonString(response, KakaoAuthResponse.class);
}

public void sendSelfMessageOrder(String accessToken, GiftOrderResponse giftOrderResponse) {
try {
var url = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
var header = "Bearer " + accessToken;

var template = getCommerceTemplate(giftOrderResponse);
var body = new LinkedMultiValueMap<String, Object>();
body.add("template_object", objectMapper.writeValueAsString(template));

restClient.post()
.uri(URI.create(url))
.header("Authorization", header)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.onStatus(statusCode -> statusCode.equals(HttpStatus.UNAUTHORIZED), (req, res) -> {
throw new InvalidKakaoTokenException(INVALID_TOKEN_MESSAGE);
})
.body(String.class);
} catch (JsonProcessingException exception) {
throw new BadRequestException("잘못된 입력으로 인해 JSON 파싱에 실패했습니다" + exception.getMessage());
}
}

private <T> T convertDtoWithJsonString(String response, Class<T> returnTypeClass) {
try {
return objectMapper.readValue(response, returnTypeClass);
} catch (JsonProcessingException exception) {
throw new RuntimeException(returnTypeClass.getName() + "의 데이터를 DTO 로 변환하는 과정에서 예외가 발생했습니다.", exception);
}
}

private KakaoTemplate getCommerceTemplate(GiftOrderResponse giftOrderResponse) {
var objectType = "commerce";
var link = new KakaoTemplateLink("https://gift.kakao.com/product/2370524");
var content = new KakaoTemplateContent(giftOrderResponse.message(), "https://img1.kakaocdn.net/thumb/[email protected]/?fname=https%3A%2F%2Fst.kakaocdn.net%2Fproduct%2Fgift%2Fproduct%2F20240417111629_616eccb9d4cd464fa06d3430947dce15.jpg", giftOrderResponse.message(), link);
var commerce = new KakaoTemplateCommerce(giftOrderResponse.optionInformation().productName() + "[" + giftOrderResponse.optionInformation().name() + "]", giftOrderResponse.optionInformation().price() * giftOrderResponse.quantity());
return new KakaoTemplate(objectType, content, commerce);
}
}
20 changes: 20 additions & 0 deletions src/main/java/gift/config/RestClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gift.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

@Bean
public RestClient restClient() {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(5000);
return RestClient.builder()
.requestFactory(factory)
.build();
}
}
Loading

0 comments on commit 531a67c

Please sign in to comment.