728x90
반응형

1. Basic Controller

controller 파일을 만들어준다.

/src/main/java/com.example.demo/controller/TeepoController

 

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TeepoController {

    @GetMapping("teepo-string")
    @ResponseBody
    public String teepoString(@RequestParam("name") String name) {
        return "hello-template" + name;
    }
}

여기서 @ResponseBody 는 API 응답으로 Body 안에 원하는 데이터를 넣어주겠다는 뜻이다.

 

브라우저에서 접속해보면

return 값이 정상적으로 반환되는 것을 확인할 수 있다.

 

 

2. JSON return Controller

json 형식으로 반환할 땐 아래와 같이 할 수 있다.

    @GetMapping("teepo-api")
    @ResponseBody
    public Teepo teepoApi(@RequestParam("name") String name) {
        Teepo teepo = new Teepo();
        teepo.setName(name);
        return teepo;
    }
    static class Teepo {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }

 

서버를 키고 확인해보면,

이와같이 json 형식으로 데이터를 받은 것을 확인할 수 있다.

 

3. Member 객체 생성

domain 폴더를 만들고 Member 에 대한 객체를 만들어보자.

com.example.demo/domain/Member

package com.example.demo.domain;

public class Member {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

멤버 리포지토리 인터페이스를 만든다.

com.example.demo/repository/MemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;
import java.util.List;
import java.util.Optional;


public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

여기서 Optional 이란 null 값을 처리하기 위해 한 번 감싸서 선언함을 의미한다.

 

리포지토리 메모리 구현체를 파일을 만든다.

com.example.demo/repository/MemoryMemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;

import java.util.*;

/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    public void clearStore() {
        store.clear();
    } 
}

 

4. Member Test

/src/text/java/com.example.demo/repository/MemoryMemberRepositoryTest

package com.example.demo.repository;

import com.example.demo.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;

import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        repository.save(member);
        //then
        Member result = repository.findById(member.getId()).get();
        assertThat(result).isEqualTo(member);
    }
    @Test
    public void findByName() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        Member result = repository.findByName("spring1").get();
        //then
        assertThat(result).isEqualTo(member1);
    }
    @Test
    public void findAll() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        List<Member> result = repository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
    }
}

여기서 get 메소드는 Optional 객체 에서 데이터를 빼오는 방법이다.

 

5. Member Service

/src/main/java/com.example.demo/service/MemberService

package com.example.demo.service;

