@WebMvcTest를 진행시 Header에 Authorization을 할당 할 경우 아래 처럼 SecurityRequirements는 다양한 형태로 생길 수 있다.

근데 똑같이 헤더 이름을 지정하고, 테스트용 토큰 문자열을 할당하는데, 어떻게 아래처럼 다양한 결과를 맞을 수 있을까?

 

일단 스웨거 UI에서 우측 상단 아래에 있는 버튼을 활성화하려면 JWT_BEARER이어야 한다.

그 외 OAUTH2 관련 스펙을 정의하는건 별도 설정을 통해서 가능한데,

우선 나는 활성화를 하고 싶은데 안되어서,

securityRequirements의 type을 결정하는건 대체 누가 해주나 싶어서 찾아 보았다.

 

결과는 아래와 같다.

 

토큰의 페이로드에 오는 데이터의 형태에 따라 결정된다고 보면 된다.

scope가 포함된 토큰을 사용시에는 OAUTH2로 형태가 지정되고,

그게 아닌 경우 JWT_BEARER,

토큰은 있으나, 형태가 불분명하면, null로 처리되더라.

jwt 토큰 검증 관련 테스트는 별개로 하고,

mvcTest의 테스트 코드에 jwt처리하는 부분을 모킹하고 아무 토큰이나 주워다 넣었는데,

하필 그게 OAUTH2 토큰이었고,

별거 아닌곳에서 헤매게 되었지만, 나름 기능상 중요한 사실을 알게된 시간.

OCP와 DIP를 지키기 위해
인터페이스와 구현체를 분리하고 다형성을 갖게끔 하는게 좋은 코드란건 알겠고,
결국 인터페이스를 상속받고 필요한 구현체만 구현하면 된다는걸 다들 알겠다고 한다.
 