import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new
            MemoryMemberRepository();
    /**
     * 회원가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증 memberRepository.save(member);
        memberRepository.save(member);
        return member.getId();
    }
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });

    }
    /**
     *전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    } }

 

6. Member Service Test

먼저 회원 서비스 코드를 DI 가능하게 변경한다.

/src/main/java/com.example.demo/service/MemberService

    ...
    
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...

 

클래스 안에서 마우스를 클릭하고 command + shift + t 를 누르면 Test를 자동으로 생성해줄 수 있다.

 

/src/test/java/com/example.demo/service/MemberServiceTest

package com.example.demo.service;

import com.example.demo.domain.Member;
import com.example.demo.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");

        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));//예외가 발생해야 한다.

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

 

 

7. 컴포넌트 스캔과 자동 의존관계 설정

Service 를 사용하는 Controller를 만들어 보자.

/src/main/java/com.example.demo/controller/MemberController

package com.example.demo.controller;

import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.

@Controller 가 있으면 스프링 빈으로 자동 등록된다.

 

 

Service 와 Repository에도 annotation 을 달아주자.

...
@Service
public class MemberService {
...

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
...

 

...
@Repository
public class MemoryMemberRepository implements MemberRepository {
...

 

8. 자바 코드로 직접 스프링 빈 등록하기

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.

 

/src/test/java/com.example.demo/SpringConfig

package com.example.demo;

import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;
import com.example.demo.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

이렇게 하면 스프링이 실행되면서 의존성을 주입해준다.

728x90
반응형
728x90
반응형

1. Java 11 과 intelliJ를 설치한다.

 

https://www.oracle.com/java/technologies/downloads/#java11-mac

 

Download the Latest Java LTS Free

Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.

www.oracle.com

 

https://www.jetbrains.com/idea/download/#section=mac

 

Download IntelliJ IDEA: The Capable & Ergonomic Java IDE by JetBrains

Download the latest version of IntelliJ IDEA for Windows, macOS or Linux.

www.jetbrains.com

 

2. 스프링 부트 스타터 사이트로 이동하고 프로젝트를 만든다.

https://start.spring.io/

 

 

3. 아래처럼 생성해주고, intelliJ 에서 build.gradle 파일로 프로젝트를 열어준다.

 

4. build 버튼을 눌러서main 함수를 실행해본다.

터미널 로그를 확인해보면 8080 포트에 서버가 동작하는 것을 확인할 수 있다.

 

5. localhost:8080 으로 들어가본다.

 

이렇게 뜨면 잘 된 것이다.

 

참고로 아래와 같이 설정을 해두면 기존보다 빨리 build 할 수 있다.

 

6. 화면을 보여주기위해 html파일을 만들어보자.

src/main/resources/static/index.html

<!DOCTYPE HTML>
    <html>
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
    </html>

 

다시 서버를 실행해보면,

잘 뜨는 것을 확인할 수 있다.

 

728x90
반응형
728x90
반응형

1. jest 모듈을 설치한다.

$ npm i jest @types/jest ts-jest

 

2. jest 설정파일을 만든다.

/jest.config.ts

module.exports = {
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

 

3. pakage.json 에 테스트 script를 수정해준다.

  "scripts": {
    "test": "jest",
    "start": "node dist/app.js",
    "build": "tsc -p .",
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/app.ts"
  },

 

4. test 파일을 만들고 아래처럼 테스트 파일을 만들어준다.

 

/tests/routers/one.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import { logger } from '../../config/winston';

// node-json-db
import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    alertCloseAccept,
    alertCloseDismiss,
    promptCloseHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,
    deleteAllCookie,

    fileRegister,
    
    findElementById,
    findElementByName,
    findElementByXpath,
    findElementByCss,

    JqChangeValueByID,
    JqRemoveAttribute,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

    popupClose

 } from '../../modules';

 describe('GET /one', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('url 잘 뜨는지 확인', async () => {
        try {
            // 브라우저에 접속
            await driver.get('https://typo.tistory.com/');

            // 현재 주소 가져오기
            const text = await driver.getCurrentUrl();

            // test
            expect(text).toEqual('https://typo.tistory.com/')
        }
        catch(Err) {
            console.log(Err)
            logger.debug(Err)
            throw Error;
        }
    })

 })

 

 

/tests/middlewares/http.test.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'
import { httpLoggingMiddleware } from '../../middlewares';

describe('MiddleWare http', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = {};
        mockResponse = {
            json: jest.fn()
        };
    });

    test('http 로깅 미들웨어 테스트', async () => {
        try {
            await httpLoggingMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
            
            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

reqest, response, next 를 mock으로 만들어 테스트해준다.

 

/tests/middlewares/timeInterval.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'

import { timeIntervalMiddleware } from '../../middlewares';

describe('MiddleWare timeInterval', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = { originalUrl : '/one' };
        mockResponse = {
            json: jest.fn()
        };
    });

    test('timeInterval 로깅 미들웨어 테스트', async () => {

        try {
            await timeIntervalMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);

            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/db/expireHandler.ts

import express, { Request, Response, NextFunction } from 'express';

// node-json-db
import { db } from '../../../config/nodejsondb';

// logger
import { loggerDebug } from '../../../config/winston';
import { expireHandler } from '../../../modules';

describe('Module expireHandler', () => {
    let dbPath : string;

    beforeEach(()=> {
        dbPath = '/one'
    });

    test('30분 지난 데이터 삭제 및 확인', async () => {
        try {
            let nowTime = new Date(); // 현재 시간

            // 30분 지난 데이터 삭제
            await expireHandler(dbPath);

            // 데이터 베이스 안 데이터들
            const datalist = await db.getData(dbPath);

            let index = 0; // 기준점이 되는 index

            // 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
            await datalist.map(async (item : any, index2: number) => {
                let beforeTime =  new Date(item.date); // 데이터들 저장된 시간
                let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
                // 저장된 지 30분이 지나면 index 체크
                if(diff > 30) {
                    index = index2;
                }
            })

            // ----- 30분 지난 데이터 없는지 확인 -----
            expect(index).toEqual(0);

        }
        catch(Err) {
            console.log("Err : ",Err)
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/alertHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,
    findElementById,

} from '../../../modules';

 // window type 선언
declare const window: typeof globalThis;

 describe('Module alertHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/alerts/alert-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('alert 테스트 ( alert 텍스트가 잘 나오는지 )', async () => {
        try {
            await (await findElementById(driver,'alertexamples')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alertText
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I am an alert box!');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('prompt 테스트', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alert text 사용하는 곳
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I prompt you');

        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/cookieHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,

    findElementById,
    findElementByName,

 } from '../../../modules';
  

 describe('Module cookieHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/cookies/adminlogin.html');

        // 로그인 및 쿠키생성
        await (await findElementByName(driver,'username')).sendKeys('Admin');
        await (await findElementByName(driver,'password')).sendKeys('AdminPass');
        await (await findElementById(driver,'login')).click();
    },15000)

    afterEach(async () => {
        await driver.quit()
    })

    test('쿠키 추가하기', async () => {
        try {
            // 쿠키추가
            await addCookie(driver,{name : "name", value : "teepo"});

            // 쿠키 가져오기
            const cookies = (await driver.manage().getCookie('name')).value;

            // 쿠키 확인
            expect(cookies).toEqual('teepo');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getOneCookie(driver,'loggedin');

            // 쿠키 확인
            expect(result.value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getAllCookie(driver);

            // 쿠키 확인
            expect(result[0].value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/driverHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,
 } from '../../../modules';
  

 describe('Module driverHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('webdriver가 잘 반환되는지 테스트', async () => {
        try {
            driver = await getDriverHandler();
            expect(driver).toBeInstanceOf(WebDriver);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/fileHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    fileRegister,

    findElementById,
    findElementByName,

    getDriverHandler,

 } from '../../../modules';
  

 describe('Module fileHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/file-upload-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

     test('fileRegister', async () => {
         try {
             await fileRegister(driver,By.id('fileinput'),['파일 위치'])
             await (await findElementByName(driver,'fileinput')).click()
             var text = await (await findElementById(driver,'uploadedfilename')).getText()
             expect(text).toEqual('image.jpg')
         }
         catch(Err) {
             loggerDebug.info(JSON.stringify(Err))
         }
     })

})

 

/tests/modules/selenium/findHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,
    findElementsById,
    findElementByName,
    findElementByXpath,

    findElementByClass,
    findElementsByName,
    findElementsByXpath,
    findElementsByClass

 } from '../../../modules';
  

 describe('Module findElementHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('findElementById', async () => {
        try {
            let text = await (await findElementById(driver,'p1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByName', async () => {
        try {
            let text = await (await findElementByName(driver,'pName1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByXpath', async () => {
        try {
            let text = await (await findElementByXpath(driver,'//*[@id="p1"]')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByClass', async () => {
        try {
            let text = await (await findElementByClass(driver,'explanation')).getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

})


describe('Module findElementsHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.close()
    })

    test('findElementsById', async () => {
        try {
            let text = await (await findElementsById(driver,'p1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByName', async () => {
        try {
            let text = await (await findElementsByName(driver,'pName1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByXpath', async () => {
        try {
            let text = await (await findElementsByXpath(driver,'//*[@id="p1"]'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByClass', async () => {
        try {
            let text = await (await findElementsByClass(driver,'explanation'))[0].getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
        });
})

 

/tests/modules/selenium/jqueryHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    JqChangeValueByID,
    JqSetAttribute,
    JqRemoveAttribute,
 } from '../../../modules';
  

 describe('Module jqueryHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/html5-form-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('JqChangeValueByID', async () => {
        try {
            let value = "teepo";
            await (await findElementById(driver,'email-field')).clear()
            await JqChangeValueByID(driver,'email-field',value);
            let text = await (await findElementById(driver,'email-field')).getAttribute('value');
            expect(text).toEqual(value)
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqSetAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBe("true")
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqRemoveAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            await JqRemoveAttribute(driver,'email-field','disabled');
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBeNull()
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/navigationHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

 } from '../../../modules';
  

 describe('Module navigation', () => {
    // 웹드라이버 설정
    let driver : WebDriver;
    let url = 'https://testpages.herokuapp.com/styled/index.html';

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,url)
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('naviGet', async () => {
        try {
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviBack', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviForward', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            await naviForward(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual('https://testpages.herokuapp.com/styled/basic-web-page-test.html');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

    test('naviRefresh', async () => {
        try {
            await naviRefresh(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/popupHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,

    naviGet,

    popupClose

 } from '../../../modules';
  

 describe('Module popupHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,'https://testpages.herokuapp.com/styled/alerts/alert-test.html');
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('popupClose', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();
            popupClose(driver);
            let windowNmber = await (await driver.getAllWindowHandles()).length;
            expect(windowNmber).toEqual(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

5. 빌드하고 테스트해본다.

$ npm run build
$ npm run test

728x90
반응형
728x90
반응형

재사용성이 많은 함수들은 모듈로 만들어두면 매우 편하다. 예를 들면 아래처럼 driver 를 선언하는 함수는 각 라우터마다 동일하게 필요하다.

        // 웹드라이버 설정
        let driver = await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build();

 

또한 driver 메소드들을 우리 입맛에 맞게끔 만들어 두면 개발할 때도 훨씬 수월할 것이다.

 

참고로 selenium javacscript의 메소드에 관한 내용들은 아래 사이트에서 자세히 확인할 수 있다.

 

Index

 

www.selenium.dev

 

 

The Selenium Browser Automation Project

Selenium automates browsers. That's it!

www.selenium.dev

1. modules 밑에 모듈을 내보내는 파일을 만든다.

 

/mdules/selenium/driverHandler.ts

import webdriver, { WebDriver } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import firefox from 'selenium-webdriver/firefox'

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// new driver 반환 함수
export const getDriverHandler = async () : Promise<WebDriver> => {
    const driver =  await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build();

    return driver;
}

 

/modules/index.ts

export * from "./db/expireHandler"

export * from "./selenium/driverHandler"

 

2. 라우터에서 모듈을 가져와 쓴다.

...

// modules
import { 
    getDriverHandler,
 } from '../modules';
 
 ...
 
// 웹드라이버 설정
let driver = await getDriverHandler();

 

이런식으로 여러개의 모듈을 만들 수 있다.

 

/modules/selenium/alertHandler.ts

import { until, WebDriver } from "selenium-webdriver";

// alert 확인 누르기
export const alertCloseAccept = async (driver : WebDriver ) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();

    // alert text 사용하는 곳
    let alertText = await alert.getText();

    // 확인버튼 클릭
    await alert.accept();

    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent();
}

// alert 닫기 누르기
export const alertCloseDismiss = async (driver : WebDriver ) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();

    // alert text 사용하는 곳
    let alertText = await alert.getText();

    // 확인버튼 클릭
    await alert.dismiss();

    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent();
}

// prompt 핸들러
export const promptCloseHandler = async (driver : WebDriver, text : string) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();
    
    // prompt text 전송
    await alert.sendKeys(text)
    
    // 확인버튼 클릭
    await alert.accept();
    
    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent(); 
}

 

/modules/selenium/navigationHandler.ts

import { WebDriver } from "selenium-webdriver";

export const naviGet = async ( driver : WebDriver, url : string) => {
    await driver.get(url);
}

export const naviBack = async (driver : WebDriver) => {
    await driver.navigate().back();
}

export const naviForward = async (driver : WebDriver) => {
    await driver.navigate().forward();
}

export const naviRefresh = async (driver : WebDriver) => {
    await driver.navigate().refresh();
}

 

/modules/selenium/cookieHandler.ts

import {  IWebDriverCookie, WebDriver } from "selenium-webdriver";

// 쿠키 추가하기
export const addCookie = async (driver : WebDriver, cookie : IWebDriverCookie ) => {
    await driver.manage().addCookie(cookie)
}

// 쿠키 하나 가져오기
export const getOneCookie = async (driver : WebDriver, key : string) : Promise<IWebDriverCookie> => {
    return await driver.manage().getCookie(key);
}

// 쿠키 전부 가져오기 
export const getAllCookie = async (driver : WebDriver) : Promise<IWebDriverCookie[]> => {
    return await driver.manage().getCookies();
}

// 쿠키 하나 지우기
export const deleteOneCookie = async (driver : WebDriver,  key : string) => {
    return await driver.manage().deleteCookie(key);
}

// 쿠키 전부 지우기
export const deleteAllCookie = async (driver : WebDriver) => {
    return await driver.manage().deleteAllCookies();
}

 

/modules/selenium/popupHandler.ts

import { WebDriver } from "selenium-webdriver";

export const popupClose = async (driver : WebDriver ) => {

    // window handles 0 빼고 모든 윈도우 닫음
    (await driver.getAllWindowHandles()).map(async (item : string, index: number)=> {
        if(index > 0) {
            await driver.switchTo().window((await driver.getAllWindowHandles())[index]);
            driver.close();
        }
    })

    // 메인 윈도우로 이동
    await driver.switchTo().window((await driver.getAllWindowHandles())[0]);

}

 

/modules/selenium/jqueryHandler.ts

import { By, WebDriver } from "selenium-webdriver";

// 해당 document의 value값을 바꿔준다.
export const JqChangeValueByID = async (driver : WebDriver, id : string, value: string ) => {
    await driver.executeScript(`document.getElementById('${id}').value='${value}';`)
}

export const JqSetAttribute = async (driver : WebDriver, id : string, attribute: string[] ) => {
    await driver.executeScript(`document.getElementById('${id}').setAttribute('${attribute[0]}','${attribute[1]}')`)
}

export const JqRemoveAttribute = async (driver : WebDriver, id : string, attribute: string ) => {
    await driver.executeScript(`document.getElementById('${id}').removeAttribute("${attribute}")`)
}

 

/modules/selenium/fileHandler.ts

import { Locator, until, WebDriver } from "selenium-webdriver";

// 경로에 맞는 파일 등록 ( 인증서 등 )
export const fileRegister = async (driver : WebDriver, func : Locator, path: string[] ) => { 
    let pathString : string = "";
    
    // 받은 배열을 줄바꿈을 포함한 문자열로 바꿔준다.
    path.map((item : string, index: number) => {
        if(index < path.length - 1) {
            pathString = pathString + item + " \n "
        }
        else {
            pathString = pathString + item
        }
    })
    
    const el = await driver.wait(until.elementLocated(func), 2000);
    return await driver.wait(until.elementIsVisible(el), 2000).sendKeys(pathString);

 

/modules/selenium/findHandler.ts

import { By, until, WebDriver } from "selenium-webdriver";

export const findElementById = async (driver: WebDriver, id: string, timeout = 2000) => {
    return await driver.wait(until.elementLocated(By.id(id)), timeout);
};
  
export const findElementByName = async (driver: WebDriver, name: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.name(name)), timeout);
};

export const findElementByCss = async (driver: WebDriver, css: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.css(css)), timeout);
};
  
export const findElementByXpath = async (driver: WebDriver, xpath: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.xpath(xpath)), timeout);
};

export const findElementByClass = async (driver: WebDriver, classname: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.className(classname)), timeout);
};

export const findElementByTagName = async (driver: WebDriver, tag: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.tagName(tag)), timeout);
};

export const findElementsById = async (driver: WebDriver, id: string) => {
  return  await driver.findElements(By.id(id));
};

export const findElementsByName = async (driver: WebDriver, name: string) => {
  return  await driver.findElements(By.name(name));
};

export const findElementsByCss = async (driver: WebDriver, css: string) => {
  return  await driver.findElements(By.css(css));
};

export const findElementsByXpath = async (driver: WebDriver, xpath: string) => {
  return  await driver.findElements(By.xpath(xpath));
};

export const findElementsByClass= async (driver: WebDriver, classname: string) => {
  return  await driver.findElements(By.className(classname));
};

export const findElementsByTagName= async (driver: WebDriver, tagName: string) => {
  return  await driver.findElements(By.tagName(tagName));
};

 

/modules/selenium/sleepHandler.ts

export const sleep = (ms : number) => {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

 

 

/modules/index.ts

export * from "./db/expireHandler"

export * from "./selenium/alertHandler"
export * from "./selenium/cookieHandler"
export * from "./selenium/driverHandler"
export * from "./selenium/fileHandler"
export * from "./selenium/jqueryHandler"
export * from "./selenium/navigationHandler"
export * from "./selenium/popupHandler"
export * from "./selenium/findHandler"
export * from "./selenium/sleepHandler"
728x90
반응형
728x90
반응형

1. node-json-db 와 shortid 를 설치해준다. 

$ npm i node-json-db shortid @types/shortid --save

 

2. config 폴더 아래 db 설정을 해준다.

/config/nodejsondb.ts

import { JsonDB } from 'node-json-db';
import { Config } from 'node-json-db/dist/lib/JsonDBConfig'

const db = new JsonDB(new Config("myDataBase", true, false, '/'));

db.push('/one/lastdate', "");
db.push('/one/data',[]);

export {db}

 

3. 라우터에 결과값을 저장하는 메소드를 사용해준다.

추가로 검색한 데이터는 로그로 남겨둔다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares';

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

// selenium
import webdriver from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';

// node-json-db
import { db } from '../config/nodejsondb';

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    // 웹드라이버 설정
    let driver = await new webdriver.Builder()
    .forBrowser('chrome')
    .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
    .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
    .build()
    try {

        // 브라우저에 접속
        await driver.get('https://typo.tistory.com/')

        // 현재 주소 가져오기
        const text = await driver.getCurrentUrl();

        // 저장 데이터
        const savedata = {
            id : shortid.generate(),
            date : new Date,
            result : text
        }

        // 데이터베이스에 저장 
        db.push('/one[]', savedata);

        // log 저장
        loggerInfo.info(JSON.stringify(savedata))

        res.send({success: true, result : text});
    }
    catch(Err) {
        loggerError.info(JSON.stringify(Err))
        console.log(Err)
        driver.close();
        res.send({success: false, result : Err})
    }
});

export default router;

 

여기서 중요한 점은 db에서 접근하는 경로 "/one" 와 라우터의 경로인 "/one"( '/'제외 )의 문자열를 일치하게 만들었다는 점이다.

이 방법으로 우린 미들웨어에서 라우터의 경로에 맞게 db에 손쉽게 접근할 수 있다. ( request.originalUrl 사용 )

아직은 이해가 안 될 수 있지만 차근차근 알게 될 것이다.

 

보통 크롤링 api 서버를 열어두고 제한을 안두면 무분별하게 라우터에 접속해서 userAgent 또는 접속 IP가 막힐 수도 있다.

때문에 userAgent를 바꿔주거나 IP를 우회하는 방법도 있지만 살짝 불법 느낌이 있어서 여기서는 전에 접속했던 시간을 비교해서 5분 간격으로 크롤링이 가능하게끔 만들어보겠다.

 

방법은 미들웨어에서 url path와 db path를 비교해서 전에 크롤링했던 데이터의 시간을 보고, 5분이 넘었는지 안넘었는지 확인 후

만약 5분이 안지난 상태면 res.send 메소드를 사용해 실패 메세지를 전송하도록 하겠다.

 

4. timeInterval 미들웨어를 만들어준다.

/middlewares/timeInterval.ts

import express, { Request, Response, NextFunction } from 'express';

const {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } = require('../config/winston');

// node-json-db
import { db } from '../config/nodejsondb';

// module
import { expireHandler } from '../modules';

export const timeIntervalMiddleware = async (req: Request, res: Response, next: NextFunction) => {
    try {
        // url
        let url = req.originalUrl;

        // 30분이 지난 데이터는 지워줌
        expireHandler(url);

        // 전체 데이터베이스 조회
        const data = await db.getData('/');

        // 처음 크롤링 할 때는 key값 ( 데이터베이스 주소 )이 없는 상태이므로 패스
        // 라우터 경로에서 '/'를 뺀 값이랑 데이터베이스의 키 값이랑 같다.
        if(!Object.keys(data).includes(url.split('/')[1])  || !(await db.getData(url + "[-1]"))) {
            next();
        }
        // 데이터가 존재할 경우 시간을 현 시간과 비교함
        else {
        	expireHandler(url);
        
            // 현재 시간
            let nowTime = new Date()
            let beforeTime = new Date(await (await db.getData(url + "[-1]")).date); // 제일 최신 데이터의 조회 시간
            let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 분 으로 계산한다.
            // 전 시간과 비교하여 차이가 5분 미만일 때
            if (diff < 5) {
                res.send({success: false, result: "5분 미만입니다."})
            }
            else {
                next();
            }
        }
        // next();
    }
    catch (Err) {
        console.log(Err)
        loggerError.info(Err)
        res.send({ success: false, result: "timeInterverError" });
    }
}

 

/middlewares/index.ts

export * from "./winston"
export * from "./timeInterval"

 

/routers/one.ts

...

// middlewares
import { httpLoggingMiddleware, timeIntervalMiddleware } from '../middlewares';

...

router.get('/', timeIntervalMiddleware ,httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {

...

 

5. 서버를 실행하고 브라우저를 켜보자.

 

크롤링이 시작하기도 전에 실패 메세지가 도착했다.

 

6. 일정 시간이 지난 데이터는 지워주는 모듈을 만든다.

node-json-db 에는 expire을 설정해주는 메소드가 없는거 같다. 때문에 일정 시간이 지난 데이터를 지워주는 모듈을 만들어봤다.

모듈화에 대해선 다음 포스트에서 제대로 다루겠다.

 

/modules/db/expireHandler.ts

// node-json-db
import { db } from '../../config/nodejsondb';

// logger
import { loggerError } from '../../config/winston';

export const expireHandler = async (dbPath: string) => {
    try {
        let nowTime = new Date(); // 현재 시간

        // 데이터 베이스 안 데이터들
        const datalist = await db.getData(dbPath);

        let index = 0; // 기준점이 되는 index

        // 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
        await datalist.map(async (item : any, index2: number) => {
            let beforeTime =  new Date(item.date); // 데이터들 저장된 시간
            let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
            // 저장된 지 30분이 지나면 index 체크
            if(diff > 30) {
                index = index + 1;
            }
        })

         // 설정한 시간을 기준점으로 잡고  그 이전에 만든 데이터를 다 삭제한다. (앞에서부터 shift)
        for (let i = 0; i < index; i ++) {
            await db.delete(dbPath + '/data[0]');
        }
    }
    catch(Err) {
        loggerError.info(JSON.stringify(Err))
    }
}

 

/modules/index.ts

export * from "./db/expireHandler"

 

 

미들웨어에 적용시킨다.

/middlewares/timeInterval.ts

...

// module
import { expireHandler } from '../modules';

export const timeIntervalMiddleware = async (req: Request, res: Response, next: NextFunction) => {
    try {
        // 30분이 지난 데이터는 지워줌
        expireHandler(req.originalUrl)
        
...
728x90
반응형
728x90
반응형

 

1. 제 어느정도 세팅했으니 selenium 을 사용해보자. 먼저 설치한다.

$ npm install --save @types/selenium-webdriver selenium-webdriver

 

2. 크롬드라이버의 위치를 .env 파일에 추가해준다. ( 환경 변수 설정 )

/.env

CHROMEDRIVER_PATH="C:\\Users\\Administrator\\Downloads\\chromedriver_win32\\chromedriver.exe"

 

3. one.ts 라우터 파일에 다음과 같이 현재 주소를 가져오는 로직을 구성해준다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares'

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

// selenium
import webdriver from 'selenium-webdriver'
import chrome from 'selenium-webdriver/chrome'

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    try {
        // 웹드라이버 설정
        let driver = await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build()

        // 브라우저에 접속
        await driver.get('https://typo.tistory.com/')

        // 현재 주소 가져오기
        const text = await driver.getCurrentUrl();

        // 현재 주소 출력
        console.log(text)

        res.send({success: true, result : text});
    }
    catch(Err) {
        logger.error(JSON.stringify(Err))
        console.log(Err)
        res.send({success: false, result : Err})
    }
});

export default router;

 

4. 서버를 실행하고 localhost:1234/one 로 접속한다.

정상적으로 뜨는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

Selenium을 파이썬에서만 쓸 수 있을 줄 알았는데 NodeJS에서도 된다는 말을 듣고 javascript로 만들어볼까 한다.

 

1. Chrome 을 다운로드한다.

https://www.google.com/chrome/index.html

 

Chrome 웹브라우저

더욱 스마트해진 Google로 더 간편하고 안전하고 빠르게.

www.google.com

 

2. 아래 사진처럼 크롬 버전을 익혀둔다.

3. 크롬 버전을 확인했으면 앞자리 수에 맞는 크롬 드라이버를 설치한다.

 

ChromeDriver - WebDriver for Chrome - Downloads

Current Releases If you are using Chrome version 104, please download ChromeDriver 104.0.5112.29 If you are using Chrome version 103, please download ChromeDriver 103.0.5060.53 If you are using Chrome version 102, please download ChromeDriver 102.0.5005.61

chromedriver.chromium.org

 

4. 윈도우로 설치해준다. ( 버전은 다를 수 있습니다. )

 

5. 압축을 해제한 후 해당 파일 경로를 알아둔다. ( 나중에 셀레니움에서 사용 )

 

 

5. NodeJS와 VSCode를 설치한다.

https://nodejs.org/ko/download/

 

다운로드 | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

개인적으로 생각하는 쓰기 좋은 vscode typescript 확장들

  1. ES7+ React/Redux/React-Native snippets
  2. ESLint
  3. Live Server
  4. Prettier

 

6. npm init 및 필요 구성 모듈을 설치해준다.

$ npm init
$ npm i express typescript ts-node nodemon @types/node @types/express

 

7. typscript를 위한 옵션을 설정하기 위해 다음 명령어를 입력해준다.

$ npx tsc --init

 

/tsconfig.json

{
  "compilerOptions": {
    "target": "es6", // 어떤 버전으로 컴파일할지 작성 
    "module": "commonjs", //어떤 모듈 방식으로 컴파일할지 설정
    "outDir": "./dist",	//컴파일 후 js 파일들이 생성되는 곳
    "rootDir": ".",	//루트 폴더
    "strict": true,	//strict 옵션 활성화
    "moduleResolution": "node",	//모듈 해석 방법 설정: 'node' (Node.js)
    "esModuleInterop": true,
     "jsx": "react"
  }
}

 

8. app.ts 파일을 만든다.

/app.ts

import express, { Request, Response, NextFunction } from 'express';

const app = express();


app.get('/welcome', (req: Request, res: Response, next: NextFunction) => {
    res.send('welcome!');
});

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

9. pakage.json 파일을 수정해준다.

...
"scripts": {
    "start": "node dist/app.js", 
    "build": "tsc -p .", 
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" app.ts"
  }
 ...
  • build : typscript 파일을 js 파일로 컴파일해준다.
  • start : 컴파일한 js 파일로 서버를 실행한다.

 

10. 빌드하고 시작해본다.

$ npm run build
$ npm run start

 

정상적으로 뜨는 것을 확인할 수 있다.

 

또한 내가 express를 쓸 때 기본적으로 사용하는게 body-parser와cookie-parser, dotenv인데, 자세한 내용은 검색하면 나온다.

 

나중에 json 형식으로 통신할 때나 쿠키를 사용할 때, 중요한 정보를 환경 변수로 사용할 때 대비하자.

 

11. body-parser , cookie-parser, dotenv를 설치한다.

$ npm install --save body-parser @types/body-parser cookie-parser @types/cookie-parser dotenv

 

 

12. app.ts 파일을 수정해준다.

import express, { Request, Response, NextFunction } from 'express';

// parser
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';

// dotenv
import dotenv from 'dotenv'
dotenv.config();

const app = express();

app.use(bodyParser.json())
app.use(cookieParser())


app.get('/welcome', (req: Request, res: Response, next: NextFunction) => {
    res.send('welcome!');
});

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

13. 좀 더 짜임새있는 구조를 위해 미리 폴더들을 만들어두자.

14. routers 밑에 파일을 만들고, app.ts 파일을 수정해준다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

router.get('/', async function (req: Request, res: Response, next: NextFunction) {
    res.send('welcome!');
});

export default router;

 

/app.ts

import express, { Request, Response, NextFunction } from 'express';

// parser
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';

// router
import oneRouter from './routers/one'

const app = express();

app.use(bodyParser.json())
app.use(cookieParser())

app.use('/one',oneRouter)

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

15. logging 미들웨어를 만들기 위해 다음 명령어로 설치해준다.

$ npm install winston @types/winston winston winston-daily-rotate-file

 

/config/winston.ts

import winston, { info } from 'winston';
import winstonDaily from 'winston-daily-rotate-file';

const logDir = 'logs';  // logs 디렉토리 하위에 로그 파일 저장
const { combine, timestamp, printf } = winston.format;

// Define log format
const logFormat = printf(info => {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */

const loggerError = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // error 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',  // error.log 파일은 /logs/error 하위에 저장 
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      watchLog: true,
      zippedArchive: true,
    }),
  ],
});

const loggerInfo = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // info 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/info',
      filename: `%DATE%.info.log`,
      maxFiles: 30,  // 30일치 로그 파일 저장
      watchLog: true,
      zippedArchive: true, 
    }),
  ],
});

const loggerHttp = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // http 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/http',
      filename: `%DATE%.http.log`,
      maxFiles: 30,  // 30일치 로그 파일 저장
      watchLog: true,
      zippedArchive: true, 
    }),
  ],
});

const loggerDebug = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // debug 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/debug', 
      filename: `%DATE%.debug.log`,
      maxFiles: 30,
      watchLog: true,
      zippedArchive: true,
    }),
  ],
});


// Production 환경이 아닌 경우(dev 등) 
if (process.env.NODE_ENV !== 'production') {
  loggerInfo.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),  // 색깔 넣어서 출력
      winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
    )
  }));
}

// Production 환경이 아닌 경우(dev 등) 
if (process.env.NODE_ENV !== 'production') {
  loggerError.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),  // 색깔 넣어서 출력
      winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
    )
  }));
}

if (process.env.NODE_ENV !== 'production') {
  loggerHttp.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),  // 색깔 넣어서 출력
      winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
    )
  }));
}