근데 정작 사용을 어떻게 하는지 대부분 모르는 것 같다.
 
 
일단 개인적으로 그다지 좋지 않은 코드의 예시다.

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

    private PayService payKakaoService;
    private PayService paySamsungService;
    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao, Samsung, Naver;
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) {

        if (payType == PayType.Kakao) {
            payKakaoService.pay();
        }

        if (payType == PayType.Samsung) {
            paySamsungService.pay();
        }

        if (payType == PayType.Naver) {
            payNaverService.pay();
        }

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() {
        this.pay(PayType.Samsung);
    }

위 코드의 단점이 뭘까? 분명 인터페이스도 썻고, 필요에 따라 구현체도 만들었고,
근데 저기에 앱쁠페이를 추가한다면? 구현체를 만들지만

public ResponseEntity<?> pay(PayType payType)

요기 안에 있는 if문을 추가해야 하지 않겠는가?!
 
이거는 DIP에 위배 되는게 아닌가? 싶기도 하고, 

PayService ps = null;

if (payType == PayType.Kakao) {
    ps = payKakaoService;
}

if (payType == PayType.Samsung) {
    ps = paySamsungService;
}

if (payType == PayType.Naver) {
    ps = payNaverService;
}

ps.pay();

아님 뭐 메서드 내 필드를 이용해서 이렇게 바꿀까? 이것도 뭐 크게 다른건 없다고 느끼는데,
그렇다고 싱글톤 기반의 빈에서 클래스 필드를 맘대로 교체할 수도 없는 노릇이고,
 
좀 더 고차원 레이어에 영향이 안가는 방법의 코드를 작성 할 수는 없을까?
 
있다, Enum과 Provider를 사용하면 된다.
Spring Container는 등록된 모든 Bean에 대한 제어가 가능하다.
일단 등록되어 있으니 가져오는 것도 가능하다.
구조적으로는 위에 if문을 사용하는 것과 차이가 없지만,
 
적어도 인터페이스를 호출 하는 부분을 타입이나, 유형 때문에 바꿀 일은 없을 거다.
 
예시 코드 들어간다.
 
참고로 제 코드가 좋은 코드는 아닙니다용..제발...
 

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

//    private PayService payKakaoService;
//    private PayService paySamsungService;
//    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao(PayKakaoService.class), Samsung(PaySamsungService.class), Naver(
            PayNaverService.class);

        private final Class<? extends PayService> clazz;
    }

    public class PayServiceProvider {

        private static ApplicationContext applicationContext;
        private static Map<PayType, Class<? extends PayService>> payServiceMap = new HashMap<>();

        static {
            for (PayType payType : PayType.values()) {
                payServiceMap.put(payType, payType.getClazz());
            }
        }

        public static PayService service(PayType payType) throws Exception {
            Class<? extends PayService> payServiceClass = payServiceMap.get(payType);

            if (payServiceClass == null) {
                throw new Exception();
            }

            return applicationContext.getBean(payServiceClass);
        }
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) throws Exception {

//        PayService ps = null;
//
//        if (payType == PayType.Kakao) {
//            ps = payKakaoService;
//        }
//
//        if (payType == PayType.Samsung) {
//            ps = paySamsungService;
//        }
//
//        if (payType == PayType.Naver) {
//            ps = payNaverService;
//        }
//
//        ps.pay();

        PayServiceProvider.service(payType).pay();

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() throws Exception {
        this.pay(PayType.Samsung);
    }

일단, 해당 코드는 provider에서 사용하는 applicationContext에 대한 주입이 없긴하다.
요건 이제 @PostConstruct를 사용해서 주입하면 되긴합니다요.
물론 주입하는 함수도 provider에 static으로 만들어 줘야 합니다.
(대충 읽고 코드만 복붙하면 안되게 하려는 함정...)
 
대강 저런식이면, 앞으로 앱쁠페이든, 티머니든 추가되면 Enum과 구현체의 추가만으로 솔리드 뭐시기를 위배하지 않으면서 해결가능하지 않을까?
 
사실 Provider 구현하기 귀찮다.
유지 보수 측면에서는 좋긴하나, 처음 구현할때 걍 if else 쓰는게 백배 편하다.
시간 비용을 초기에 쓸거냐 나중에 쓸거냐 차이긴한데,
 
아래는 좀 더 편한 방법

private List<PayService> payServices;

@PostMapping("/payment")
public ResponseEntity<?> pay(PayType payType) throws Exception {

    payServices.stream().filter(payService -> payService.check(payType)).findFirst()
        .ifPresent(PayService::pay);
    return ResponseEntity.ok().build();
}

 
스프링은 주입시 배열로 필드를 선언하면 해당 인터페이스에 해당하는 빈들을 모두 가져 오는데요, 이걸 몰랐던건 아닌데, 이렇게 응용할 생각은 못 했네요. 멍청이...
 
아는분이 알려주신건데 아래 링크 통해 배우셨대요!
https://youtu.be/3MTf43_RcVM

 

문제가 된다면 삭제 하도록 할게요.

 

개인정보 교육 꼭 들어야 합니다!

하지만 이런 방법으로 패스도 가능하다는걸 알고 있다면 좋겠죠?

 

 

function goLast(){
	setTimeout(() => {  
    	window.document.getElementById('contentsFrame').contentWindow.document.getElementById('frame').contentWindow.document.getElementById('MPlayer').currentTime=999999
		setTimeout(() => {
			window.document.getElementById('contentsFrame').contentWindow.document.getElementById('frame').contentWindow.document.getElementsByClassName('next')[0].click();
		}, 1000)
	}, 2000)
}

window.document.getElementById('contentsFrame')
.contentWindow.document
.getElementById('frame')
.addEventListener('load', goLast, false);

goLast();

 

goLast 함수는 비디오 플레이어의 시간 컨트롤러의 값을 매우 크게 변경하고(종료지점으로), 넥스트 페이지 버튼을 클릭하게 해줌.

그리고 이 함수를 최초 실행하게하고, frame(iframe) load이벤트에 걸어둠.

다음페이지가 호출되고 frame의 내용이 변경되고 로드가 완료되면 다시 goLast를 실행하는 구조.

 

돔 구조만 파악하면 재미삼아 간단하게 짤 수 있는 코드,

 

실행은 개발자 도구의 콘솔에서...

 

현재 위 사이트는 body > iframe > iframe > video 와 같은 형태로 되어 있어요.

화면 기록 2022-11-29 오후 3.58.01.mp4
17.10MB

spring 에서 log file 경로를  root인 절대 경로에서 시작하기 마련인데...

/logs/app-name/...

 

뭐 위와 같은 형태의 경로에 저장한다고 하자,

 

그럼 윈도우에서는 위 경로는 알아서 C:\logs...

이 처럼 만들어 줄 것이다.

 

mac은 해당 경로에 대한 접근 권한이 없어서 오류가 나서...

 

ln -s 소프트 심볼릭 링크를 이용하려 했지만,

이것도 루트는 리드온리라고;;

 

구글링해보니, 루트에 쓰기권한을 주는 방법도 있던데, 위험해 보이고,

 

다르게 심볼릭링크를 만드는 방법을 찾아보았다.

 

일단 루트에 /logs 라는 경로를 만들고 싶다고 가정하자,

 

$ sudo vi /etc/synthetic.conf

# /etc/synthetic.conf
logs    /Users/${userid}/logs
# /Users의 logs는 mkdir로 생성

위 처럼 해주고 재부팅을 해보면...

 

root 에서 ls를 입력해보면, logs라는 심볼릭 링크가 생성된걸 확인 할 수 있다.

 

주의할 점은 synthetic.conf 파일 내의 공백은 tab이라는 점...

> ssh-add -l
> sudo ssh-add -K [key file path]

 

재부팅하면 사라지는...마법을 방지.

https://docs.google.com/spreadsheets/d/1BYsqJlHgP2DQnUZMnTLkU3jrnmUsiLd2BifTcdpECDU/copy

 

Google Sheets - 스프레드시트를 작성하고 수정할 수 있으며 무료입니다.

하나의 계정으로 모든 Google 서비스를 스프레드시트로 이동하려면 로그인하세요.

accounts.google.com

 

위 시트를 복사하고, 확장 프로그램 > App Script 로 가서 필요한 스크립트를 수정(수정안해도 상관없음)

스크립에서는 구글의 메일앱을 사용한다.

https://developers.google.com/apps-script/reference/mail/mail-app

 

Class MailApp  |  Apps Script  |  Google Developers

Send feedback Class MailApp MailApp Sends email. This service allows users to send emails with complete control over the content of the email. Unlike GmailApp, MailApp's sole purpose is sending email. MailApp cannot access a user's Gmail inbox. Changes to

developers.google.com

 

해당 스크립트는 꼭 저장 후 파일 > 버전관리 에서 새로운 버전을 발행 후 게시 > 웹앱게시를 통해 게시를 해줘야 한다.

그럼 해당 링크를 html의 form에서 action으로 지정해주면 된다.

 

 

위 화면에서 버전에 대한걸 저장하고,

위 화면에서 최신 버전을 가져와서 배포 준비를 한다.

최초에는 메일앱을 사용하기 위한 권한 부여를 진행한다.

 

위 주소를 복사해서 form의 action에 입력한다.

form의 각 필드의 name property는 시트에서 지정된 컬럼명을 사용한다.

 

이렇게 해서 submit을 하면 해당 내용은 스프레드 시트에 쌓임과 동시에 메일로 전송이 된다.

script 에서 TO_ADDRESS 변수를 지정한 경우 지정된 메일로 메일이 가며, 주석처리로 냅둔 경우 data-email에 적힌 메일로 전송하게 된다.

 

추가적으로 

https://github.com/dwyl/learn-to-send-email-via-google-script-html-no-server/blob/master/google-apps-script.js

 

위 스크립트 처럼 xhr을 통해 폼데이터 전송으로 사용할 경우 깔끔하게 적용 가능하다.

 

위 깃 주소에 샘플페이지 또한 포함되어 있다.

 

본 글은 아래 글을 참고해 작성하였으며, 아래 글은 더욱 자세히 써있다.

 

 

https://kutar37.tistory.com/entry/%EC%A0%95%EC%A0%81-HTML-form%ED%83%9C%EA%B7%B8%EC%97%90%EC%84%9C-%EB%A9%94%EC%9D%BC%EB%B3%B4%EB%82%B4%EA%B8%B0-Google-Apps-Mail

 

정적 HTML form태그에서 메일보내기 : Google Apps Mail

html에서 mailto를 사용하면, 구현은 물론 간단합니다만 outlook과 같은 쓸데없는 프로그램을 실행해 사용자에게 불편을 초래합니다. 아래와 같이 서버 없는 순수 HTML과 자바스크립트로 메일보내기

kutar37.tistory.com

 

컴포넌트끼리 통신을 하기 위해 props와 $emit 같은 아이들을 사용하고

좀 더 복잡한 관계를 위해 provide/inject 를 사용한다고 했었더랬지...

 

그리고 대규모라면? 더더더 복잡하다면...

 

중앙집중식 저장소 역할을 하는 Vuex가 있다.

(예전에 투비소프트 제품에서 글로벌데이터셋과 비슷한 느낌)

처음에 프로젝트 생성할때 지정해도 되고~(추천)

npm install vuex 해도 되고~

 

자주 사용되는 코드 데이터나, 사용자 정보등을 담아주면 편하겠지 싶은...

 

처음 생성시 지정하면 알아서 store라는 폴더와 그 안에 기초적인 내용물이 생성되고,

main.js 에선 

import store from './store'

위와 같은 코드를 포함 하고 있게 된다.

 

1. state

사용할 변수들을 지정하는 곳!

사용 방법은 아주 간단하다.

2. mutations

이거 잘 모르겠다...

state에 저장된 값을 바꾸려면 직접 바꾸지 말고 이걸 쓰라는데...

직접 바꿔짐...

(strict를 true로 하면 오류남. 근데 운영모드에선 이거 하면 값 변경 감지에 따른 성능에 이슈가 있다고 하니...)

값 변경에 따른 추적을 위해서 라고 하는데~~

여튼 직접 바꾸지말고 이걸 사용하는걸로, 

페이로드 전달이 가능하고 동기방식이라고함.

포인트는 state의 값을 변경 한다는 점!

 

 

3. getters

이건 computed를 생각하면 된다.

기존 데이터(state)를 활용한 추가연산의 로직이 있는 경우 해당 로직을 반복사용하기보단...

computed를 쓰듯이, getters를 활용하면 되는!

 

4. actions

mutations와 비슷하지만 다른!

비동기 처리를 위주로 하고 state를 직접 변경하는 것이 아닌, 내부에서 mutations을 호출하는 형태로 사용한다.

 

 

 

 

state와 mutations가 뭔가 private한 느낌이라면

getters와 actions는 public한 느낌이랄까?

 

간단한 게시판 위주의 커뮤니티 같은 사이트를 제작하려 했으나, 

당분간 일이 바빠질 예정으로 미뤄야 할듯...ㅠ 

지난 글에서 부모에서 자식 컴포넌트로 데이터를 내려주는 props에 대해 작성하였는데!...

props의 단점은 계층구조가 복잡해지면 복잡해 질 수록~~

전달의 전달의 전달 자체가 복잡해 진다는것...ㅠ

 

이럴때 쓰는것이 Provide(상위)/Inject(하위)

 

상위 컴포넌트에서

...
  provide: function () {
    return {
      testProvide: 'testValue'
    }
  },
...

작성하고

 

하위 컴포넌트에서

...
  inject: ['testProvide'],
  mounted () {
    console.log(this.testProvide)
...

위 처럼 작성하면 됨!

계층 구조에 관계없이 순서만 맞으면 inject로 사용 가능하긴한데...

단점은 어떤 상위 컴포넌트에서 오는 데이터인지 알 수 없다는 점!

 

모르면 파일내 찾기라는 방법이 있징!!

혹은 컴포넌트를 알 수 있는 어떤 id라던가? 그런걸 옵션에 지정해 주면 어떨런지?

데이터통신하면 학부시절 D 맞았던 데이터통신을 잊을래야 잊을 수가 없다.

컴공 암기과목 끝판왕. 

 

아래는 Parent.vue

...
    <Children :p-image-style="imageStyle" :p-img-src="imgSrc" @for-child="forChild" ref="imgComp" />
    <div>
      <button @click="callChild">자식함수호출</button>
    </div>
...
import Children from '@/components/Children.vue'

...
  components: {
    Children
  }
...

아래는 Children.vue

<template>
  <div>
    <img :src="imgSrc" :style="imageStyle" ref="img">
    <div>
      <button @click="callParent">부모함수호출</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Children',
  props: {
    pImgSrc: {
      type: String,
      required: true
    },
    pImageStyle: {
      type: Object,
      required: false
    }
  },
  mounted () {
    this.imgSrc = this.pImgSrc
    this.imageStyle = this.pImageStyle
  },
  data: function () {
    return {
      imageStyle: {
        maxWidth: '1000px'
      },
      imgSrc: ''
    }
  },
  methods: {
    forParent () {
      console.log('부모 컴포넌트에서 자식 컴포넌트의 함수를 호출')
    },
    callParent () {
      const arg01 = 'value-arg01'
      const arg02 = 'value-arg02'
      this.$emit('for-child', arg01, arg02)
    }
  }
}
</script>

<style scoped>

</style>

 

하나의 컴포넌트에 다른 컴포넌트 요소를 사용하기 위해, 위 처럼 작성을 할 수 있음

Children Element에서 다양한 속성들이 지정되어 있는데, 이에 대하여 알아보자!

 

1. props를 이용한 데이터 전달

children.vue에 props를 보면 pImgSrc와 pImgStyle이 있고, 

parent.vue에는 Children 엘리먼트에

:p-image-style="imageStyle" :p-img-src="imgSrc" 

위와 같은 내용이 있다.

 props에는 카멜로 표기되어 있으나, 위처럼 케밥으로 변경하여 표기해도 되고, 그냥 카멜로 표기해도 상관없음!

 

내용 자체는 간단한...

p-img-src 에 imgSrc를 전달 한다 라는 의미!

 

여기서 중요한 점은 v-bind(:)를 사용했을 때와 아닌때의 차이!

:를 사용하지 않았을 경우에는 단순히 문자열을 전달하는 것이고,

반대로 사용했을때는 data의 값이나 정의된 함수도 전달이 가능한점!

 

주의할점은

:를 사용하면 부모 컴포넌트에서 시시각각 변하는 데이터도 그대로 반영되기 때문에, 자식컴포넌트에서 prop을 그대로 사용하기보단 data로 전달 받아서 사용하는게 좋다고 한다.

 

children.vue의 mounted()를 보면 알 수 있쒀.

 

또한 props에서 type, required, default를 지정 할 수 있는데,

객체나 배열은 default가 팩토리 함수로 생겨먹어야 한다고 한다.

 

2. 부모 컴포넌트에서 자식놈 함수 호출(or 데이터변경)!

이건 뭐 사실 함수뿐 아니라 자식놈 data도 손 쉽게 수정은 가능하다. 바로...

children element의 ref="imgComp" 때문인데...

parent.vue에서 접근하는 방식은 this.$refs.imgComp다...

 

이게 chilren.vue에서는 this = this.$refs.imgComp 같은 맥락이라고 보면 된다.

고로 this.forParent() = this.$refs.imgComp.forParent()

이런셈...

 

 

3. 자식 컴포넌트에서 부모 함수 호출!

이것도 매우 간단하다!

일단 부모에 함수가 있어야 하고,

...

    forChild (arg01, arg02) {
        console.log('for-child', arg01, arg02)
    }

...

자식 컴포넌트를 엘리먼트로 작성해줄때 해당 함수를 사용 할 수 있게끔 속성으로 선언을 해준다.

@for-child="forChild"

위처럼...

이것도 마찬가지로 케밥이든 카멜이든 알아서 잘 되고,

좌변은 자식컴포넌트에서 사용할 이름이고 우변은 부모컴포넌트에 선언된 함수 이름이다.

호출은 자식 컴포넌트에서

...

    callParent () {
      const arg01 = 'value-arg01'
      const arg02 = 'value-arg02'
      this.$emit('for-child', arg01, arg02)
    }

...

 

위 부분을 참고하시믄 되겠다~

자 Data는 지난 글까지해서 대강 끝났어,

이제 Event에 대해 알아볼건데, 여기서는 Mehtods에 만들어진 애들을 사용 할 수 있어.

 

this.$emit 을 사용한 상하위 컴포넌트 간의 통신은 다루지 않을 생각이야, 그냥 쓰면 되는거라...

 

일단 이벤트는 클릭~ 키입력~ 변경~ 뭐 대강 이 정도? 

 

기존에 elements에 onchange, onclick 이런 속성을 사용했다면,

v-on:click, v-on:change, v-on:keyup 이렇게 할 수 있지,

그리고 v-on:이라는 디렉티브는 v-bind:를 :로 바꿀 수 있는 것 처럼,

@로 바꿀수 있어.

v-on:keyup >> @keyup

 

코드
결과

 

자 보면 함수 하나로...세가지 이벤트를 하고 있지

함수 3개 만들기 귀찮았어

 

어때 쉽지? 

 

computed & watch

 

그리고 methods와 비슷하게 computed와 watch가 있는데...

둘의 공통점은 함수 형태를 template에서 data처럼 사용 할 수 있다는거야.

 

근데 watch는 선언된 함수 안에서 사용된 데이터의 변경이 발생하지 않으면, 작동을 안한다는거지!

 

즉, computed는 무조건 실행, watch는 변경시에만 실행 이라는 차이가 있어.

+ Recent posts