if (process.env.NODE_ENV !== 'production') {
  loggerDebug.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),  // 색깔 넣어서 출력
      winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
    )
  }));
}

export { 
  loggerInfo,
  loggerError,
  loggerHttp,
  loggerDebug,
 };

 

/middlewares/index.ts

export * from "./http"

 

/middlewares/http.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../config/winston'

interface LOGSTR {
    url : string;
    method : string;
    query? : Object;
    body? : Object;
}

export const httpLoggingMiddleware = async (req: Request, res: Response, next: NextFunction) => {

    let logStr: LOGSTR = {
        url : "",
        method: "",
        query: "",
        body: "",
    }

    try {
        // 접속 경로
        logStr.url = req.originalUrl;

        // 메소드
        logStr.method = req.method;

        switch (req.method) {
            case 'GET':
                logStr.query = req.query;
                break;
            case 'POST':
                logStr.body = req.body;
                break;
            case 'PATCH':
                logStr.body = req.body;
                break;
            case 'DELETE':
                logStr.query = req.query;
                break;
        }

        loggerHttp.info(JSON.stringify(logStr))

        next();
    }
    catch (Err) {
        loggerError.info(Err)
        res.send({ success: false });
    }
}

 

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares'

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    res.send('welcome!');
});

export default router;

 

 

16. /one 라우터에 접근해보고 로그 데이터가 잘 생성되는지 확인한다.

get 메소드는 브라우저 url 창에 입력해도 된다.

728x90
반응형
728x90
반응형

리눅스 서버로 실행할 때는 로컬에 vscode를 설치한 후 원격으로 접속해서 연결했지만 윈도우 서버에서는 인스턴스 안에 직접 vscode 를 설치하고 실행하였기에 포트포워딩을 로컬호스트로 해주지 않는다.

 

이를 위해서 ( 외부에서 접속하기 위해서 ) 해야될 것이 2가지 있는데,

 

1. 하나는 이미 해두었던 settings.py 파일에 아래 구문을 추가하는 것.

ALLOWED_HOSTS = ['*']

 

이 구문을 추가해주면 123.45.67.89:8000/ 이런식으로 외부에서 접속이 가능하다. 물론 나중에는 수정해야겠지만 지금은 테스트니까

 

2. 윈도우 서버에서는 방화벽을 따로 설정해주어야 한다. 탐색기에서 firewall을 검색하고 Windows Defender FireWall에 들어간다.

 

 

3. 인바운드에 기존에 있던 80에 관련된 규칙들에 Enabled를 활성화해주고 8000번 포트( django )를 열어준다. 

 

 

이 준비까지 다 되었으면 내 컴퓨터 로컬호스트에서 인터넷이나 postman 으로 응답을 확인할 수 있다.

 

4. 이제 셀레니움을 설치해서 사용해보자.

$ pip install selenium

 

5. 라우터에 다음 내용을 추가해준다. 여기서 chromedriver 는 전 포스트에서 설치했던 실행 파일의 위치이다.

/chwideukapp/views.py

from time import sleep
from rest_framework.views import APIView
from rest_framework.response import Response
from selenium import webdriver

chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument("--single-process")
chrome_options.add_argument("--disable-dev-shm-usage")
path = "C:\\Users\\Administrator\\Downloads\\chromedriver_win32\\chromedriver.exe"
driver = webdriver.Chrome(path, chrome_options=chrome_options)


class ChwideukRouter(APIView):
    def get(self, request):
        driver.get("https://typo.tistory.com/")

        print("+" * 100)
        print(driver.title)   # 크롤링한 페이지의 title 정보
        print(driver.current_url)  # 현재 크롤링된 페이지의 url
        print("-" * 100)

        title = driver.title
        url = driver.current_url

        driver.close()
        return Response({'title': title, 'url': url})

    def post(self, request):
        driver.get("https://typo.tistory.com/")

        print("+" * 100)
        print(driver.title)   # 크롤링한 페이지의 title 정보
        print(driver.current_url)  # 현재 크롤링된 페이지의 url
        print("-" * 100)

        title = driver.title
        url = driver.current_url

        driver.close()
        return Response({'title': title, 'url': url})

 

6. 서버를 키고 postman으로 요청을 보내보자.

$ python mnage.py runserver 0.0.0.0:8000

정상적으로 뜨는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

전 포스트에서 프로젝트까지 만들었었다.

 

1. settings.py 파일에 rest_framework 를 추가해준다.

ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework'
]

 

2. 새 마이그레이션을 생성하고 슈퍼유저를 생성해준다.

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser

 

3. 앱을 하나 만들어준다. ( 이름은 상관없습니다. )

$ python manage.py startapp chwideukapp

 

4. apiServer 폴더 안 settings.py 에 만든 앱을 추가해준다.

/apiServer/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'chwideukapp'
]

 

5. 새로 만든 앱의 models.py 파일을 수정해준다.

/chwideukapp/models.py

from django.db import models


class ChwideukModel(models.Model):
    title = models.CharField(max_length=70, default='')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

 

6. 새 마이그레이션을 생성한다.

$ python manage.py makemigrations
$ python manage.py migrate

 

7. serializers.py 파일을 만든다.

/chwideukapp/serializers.py

from rest_framework import serializers
from .models import ChwideukModel


class ChwideukSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChwideukModel
        fields = ['title']

 

8. views.py 파일을 수정해준다. 

from rest_framework.views import APIView
from rest_framework.response import Response


class ChwideukRouter(APIView):
    def get(self, request):
        return Response({'success': True})

    def post(self, request):
        return Response({'success': False})

 

9. apiServer 폴더의 urls.py에 방금 만든 라우터를 추가해준다.

from django.contrib import admin
from django.urls import include, path

from chwideukapp.views import ChwideukRouter


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/chwideuk/', ChwideukRouter.as_view())
]

 

728x90
반응형
728x90
반응형

 

1. aws에서 Windows 인스턴스를 만들고 고정 IP를 만들어주어 연결해준다.

 

AWS LightSail(1) - 인스턴스 생성하기

정말 빠르게 AWS LightSail로 인스턴스를 만들어보자. Lightsail에 관하여 https://lightsail.aws.amazon.com/ls/docs/ko_kr/all 1. aws 사이트에 회원가입, 로그인을 하고 Lightsail 서버를 찾아서 들어간다. 2...

typo.tistory.com

 

2. 맥북 환경이면 Microsoft Remote Desktop 앱을 깔아서 아이디와 비밀번호 연결 후 접속해준다.

인스턴스 초기 아이디 비밀번호는 aws 에서 확인하실 수 있습니다.

( aws 에서 RDP로 접속하니 좀 느렸습니다. )

 

3. aws 에서는 윈도우 서버를 만들고 파일을 다운받으려 할 때 에러가 뜬다. 아래처럼 해주자.

  1. EC2 Windows 인스턴스에 연결합니다.
  2. Windows 시작 메뉴를 열고 서버 관리자를 엽니다.
  3. EC2 Windows 인스턴스에서 실행 중인 Windows Server 버전에 대한 지침을 따릅니다.
    Windows Server 2012 R2, Windows Server 2016 또는 Windows Server 2019
    : 탐색 창에서 [Server Manager]를 선택합니다. [로컬 서버]를 선택합니다. [IE 보안 강화 구성]에서 [켜기]를 선택합니다.
    Windows Server 2008 R2: 탐색 창에서 [Server Manager]를 선택합니다. [서버 요약 - 보안 정보] 섹션에서 [IE ESC 구성]을 선택합니다.
  4. [관리자]에 대해 [끄기]를 선택합니다.
    [사용자]에 대해 [끄기]를 선택합니다.
    [확인]을 선택합니다.
  5. 서버 관리자를 닫습니다.

 

Internet Explorer를 사용하여 파일을 다운로드할 수 있도록 EC2 Windows 인스턴스 구성

인터넷에서 Amazon Elastic Compute Cloud(Amazon EC2) Windows 인스턴스에 타사 소프트웨어를 다운로드해야 합니다. Internet Explorer 보안 구성이 내 시도를 차단하고 있습니다. 다운로드를 활성화하려면 어떻

aws.amazon.com

 

4. Chrome 을 다운로드한다.

https://www.google.com/chrome/index.html

 

Chrome 웹브라우저

더욱 스마트해진 Google로 더 간편하고 안전하고 빠르게.

www.google.com

 

5. 아래 사진처럼 크롬 버전을 익혀둔다.

6. 크롬 버전을 확인했으면 앞자리 수에 맞는 크롬 드라이버를 설치한다.

 

ChromeDriver - WebDriver for Chrome - Downloads

Current Releases If you are using Chrome version 104, please download ChromeDriver 104.0.5112.29 If you are using Chrome version 103, please download ChromeDriver 103.0.5060.53 If you are using Chrome version 102, please download ChromeDriver 102.0.5005.61

chromedriver.chromium.org

 

7. 윈도우로 설치해준다. ( 버전은 다를 수 있습니다. )

 

8. 압축을 해제한 후 해당 파일 경로를 알아둔다. ( 나중에 셀레니움에서 사용 )

 

9. 개발환경을 위해 Vscode와 파이썬을 설치해준다.

 

 

Visual Studio Code - Code Editing. Redefined

Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.  Visual Studio Code is free and available on your favorite platform - Linux, macOS, and Windows.

code.visualstudio.com

 

 

Download Python

The official home of the Python Programming Language

www.python.org

 

파이썬을 설치할 때 환경변수를 지정해두면 좋다.

 

10. 작업하길 원하는 위치에 폴더를 만들고 가상환경을 만들어준다.

$ python -m venv example

 

11. django와 djangorestframework를 설치해준다.

$ pip install django
$ pip install djangorestframework markdown
$ django-admin startproject apiServer

 

12. VSCode 에서 보면 파일 구조는 이럴 것이다.

 

 

 

참고로 아래 확장 설치하시면 편합니다 

728x90
반응형

+ Recent posts